# CI for the wiki stack. # # One job, stopping at the first failure - the stack has no artifact to build # and nothing to deploy, so the pipeline's whole job is "does the machinery # still hold together, and does the distribution it produces still work". # # Runner: `linux-docker` is one of this Gitea instance's three routing labels # (alongside `container-builder` and `k3s-deploy`). The job image is named # explicitly rather than inherited from the runner's label mapping, which is # not documented anywhere: `debian:trixie-slim` is the base the instance's # container-build workflows already use, and Trixie's python3 is 3.13, past # the 3.11 floor `doctor` enforces. # # Pinning that image makes `nodejs` this workflow's own responsibility. # `actions/checkout` is a JavaScript action, and act_runner runs it with `node` # *inside the job container* - a slim Debian has none, and the job dies with # exit 127 before any step of ours runs. The shape below (apt `nodejs` first, # then `checkout@v7`) is the one proven on this instance by # torben/gitea-mcp@ci-build, workflow `ci-build.yaml`, runs 42-45. # # Triggers: content commits are excluded. `publish` touches kb/, raw/ and work/ # and never the stack, so running the suite for them would be pure noise. The # exclusions are deliberately literal rather than a `!**/CONTRACT.md` negation, # whose support in Gitea's filter matching is unverified: every pattern here # names content, so anything unanticipated still triggers CI. The list is # repeated rather than shared through a YAML anchor for the same reason - # GitHub's parser rejects anchors outright, and Gitea's is not documented to # accept them. `kb/CONTRACT.md` is absent on purpose: it is a stack file that # happens to live under a content directory, and it must keep its CI. # # That the filter works is now observed, not assumed (Gitea issue #11): commit # f916376 published only kb/ and raw/ paths and produced no run at all, while # the stack commits on either side of it (adfa220, 40adbb7) each produced two. # Gitea evaluates these patterns the way GitHub does. Do not re-derive this. name: CI on: push: branches: [main] paths-ignore: - 'kb/*/**' - 'kb/index.md' - 'kb/log.md' - 'kb/provenance.md' - 'raw/*/**' - 'work/*/**' - 'reports/*/**' pull_request: branches: [main] paths-ignore: - 'kb/*/**' - 'kb/index.md' - 'kb/log.md' - 'kb/provenance.md' - 'raw/*/**' - 'work/*/**' - 'reports/*/**' workflow_dispatch: jobs: verify: runs-on: linux-docker container: image: debian:trixie-slim env: # Scope the Iteration Budget Gate to this run instead of letting it fall # back to the parent PID, and keep the trace out of the checkout so the # working tree stays clean for the ignore-rule checks. WIKITOOL_SESSION_ID: ci-${{ github.run_id }} WIKI_TRACE_DIR: /tmp/wikitool-trace DIST_DIR: /tmp/dist steps: - name: System dependencies # `nodejs` is not for us - it is what act_runner needs to execute the # JavaScript action in the next step. It has to be installed before the # checkout, which is why this step comes first. run: | set -eu apt-get update -qq apt-get install -y --no-install-recommends \ python3 python3-venv git nodejs ripgrep ca-certificates rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v7 with: # The version gate diffs against the pushed range's base, so the # shallow default clone is not enough. fetch-depth: 0 - name: Tool environment run: | set -eu git config --global --add safe.directory "$GITHUB_WORKSPACE" python3 -m venv tools/.venv tools/.venv/bin/pip install --quiet --upgrade pip tools/.venv/bin/pip install --quiet -r tools/requirements.txt # pytest-cov is CI-only: tools/requirements.txt describes what an # *instance* needs at runtime and ships with `dist export`, and an # instance does not measure this suite. Installed beside pytest for # the same reason pytest itself is. tools/.venv/bin/pip install --quiet pytest pytest-cov - name: Tests # Not run with WIKI_TRACE=0: two telemetry tests assert that a trace is # written, and disabling the emitter globally fails them. The suite # redirects WIKI_TRACE_DIR per test on its own. # # One run, not two. This job used to be the only place the suite met a # machine with no global git configuration, which is how Gitea #8 was # found - two tests that silently read the developer's `git config # user.name`. That hole is now closed in the suite itself: the autouse # `hermetic_environment` fixture gives every test an empty HOME, no # git configuration and none of the tool's own environment, so this # container is no longer a special environment worth a second run. # See instructions/dev/testing-conventions.md. # # Coverage is reported, not enforced: there is deliberately no # `--cov-fail-under` yet (Gitea #10). The threshold gets set in its own # later commit, with the measured number as its justification - one # picked before the number is either too low to bite or too high to # survive the next honest commit, and the second kind gets lowered # instead of earned. Config: tools/.coveragerc. run: | set -eu cd tools .venv/bin/python -m pytest -q \ --cov --cov-report=term --cov-report=xml --cov-report=html - name: Coverage report # `always()`: a red suite is exactly when the per-module numbers are # worth reading, and the upload must not disappear with the failure. # v3, not v4 - v4 is restricted on this Gitea instance; v3 is what is # proven here (torben/gitea-mcp@ci-build, ci-build.yaml, runs # 42-45). if: always() uses: actions/upload-artifact@v3 with: name: coverage-${{ github.run_id }} path: | tools/coverage.xml tools/htmlcov/ retention-days: 14 - name: Verify the development tree run: | set -eu tools/wikitool docs verify tools/wikitool instructions verify tools/wikitool lint --fail-on-error - name: Version gate # A stack change with no version bump cannot be released, because the # release would carry a change nobody named. Scoped to what # `dist export` actually ships as behaviour - prose docs and these # workflows are not in it, and a typo fix should not force a bump. env: BASE_SHA: ${{ github.event.pull_request.base.sha }} BEFORE_SHA: ${{ github.event.before }} run: | set -eu base="${BASE_SHA:-${BEFORE_SHA:-}}" case "$base" in ""|0000000000000000000000000000000000000000) echo "No base commit to compare against - skipping the version gate." exit 0 ;; esac if ! git cat-file -e "${base}^{commit}" 2>/dev/null; then echo "Base commit $base is not in this clone - skipping the version gate." exit 0 fi changed="$(git diff --name-only "$base" HEAD)" stack="$(printf '%s\n' "$changed" \ | grep -E '^(tools/|types/|instructions/|AGENTS\.md$|[^/]+/CONTRACT\.md$)' || true)" if [ -z "$stack" ]; then echo "No stack paths touched - no version bump required." exit 0 fi if printf '%s\n' "$changed" | grep -qx 'VERSION'; then echo "Stack changed, and VERSION moved to $(cat VERSION)." exit 0 fi echo "Stack paths changed without a VERSION bump:" printf '%s\n' "$stack" | sed 's/^/ /' echo "" echo 'Fix: tools/wikitool version bump --patch --title ""' echo 'Then `docs verify` holds VERSION and CHANGES.md together.' exit 1 - name: Export the distribution run: tools/wikitool dist export "$DIST_DIR" - name: The distribution works as a fresh instance # Replays instructions/setup-instance.md end to end, minus its four # interactive decision points. What this tests is the release artifact # as an artifact: the documented path from an unpacked export to a # verified instance. Running one `instructions verify` against the # export would only have re-checked the file it just copied. # # Personalization is stubbed the same way the identity is: the real # step interviews the user, so CI substitutes a fixed answer - here, # the template minus its sentinel line. That is deliberately the # cheapest thing `doctor`'s personalization check accepts, because # what is under test is that the export *carries* the templates, not # what a person would write into them. run: | set -eu cd "$DIST_DIR" git init -q -b main git config user.name "CI Instance" git config user.email "ci@example.invalid" for personal in USER SOUL; do grep -v 'wikitool:template-unfilled' "$personal.md.template" > "$personal.md" done python3 -m venv tools/.venv tools/.venv/bin/pip install --quiet -r tools/requirements.txt tools/wikitool instructions sync tools/wikitool index rebuild tools/wikitool sources rebuild-index tools/wikitool doctor tools/wikitool docs verify tools/wikitool instructions verify tools/wikitool lint --fail-on-error tools/wikitool version show # A fresh instance owes no migration: dist export declares its content # version, so `status` must answer rather than ask for a baseline. tools/wikitool migrate status