ci-actions — the platform-owned custom-app pipeline
This directory is synced verbatim into the dealsage-platform/ci-actions repo on
each environment's Gitea by infra/gitea/gitea-org.sh (the same way
templates/custom-app is synced into the template repo), and tagged v1 plus
v<VERSION> from the VERSION file. App repos call it from a one-step
workflow:
- uses: https://git.dealsage.io/dealsage-platform/ci-actions/pipeline@v1
with:
publish-token: ${{ secrets.PUBLISH_TOKEN }}
Why it exists: an app repo receives its workflow once, when it is generated from the template, and never again. Keeping the pipeline here means a fix ships to every app on its next push, instead of being patched repo by repo.
What pipeline does
| Step | Script | Notes |
|---|---|---|
| Tooling | scripts/ensure-tools.sh |
curl + jq; AWS CLI only for the S3 upload path |
| Manifest | scripts/manifest.sh |
validates .dealsage/app.json, exports APP_ID, CHECKS_JSON |
| Install | (action input) | npm install --no-audit --no-fund by default |
| Checks | scripts/run-checks.sh |
runs each declared check, posts every result to record-app-check; a failed required check stops here |
| Build | scripts/build.sh |
npm run build with VITE_SUPABASE_URL/ANON_KEY from org variables; recorded as the build check |
| Publish | scripts/publish.sh |
allocate → upload → promote request; DealSage applies the app's publish policy |
The action never decides whether a build goes live. record-app-version's
promote action does, from the app's publish_policy and default branch, and
answers promoted: true|false plus the channel the version landed on
(live or branch/<name>). Every build of every branch becomes a version, so
any of them can be opened by URL and promoted later.
The manifest (.dealsage/app.json)
Owned by the app's team; schema in manifest.schema.json.
{
"appId": "…",
"checks": [
{ "id": "typecheck", "name": "TypeScript", "run": "npm run typecheck" },
{ "id": "test", "name": "Unit tests", "run": "npm test -- --run", "required": true },
{ "id": "conventions", "uses": "conventions", "required": false }
],
"dependencies": { "objectTypes": [ { "apiName": "contract_agreement", "schemaVersion": 2 } ] }
}
required defaults to true. Platform checks (uses) live in pipeline/checks/
and run inside the same job; conventions verifies the repo still has the
shape the platform relies on.
Environment the caller provides
From org variables: DEALSAGE_FUNCTIONS_URL, SUPABASE_URL,
SUPABASE_ANON_KEY, and either CUSTOM_APPS_WORKER_URL (bundles are PUT
through the serving Worker with the short-lived upload token from allocate,
so CI holds no R2 key) or R2_ACCOUNT_ID + R2_BUCKET with the
R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY secrets (S3 sync, the path prod
uses today). PUBLISH_TOKEN is the app's own repo secret.
Testing locally
test/run-local.sh builds a throwaway app repo and runs the scripts against a
stub DealSage (test/stub_server.py) for three scenarios: auto policy, a
failing required check, and manual policy. Run it after any change here; it
needs bash, jq, git, node and python3.
Releasing a change
Bump VERSION for anything user-visible. Non-breaking changes keep the v1
tag moving; a breaking change (new required input, different env contract)
gets v2 and a stub update in the template plus a platform-files refresh for
existing apps.