feat(board): first-class tickets - board cards become durable tickets… #1176
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy API Worker | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'workers/api/**' | |
| - 'packages/sdk/**' | |
| - '.github/workflows/deploy-api.yml' | |
| # The build chain moved OUT of this file into a composite action (#779). Without this | |
| # entry an edit to the shared chain would trigger no deploy at all — the duplication it | |
| # replaced was at least watched, because it lived in the path above. | |
| - '.github/actions/build-platform/**' | |
| workflow_dispatch: {} | |
| concurrency: | |
| group: deploy-api | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # `pnpm test` below now includes migration-hygiene.test.ts, which shells out to | |
| # check-migrations.mjs --require-history. That guard REFUSES a shallow clone by | |
| # design: a history-dependent check with no history passes by verifying nothing, | |
| # so it fails loudly instead. Depth 1 therefore red-lined this deploy from the | |
| # commit the guard landed in (48eb443) — the API fixes were merged and undeployed | |
| # while Deploy Host stayed green, so the console looked fine and the worker was stale. | |
| fetch-depth: 0 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| # The install + build chain shared with deploy-host.yml and (still hand-rolled) ci.yml. | |
| # Must come AFTER pnpm/action-setup and setup-node above — the composite shells out to | |
| # `pnpm` directly and has no setup of its own. | |
| # | |
| # Why an API deploy builds the CONSOLE and the HOST pages at all: `pnpm test` below runs | |
| # the WHOLE suite. It imports @proagentstore/sdk through its exports map (its built dist), | |
| # and it collects workers/host/src/admin-api-proxy.test.ts, which imports the host worker's | |
| # index.ts, which imports ./pages.js at module load. `pages.ts` is build output that | |
| # build.js inlines store/ into, and build.js reads the console and admin Vite bundles and | |
| # the generated store/docs. Miss any link and the deploy stops for a reason unrelated to | |
| # the API — which is exactly what happened when this chain was hand-copied without the | |
| # docs steps: run 34004410070 died at `node build.js` with "No docs at store/docs", fixed | |
| # in 0ca213b3. Extracting the chain (#779) is what stops the copy existing to go stale. | |
| - uses: ./.github/actions/build-platform | |
| - name: Run tests | |
| run: pnpm test | |
| - name: Apply D1 migrations | |
| uses: cloudflare/wrangler-action@v3 | |
| with: | |
| apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| workingDirectory: workers/api | |
| command: d1 migrations apply pags --remote | |
| - name: Deploy API worker | |
| # --var API_BUILD overrides the wrangler.toml "dev" default with the real 12-char git SHA | |
| # (#735) so every server-side error_log row carries the build that wrote it. 12 chars | |
| # matches the client-side convention from #539 and is collision-free in practice (the | |
| # birthday bound is ~2^48, orders of magnitude beyond any realistic repo history). | |
| run: | | |
| SHORT_SHA=$(git rev-parse --short=12 HEAD) | |
| cd workers/api && pnpm exec wrangler deploy --var "API_BUILD:${SHORT_SHA}" | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| - name: Smoke test | |
| # The admin perimeter's mode is asserted, not just the 200 (#108 C4). `/health` reports | |
| # `adminPerimeter` = off | audit | enforce, derived from Worker SECRETS — which live outside | |
| # this repo, so a `wrangler secret delete`, a typo'd value or a deploy that lost them turns | |
| # the Cloudflare Access gate back to `off` with every automated signal still green. `off` | |
| # is indistinguishable from healthy unless something says what it is SUPPOSED to be. | |
| # | |
| # EXPECTED_ADMIN_PERIMETER is a repo VARIABLE (Settings → Secrets and variables → Actions → | |
| # Variables), defaulting to `off`, which is what production runs today. It is moved by | |
| # hand, in step with the rollout in docs/admin-access-perimeter.md: set it to `audit` when | |
| # CF_ACCESS_TEAM_DOMAIN + CF_ACCESS_AUD go in, and to `enforce` with CF_ACCESS_ENFORCE. | |
| # The worker is ALREADY deployed when this runs, so a mismatch does not roll anything | |
| # back — it makes the drift a red deploy instead of a silently re-opened admin surface. | |
| # | |
| # The public catalogue is checked separately from `/health` (#830). A Worker can be | |
| # reachable while its D1-backed catalogue query is broken; MCP's `list_agents` depends | |
| # on this endpoint, and a coding template must remain subscribable through it. | |
| env: | |
| EXPECTED_ADMIN_PERIMETER: ${{ vars.EXPECTED_ADMIN_PERIMETER || 'off' }} | |
| run: | | |
| for i in 1 2 3 4 5; do | |
| STATUS=$(curl -sS -o /tmp/health.json -w "%{http_code}" https://api.proagentstore.online/health) | |
| if [ "$STATUS" = "200" ]; then | |
| echo "Health check passed" | |
| ACTUAL=$(jq -r '.adminPerimeter // "absent"' /tmp/health.json) | |
| if [ "$ACTUAL" != "$EXPECTED_ADMIN_PERIMETER" ]; then | |
| echo "::error::Admin perimeter is '$ACTUAL' but EXPECTED_ADMIN_PERIMETER is '$EXPECTED_ADMIN_PERIMETER'." | |
| echo "If the CF_ACCESS_* secrets were changed on purpose, update the EXPECTED_ADMIN_PERIMETER repo variable to match." | |
| echo "If they were not, the Cloudflare Access gate on /v1/admin/* has drifted — see docs/admin-access-perimeter.md." | |
| exit 1 | |
| fi | |
| echo "Admin perimeter is '$ACTUAL', as expected" | |
| CATALOGUE_STATUS=$(curl -sS -o /tmp/agents.json -w "%{http_code}" "https://api.proagentstore.online/v1/agents?limit=500") | |
| if [ "$CATALOGUE_STATUS" = "200" ] && jq -e '(.agents | type == "array") and any(.agents[]?; .slug == "coder" or .slug == "coder-repo" or .slug == "tmux-coder")' /tmp/agents.json > /dev/null; then | |
| echo "Public catalogue contains a coding agent" | |
| exit 0 | |
| fi | |
| echo "Attempt $i: public catalogue returned $CATALOGUE_STATUS or no coding agent, retrying..." | |
| else | |
| echo "Attempt $i: health check got $STATUS, retrying..." | |
| fi | |
| sleep 5 | |
| done | |
| echo "Smoke test failed after 5 attempts: /health must return 200 with the expected admin perimeter, and /v1/agents must return a coding agent." | |
| exit 1 |