CI sandbox smoke
The Console Sample Smoke job in .github/workflows/ci.yml runs samples/FreeAgent.Client.ConsoleSample with --seed-turpinverse --run-all against the FreeAgent sandbox API.
Default CI (verify, build-and-test) uses mocked HTTP handlers only. Live sandbox smoke is an additional signal that the SDK still works against a real OAuth-protected API.
Data mutations
--run-all is a live integration harness: it creates, updates, and deletes probe data in the connected sandbox account (invoices, contacts, notes, and similar). Use a dedicated CI sandbox identity (recommended below), not a production account.
The console sample refuses --run-all and --seed-turpinverse when the resolved environment is not sandbox. See #136 for the write-guard contract.
What you need
| Item | Purpose |
|---|---|
| Sandbox OAuth app | Client ID and secret from dev.sandbox.freeagent.com |
| Sandbox user account | Approves OAuth once when you obtain the refresh token (sandbox signup) |
| Refresh token | Long-lived credential stored as FREEAGENT_REFRESH_TOKEN in GitHub Actions |
These are not your FreeAgent login password. CI never opens a browser; it reuses a refresh token you obtained earlier.
Recommended: dedicated CI identity
Create a separate sandbox OAuth application (for example freeagent-dotnet-ci) and approve it with a dedicated sandbox user. That keeps CI credentials isolated from day-to-day development.
Register this redirect URI on the OAuth app:
http://127.0.0.1:8765/callback
One-time bootstrap (local)
-
Configure client ID and secret (shared with the Blazor sample):
cd samples/FreeAgent.Client.ConsoleSample dotnet user-secrets set "FreeAgent:ClientId" "<sandbox-client-id>" dotnet user-secrets set "FreeAgent:ClientSecret" "<sandbox-client-secret>" -
Obtain and print a refresh token:
dotnet run -- --bootstrap-refresh-tokenComplete OAuth in the browser. On WSL, copy the authorisation URL manually and paste the full redirect URL back into the terminal when prompted.
-
Verify locally before configuring GitHub:
export FREEAGENT_REFRESH_TOKEN="<refresh-token-from-step-2>" dotnet run -- --seed-turpinverse --run-all
Configure GitHub repository secrets
Set three repository secrets (Settings → Secrets and variables → Actions):
| Secret | Value |
|---|---|
FREEAGENT_CLIENT_ID |
Sandbox OAuth identifier |
FREEAGENT_CLIENT_SECRET |
Sandbox OAuth secret |
FREEAGENT_REFRESH_TOKEN |
Refresh token from bootstrap |
Or via the GitHub CLI:
gh secret set FREEAGENT_CLIENT_ID --repo markheydon/freeagent-dotnet
gh secret set FREEAGENT_CLIENT_SECRET --repo markheydon/freeagent-dotnet
gh secret set FREEAGENT_REFRESH_TOKEN --repo markheydon/freeagent-dotnet
How the workflow behaves
| Trigger | Secrets configured? | Result |
|---|---|---|
| Pull request | No | Smoke skipped (forks and draft work stay unblocked) |
| Pull request | Yes | Smoke runs (secrets are available to workflows from this repository) |
Push to main |
No | Workflow fails (credentials required on main) |
Push to main |
Yes | Smoke runs |
Weekly schedule / manual dispatch on main |
No | Workflow fails |
Weekly schedule / manual dispatch on main |
Yes | Smoke runs |
Before --run-all, the job:
- Refreshes the access token via
scripts/ci/refresh-freeagent-access-token.sh - Registers the access token with GitHub Actions log masking (
::add-mask::) before passing it to the smoke step — repository secrets are masked automatically, but step outputs are not - Seeds Turpinverse canon data with
--seed-turpinverse, then runs the console sample smoke suite with--run-allusing the fresh access token
Seeding upserts contacts, projects, tasks, sales documents, notes, and timeslips by stable keys. It is idempotent but does not remove stale sandbox records — delete unwanted probe data manually when refreshing a CI sandbox.
Turpinverse canon is pinned by commit SHA in the console sample appsettings.json (Turpinverse:CanonRef) so CI does not drift when upstream main changes. Bump the SHA deliberately when adopting new canon fixtures.
The seed step adds many API calls before smoke (list + upsert per canon item, with 600 ms pacing). The job timeout is 25 minutes, which should remain sufficient; watch the first CI run after enabling seed.
Canon Paid invoices are seeded as sent/open only; recording bank payments is out of scope for the Turpinverse fixtures.
If FreeAgent returns a new refresh token during that exchange, update the FREEAGENT_REFRESH_TOKEN repository secret manually (GitHub Actions cannot rotate secrets with the default GITHUB_TOKEN).
When smoke breaks
Common causes:
- Refresh token revoked (disconnect the app in FreeAgent, or security revocation)
- OAuth client secret rotated without updating GitHub secrets
- Sandbox rate limits (the job runs weekly plus on each
mainpush when secrets exist)
Re-run bootstrap (--bootstrap-refresh-token) and update the three repository secrets.
Related
- Console sample README — interactive examples and
--run-all - #126 — post-v1.0 API freshness policy
- #134 — linked-resource PUT sandbox verification