startos-compat-smoke #20
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
| # StartOS config round-trip smoke in canonical CI. | |
| # | |
| # What this proves | |
| # ---------------- | |
| # The smoke script in | |
| # artifacts/api-server/scripts/smoke-startos-compat.mjs drives the *real* | |
| # published `start9/compat:latest` Docker image — the same binary the StartOS | |
| # manifest's `config.get` / `config.set` procedures invoke — against the shipped | |
| # assets/config_spec.yaml + assets/config_rules.yaml, feeds the bytes the tool | |
| # writes through deploy/startos/docker_entrypoint.mjs (the production `main` | |
| # entrypoint), and asserts the env mapping the API server would end up reading. | |
| # | |
| # Why this exists | |
| # --------------- | |
| # The vitest suite in | |
| # artifacts/api-server/src/__tests__/startos-entrypoint.test.ts only pins the | |
| # mapping against a *captured* fixture (CAPTURED_COMPAT_CONFIG) — verbatim bytes | |
| # the real tool once emitted. That capture is manual: if the spec or entrypoint | |
| # changes, someone has to remember to re-run `compat` and refresh the bytes, or | |
| # the fixture silently drifts. The smoke removes the manual step by exercising | |
| # the real tool, so a spec/entrypoint drift fails loudly on push/PR. | |
| # | |
| # This is the canonical-CI counterpart to the `startos-compat` Replit validation | |
| # step (pnpm `smoke:startos-compat`). The Replit validation degrades gracefully | |
| # where Docker is unavailable; none of the existing GitHub Actions workflows ran | |
| # this Docker-based round-trip, so a drift would not be caught on push/PR in | |
| # canonical CI. This job closes that gap (Docker is available by default on | |
| # GitHub-hosted runners). | |
| # | |
| # Fork-gating | |
| # ----------- | |
| # Gated on `github.event.repository.fork == false` like the other | |
| # canonical-only jobs (see release.yml / cve-appendix-release.yml), so forks do | |
| # not fail on it. Note `github.event.repository` is NOT populated for | |
| # `schedule` events (`null == false` is false), so the job-level `if` also | |
| # admits `github.event_name == 'schedule'`. GitHub does not run scheduled | |
| # workflows on forks by default, so the scheduled path stays canonical-only in | |
| # practice — the same shape pnpm-audit.yml / onion-smoke.yml rely on for their | |
| # scheduled issue-writing steps. | |
| # | |
| # Triggers | |
| # -------- | |
| # - `push` / `pull_request` (path-filtered): a spec/entrypoint/script change | |
| # fails the PR check loudly. | |
| # - `schedule` daily: catches out-of-band drift in the moving | |
| # `start9/compat:latest` Docker image even when nothing in the repo changes. | |
| # If the upstream packaging tool changes its serialization behaviour (e.g. | |
| # how it quotes colon-bearing scalars) between our PRs, the path-filtered | |
| # triggers would not fire until someone happens to touch a gated path; the | |
| # daily run closes that window. This mirrors onion-smoke.yml's daily | |
| # `schedule:` for between-release drift. | |
| # - `workflow_dispatch`: run on demand. | |
| # | |
| # Failure behaviour | |
| # ----------------- | |
| # The script's existing graceful-skip (it prints a SKIP notice and exits 0 when | |
| # no Docker daemon is reachable or the image cannot be pulled) keeps the job | |
| # green if a runner ever lacks a working Docker daemon. A real spec/entrypoint/ | |
| # upstream-tool drift exits non-zero and fails the job. | |
| # | |
| # On a SCHEDULED failure, a follow-up step opens (or updates) a | |
| # `startos-compat-smoke-failure` GitHub issue so an upstream-tool regression is | |
| # surfaced in the issue tracker, not lost in the workflow-run list (same pattern | |
| # as onion-smoke.yml). On the next green scheduled run the issue is auto-closed. | |
| # Push/PR failures already block the PR check, so they don't open an issue. | |
| name: startos-compat-smoke | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "assets/config_spec.yaml" | |
| - "assets/config_rules.yaml" | |
| - "deploy/startos/**" | |
| - "artifacts/api-server/scripts/smoke-startos-compat.mjs" | |
| - ".github/workflows/startos-compat-smoke.yml" | |
| - "package.json" | |
| - "pnpm-workspace.yaml" | |
| - "pnpm-lock.yaml" | |
| push: | |
| branches: [main] | |
| paths: | |
| - "assets/config_spec.yaml" | |
| - "assets/config_rules.yaml" | |
| - "deploy/startos/**" | |
| - "artifacts/api-server/scripts/smoke-startos-compat.mjs" | |
| - ".github/workflows/startos-compat-smoke.yml" | |
| - "package.json" | |
| - "pnpm-workspace.yaml" | |
| - "pnpm-lock.yaml" | |
| schedule: | |
| # 15:41 UTC daily. Off-the-hour to dodge GHA scheduler congestion, and | |
| # offset from the other scheduled jobs (pnpm-audit 13:17, onion-smoke | |
| # 14:23) so a slow scheduler doesn't collide them. | |
| - cron: "41 15 * * *" | |
| permissions: | |
| contents: read | |
| issues: write | |
| concurrency: | |
| group: startos-compat-smoke-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| startos-compat: | |
| name: Real-tool StartOS config round-trip | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # Canonical-only: forks don't have (and shouldn't fail on) this gate. The | |
| # `github.event_name == 'schedule'` clause is required because | |
| # `github.event.repository` is null on scheduled events (see header); | |
| # GitHub keeps the scheduled path canonical-only by not running schedules | |
| # on forks. | |
| if: ${{ github.event_name == 'schedule' || github.event.repository.fork == false }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Run StartOS config round-trip smoke | |
| id: smoke | |
| run: pnpm --filter @workspace/api-server run smoke:startos-compat | |
| - name: Open or update failure issue (scheduled) | |
| if: failure() && github.event_name == 'schedule' && steps.smoke.outcome == 'failure' | |
| uses: actions/github-script@v7 | |
| env: | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| script: | | |
| const title = "startos-compat-smoke: StartOS config round-trip regression"; | |
| const marker = "<!-- startos-compat-smoke-failure -->"; | |
| const runUrl = process.env.RUN_URL; | |
| const body = [ | |
| marker, | |
| "The daily StartOS config round-trip smoke failed.", | |
| "", | |
| "This job drives the *real* published `start9/compat:latest` Docker image against `assets/config_spec.yaml` + `assets/config_rules.yaml` and feeds the bytes it writes through `deploy/startos/docker_entrypoint.mjs`. Because nothing in the repo has to change for this scheduled run, a failure here usually means the **upstream packaging tool changed its serialization behaviour** (e.g. how it quotes colon-bearing scalars) — not a local edit.", | |
| "", | |
| `Run: ${runUrl}`, | |
| "", | |
| "Most-likely causes, in rough order of how often they bite:", | |
| "1. `start9/compat:latest` changed its YAML/scalar serialization (colon-quoting, `~`/null handling, default baking) since the last run — the captured fixture `CAPTURED_COMPAT_CONFIG` in `artifacts/api-server/src/__tests__/startos-entrypoint.test.ts` now needs re-capturing.", | |
| "2. A change to `assets/config_spec.yaml` / `assets/config_rules.yaml` / `deploy/startos/docker_entrypoint.mjs` landed via a path that the smoke's `paths:` filter didn't gate.", | |
| "3. The image could not be pulled or run on the runner (transient) — re-run before chasing a regression; a genuine pull/daemon outage exits 0 (SKIP) and would not have opened this issue.", | |
| "", | |
| "Local repro: `pnpm --filter @workspace/api-server run smoke:startos-compat` (needs Docker). See the workflow header in `.github/workflows/startos-compat-smoke.yml`.", | |
| ].join("\n"); | |
| const { data: open } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: "open", | |
| labels: "startos-compat-smoke", | |
| per_page: 100, | |
| }); | |
| const existing = open.find((i) => i.body && i.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: existing.number, | |
| body: `Still failing as of ${new Date().toISOString()}.\n\nRun: ${runUrl}`, | |
| }); | |
| core.notice(`Updated existing failure issue #${existing.number}.`); | |
| } else { | |
| const { data: created } = await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title, | |
| body, | |
| labels: ["startos-compat-smoke"], | |
| }); | |
| core.notice(`Opened failure issue #${created.number}.`); | |
| } | |
| - name: Auto-close failure issue on green run (scheduled) | |
| if: success() && github.event_name == 'schedule' && steps.smoke.outcome == 'success' | |
| uses: actions/github-script@v7 | |
| env: | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| script: | | |
| const marker = "<!-- startos-compat-smoke-failure -->"; | |
| const runUrl = process.env.RUN_URL; | |
| const { data: open } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: "open", | |
| labels: "startos-compat-smoke", | |
| per_page: 100, | |
| }); | |
| for (const issue of open) { | |
| if (!issue.body || !issue.body.includes(marker)) continue; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: `Recovered. Most recent green run: ${runUrl}`, | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| state: "closed", | |
| }); | |
| core.notice(`Auto-closed recovered failure issue #${issue.number}.`); | |
| } |