ci: the tests gate the publish — ci.yml folds into build.yml
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
extension / lint (push) Successful in 21s
CI and images / frontend-build (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 31s
CI and images / integration (push) Successful in 2m10s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 1m51s
CI and images / smoke-web (push) Successful in 57s
CI and images / promote (push) Skipped
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
extension / lint (push) Successful in 21s
CI and images / frontend-build (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 31s
CI and images / integration (push) Successful in 2m10s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 1m51s
CI and images / smoke-web (push) Successful in 57s
CI and images / promote (push) Skipped
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-<sha>`), the weekly base refresh, and the `ext-<version>` 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -79,7 +79,7 @@ jobs:
|
|||||||
- name: Resolve the Postgres service and install deps
|
- name: Resolve the Postgres service and install deps
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
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.
|
# 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)
|
PG=$(docker ps --filter "name=compare" --filter "ancestor=pgvector/pgvector:pg16" -q | head -n1)
|
||||||
test -n "$PG"
|
test -n "$PG"
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
echo "DB_HOST=$PG_IP" >> "$GITHUB_ENV"
|
echo "DB_HOST=$PG_IP" >> "$GITHUB_ENV"
|
||||||
# Socket probe in python, not bash's /dev/tcp — these steps run under
|
# 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
|
# `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=""
|
pg_ready=""
|
||||||
for i in $(seq 1 60); do
|
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
|
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
|
||||||
|
|||||||
+362
-13
@@ -1,4 +1,4 @@
|
|||||||
name: Build images
|
name: CI and images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -17,6 +17,18 @@ on:
|
|||||||
# under a second name, and the stack that needed the second name is being
|
# under a second name, and the stack that needed the second name is being
|
||||||
# collapsed onto the consolidated image.)
|
# collapsed onto the consolidated image.)
|
||||||
branches: [main, dev]
|
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`
|
# 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
|
# 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.
|
# The injected GITHUB_TOKEN cannot be used — it lacks write:package.
|
||||||
|
|
||||||
jobs:
|
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.<minutes> 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-<version>
|
# Sign-or-fetch-from-cache: signs the extension via AMO if no ext-<version>
|
||||||
# Forgejo release exists yet, otherwise downloads the cached signed XPI.
|
# Forgejo release exists yet, otherwise downloads the cached signed XPI.
|
||||||
# Result is uploaded as an Actions artifact for build-web to consume.
|
# 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
|
# everything. A condition that is always true reads as if some path avoids
|
||||||
# it, which is worse than no condition.
|
# it, which is worse than no condition.
|
||||||
sign-extension:
|
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
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
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
|
# Unpadded, and only here: AMO's grammar rejects a leading zero, so the
|
||||||
# extension renders rule 148's numbers without the family's padding
|
# extension renders rule 148's numbers without the family's padding
|
||||||
# (milestone 318 step 8). Same value, one character narrower per segment;
|
# (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.
|
# published regex before this job ever calls AMO.
|
||||||
#
|
#
|
||||||
# The committed "version" in manifest.json / package.json decides NOTHING
|
# 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
|
# 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
|
# 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
|
# it. Working tree only — never committed, per the note on the derive
|
||||||
# step.
|
# step.
|
||||||
- name: Stamp the derived version into manifest.json + package.json
|
- 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
|
# 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
|
# default behaviour is exactly what we want: a failed sign skips build-web
|
||||||
# rather than shipping an image without its XPI.
|
# 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
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
@@ -771,7 +1102,7 @@ jobs:
|
|||||||
# refresh rebuilds against freshly resolved base images, and the web
|
# refresh rebuilds against freshly resolved base images, and the web
|
||||||
# image's runtime is a line of UNPINNED Debian packages (ffmpeg,
|
# image's runtime is a line of UNPINNED Debian packages (ffmpeg,
|
||||||
# libjpeg62-turbo, libpq5, megatools…) re-resolved on every build.
|
# 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
|
# install requirements.txt, and a base bump changes neither. So
|
||||||
# refreshed bytes have to be proven before :latest names them, and
|
# refreshed bytes have to be proven before :latest names them, and
|
||||||
# proving needs a moment between "built" and "published" to occupy.
|
# proving needs a moment between "built" and "published" to occupy.
|
||||||
@@ -1153,9 +1484,9 @@ jobs:
|
|||||||
|
|
||||||
# Does the image a refresh just built still work?
|
# Does the image a refresh just built still work?
|
||||||
#
|
#
|
||||||
# This is the gate the base refresh never had. `ci.yml` cannot be it: its
|
# This is the gate the base refresh never had. The five verification lanes
|
||||||
# lanes run on ci-python:3.14 and install requirements.txt, and a base bump
|
# cannot be it: they run on ci-python:3.14 and install requirements.txt, and
|
||||||
# changes neither — all five stay green through a refresh that breaks the
|
# 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,
|
# product. What a refresh re-resolves is the Dockerfile's apt layer (ffmpeg,
|
||||||
# unar, libpq5, postgresql-client, zstd, megatools, libjpeg62-turbo,
|
# unar, libpq5, postgresql-client, zstd, megatools, libjpeg62-turbo,
|
||||||
# libwebp7, libpng16-16), unpinned, every build.
|
# libwebp7, libpng16-16), unpinned, every build.
|
||||||
@@ -1165,7 +1496,8 @@ jobs:
|
|||||||
# pass while a codec removal broke every thumbnail in the library.
|
# 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
|
# 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).
|
# 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
|
# 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
|
# 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 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.
|
# moves. Not running is not the same as passing.
|
||||||
runs-on: python-ci
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
@@ -1257,7 +1589,7 @@ jobs:
|
|||||||
IS_CANDIDATE: ${{ needs.build-web.outputs.candidate }}
|
IS_CANDIDATE: ${{ needs.build-web.outputs.candidate }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
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
|
# in a container against a mounted docker socket, so the services are
|
||||||
# SIBLINGS reachable by IP, not by hostname.
|
# SIBLINGS reachable by IP, not by hostname.
|
||||||
PG=$(docker ps --filter "name=smoke" --filter "ancestor=pgvector/pgvector:pg16" -q | head -n1)
|
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
|
# 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
|
# `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=""
|
pg_ready=""
|
||||||
for i in $(seq 1 60); do
|
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
|
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
|
fi
|
||||||
echo "promote: both channel tags moved"
|
echo "promote: both channel tags moved"
|
||||||
build-agent:
|
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
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
@@ -1876,7 +2225,7 @@ jobs:
|
|||||||
# refresh rebuilds against freshly resolved base images, and the web
|
# refresh rebuilds against freshly resolved base images, and the web
|
||||||
# image's runtime is a line of UNPINNED Debian packages (ffmpeg,
|
# image's runtime is a line of UNPINNED Debian packages (ffmpeg,
|
||||||
# libjpeg62-turbo, libpq5, megatools…) re-resolved on every build.
|
# 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
|
# install requirements.txt, and a base bump changes neither. So
|
||||||
# refreshed bytes have to be proven before :latest names them, and
|
# refreshed bytes have to be proven before :latest names them, and
|
||||||
# proving needs a moment between "built" and "published" to occupy.
|
# proving needs a moment between "built" and "published" to occupy.
|
||||||
|
|||||||
@@ -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.<minutes> 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
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
name: extension
|
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
|
# `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,
|
# 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.
|
# eliminating the prior race between build.yml and a separate extension.yml.
|
||||||
@@ -10,19 +11,17 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'extension/**'
|
- 'extension/**'
|
||||||
- '.forgejo/workflows/extension.yml'
|
- '.forgejo/workflows/extension.yml'
|
||||||
# test/version.spec.js asserts things ABOUT the other two workflows —
|
# test/version.spec.js asserts things ABOUT build.yml — that it does not
|
||||||
# that neither inlines the packaged-file set, and that build.yml derives
|
# inline the packaged-file set, and that build.yml derives
|
||||||
# the shipped version rather than reading it out of the repo. A
|
# 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
|
# 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
|
# it. build.yml joined the list at milestone 271 step 5, when the spec
|
||||||
# started asserting against it.
|
# started asserting against it.
|
||||||
- '.forgejo/workflows/ci.yml'
|
|
||||||
- '.forgejo/workflows/build.yml'
|
- '.forgejo/workflows/build.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'extension/**'
|
- 'extension/**'
|
||||||
- '.forgejo/workflows/ci.yml'
|
|
||||||
- '.forgejo/workflows/build.yml'
|
- '.forgejo/workflows/build.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|||||||
@@ -271,10 +271,20 @@ Four deployable pieces, built by `.forgejo/workflows/build.yml`:
|
|||||||
|
|
||||||
## CI / Forgejo setup
|
## CI / Forgejo setup
|
||||||
|
|
||||||
Four workflows: `ci.yml` (lint, extension-version check, backend unit tests,
|
Three workflows: `build.yml` (the five verification lanes — lint,
|
||||||
frontend build, integration), `extension.yml` (extension lint, vitest, XPI
|
extension-version check, backend unit tests, frontend build, integration — and
|
||||||
content verification), `build.yml` (sign + publish), and `release.yml`, which
|
then sign + publish), `extension.yml` (extension lint, vitest, XPI content
|
||||||
runs only on a `v*` tag and publishes a changelog without building anything.
|
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`
|
**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
|
label.** `runs-on: python-ci` only schedules the job onto a runner; every job
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ be a check (with retries) at the start of the container."* Yes.
|
|||||||
|
|
||||||
## A TCP connect, not a query
|
## 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
|
It answers the question that is actually being asked — is something listening
|
||||||
— and it cannot fail for a reason that retrying will never fix.
|
— and it cannot fail for a reason that retrying will never fix.
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -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
|
- Integration uses Fabled-Git Actions `services:` + socket-discovered bridge IPs
|
||||||
because `act_runner` (swarm-runner v0.6+) puts services on the default
|
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
|
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
|
`fabled-git.md` "CI philosophy" section and FC's `build.yml` integration lane
|
||||||
example.
|
is the canonical example.
|
||||||
- No `package-lock.json` is tracked yet (FC's `feedback_no_local_runs`
|
- No `package-lock.json` is tracked yet (FC's `feedback_no_local_runs`
|
||||||
memory bans `npm install` locally). Using `npm install` rather than
|
memory bans `npm install` locally). Using `npm install` rather than
|
||||||
`npm ci` until a lockfile lands.
|
`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
|
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
|
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
|
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
|
against that exact regex, plus a `YYYY.M.D.HHMM` shape check that would catch
|
||||||
a regression to the pre-318 `1.0.<minutes>` — which AMO would accept and which
|
a regression to the pre-318 `1.0.<minutes>` — which AMO would accept and which
|
||||||
orders below everything already signed. Checking here is the whole point: AMO
|
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
|
`scripts/artifacts.sh version extension` **delegates** to `packaging.sh` so
|
||||||
the two cannot answer differently.
|
the two cannot answer differently.
|
||||||
- Every job that derives anything checks out with `fetch-depth: 0` — all four
|
- 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
|
(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
|
`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
|
one commit and derives a wrong, too-low value **rather than failing**, so the
|
||||||
|
|||||||
+1
-1
@@ -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
|
**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
|
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
|
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
|
`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
|
push — the cheap place to find out, because AMO 409s on re-signing and a
|
||||||
rejected version is burned for good.
|
rejected version is burned for good.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Firefox extension for FabledCurator",
|
"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": {
|
"scripts": {
|
||||||
"lint": "set -f; web-ext lint --source-dir=. --no-config-discovery --ignore-files $(sh scripts/packaging.sh ignore)",
|
"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",
|
"start": "set -f; web-ext run --source-dir=. --no-config-discovery --ignore-files $(sh scripts/packaging.sh ignore) --firefox=firefox",
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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.
|
// lane and build.yml both run on ci-python.
|
||||||
const packaging = (cmd) =>
|
const packaging = (cmd) =>
|
||||||
execFileSync('sh', [path.join(EXT_DIR, 'scripts', 'packaging.sh'), 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', () => {
|
it('emits glob patterns literally, never expanded against the working tree', () => {
|
||||||
// The script iterates its lists with deliberate word-splitting, so it must
|
// The script iterates its lists with deliberate word-splitting, so it must
|
||||||
// run with pathname expansion off. Without that, invoking it from a cwd
|
// 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
|
// `test/**` into the individual spec files, and the pathspec silently stops
|
||||||
// covering anything added later.
|
// covering anything added later.
|
||||||
const pathspec = packaging('pathspec')
|
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', () => {
|
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
|
// manual-bump guard that milestone 271 step 5 retired. Nothing inlines the
|
||||||
// set today, and nothing should start to: a literal :(exclude)extension/...
|
// set today, and nothing should start to: a literal :(exclude)extension/...
|
||||||
// in a workflow means someone bypassed the shared definition, which is
|
// 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
|
// 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
|
// 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.
|
// the same grammar against the DERIVED value, which is the one AMO sees.
|
||||||
for (const file of ['manifest.json', 'package.json']) {
|
for (const file of ['manifest.json', 'package.json']) {
|
||||||
expect(read(file).version, `${file} version is not AMO-shaped`).toMatch(AMO)
|
expect(read(file).version, `${file} version is not AMO-shaped`).toMatch(AMO)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Run INSIDE the image, not against the source tree. That distinction is the
|
Run INSIDE the image, not against the source tree. That distinction is the
|
||||||
entire reason this file exists.
|
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
|
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
|
breaks the product. What a refresh actually re-resolves is this, from the
|
||||||
Dockerfile:
|
Dockerfile:
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
The async db fixture provides an AsyncSession bound to a transaction that
|
The async db fixture provides an AsyncSession bound to a transaction that
|
||||||
gets rolled back after each test. CI provisions a real Postgres + pgvector
|
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.
|
migration code paths.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user