Skip to content

startos-compat-smoke #14

startos-compat-smoke

startos-compat-smoke #14

# 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}.`);
}