ci: gate docs job on PRs by label/path, gate test job by label too
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:
+104
-16
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user