# 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` from the `VERSION` file. App repos call it from a one-step workflow: ```yaml - 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/`). 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`. ```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.