ci: gate docs job on PRs by label/path, gate test job by label too
CI / test (pull_request) Successful in 2m34s
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 24s

Adds scripts/** to the push path filter -- it was missing entirely, so
that directory never triggered CI regardless of what changed there.

pull_request no longer has a paths: filter (moved that check inside each
job, in shell, since a label-only PR with no relevant diff still needs
to trigger the workflow for the job-level label check to ever run).
Added labeled/unlabeled to pull_request types for the same reason.

Splits the old build-docs job into a docs job with two sections: doc
tests/build (generate concat, verify-manual.py, compile man page) run
whenever relevant on any event; publish (site build, Cloudflare deploy,
commit-back) is step-gated to push/dispatch only, as before. The docs
job now also runs on a PR when it's labeled Kind/Documentation or
Area/Docs, or its diff touches docs/manual/**, docs/build-manual.py,
docs/manualtools.py, docs/verify-manual.py, or docs/site/**.

test gains the same shape: also runs on a PR labeled Kind/Testing,
Area/Tests, Area/CI, or Area/Scripts, independent of what it touches.

docs no longer needs: test. main's branch protection already requires
test to pass before a PR merges, so by the time a push-to-main reaches
this job, test has necessarily already passed; re-checking it here
would be redundant. Leaves the same gap as
block_admin_merge_override=false on that rule: a direct admin push
bypasses it, an accepted trust boundary, not a new one.
This commit is contained in:
2026-09-23 16:18:29 -04:00
parent 6074687a80
commit 4797af85f3
+104 -16
View File
@@ -16,10 +16,17 @@ on:
- "completions/**" - "completions/**"
- "integrations/**" - "integrations/**"
- "tests/**" - "tests/**"
- "scripts/**"
pull_request: pull_request:
branches: branches:
- main - main
paths: *ci-paths # No `paths:` filter here (unlike push, above): a PR carrying
# Kind/Testing, Area/Docs, etc. must still trigger this workflow even
# when its diff touches nothing in ci-paths, or the label-based gates
# in the test/docs jobs below would never get a chance to evaluate.
# `labeled`/`unlabeled` cover a label added after the PR is already
# open, without a new commit.
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
job: job:
@@ -30,7 +37,7 @@ on:
options: options:
- all - all
- test - test
- build-docs - docs
jobs: jobs:
# This workflow file is mirrored to GitHub as-is, but the runner label # This workflow file is mirrored to GitHub as-is, but the runner label
@@ -48,8 +55,41 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITEA_TOKEN }} token: ${{ secrets.GITEA_TOKEN }}
fetch-depth: 0
# Runs unconditionally and always sets an output, so every step
# after it can gate on a single `steps.relevance.outputs.run`
# check instead of repeating the label/path OR-chain everywhere.
# push/workflow_dispatch are always relevant -- push is already
# path-filtered above, and a manual dispatch is explicit intent.
# A pull_request is relevant if it carries a testing-related label
# (independent of what it touches -- see the `on.pull_request`
# comment above) or if its diff touches a ci-paths pattern (the
# same list the push trigger above filters on; duplicated here in
# shell glob form since a PR event isn't pre-filtered by paths).
- name: Determine relevance
id: relevance
env:
PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
if printf '%s' "$PR_LABELS" | grep -qE '"name":[[:space:]]*"(Kind/Testing|Area/Tests|Area/CI|Area/Scripts)"'; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
git fetch origin "${{ github.event.pull_request.base.ref }}"
if git diff --name-only "origin/${{ github.event.pull_request.base.ref }}...HEAD" \
| grep -qE '^(docs/manual/|docs/build-manual\.py$|docs/manualtools\.py$|docs/verify-manual\.py$|docs/site/|functions/|conf\.d/|config\.fish$|completions/|integrations/|tests/|scripts/)'; then
echo "run=true" >>"$GITHUB_OUTPUT"
else
echo "run=false" >>"$GITHUB_OUTPUT"
fi
- name: Install fish - name: Install fish
if: steps.relevance.outputs.run == 'true'
run: | run: |
sudo apt-get -o Acquire::Retries=3 update -qq sudo apt-get -o Acquire::Retries=3 update -qq
# apt-utils, so debconf has a target for the "delaying package # apt-utils, so debconf has a target for the "delaying package
@@ -67,22 +107,27 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish
- name: Run fish config tests - name: Run fish config tests
if: steps.relevance.outputs.run == 'true'
run: fish tests/run-tests.fish run: fish tests/run-tests.fish
# PRs only need the test job as a gate -- build-docs commits generated # Documentation tests/build, and (push/dispatch only) publish. Split
# files straight to the checked-out branch and deploys the Cloudflare # into two sections within one job rather than two jobs: the publish
# Pages production site, neither of which should ever happen from a PR # steps need the files the build steps just generated, and passing
# (PR content isn't main yet, and a fork/branch push shouldn't touch # those between separate jobs would need upload/download-artifact for
# prod). It only runs on push to main or an explicit workflow_dispatch. # no real benefit here.
build-docs: #
needs: test # Does NOT `need: test` (the old build-docs job did, gating publish
# on it). main now has branch protection requiring the test job to
# pass before a PR can merge, so by the time a push-to-main reaches
# this job, test has already passed as a condition of getting here --
# re-checking it in-workflow would be redundant. The one gap that
# leaves is a direct admin push bypassing the PR flow entirely; that's
# the same trust already extended by leaving block_admin_merge_override
# off on the branch protection rule, not a new hole.
docs:
if: | if: |
github.server_url != 'https://github.com' && github.server_url != 'https://github.com' &&
github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == 'docs')
always() &&
(github.event.inputs.job == 'build-docs' ||
((github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all') &&
needs.test.result == 'success'))
runs-on: racknerd-mini runs-on: racknerd-mini
env: env:
# Silences Node's internal "punycode module is deprecated" notice # Silences Node's internal "punycode module is deprecated" notice
@@ -94,8 +139,34 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITEA_TOKEN }} token: ${{ secrets.GITEA_TOKEN }}
fetch-depth: 0
# Same shape as the test job's identical step; see its comment.
# Only the label set and path patterns differ, narrowed to the
# docs-specific subset of ci-paths.
- name: Determine relevance
id: relevance
env:
PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
if printf '%s' "$PR_LABELS" | grep -qE '"name":[[:space:]]*"(Kind/Documentation|Area/Docs)"'; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
git fetch origin "${{ github.event.pull_request.base.ref }}"
if git diff --name-only "origin/${{ github.event.pull_request.base.ref }}...HEAD" \
| grep -qE '^(docs/manual/|docs/build-manual\.py$|docs/manualtools\.py$|docs/verify-manual\.py$|docs/site/)'; then
echo "run=true" >>"$GITHUB_OUTPUT"
else
echo "run=false" >>"$GITHUB_OUTPUT"
fi
- name: Install dependencies - name: Install dependencies
if: steps.relevance.outputs.run == 'true'
run: | run: |
sudo apt-get -o Acquire::Retries=3 update -qq sudo apt-get -o Acquire::Retries=3 update -qq
# apt-utils: see the "Install fish" step's identical comment in # apt-utils: see the "Install fish" step's identical comment in
@@ -107,6 +178,7 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish
- name: Generate concatenated markdown - name: Generate concatenated markdown
if: steps.relevance.outputs.run == 'true'
run: python3 docs/build-manual.py --concat -o docs/fish-config.md run: python3 docs/build-manual.py --concat -o docs/fish-config.md
# Regeneration MUST run before verification: verify-manual.py's # Regeneration MUST run before verification: verify-manual.py's
@@ -114,13 +186,19 @@ jobs:
# against docs/fish-config.md on disk. Before this step ran, that # against docs/fish-config.md on disk. Before this step ran, that
# file was still the stale pre-push copy, so any ordinary edit under # file was still the stale pre-push copy, so any ordinary edit under
# docs/manual/** failed the round-trip check before anything was # docs/manual/** failed the round-trip check before anything was
# regenerated. Do not reorder this back — verification still gates # regenerated. Do not reorder this back -- verification still gates
# pandoc and the auto-commit below, it just no longer requires a # pandoc and the auto-commit below, it just no longer requires a
# contributor to hand-sync the generated file before pushing. # contributor to hand-sync the generated file before pushing.
#
# This is the "documentation tests" section: on a PR, it runs
# (and can fail the job) whenever relevant, without needing the
# publish steps below to run at all.
- name: Verify manual integrity - name: Verify manual integrity
if: steps.relevance.outputs.run == 'true'
run: python3 docs/verify-manual.py run: python3 docs/verify-manual.py
- name: Compile man page - name: Compile man page
if: steps.relevance.outputs.run == 'true'
run: | run: |
pandoc --standalone \ pandoc --standalone \
--from markdown \ --from markdown \
@@ -128,21 +206,30 @@ jobs:
docs/fish-config.md \ docs/fish-config.md \
-o docs/fish-config.1 -o docs/fish-config.1
# ──────────────────────── Publish only ───────────────────────
# Everything below deploys the production site and commits
# generated files straight to the checked-out branch. Never runs
# from a pull_request -- PR content isn't main yet, and a
# fork/branch push shouldn't touch prod.
- name: Set up Node - name: Set up Node
if: github.event_name != 'pull_request'
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "24" node-version: "24"
- name: Generate site content - name: Generate site content
if: github.event_name != 'pull_request'
run: python3 docs/build-manual.py --site run: python3 docs/build-manual.py --site
- name: Build project wiki - name: Build project wiki
if: github.event_name != 'pull_request'
working-directory: docs/site working-directory: docs/site
run: | run: |
npm ci --no-fund npm ci --no-fund
npx astro build npx astro build
- name: Deploy to Cloudflare Pages - name: Deploy to Cloudflare Pages
if: github.event_name != 'pull_request'
working-directory: docs/site working-directory: docs/site
env: env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
@@ -154,6 +241,7 @@ jobs:
--commit-dirty=true --commit-dirty=true
- name: Commit generated docs - name: Commit generated docs
if: github.event_name != 'pull_request'
env: env:
BOT_GPG_KEY: ${{ secrets.CI_GPG_PRIVATE_KEY }} BOT_GPG_KEY: ${{ secrets.CI_GPG_PRIVATE_KEY }}
run: | run: |
@@ -211,4 +299,4 @@ jobs:
- name: Note that CI runs on Gitea - name: Note that CI runs on Gitea
run: | run: |
echo "This repository mirrors from Gitea (git.rootiest.dev), where CI actually runs." echo "This repository mirrors from Gitea (git.rootiest.dev), where CI actually runs."
echo "See the commit's status on the Gitea instance for the real test/build-docs results." echo "See the commit's status on the Gitea instance for the real test/docs results."