'Configure CI/CD pipeline for AppFolio property management integrations.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add jeremylongshore/tons-of-skills-marketplace --skill appfolio-ci-integration --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Appfolio Ci Integration?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeremylongshore-appfolio-ci-integration-14de145f)More formats (shields.io, HTML) on the badges page.
---
name: appfolio-ci-integration
description: 'Configure CI/CD pipeline for AppFolio property management integrations.
Trigger: "appfolio CI".
'
allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(curl:*), Grep
version: 1.5.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- property-management
- appfolio
- real-estate
compatibility: Designed for Claude Code
---
# AppFolio CI Integration
## Overview
Configure CI pipelines that validate AppFolio property management API integrations using a two-tier strategy. Unit tests mock the AppFolio REST client to verify tenant lookup, work order creation, and property listing logic without consuming API quota. Integration tests run against the AppFolio sandbox environment on main-branch merges only, using Basic Auth credentials stored as GitHub secrets. This keeps PR feedback fast and free while catching real API contract drift before production deploys.
## Prerequisites
- A repository with Actions enabled, a locked dependency install, and unit tests
that use only synthetic tenant, property, and work-order fixtures.
- A separately managed AppFolio sandbox credential and a pre-provisioned,
non-production fixture record that the integration owner is allowed to read.
- Defined CI ownership, a fixed request budget, and an incident path for
credential rotation, provider outage, or sandbox data reset.
## Instructions
1. Keep pull-request checks mock-only; do not expose AppFolio credentials to
untrusted code or forked pull requests.
2. Gate the live sandbox job to protected `main` pushes after lint, type, and
unit checks pass, and inject credentials only through repository secrets.
3. Limit the integration run to a known read-only fixture, one worker, and the
stated request budget; record only redacted status and count evidence.
4. Treat failed authentication, rate limits, or missing fixtures as a blocked
release condition rather than creating shared sandbox data from CI.
## GitHub Actions Workflow
```yaml
# .github/workflows/appfolio-tests.yml
name: AppFolio API Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run lint && npm run typecheck
- run: npm test -- --testPathPattern=unit # No API credentials needed
integration-tests:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: unit-tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm test -- --testPathPattern=integration
env:
APPFOLIO_CLIENT_ID: ${{ secrets.APPFOLIO_CLIENT_ID }}
APPFOLIO_CLIENT_SECRET: ${{ secrets.APPFOLIO_CLIENT_SECRET }}
APPFOLIO_BASE_URL: ${{ secrets.APPFOLIO_SANDBOX_URL }}
```
## Mock-Based Unit Tests
```typescript
// tests/unit/work-order-service.test.ts
import { describe, it, expect, vi } from 'vitest';
import { createWorkOrder } from '../../src/services/work-order-service';
import * as appfolioClient from '../../src/lib/appfolio-client';
vi.mock('../../src/lib/appfolio-client');
describe('WorkOrderService', () => {
it('creates a maintenance work order for a property', async () => {
vi.mocked(appfolioClient.post).mockResolvedValue({
id: 'wo-4821',
property_id: 'prop-100',
category: 'Plumbing',
status: 'Open',
});
const result = await createWorkOrder('prop-100', 'Plumbing', 'Leaking faucet unit 3B');
expect(result.status).toBe('Open');
expect(appfolioClient.post).toHaveBeenCalledWith('/work_orders', {
property_id: 'prop-100',
category: 'Plumbing',
description: 'Leaking faucet unit 3B',
});
});
});
```
## Integration Tests
```typescript
// tests/integration/tenant-lookup.test.ts
import { describe, it, expect } from 'vitest';
import { AppFolioClient } from '../../src/lib/appfolio-client';
const canRun = process.env.APPFOLIO_CLIENT_ID && process.env.APPFOLIO_CLIENT_SECRET;
describe.skipIf(!canRun)('AppFolio Tenant Lookup (live sandbox)', () => {
const client = new AppFolioClient({
clientId: process.env.APPFOLIO_CLIENT_ID!,
clientSecret: process.env.APPFOLIO_CLIENT_SECRET!,
baseUrl: process.env.APPFOLIO_BASE_URL!,
});
it('lists tenants for a known property', async () => {
const tenants = await client.get('/tenants', { property_id: 'prop-100' });
expect(Array.isArray(tenants)).toBe(true);
expect(tenants[0]).toHaveProperty('lease_status');
});
});
```
## CI Cost Management
```typescript
// tests/helpers/api-budget.ts
let callCount = 0;
const MAX_CALLS_PER_RUN = 25; // AppFolio sandbox has 100 req/min rate limit
export function trackApiCall(): void {
callCount++;
if (callCount > MAX_CALLS_PER_RUN) {
throw new Error(
`CI API budget exceeded: ${callCount}/${MAX_CALLS_PER_RUN} calls. ` +
'Reduce integration test scope or split across jobs.'
);
}
}
export function getCallCount(): number { return callCount; }
```
## Error Handling
| CI Issue | Cause | Fix |
|----------|-------|-----|
| 401 Unauthorized in integration job | Expired or rotated sandbox credentials | Regenerate `APPFOLIO_CLIENT_ID` and `APPFOLIO_CLIENT_SECRET` in GitHub Secrets |
| 429 Too Many Requests | Sandbox rate limit (100 req/min) hit by parallel tests | Run integration tests with `--maxWorkers=1` |
| Tenant list empty | Sandbox data periodically reset by AppFolio | Stop the job and restore the pre-provisioned fixture outside CI |
| Typecheck fails on API response | AppFolio schema updated without notice | Regenerate types from OpenAPI spec, update interfaces |
| Integration job skipped | Branch protection rule not matching `refs/heads/main` | Verify workflow `if` condition matches your default branch name |
## Output
- Pull-request results from mock-only lint, type, and unit checks
- A protected-branch sandbox receipt with the redacted provider status, fixture
identity, request count, and integration test outcome
- A bounded release decision that blocks on unavailable credentials, provider
errors, fixture drift, or request-budget exhaustion
## Examples
For a work-order mapping change, add a synthetic mock response and verify the
new field in the PR suite. After merge, the protected main workflow reads one
pre-provisioned sandbox work order and confirms the normalized shape without
creating tenants, properties, or work orders. If the fixture is unavailable,
the credential fails, or the request budget is exceeded, mark the release
blocked and have the sandbox owner restore the fixture or credential before
rerunning the live lane.
## Resources
- [AppFolio Stack API Documentation](https://www.appfolio.com/stack/partners/api)
- [GitHub Actions Encrypted Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)
## Next Steps
See `appfolio-deploy-integration`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!