The default `secrets.GITEA_TOKEN` is Gitea Actions' built-in synthetic bot identity, not a whitelistable user account. main's branch protection rejects its pushes outright regardless of retries (run 983, run 990) -- the retry/rebase loop in the next step was built for a non-fast-forward race (run 976), not a bare permission rejection, so it can't recover from this. Point the docs job's checkout token at BOT_PUSH_TOKEN, a PAT on the already-bypass-whitelisted rootiest account, so the later push succeeds. Commit authorship and GPG signing (fishconfig-bot) are set separately via git config a few steps later and are unaffected -- push auth and commit identity are independent.
311 lines
14 KiB
YAML
311 lines
14 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
paths: &ci-paths
|
|
- "docs/manual/**"
|
|
- "docs/build-manual.py"
|
|
- "docs/manualtools.py"
|
|
- "docs/verify-manual.py"
|
|
- "docs/site/**"
|
|
- "functions/**"
|
|
- "conf.d/**"
|
|
- "config.fish"
|
|
- "completions/**"
|
|
- "integrations/**"
|
|
- "tests/**"
|
|
- "scripts/**"
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
# 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:
|
|
inputs:
|
|
job:
|
|
description: "Job to run"
|
|
required: false
|
|
default: all
|
|
type: choice
|
|
options:
|
|
- all
|
|
- test
|
|
- docs
|
|
|
|
jobs:
|
|
# This workflow file is mirrored to GitHub as-is, but the runner label
|
|
# below (racknerd-mini) only exists on the Gitea instance -- on GitHub
|
|
# the job just sits queued forever with no matching runner, so the
|
|
# mirror never gets a completed status. Gate the real jobs to Gitea and
|
|
# let the github-mirror job below stand in on GitHub instead.
|
|
test:
|
|
if: |
|
|
github.server_url != 'https://github.com' &&
|
|
(github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == 'test')
|
|
runs-on: racknerd-mini
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
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
|
|
if: steps.relevance.outputs.run == 'true'
|
|
run: |
|
|
sudo apt-get -o Acquire::Retries=3 update -qq
|
|
# apt-utils, so debconf has a target for the "delaying package
|
|
# configuration" notice's OWN follow-up run (irrelevant to
|
|
# anything else in this job). That notice still fires exactly
|
|
# once here regardless -- verified live against two orderings
|
|
# (bundled with another package, and installed fully alone,
|
|
# first) -- because it fires *during* apt-utils' own first
|
|
# install, before debconf considers it "installed". Nothing
|
|
# this workflow controls can pre-seed that; accepted as
|
|
# unavoidable, same as the runs-on/checkout hint noise below.
|
|
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y apt-utils software-properties-common
|
|
sudo add-apt-repository -y ppa:fish-shell/release-4
|
|
sudo apt-get -o Acquire::Retries=3 update -qq
|
|
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish
|
|
|
|
- name: Run fish config tests
|
|
if: steps.relevance.outputs.run == 'true'
|
|
run: fish tests/run-tests.fish
|
|
|
|
# Documentation tests/build, and (push/dispatch only) publish. Split
|
|
# into two sections within one job rather than two jobs: the publish
|
|
# steps need the files the build steps just generated, and passing
|
|
# those between separate jobs would need upload/download-artifact for
|
|
# no real benefit here.
|
|
#
|
|
# 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: |
|
|
github.server_url != 'https://github.com' &&
|
|
(github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == 'docs')
|
|
runs-on: racknerd-mini
|
|
env:
|
|
# Silences Node's internal "punycode module is deprecated" notice
|
|
# (astro's toolchain still requires it transitively) on every node
|
|
# invocation in this job, setup-node's own included.
|
|
NODE_OPTIONS: --no-deprecation
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
# The default GITEA_TOKEN is Gitea Actions' built-in synthetic
|
|
# bot identity, not a real account -- main's branch protection
|
|
# rejects its pushes outright (run 983, run 990), and it can't
|
|
# be whitelisted because it isn't an addable user. BOT_PUSH_TOKEN
|
|
# is a PAT on the rootiest account (already bypass-whitelisted)
|
|
# used only so this job's later push succeeds; commit authorship
|
|
# and GPG signing below still use the fishconfig-bot identity,
|
|
# which is unrelated to push auth.
|
|
token: ${{ secrets.BOT_PUSH_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
|
|
if: steps.relevance.outputs.run == 'true'
|
|
run: |
|
|
sudo apt-get -o Acquire::Retries=3 update -qq
|
|
# apt-utils: see the "Install fish" step's identical comment in
|
|
# the test job for why its own debconf notice is accepted, not
|
|
# chased further.
|
|
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y apt-utils software-properties-common
|
|
sudo add-apt-repository -y ppa:fish-shell/release-4
|
|
sudo apt-get -o Acquire::Retries=3 update -qq
|
|
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish
|
|
|
|
- name: Generate concatenated markdown
|
|
if: steps.relevance.outputs.run == 'true'
|
|
run: python3 docs/build-manual.py --concat -o docs/fish-config.md
|
|
|
|
# Regeneration MUST run before verification: verify-manual.py's
|
|
# test_concat_roundtrips_original compares a freshly-built concat
|
|
# 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
|
|
# docs/manual/** failed the round-trip check before anything was
|
|
# regenerated. Do not reorder this back -- verification still gates
|
|
# pandoc and the auto-commit below, it just no longer requires a
|
|
# 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
|
|
if: steps.relevance.outputs.run == 'true'
|
|
run: python3 docs/verify-manual.py
|
|
|
|
- name: Compile man page
|
|
if: steps.relevance.outputs.run == 'true'
|
|
run: |
|
|
pandoc --standalone \
|
|
--from markdown \
|
|
--to man \
|
|
docs/fish-config.md \
|
|
-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
|
|
if: github.event_name != 'pull_request'
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Generate site content
|
|
if: github.event_name != 'pull_request'
|
|
run: python3 docs/build-manual.py --site
|
|
|
|
- name: Build project wiki
|
|
if: github.event_name != 'pull_request'
|
|
working-directory: docs/site
|
|
run: |
|
|
npm ci --no-fund
|
|
npx astro build
|
|
|
|
- name: Deploy to Cloudflare Pages
|
|
if: github.event_name != 'pull_request'
|
|
working-directory: docs/site
|
|
env:
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
|
run: |
|
|
npx --yes wrangler pages deploy dist/ \
|
|
--project-name=fish-config-docs \
|
|
--branch=main \
|
|
--commit-dirty=true
|
|
|
|
- name: Commit generated docs
|
|
if: github.event_name != 'pull_request'
|
|
env:
|
|
BOT_GPG_KEY: ${{ secrets.CI_GPG_PRIVATE_KEY }}
|
|
run: |
|
|
# actions@gitea was never a verified email on any account, so
|
|
# these commits could never show as signed/verified regardless
|
|
# of server config. fishconfig-bot is a dedicated Gitea account
|
|
# (verified email, no login) that owns this key; the private
|
|
# key lives only in the CI_GPG_PRIVATE_KEY repo secret.
|
|
#
|
|
# Secret is base64-encoded: a raw multi-line armored key piped
|
|
# through `echo "$VAR" | gpg --import` came out CRC-corrupted
|
|
# ("Invalid keyring") the first time this ran -- something in
|
|
# the secret/env round-trip mangles embedded newlines. Base64
|
|
# collapses it to one line immune to that.
|
|
export GNUPGHOME="$(mktemp -d)"
|
|
chmod 700 "$GNUPGHOME"
|
|
echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf"
|
|
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
|
|
command -v gpg >/dev/null || sudo apt-get install -y --no-install-recommends gnupg
|
|
echo "$BOT_GPG_KEY" | base64 -d | gpg --batch --quiet --import
|
|
git config user.name "Gitea Actions Bot"
|
|
git config user.email "fishconfig-bot@git.rootiest.dev"
|
|
git config user.signingkey CAA082C2F3467E1F7217AD492075C120312D23F4
|
|
git config commit.gpgsign true
|
|
git add docs/fish-config.md docs/fish-config.1 conf.d/__fish_config_op_registry.fish
|
|
git diff --cached --quiet && echo "No changes to commit" && exit 0
|
|
git commit -m "chore(docs): regenerate manual, man page, and component registry"
|
|
|
|
# npm ci + astro build + the Cloudflare deploy above can take
|
|
# several minutes, so main can move (another PR merges) before
|
|
# this push lands -- a bare `git push` was seen rejected as
|
|
# non-fast-forward for exactly that reason (run 976). This
|
|
# commit only ever touches generated files, so a rebase onto
|
|
# whatever landed is always mechanical; retry it a few times
|
|
# against a live race instead of failing the whole job.
|
|
pushed=0
|
|
for attempt in 1 2 3; do
|
|
if git push; then
|
|
pushed=1
|
|
break
|
|
fi
|
|
echo "push rejected (attempt $attempt/3), rebasing onto origin/main..." >&2
|
|
git fetch origin main
|
|
git rebase origin/main
|
|
done
|
|
test "$pushed" -eq 1
|
|
|
|
# Stand-in for the GitHub mirror so the commit gets a completed status
|
|
# instead of the real jobs above sitting queued forever for a
|
|
# self-hosted runner that only exists on the Gitea instance.
|
|
github-mirror:
|
|
if: github.server_url == 'https://github.com'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Note that CI runs on Gitea
|
|
run: |
|
|
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/docs results."
|