From 274f7ffe2189f5b037ef287ed86e907f7f7df28f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 23 Sep 2026 11:15:42 -0400 Subject: [PATCH] =?UTF-8?q?ci:=20the=20tests=20gate=20the=20publish=20?= =?UTF-8?q?=E2=80=94=20ci.yml=20folds=20into=20build.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator, 2026-09-23: *"tighten the gate so :dev can't publish on red tests"*, then *"I don't want failing builds to publish anywhere going forward."* Run 7348 is the worked example. The backend unit lane went red on `2f8f0bc` and `build-web` pushed `:dev` in the same minute, because the lanes and the build were SEPARATE WORKFLOWS on the same push trigger. Neither could see the other's verdict. `:dev` was a "it built" signal, never a "it passed" one, and nothing about that was visible from either run. Two workflows cannot express the gate. A `needs:` edge only exists inside one graph. So `ci.yml`'s five lanes move into `build.yml` and `ci.yml` is deleted; `sign-extension`, `build-web` and `build-agent` now need all five. Nothing here is a new mechanism — it is the same edge that has gated `promote` since milestone 362 step 4, and it keeps that step's hardest-won property: **not running is not the same as passing.** `needs` treats a SKIPPED dependency as unsatisfied, so a lane that silently skips itself blocks the publish exactly as a failing one does. Run 5290 is why that is worth stating. Scope, said plainly rather than implied: - Gated: every image tag (`:dev`, `:latest`, `:c-`), the weekly base refresh, and the `ext-` signed-XPI release asset — `sign-extension` publishes too, so it is gated with the rest. - Not gated, deliberately: `extension.yml` publishes nothing, and `release.yml` runs on a `v*` tag, generates notes rather than an artifact, and its commit already went through main's gated build. - `pull_request` (Renovate bumps into `dev`) comes across with the lanes. Its runs are the lanes and nothing else, via an `if:` on each publishing job rather than an inference from the `needs` chain. The cost, accepted knowingly: this workflow queues per branch and never cancels, so on two pushes in quick succession the second's lint feedback waits out the first's build. A slower red beats a fast red that ships. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR --- .forgejo/workflows/baseline.yml | 4 +- .forgejo/workflows/build.yml | 375 ++++++++++++++++++++++++++- .forgejo/workflows/ci.yml | 294 --------------------- .forgejo/workflows/extension.yml | 9 +- README.md | 18 +- backend/app/scripts/wait_for_deps.py | 2 +- ci-requirements.md | 9 +- extension/README.md | 2 +- extension/package.json | 2 +- extension/test/version.spec.js | 8 +- scripts/smoke_image.py | 2 +- tests/conftest.py | 2 +- 12 files changed, 396 insertions(+), 331 deletions(-) delete mode 100644 .forgejo/workflows/ci.yml diff --git a/.forgejo/workflows/baseline.yml b/.forgejo/workflows/baseline.yml index 56248fd..f56f4ba 100644 --- a/.forgejo/workflows/baseline.yml +++ b/.forgejo/workflows/baseline.yml @@ -79,7 +79,7 @@ jobs: - name: Resolve the Postgres service and install deps run: | set -eux - # Same service-IP dance as ci.yml's integration job; see the long + # Same service-IP dance as build.yml's integration job; see the long # comment there for why the job name must stay separator-free. PG=$(docker ps --filter "name=compare" --filter "ancestor=pgvector/pgvector:pg16" -q | head -n1) test -n "$PG" @@ -89,7 +89,7 @@ jobs: echo "DB_HOST=$PG_IP" >> "$GITHUB_ENV" # Socket probe in python, not bash's /dev/tcp — these steps run under # `sh -e`, where that path does not exist. Same fix and same reasoning - # as ci.yml's integration job; see the comment there. + # as build.yml's integration job; see the comment there. pg_ready="" for i in $(seq 1 60); do if python -c "import socket,sys; s=socket.socket(); s.settimeout(2); sys.exit(0 if s.connect_ex(('$PG_IP', 5432)) == 0 else 1)"; then diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index e01fdc9..2a41064 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build images +name: CI and images on: push: @@ -17,6 +17,18 @@ on: # under a second name, and the stack that needed the second name is being # collapsed onto the consolidated image.) branches: [main, dev] + + # Renovate opens PRs from `renovate/*` branches into `dev`. Those branches + # never push to dev/main, so the push trigger above gives them NO pre-merge + # CI — a bump could only be validated after it was already merged. Base + # `dev` only: it deliberately does NOT fire on dev→main PRs, which rely on + # the dev push run, so no duplicate runs. FC has no fork PRs (single-operator + # Forgejo repo), so secrets-on-PR is not a concern. + # + # Nothing PUBLISHES on this trigger — see the `if:` on the three publishing + # jobs below. A PR run is the lanes and nothing else. + pull_request: + branches: [dev] # # NO tag trigger (milestone 318 step 2). A `v*` tag names a commit `main` # already built and published; rebuilding it produces the same source under @@ -161,6 +173,299 @@ env: # The injected GITHUB_TOKEN cannot be used — it lacks write:package. jobs: + # --------------------------------------------------------------------------- + # THE LANES. Merged in from `ci.yml`, which is deleted, 2026-09-23. + # + # They were a separate workflow on the same push trigger, which meant the + # build could not see their verdict and published regardless. Run 7348 is the + # worked example: the unit lane went red on `2f8f0bc` and `build-web` pushed + # `:dev` anyway, in the same minute. Operator: *"tighten the gate so :dev + # can't publish on red tests"*, then *"I don't want failing builds to publish + # anywhere going forward."* + # + # Two workflows cannot express that. A `needs:` edge only exists inside one + # graph — so the lanes and the publish are one graph now, and the gate is the + # `needs:` on the three publishing jobs rather than anything new. + # + # The cost, accepted knowingly: this workflow QUEUES per branch and never + # cancels (see `concurrency:` above), so on two pushes in quick succession + # the second push's lint feedback waits out the first run's build. That is + # the price of the edge, and it is the right way round — a slower red is + # better than a fast red that ships. + # --------------------------------------------------------------------------- + + # Fast-fail lint lane. ruff is pre-installed in the ci-python image, so + # this runs with NO dependency install and surfaces the most common bounce + # class (lint: I001 / UP037 / ASYNC109 / W293 …) in seconds — instead of + # after the backend job's ~30-60s wheel install. ruff is static analysis, + # so no DB/secret env is needed. + lint: + runs-on: python-ci + container: + image: git.fabledsword.com/bvandeusen/ci-python:3.14 + steps: + - uses: actions/checkout@v4 + - name: Ruff lint + # agent/ included so the GPU-agent is linted before its image is built + # (build.yml only `docker build`s it — this is where it gets checked). + # scripts/ likewise: release_notes.py runs only on a tag push, so a + # syntax or import error there would otherwise surface at the one + # moment nobody wants to debug a workflow. + run: ruff check backend/ tests/ alembic/ agent/ scripts/ + - name: Agent syntax check + # The agent's runtime deps (torch/transformers/ultralytics) aren't in the + # CI image, so we can't import it — but compileall parses every module, + # catching syntax errors before the image build. + run: python -m compileall -q agent/fc_agent + + # The extension version is DERIVED, not hand-maintained (milestone 271 step + # 4): build.yml computes it from the commit TIME of the newest packaged + # extension change and stamps it into manifest.json / package.json at build + # time. The guard that used to live here — "packaged files changed but nobody + # bumped the version" — was therefore checking a fact that had stopped + # existing. Worse than useless: it would have failed this lane on every real + # extension change, demanding a bump that decides nothing. Retired 2026-08-27 + # rather than left running beside the new mechanism (rule 22). + # + # Two things are still worth asserting, and this is the only lane that can: + # the extension.yml suite runs on node:24-slim, which is exactly why + # version.spec.js sticks to packaging.sh's git-free subcommands. + # 1. the derivation actually resolves on this commit + # 2. the derived string is one AMO will accept, checked against Mozilla's + # own published grammar rather than a loose "digits and dots" + # + # The MAJOR.MINOR-agreement check that used to be (2) is gone with milestone + # 318 step 8: the committed version no longer seeds anything, so there is no + # hand-set part left for the two files to disagree about. + # + # Deliberately NOT checked here: that the derived value beats what has already + # been signed. That guard belongs in build.yml, where it compares against the + # real ext-* releases. Comparing against origin/main here would be wrong — + # dev legitimately derives a LOWER value whenever main is ahead on the + # extension, and a lane that fails for being behind is a lane people learn to + # ignore. + extension-version: + runs-on: python-ci + container: + image: git.fabledsword.com/bvandeusen/ci-python:3.14 + steps: + - uses: actions/checkout@v4 + with: + # The derivation needs real history: a depth-1 clone sees one commit + # and produces a wrong, too-low value RATHER THAN FAILING. Checking + # that here is half the point of the lane. + fetch-depth: 0 + - name: Extension version derives cleanly + run: | + set -eu + # busybox sh on the act_runner — no bashisms (family rule). + VERSION=$(sh extension/scripts/packaging.sh version) + echo "derived: $VERSION" + + # Mozilla's published grammar for AMO, transcribed verbatim from + # MDN's manifest.json/version page: + # + # ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$ + # + # Not the looser `^[0-9]+(\.[0-9]+)*$` this lane used to carry. That + # one passes `2026.08.29.0201`, which AMO REJECTS — a segment must be + # the single digit 0 or start 1-9 — and it also passes five segments, + # where AMO allows four. Both would surface as a failed sign with the + # version already burned: AMO 409s on re-signing, so a rejected value + # cannot be reclaimed and cannot be reused. This lane is the cheap + # place to find out. (#3138, milestone 318 step 8.) + if ! echo "$VERSION" | grep -qE '^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$'; then + echo "ERROR: derived version '$VERSION' is not a version AMO accepts." + echo "AMO's grammar: ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$" + echo "Most likely cause: a zero-padded segment (08, 0201). The rest" + echo "of the family pads; the extension must not — see packaging.sh." + exit 1 + fi + + # ...and the shape this project actually derives. AMO would happily + # take `1.0.3500147` too, so the grammar check alone would not notice + # a regression to the pre-318 shape — which orders BELOW everything + # signed since, and is unrecoverable once Firefox has the higher one. + if ! echo "$VERSION" | grep -qE '^20[0-9][0-9]\.[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,4}$'; then + echo "ERROR: derived version '$VERSION' is not YYYY.M.D.HHMM." + echo "Rule 148's CalVer is what build.yml signs; the old" + echo "1.0. shape would order below every ext-2026.* release." + exit 1 + fi + echo "OK: derived version $VERSION" + + backend-lint-and-test: + runs-on: python-ci + container: + image: git.fabledsword.com/bvandeusen/ci-python:3.14 + env: + # DB_PASSWORD and SECRET_KEY are required by config.py at import time + # even though unit tests don't actually touch the DB or use the secret. + DB_PASSWORD: ci_unit_test_placeholder + SECRET_KEY: ci_unit_test_placeholder + steps: + - uses: actions/checkout@v4 + with: + # Full history for tests/test_artifact_identity.py, which derives + # each artifact's revision to check the identity scheme. On a + # depth-1 clone that derivation either fails or returns the tip sha + # — so the lane would go green while asserting nothing, which is + # the one outcome worse than a red one. + fetch-depth: 0 + + # Cache step removed 2026-05-26: act_runner's cache backend has been + # broken on this homelab runner since 2026-05-15 (first as request- + # timeout warnings, then as hard "Cannot find module .../dist/restore/ + # index.js" failures that tank the whole job). The cache step targeted + # ~/.cache/pip but the install below uses `uv pip install` primarily, + # whose own cache lives at ~/.cache/uv — so the cache step's real + # benefit was marginal even when working. Cost of removal: ~30s of + # wheel downloads per job. Future re-enable: mount ~/.cache/uv as a + # docker volume at the runner level (skips actions/cache entirely), + # or fix the runner-side cache backend (clear /var/run/act/actions/*, + # pin act_runner version, etc.). + + - name: Install Python deps + # ruff is pre-installed in the ci-python image (see CI-Runner/CI-python/ + # Dockerfile's RUFF_VERSION). Per FabledRulebook ci-runners.md, toolchain + # versions live on the runner image, not here. + # uv: 5-10x faster wheel resolve than pip for cold caches. + # Falls back to pip install on uv-missing runners (older images). + run: | + if command -v uv >/dev/null 2>&1; then + uv pip install --system -r requirements.txt pytest pytest-asyncio + else + pip install -r requirements.txt pytest pytest-asyncio + fi + + # Ruff moved to the dedicated fast `lint` job above (fails in seconds, + # no dep install). This job is now unit tests only. + - name: Pytest (unit only — integration runs in the integration job) + run: pytest tests/ -v -m "not integration" + + frontend-build: + runs-on: python-ci + container: + image: git.fabledsword.com/bvandeusen/ci-python:3.14 + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + # No package-lock.json is tracked yet (we don't run npm locally per + # feedback-no-local-runs). Using `npm install` instead of `npm ci`. + # If we want strict lockfile-based reproducibility later, commit a + # package-lock.json and flip this back to `npm ci`. + - run: npm install --no-audit --no-fund + # No type-check step: the frontend is pure JS (no .ts files, no JSDoc), + # so a type-checker has nothing to do. The vue-tsc devDep + its `check` + # script were dropped 2026-07-11 rather than bumped to v3. If we add + # TS/JSDoc later, re-add a tsconfig.json + vue-tsc + a type-check step. + - run: npm run test:unit + - run: npm run build + + # Single integration job — collapsed from a 3-way shard split on 2026-06-04. + # The shards existed to parallelize ~8.5min of integration tests; once the + # throwaway Postgres runs with fsync OFF (the durability step below) the whole + # suite runs in ~45s, so the split only triplicated the ~2min fixed overhead + # (container + `uv pip install` + `alembic upgrade head`) and burned 3 of 6 + # runner slots for no wall-clock gain. One job now: spin up once, install + # once, migrate once, run every integration test. + # + # The docker-ps filter scopes to THIS job's own Postgres/Redis service + # containers by job name. act_runner strips underscores from job names when + # labelling containers (`int_api` matched nothing on 2026-05-25), so the name + # stays separator-free (`integration`). The step prints `docker ps -a` first + # so a future naming-convention shift surfaces in the log without a + # guess-and-push cycle. + # + # Pre-baking requirements.txt into ci-python:3.14 is intentionally NOT done — + # per ci-requirements.md, FC is the only Python consumer of that image and the + # CI-Runner "add deps to image when used by >1 project" rule keeps it per-job. + integration: + runs-on: python-ci + container: + image: git.fabledsword.com/bvandeusen/ci-python:3.14 + env: + DB_USER: fabledcurator + DB_PASSWORD: ci_integration + DB_PORT: "5432" + DB_NAME: fabledcurator_test + SECRET_KEY: ci_integration_placeholder + services: + postgres: + image: pgvector/pgvector:pg16 + env: + POSTGRES_USER: fabledcurator + POSTGRES_PASSWORD: ci_integration + POSTGRES_DB: fabledcurator_test + options: >- + --health-cmd "pg_isready -U fabledcurator" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@v4 + - name: Integration suite (resolve service IPs, migrate, test) + run: | + set -eux + echo "=== container landscape (diagnostic for filter scoping) ===" + docker ps -a --format '{{.ID}} {{.Image}} -> {{.Names}}' + echo "=== end landscape ===" + PG=$(docker ps --filter "name=integration" --filter "ancestor=pgvector/pgvector:pg16" -q | head -n1) + RD=$(docker ps --filter "name=integration" --filter "ancestor=redis:7-alpine" -q | head -n1) + test -n "$PG" && test -n "$RD" + PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG") + RD_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$RD") + test -n "$PG_IP" && test -n "$RD_IP" + export DB_HOST="$PG_IP" + export CELERY_BROKER_URL="redis://$RD_IP:6379/0" + export CELERY_RESULT_BACKEND="redis://$RD_IP:6379/0" + # These steps run under `sh -e`, not bash, so bash's /dev/tcp magic + # path does not exist here — the probe this loop used to run could + # never succeed and simply burned the full 120s on every run, green + # or red, then continued without having established anything. Python + # is in the image and needs no installed package for a socket + # connect, so it is the probe. Exhausting the budget is now a named + # failure rather than a silent fall-through (rule 156): if Postgres + # is genuinely not up, that is what the log should say, instead of + # whatever the first query happens to raise two minutes later. + pg_ready="" + for i in $(seq 1 60); do + if python -c "import socket,sys; s=socket.socket(); s.settimeout(2); sys.exit(0 if s.connect_ex(('$PG_IP', 5432)) == 0 else 1)"; then + pg_ready=1 + break + fi + sleep 2 + done + if [ -z "$pg_ready" ]; then + echo "postgres at $PG_IP:5432 did not accept a connection within 120s" + exit 1 + fi + if command -v uv >/dev/null 2>&1; then + uv pip install --system -r requirements.txt pytest pytest-asyncio + else + pip install -r requirements.txt pytest pytest-asyncio + fi + # Relax durability on the throwaway CI Postgres so the per-test + # TRUNCATE's commit-fsync — the integration teardown's dominant cost + # (~1.5-2s/test, which collapsed the suite from ~13min to ~45s) — is + # skipped. fsync/full_page_writes are sighup GUCs and synchronous_commit + # is user-context, so ALTER SYSTEM + pg_reload_conf() applies them with + # NO restart. Ephemeral DB ⇒ fsync-off is safe. Non-fatal so a perms + # surprise can't red the job; fabledcurator is the postgres image's + # bootstrap superuser. + python -c "import os,psycopg; c=psycopg.connect(host=os.environ['DB_HOST'],port=5432,user=os.environ['DB_USER'],password=os.environ['DB_PASSWORD'],dbname=os.environ['DB_NAME'],autocommit=True); [c.execute(q) for q in ('ALTER SYSTEM SET fsync=off','ALTER SYSTEM SET synchronous_commit=off','ALTER SYSTEM SET full_page_writes=off','SELECT pg_reload_conf()')]; c.close()" || echo 'WARN: durability GUC relax failed (continuing)' + alembic upgrade head + pytest tests/ -v -m integration --durations=15 + # Sign-or-fetch-from-cache: signs the extension via AMO if no ext- # Forgejo release exists yet, otherwise downloads the cached signed XPI. # Result is uploaded as an Actions artifact for build-web to consume. @@ -191,6 +496,23 @@ jobs: # everything. A condition that is always true reads as if some path avoids # it, which is worse than no condition. sign-extension: + # THE GATE (2026-09-23). Every lane above must have PASSED before this job + # exists at all — so a red suite does not produce an image, let alone push + # one. Nothing here is a new mechanism: it is the same `needs:` edge that + # has gated `promote` since milestone 362 step 4, and it carries that + # step's hardest-won property unchanged — **not running is not the same as + # passing.** `needs` treats a SKIPPED dependency as unsatisfied, so a lane + # that silently skips itself blocks the publish exactly as a failing one + # does. Run 5290 is why that is worth saying out loud: `smoke-web` skipped + # itself through a job-level `if:` that could not read `env`, and a design + # where only a FAILED gate blocks would have published unverified images + # while reporting success. + needs: [lint, extension-version, backend-lint-and-test, frontend-build, integration] + # A pull_request run is the lanes and nothing else. This is the ONLY thing + # separating "validate a Renovate bump" from "publish a Renovate bump", so + # it is stated on each publishing job rather than inferred from a `needs` + # chain that a later edit could quietly break. + if: github.event_name != 'pull_request' runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 @@ -243,7 +565,7 @@ jobs: # Unpadded, and only here: AMO's grammar rejects a leading zero, so the # extension renders rule 148's numbers without the family's padding # (milestone 318 step 8). Same value, one character narrower per segment; - # ci.yml's extension-version lane checks the string against Mozilla's + # the extension-version lane above checks the string against Mozilla's # published regex before this job ever calls AMO. # # The committed "version" in manifest.json / package.json decides NOTHING @@ -391,7 +713,8 @@ jobs: # web-ext signs whatever manifest.json says, so the derived value has to # reach the tree before signing. package.json is written too: the two are - # required to agree (ci.yml's guard), and a local `npm run build` reads + # required to agree (the extension-version lane's guard), and a local + # `npm run build` reads # it. Working tree only — never committed, per the note on the derive # step. - name: Stamp the derived version into manifest.json + package.json @@ -518,7 +841,15 @@ jobs: # FAILED one. With no tag trigger, sign-extension always runs, so the # default behaviour is exactly what we want: a failed sign skips build-web # rather than shipping an image without its XPI. - needs: [sign-extension] + needs: [sign-extension, lint, extension-version, backend-lint-and-test, + frontend-build, integration] + # The lanes in that list are THE GATE (2026-09-23) — see sign-extension's + # copy of this comment for why, including the property that a SKIPPED lane + # blocks as firmly as a failing one. They are repeated here rather than + # inherited through `sign-extension`: this job is what pushes the channel + # tag, and the one place the gate must be legible is the place that + # publishes. + if: github.event_name != 'pull_request' runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 @@ -771,7 +1102,7 @@ jobs: # refresh rebuilds against freshly resolved base images, and the web # image's runtime is a line of UNPINNED Debian packages (ffmpeg, # libjpeg62-turbo, libpq5, megatools…) re-resolved on every build. - # Nothing in ci.yml can see that: its lanes run on ci-python:3.14 and + # No verification LANE can see that: they run on ci-python:3.14 and # install requirements.txt, and a base bump changes neither. So # refreshed bytes have to be proven before :latest names them, and # proving needs a moment between "built" and "published" to occupy. @@ -1153,9 +1484,9 @@ jobs: # Does the image a refresh just built still work? # - # This is the gate the base refresh never had. `ci.yml` cannot be it: its - # lanes run on ci-python:3.14 and install requirements.txt, and a base bump - # changes neither — all five stay green through a refresh that breaks the + # This is the gate the base refresh never had. The five verification lanes + # cannot be it: they run on ci-python:3.14 and install requirements.txt, and + # a base bump changes neither — all five stay green through a refresh that breaks the # product. What a refresh re-resolves is the Dockerfile's apt layer (ffmpeg, # unar, libpq5, postgresql-client, zstd, megatools, libjpeg62-turbo, # libwebp7, libpng16-16), unpinned, every build. @@ -1165,7 +1496,8 @@ jobs: # pass while a codec removal broke every thumbnail in the library. # # Refresh-only. On a push the bytes came from a commit, and a commit is what - # ci.yml already tests. + # the lanes above already test — and since 2026-09-23 they gate the build, so + # those bytes could not exist without them having passed. # # Reports a verdict; it does not yet gate the promote (milestone 362 step 4). # Landing the gate and the thing it gates in one change would mean the first @@ -1210,7 +1542,7 @@ jobs: # # The cost is one pull and boot on a reuse-hit push, re-smoking bytes # that were smoked when they were built. That is the price of a harness - # that tests itself, and it overlaps ci.yml's lanes, so little wall-clock + # that tests itself, and it overlaps the lanes above, so little wall-clock # moves. Not running is not the same as passing. runs-on: python-ci container: @@ -1257,7 +1589,7 @@ jobs: IS_CANDIDATE: ${{ needs.build-web.outputs.candidate }} run: | set -eux - # Service discovery mirrors ci.yml's integration lane: these jobs run + # Service discovery mirrors the integration lane above: these jobs run # in a container against a mounted docker socket, so the services are # SIBLINGS reachable by IP, not by hostname. PG=$(docker ps --filter "name=smoke" --filter "ancestor=pgvector/pgvector:pg16" -q | head -n1) @@ -1269,7 +1601,7 @@ jobs: # Socket probe in python, not bash's /dev/tcp — these steps run under # `sh -e`, where that path does not exist. Same fix and reasoning as - # ci.yml's integration job; see the comment there. + # the integration lane above; see the comment there. pg_ready="" for i in $(seq 1 60); do if python -c "import socket,sys; s=socket.socket(); s.settimeout(2); sys.exit(0 if s.connect_ex(('$PG_IP', 5432)) == 0 else 1)"; then @@ -1689,6 +2021,23 @@ jobs: fi echo "promote: both channel tags moved" build-agent: + # THE GATE (2026-09-23). Every lane above must have PASSED before this job + # exists at all — so a red suite does not produce an image, let alone push + # one. Nothing here is a new mechanism: it is the same `needs:` edge that + # has gated `promote` since milestone 362 step 4, and it carries that + # step's hardest-won property unchanged — **not running is not the same as + # passing.** `needs` treats a SKIPPED dependency as unsatisfied, so a lane + # that silently skips itself blocks the publish exactly as a failing one + # does. Run 5290 is why that is worth saying out loud: `smoke-web` skipped + # itself through a job-level `if:` that could not read `env`, and a design + # where only a FAILED gate blocks would have published unverified images + # while reporting success. + needs: [lint, extension-version, backend-lint-and-test, frontend-build, integration] + # A pull_request run is the lanes and nothing else. This is the ONLY thing + # separating "validate a Renovate bump" from "publish a Renovate bump", so + # it is stated on each publishing job rather than inferred from a `needs` + # chain that a later edit could quietly break. + if: github.event_name != 'pull_request' runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 @@ -1876,7 +2225,7 @@ jobs: # refresh rebuilds against freshly resolved base images, and the web # image's runtime is a line of UNPINNED Debian packages (ffmpeg, # libjpeg62-turbo, libpq5, megatools…) re-resolved on every build. - # Nothing in ci.yml can see that: its lanes run on ci-python:3.14 and + # No verification LANE can see that: they run on ci-python:3.14 and # install requirements.txt, and a base bump changes neither. So # refreshed bytes have to be proven before :latest names them, and # proving needs a moment between "built" and "published" to occupy. diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index fb71947..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,294 +0,0 @@ -name: CI - -# CI lanes per FabledRulebook/forgejo.md "CI philosophy": -# - lint: ruff only, no dep install — fast-fail for the common lint bounce. -# - extension-version: the derived version resolves and is a shape AMO takes. -# - backend-lint-and-test: `pytest -m "not integration"`, no service containers. -# - frontend-build: vitest unit + vite build. -# - integration: pgvector + redis service containers; alembic + `pytest -m integration`. - -on: - push: - branches: [dev, main] - # Renovate opens PRs from `renovate/*` branches into `dev`. Those branches - # never push to dev/main, so the push trigger above gives them NO pre-merge - # CI — a bump could only be validated after it was already merged. This - # pull_request trigger (base `dev` only) validates Renovate PRs before merge. - # It deliberately does NOT fire on dev→main PRs (base `main`), which still - # rely on the dev push run — so no duplicate runs. FC has no fork PRs - # (single-operator Forgejo repo), so secrets-on-PR is not a concern. - pull_request: - branches: [dev] - -jobs: - # Fast-fail lint lane. ruff is pre-installed in the ci-python image, so - # this runs with NO dependency install and surfaces the most common bounce - # class (lint: I001 / UP037 / ASYNC109 / W293 …) in seconds — instead of - # after the backend job's ~30-60s wheel install. ruff is static analysis, - # so no DB/secret env is needed. - lint: - runs-on: python-ci - container: - image: git.fabledsword.com/bvandeusen/ci-python:3.14 - steps: - - uses: actions/checkout@v4 - - name: Ruff lint - # agent/ included so the GPU-agent is linted before its image is built - # (build.yml only `docker build`s it — this is where it gets checked). - # scripts/ likewise: release_notes.py runs only on a tag push, so a - # syntax or import error there would otherwise surface at the one - # moment nobody wants to debug a workflow. - run: ruff check backend/ tests/ alembic/ agent/ scripts/ - - name: Agent syntax check - # The agent's runtime deps (torch/transformers/ultralytics) aren't in the - # CI image, so we can't import it — but compileall parses every module, - # catching syntax errors before the image build. - run: python -m compileall -q agent/fc_agent - - # The extension version is DERIVED, not hand-maintained (milestone 271 step - # 4): build.yml computes it from the commit TIME of the newest packaged - # extension change and stamps it into manifest.json / package.json at build - # time. The guard that used to live here — "packaged files changed but nobody - # bumped the version" — was therefore checking a fact that had stopped - # existing. Worse than useless: it would have failed this lane on every real - # extension change, demanding a bump that decides nothing. Retired 2026-08-27 - # rather than left running beside the new mechanism (rule 22). - # - # Two things are still worth asserting, and this is the only lane that can: - # the extension.yml suite runs on node:24-slim, which is exactly why - # version.spec.js sticks to packaging.sh's git-free subcommands. - # 1. the derivation actually resolves on this commit - # 2. the derived string is one AMO will accept, checked against Mozilla's - # own published grammar rather than a loose "digits and dots" - # - # The MAJOR.MINOR-agreement check that used to be (2) is gone with milestone - # 318 step 8: the committed version no longer seeds anything, so there is no - # hand-set part left for the two files to disagree about. - # - # Deliberately NOT checked here: that the derived value beats what has already - # been signed. That guard belongs in build.yml, where it compares against the - # real ext-* releases. Comparing against origin/main here would be wrong — - # dev legitimately derives a LOWER value whenever main is ahead on the - # extension, and a lane that fails for being behind is a lane people learn to - # ignore. - extension-version: - runs-on: python-ci - container: - image: git.fabledsword.com/bvandeusen/ci-python:3.14 - steps: - - uses: actions/checkout@v4 - with: - # The derivation needs real history: a depth-1 clone sees one commit - # and produces a wrong, too-low value RATHER THAN FAILING. Checking - # that here is half the point of the lane. - fetch-depth: 0 - - name: Extension version derives cleanly - run: | - set -eu - # busybox sh on the act_runner — no bashisms (family rule). - VERSION=$(sh extension/scripts/packaging.sh version) - echo "derived: $VERSION" - - # Mozilla's published grammar for AMO, transcribed verbatim from - # MDN's manifest.json/version page: - # - # ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$ - # - # Not the looser `^[0-9]+(\.[0-9]+)*$` this lane used to carry. That - # one passes `2026.08.29.0201`, which AMO REJECTS — a segment must be - # the single digit 0 or start 1-9 — and it also passes five segments, - # where AMO allows four. Both would surface as a failed sign with the - # version already burned: AMO 409s on re-signing, so a rejected value - # cannot be reclaimed and cannot be reused. This lane is the cheap - # place to find out. (#3138, milestone 318 step 8.) - if ! echo "$VERSION" | grep -qE '^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$'; then - echo "ERROR: derived version '$VERSION' is not a version AMO accepts." - echo "AMO's grammar: ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$" - echo "Most likely cause: a zero-padded segment (08, 0201). The rest" - echo "of the family pads; the extension must not — see packaging.sh." - exit 1 - fi - - # ...and the shape this project actually derives. AMO would happily - # take `1.0.3500147` too, so the grammar check alone would not notice - # a regression to the pre-318 shape — which orders BELOW everything - # signed since, and is unrecoverable once Firefox has the higher one. - if ! echo "$VERSION" | grep -qE '^20[0-9][0-9]\.[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,4}$'; then - echo "ERROR: derived version '$VERSION' is not YYYY.M.D.HHMM." - echo "Rule 148's CalVer is what build.yml signs; the old" - echo "1.0. shape would order below every ext-2026.* release." - exit 1 - fi - echo "OK: derived version $VERSION" - - backend-lint-and-test: - runs-on: python-ci - container: - image: git.fabledsword.com/bvandeusen/ci-python:3.14 - env: - # DB_PASSWORD and SECRET_KEY are required by config.py at import time - # even though unit tests don't actually touch the DB or use the secret. - DB_PASSWORD: ci_unit_test_placeholder - SECRET_KEY: ci_unit_test_placeholder - steps: - - uses: actions/checkout@v4 - with: - # Full history for tests/test_artifact_identity.py, which derives - # each artifact's revision to check the identity scheme. On a - # depth-1 clone that derivation either fails or returns the tip sha - # — so the lane would go green while asserting nothing, which is - # the one outcome worse than a red one. - fetch-depth: 0 - - # Cache step removed 2026-05-26: act_runner's cache backend has been - # broken on this homelab runner since 2026-05-15 (first as request- - # timeout warnings, then as hard "Cannot find module .../dist/restore/ - # index.js" failures that tank the whole job). The cache step targeted - # ~/.cache/pip but the install below uses `uv pip install` primarily, - # whose own cache lives at ~/.cache/uv — so the cache step's real - # benefit was marginal even when working. Cost of removal: ~30s of - # wheel downloads per job. Future re-enable: mount ~/.cache/uv as a - # docker volume at the runner level (skips actions/cache entirely), - # or fix the runner-side cache backend (clear /var/run/act/actions/*, - # pin act_runner version, etc.). - - - name: Install Python deps - # ruff is pre-installed in the ci-python image (see CI-Runner/CI-python/ - # Dockerfile's RUFF_VERSION). Per FabledRulebook ci-runners.md, toolchain - # versions live on the runner image, not here. - # uv: 5-10x faster wheel resolve than pip for cold caches. - # Falls back to pip install on uv-missing runners (older images). - run: | - if command -v uv >/dev/null 2>&1; then - uv pip install --system -r requirements.txt pytest pytest-asyncio - else - pip install -r requirements.txt pytest pytest-asyncio - fi - - # Ruff moved to the dedicated fast `lint` job above (fails in seconds, - # no dep install). This job is now unit tests only. - - name: Pytest (unit only — integration runs in the integration job) - run: pytest tests/ -v -m "not integration" - - frontend-build: - runs-on: python-ci - container: - image: git.fabledsword.com/bvandeusen/ci-python:3.14 - defaults: - run: - working-directory: frontend - steps: - - uses: actions/checkout@v4 - # No package-lock.json is tracked yet (we don't run npm locally per - # feedback-no-local-runs). Using `npm install` instead of `npm ci`. - # If we want strict lockfile-based reproducibility later, commit a - # package-lock.json and flip this back to `npm ci`. - - run: npm install --no-audit --no-fund - # No type-check step: the frontend is pure JS (no .ts files, no JSDoc), - # so a type-checker has nothing to do. The vue-tsc devDep + its `check` - # script were dropped 2026-07-11 rather than bumped to v3. If we add - # TS/JSDoc later, re-add a tsconfig.json + vue-tsc + a type-check step. - - run: npm run test:unit - - run: npm run build - - # Single integration job — collapsed from a 3-way shard split on 2026-06-04. - # The shards existed to parallelize ~8.5min of integration tests; once the - # throwaway Postgres runs with fsync OFF (the durability step below) the whole - # suite runs in ~45s, so the split only triplicated the ~2min fixed overhead - # (container + `uv pip install` + `alembic upgrade head`) and burned 3 of 6 - # runner slots for no wall-clock gain. One job now: spin up once, install - # once, migrate once, run every integration test. - # - # The docker-ps filter scopes to THIS job's own Postgres/Redis service - # containers by job name. act_runner strips underscores from job names when - # labelling containers (`int_api` matched nothing on 2026-05-25), so the name - # stays separator-free (`integration`). The step prints `docker ps -a` first - # so a future naming-convention shift surfaces in the log without a - # guess-and-push cycle. - # - # Pre-baking requirements.txt into ci-python:3.14 is intentionally NOT done — - # per ci-requirements.md, FC is the only Python consumer of that image and the - # CI-Runner "add deps to image when used by >1 project" rule keeps it per-job. - integration: - runs-on: python-ci - container: - image: git.fabledsword.com/bvandeusen/ci-python:3.14 - env: - DB_USER: fabledcurator - DB_PASSWORD: ci_integration - DB_PORT: "5432" - DB_NAME: fabledcurator_test - SECRET_KEY: ci_integration_placeholder - services: - postgres: - image: pgvector/pgvector:pg16 - env: - POSTGRES_USER: fabledcurator - POSTGRES_PASSWORD: ci_integration - POSTGRES_DB: fabledcurator_test - options: >- - --health-cmd "pg_isready -U fabledcurator" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - steps: - - uses: actions/checkout@v4 - - name: Integration suite (resolve service IPs, migrate, test) - run: | - set -eux - echo "=== container landscape (diagnostic for filter scoping) ===" - docker ps -a --format '{{.ID}} {{.Image}} -> {{.Names}}' - echo "=== end landscape ===" - PG=$(docker ps --filter "name=integration" --filter "ancestor=pgvector/pgvector:pg16" -q | head -n1) - RD=$(docker ps --filter "name=integration" --filter "ancestor=redis:7-alpine" -q | head -n1) - test -n "$PG" && test -n "$RD" - PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG") - RD_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$RD") - test -n "$PG_IP" && test -n "$RD_IP" - export DB_HOST="$PG_IP" - export CELERY_BROKER_URL="redis://$RD_IP:6379/0" - export CELERY_RESULT_BACKEND="redis://$RD_IP:6379/0" - # These steps run under `sh -e`, not bash, so bash's /dev/tcp magic - # path does not exist here — the probe this loop used to run could - # never succeed and simply burned the full 120s on every run, green - # or red, then continued without having established anything. Python - # is in the image and needs no installed package for a socket - # connect, so it is the probe. Exhausting the budget is now a named - # failure rather than a silent fall-through (rule 156): if Postgres - # is genuinely not up, that is what the log should say, instead of - # whatever the first query happens to raise two minutes later. - pg_ready="" - for i in $(seq 1 60); do - if python -c "import socket,sys; s=socket.socket(); s.settimeout(2); sys.exit(0 if s.connect_ex(('$PG_IP', 5432)) == 0 else 1)"; then - pg_ready=1 - break - fi - sleep 2 - done - if [ -z "$pg_ready" ]; then - echo "postgres at $PG_IP:5432 did not accept a connection within 120s" - exit 1 - fi - if command -v uv >/dev/null 2>&1; then - uv pip install --system -r requirements.txt pytest pytest-asyncio - else - pip install -r requirements.txt pytest pytest-asyncio - fi - # Relax durability on the throwaway CI Postgres so the per-test - # TRUNCATE's commit-fsync — the integration teardown's dominant cost - # (~1.5-2s/test, which collapsed the suite from ~13min to ~45s) — is - # skipped. fsync/full_page_writes are sighup GUCs and synchronous_commit - # is user-context, so ALTER SYSTEM + pg_reload_conf() applies them with - # NO restart. Ephemeral DB ⇒ fsync-off is safe. Non-fatal so a perms - # surprise can't red the job; fabledcurator is the postgres image's - # bootstrap superuser. - python -c "import os,psycopg; c=psycopg.connect(host=os.environ['DB_HOST'],port=5432,user=os.environ['DB_USER'],password=os.environ['DB_PASSWORD'],dbname=os.environ['DB_NAME'],autocommit=True); [c.execute(q) for q in ('ALTER SYSTEM SET fsync=off','ALTER SYSTEM SET synchronous_commit=off','ALTER SYSTEM SET full_page_writes=off','SELECT pg_reload_conf()')]; c.close()" || echo 'WARN: durability GUC relax failed (continuing)' - alembic upgrade head - pytest tests/ -v -m integration --durations=15 diff --git a/.forgejo/workflows/extension.yml b/.forgejo/workflows/extension.yml index f29cad9..b2e9876 100644 --- a/.forgejo/workflows/extension.yml +++ b/.forgejo/workflows/extension.yml @@ -1,5 +1,6 @@ name: extension -# Lint + unit tests. The sign-and-publish dance moved into build.yml's +# Lint + unit tests. Deliberately NOT a publishing lane, which is why it is +# not part of build.yml's gate. The sign-and-publish dance moved into build.yml's # `sign-extension` job (2026-05-25) — `:latest` now always bundles the XPI # because sign-extension runs as a build-web dependency in the SAME workflow, # eliminating the prior race between build.yml and a separate extension.yml. @@ -10,19 +11,17 @@ on: paths: - 'extension/**' - '.forgejo/workflows/extension.yml' - # test/version.spec.js asserts things ABOUT the other two workflows — - # that neither inlines the packaged-file set, and that build.yml derives + # test/version.spec.js asserts things ABOUT build.yml — that it does not + # inline the packaged-file set, and that build.yml derives # the shipped version rather than reading it out of the repo. A # workflow-only edit can therefore break this suite, so it has to trigger # it. build.yml joined the list at milestone 271 step 5, when the spec # started asserting against it. - - '.forgejo/workflows/ci.yml' - '.forgejo/workflows/build.yml' pull_request: branches: [main] paths: - 'extension/**' - - '.forgejo/workflows/ci.yml' - '.forgejo/workflows/build.yml' workflow_dispatch: diff --git a/README.md b/README.md index a3385f5..4699a1a 100644 --- a/README.md +++ b/README.md @@ -271,10 +271,20 @@ Four deployable pieces, built by `.forgejo/workflows/build.yml`: ## CI / Forgejo setup -Four workflows: `ci.yml` (lint, extension-version check, backend unit tests, -frontend build, integration), `extension.yml` (extension lint, vitest, XPI -content verification), `build.yml` (sign + publish), and `release.yml`, which -runs only on a `v*` tag and publishes a changelog without building anything. +Three workflows: `build.yml` (the five verification lanes — lint, +extension-version check, backend unit tests, frontend build, integration — and +then sign + publish), `extension.yml` (extension lint, vitest, XPI content +verification), and `release.yml`, which runs only on a `v*` tag and publishes a +changelog without building anything. + +**The lanes and the publish are one workflow on purpose.** They were two +(`ci.yml` and `build.yml`) until 2026-09-23, on the same push trigger, which +meant the build could not see the tests' verdict and published whatever it +built — a red unit lane and a fresh `:dev` image, in the same minute. A +`needs:` edge only exists inside one workflow graph, so the two are one graph +and the gate is that edge: a lane that fails, **or that merely skips**, leaves +the publishing jobs unrun. Pull-request runs (Renovate bumps into `dev`) are +the lanes and nothing else. **The toolchain each job runs in is its `container.image`, not its `runs-on` label.** `runs-on: python-ci` only schedules the job onto a runner; every job diff --git a/backend/app/scripts/wait_for_deps.py b/backend/app/scripts/wait_for_deps.py index a880e28..bf38195 100644 --- a/backend/app/scripts/wait_for_deps.py +++ b/backend/app/scripts/wait_for_deps.py @@ -26,7 +26,7 @@ be a check (with retries) at the start of the container."* Yes. ## A TCP connect, not a query -The same probe `ci.yml`'s integration lane and the build smoke already use. +The same probe `build.yml`'s integration lane and the build smoke already use. It answers the question that is actually being asked — is something listening — and it cannot fail for a reason that retrying will never fix. diff --git a/ci-requirements.md b/ci-requirements.md index 54d9ef9..90c8b7b 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -46,8 +46,8 @@ per `docs/process.md`'s "add deps to the image when used by >1 project". - Integration uses Fabled-Git Actions `services:` + socket-discovered bridge IPs because `act_runner` (swarm-runner v0.6+) puts services on the default bridge with no embedded DNS. The pattern is documented in the rulebook's - `fabled-git.md` "CI philosophy" section and FC's `ci.yml` is the canonical - example. + `fabled-git.md` "CI philosophy" section and FC's `build.yml` integration lane + is the canonical example. - No `package-lock.json` is tracked yet (FC's `feedback_no_local_runs` memory bans `npm install` locally). Using `npm install` rather than `npm ci` until a lockfile lands. @@ -83,7 +83,7 @@ per `docs/process.md`'s "add deps to the image when used by >1 project". digit `0` or starts 1-9, and there are at most four. `2026.08.29.0201` is rejected; `2026.8.29.201` is the same value one character narrower per segment, and rule 148 defines comparison as numeric per segment, so nothing is - reordered. `ci.yml`'s `extension-version` lane asserts the derived string + reordered. `build.yml`'s `extension-version` lane asserts the derived string against that exact regex, plus a `YYYY.M.D.HHMM` shape check that would catch a regression to the pre-318 `1.0.` — which AMO would accept and which orders below everything already signed. Checking here is the whole point: AMO @@ -91,7 +91,8 @@ per `docs/process.md`'s "add deps to the image when used by >1 project". `scripts/artifacts.sh version extension` **delegates** to `packaging.sh` so the two cannot answer differently. - Every job that derives anything checks out with `fetch-depth: 0` — all four - `build.yml` jobs, `ci.yml`'s `extension-version` and `backend-lint-and-test` + publishing `build.yml` jobs, its `extension-version` and `backend-lint-and-test` + lanes (for `tests/test_artifact_paths.py` and `test_artifact_identity.py`), and `release.yml`, which additionally walks the tag graph. A depth-1 clone sees one commit and derives a wrong, too-low value **rather than failing**, so the diff --git a/extension/README.md b/extension/README.md index e4cb3e4..867c6a1 100644 --- a/extension/README.md +++ b/extension/README.md @@ -68,7 +68,7 @@ rejected, and at most four segments are allowed. The extension therefore emits **the same numbers unpadded**: `2026.8.29.201` where the rest of the family says `2026.08.29.0201`. Rule 148 already defines comparison as numeric per segment, under which the two are equal, so nothing is reordered by the choice -and left-padding each segment recovers the family string exactly. `ci.yml`'s +and left-padding each segment recovers the family string exactly. `build.yml`'s `extension-version` lane checks the derived string against that regex on every push — the cheap place to find out, because AMO 409s on re-signing and a rejected version is burned for good. diff --git a/extension/package.json b/extension/package.json index 638582e..98db8ea 100644 --- a/extension/package.json +++ b/extension/package.json @@ -3,7 +3,7 @@ "version": "1.0.11", "private": true, "description": "Firefox extension for FabledCurator", - "comment_ignore_files": "The --ignore-files list comes from scripts/packaging.sh, the single source of truth shared with ci.yml's guard and the derived-version patch count. `set -f` is REQUIRED before the substitution: without it the shell globs `test/**` against the working tree and silently narrows the pattern to whatever files happen to exist.", + "comment_ignore_files": "The --ignore-files list comes from scripts/packaging.sh, the single source of truth shared with build.yml's guard and the derived-version patch count. `set -f` is REQUIRED before the substitution: without it the shell globs `test/**` against the working tree and silently narrows the pattern to whatever files happen to exist.", "scripts": { "lint": "set -f; web-ext lint --source-dir=. --no-config-discovery --ignore-files $(sh scripts/packaging.sh ignore)", "start": "set -f; web-ext run --source-dir=. --no-config-discovery --ignore-files $(sh scripts/packaging.sh ignore) --firefox=firefox", diff --git a/extension/test/version.spec.js b/extension/test/version.spec.js index b84a7e1..a8f945e 100644 --- a/extension/test/version.spec.js +++ b/extension/test/version.spec.js @@ -10,7 +10,7 @@ const readText = (...seg) => readFileSync(path.join(EXT_DIR, ...seg), 'utf8') // Only the git-free subcommands are exercised here: `version` shells out to // git, and the extension lane runs on node:24-bookworm-slim which may not ship -// it. That one is covered where git is guaranteed — ci.yml's extension-version +// it. That one is covered where git is guaranteed — build.yml's extension-version // lane and build.yml both run on ci-python. const packaging = (cmd) => execFileSync('sh', [path.join(EXT_DIR, 'scripts', 'packaging.sh'), cmd], { @@ -39,7 +39,7 @@ describe('packaging.sh — the single definition of what ships', () => { it('emits glob patterns literally, never expanded against the working tree', () => { // The script iterates its lists with deliberate word-splitting, so it must // run with pathname expansion off. Without that, invoking it from a cwd - // where test/ exists (exactly how ci.yml and vitest call it) expands + // where test/ exists (exactly how build.yml and vitest call it) expands // `test/**` into the individual spec files, and the pathspec silently stops // covering anything added later. const pathspec = packaging('pathspec') @@ -116,7 +116,7 @@ describe('consumers delegate rather than keeping their own copy', () => { ) it('no workflow hardcodes the packaged-file set', () => { - // ci.yml used to substitute `packaging.sh pathspec` directly, for the + // build.yml used to substitute `packaging.sh pathspec` directly, for the // manual-bump guard that milestone 271 step 5 retired. Nothing inlines the // set today, and nothing should start to: a literal :(exclude)extension/... // in a workflow means someone bypassed the shared definition, which is @@ -157,7 +157,7 @@ describe('extension version', () => { // // It is still asserted, for one reason: `npm run build` locally packages // whatever is committed, so a value AMO would reject turns a local build - // into a confusing failure with no CI signal ahead of it. ci.yml checks + // into a confusing failure with no CI signal ahead of it. build.yml checks // the same grammar against the DERIVED value, which is the one AMO sees. for (const file of ['manifest.json', 'package.json']) { expect(read(file).version, `${file} version is not AMO-shaped`).toMatch(AMO) diff --git a/scripts/smoke_image.py b/scripts/smoke_image.py index 59a1a5e..c84ec50 100644 --- a/scripts/smoke_image.py +++ b/scripts/smoke_image.py @@ -3,7 +3,7 @@ Run INSIDE the image, not against the source tree. That distinction is the entire reason this file exists. -`ci.yml`'s lanes run on `ci-python:3.14` and install `requirements.txt`. A base +`build.yml`'s lanes run on `ci-python:3.14` and install `requirements.txt`. A base refresh changes neither, so all five lanes stay green through a base bump that breaks the product. What a refresh actually re-resolves is this, from the Dockerfile: diff --git a/tests/conftest.py b/tests/conftest.py index be692d5..1877e1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ The async db fixture provides an AsyncSession bound to a transaction that gets rolled back after each test. CI provisions a real Postgres + pgvector -(see .forgejo/workflows/ci.yml), so tests exercise the actual schema and +(see .forgejo/workflows/build.yml), so tests exercise the actual schema and migration code paths. """