The mirror section told contributors a label added on Gitea "must be
created on the mirror too -- no automation does it for you." That's no
longer true, and a stale instruction to do something by hand is worse than
none, since it invites a manual edit that the next scheduled sync would
overwrite anyway.
Describe what the sync actually does: what it creates, updates, and prunes,
that an in-use label is never deleted, the --dry-run and --self-test flags,
and the rename caveat that follows from matching labels by name. Also
record the GH_MIRROR_TOKEN secret the workflow needs and its exact scopes,
since that's the one part of this that can't be automated.
The exclusive-labels caveat below it is unaffected and stays as written.
Labels don't travel with a mirror push -- mirroring copies files, not
repository settings -- but they matter on the GitHub side anyway, because
GitHub reads the same .github/ISSUE_TEMPLATE/ files and silently drops a
labels: entry naming a label it doesn't have. Until now the only thing
keeping the two sets aligned was remembering to do it by hand, which is
exactly the kind of thing that gets forgotten and then fails invisibly.
Add scripts/sync-labels.py and a workflow that runs it daily, on any change
to the script itself, and on manual dispatch. Gitea stays the source of
truth: labels are managed there and GitHub is made to match.
- **Creates and updates** anything missing or drifted. Colors and
descriptions are normalized before comparison -- Gitea returns colors
bare, GitHub sometimes with a leading '#', and a description may be null
on one side and "" on the other -- so a steady state is a true no-op
rather than a rewrite of all 33 labels every run.
- **Deletes only unused extras.** An extra label on the mirror is removed
only when no issue or PR there carries it; one in use is reported with
its count and left alone. An unattended scheduled job must not be able to
strip a label off somebody's issue.
- **Refuses to run on an empty source**, since treating that as truth would
propose deleting every label on the mirror.
- **--dry-run** prints the plan and changes nothing; **--self-test** checks
the diff logic offline against fixtures, and gates the sync step in CI so
a broken diff can't mutate anything.
Stdlib only, so the CI step installs nothing beyond python3. The job is
gated with the same github.server_url check ci.yml uses -- without it the
mirror would queue this forever against a runner that only exists on Gitea.
Labels are matched by name, so a rename reads as delete-plus-create; the
new name is created and the old is pruned only if unused. The two forges
share no stable label ID, so a rename can't be tracked across them.
The templates were written against Gitea's schema alone, but the mirror
serves the same .github/ISSUE_TEMPLATE/ files to GitHub, where two of them
would have silently failed:
- **config.yaml -> config.yml.** Gitea accepts either spelling
(modules/structs/issue.go: `base == "config.yaml" || base == "config.yml"`),
GitHub only recognizes config.yml. Under the old name the mirror's
template chooser would have shown neither the contact links nor the
blank-issue setting.
- **bug.yaml `about:` -> bug.yml `description:`.** GitHub requires
`description` on a YAML issue form; Gitea's IssueTemplate.About carries
the comment "Using description in a template file is compatible" and
falls back to it at modules/issue/template/unmarshal.go:126. So
`description` is the one spelling both forges accept. The markdown
templates keep `about:`, which is correct for their format on both.
Both files now carry a comment explaining the constraint, so neither gets
"tidied" back into a broken state.
Also add a contact link pointing at the canonical Gitea tracker, so someone
arriving from the mirror is steered to the right place before they file,
and document mirror parity in CONTRIBUTING.md: the labels must be created
on both sides by hand, since mirroring copies files rather than repository
settings and GitHub drops a labels: entry naming a label it doesn't have.
Note too that GitHub has no exclusive labels, so the one-of rule on
Priority/, Reviewed/, and Status/ holds only by convention there.
The repo carried Gitea's stock label set with no written rule for applying
it, so labels were effectively unused. Establish the standard: every issue
and PR carries exactly one Kind/ and at least one Area/, and document what
each group means, which are exclusive, and who applies them.
Extend the taxonomy to make that workable:
- **Kind/Refactor, Kind/Chore, Kind/Performance** — the stock Kind/ group
couldn't describe a refactor, a chore, or a perf change, which is much of
this repo's history. The group now maps one-to-one onto the Conventional
Commits types already in use, so a PR's label and title agree.
- **A new non-exclusive Area/ group** over the ten subsystems (functions,
completions, config, docs, tests, CI, integrations, prompt & theme,
components, scripts). This is what makes the tracker searchable by
subsystem; Kind/ alone can't answer "what's outstanding in the docs
pipeline?".
- **good first issue and help wanted**, deliberately unscoped rather than
under Status/. Status/ is exclusive, and an issue is often both blocked
and open for someone to pick up; keeping these outside the group lets
them coexist with a real status.
Priority/, Reviewed/, and Status/ are documented as exclusive and
maintainer-applied, with the rules that keep them meaningful — no
Priority/ label means ordinary priority, Reviewed/Confirmed means actually
reproduced, and a Status/ label is removed once it stops being true.
Also add labeling to the standing PR rules in Branching & Pull Requests,
so it's visible where the rest of the PR checklist lives.
Issues had no template at all, so a report arrived in whatever shape the
reporter chose — most often without a fish version, a reproduction, or the
full error text, which is what actually stalls a bug.
Add three templates under .github/ISSUE_TEMPLATE/, beside the PR template
so the GitHub mirror offers the same set:
- **bug.yaml** — a Gitea issue form rather than markdown. Version, OS,
area, reproduction, expected and actual behavior are required fields, so
an unactionable report can't be submitted in the first place. The Area
dropdown exists because contributors without push access can't set an
Area/ label themselves.
- **feature.md** and **docs.md** — comment-guided markdown in the same
house style as PULL_REQUEST_TEMPLATE.md, since what they ask for is
open-ended prose. feature.md carries `## Acceptance criteria`, the
issue-side counterpart to a PR's `## Verification`. docs.md insists on
the docs/manual/** source rather than the generated page, which the next
CI run would overwrite.
- **config.yaml** — keeps blank issues enabled for what the three don't
cover, and links the contributing guide and the customization docs.
Each template pre-applies its Kind/ label. Document the set, the plain-
description title convention (an issue states a problem; the conventional
subject belongs on the PR that closes it), and the triage split in a new
CONTRIBUTING.md § Issues.
Gitea treats a `WIP:` title prefix as a draft marker and refuses to merge
while it's present. Verified empirically against this instance by
temporarily retitling PR #123: the API flipped `draft` to true and
`mergeable` to false, and both reverted when the prefix was removed.
The distinction from the verification merge gate is the point, so it's
stated explicitly in both files: `WIP:` means more code changes are
coming, NOT "finished but unverified". A complete branch with unticked
`## Verification` boxes is an ordinary PR — that case is already covered
by the merge gate and does not want the prefix. The two signals are
independent and can occur in any combination.
Documented as a merge rule under Branching & Pull Requests, next to the
verification gate, and in the template's title guidance where an author
picking a title will actually encounter it.
The old name implied the list was only for by-hand steps, which sat badly
with the fact that most entries in practice are programmatic — the test
suite, `fish_indent`, `docs/verify-manual.py`, CI. Dropping "Manual" lets
one list carry both kinds, and gives the checkbox state real meaning:
- **Checked** — verified, whether it ran programmatically or the author
performed it by hand.
- **Unchecked** — an outstanding manual check the reviewer still has to
perform. Left visible rather than dropped.
- **The list is the merge gate.** A PR isn't merged until every box is
checked; added as an explicit rule under Branching & Pull Requests
alongside the other merge rules.
Because unchecked boxes now block the merge, the guidance also states that
only resolvable checks belong here — one nobody can run would block the PR
indefinitely. Genuinely unverifiable caveats and assumptions go in
`## Notes`, which already existed in the optional-sections list.
Incidentally realigns with PRs 121-122, which had already shortened the
heading to `## Verification`; that part of the recent drift was right.
The repo has no issue-tracking history yet — a grep across all 122 prior
PRs found zero `Closes #N` references — so this is forward-looking rather
than derived from precedent. Establishing it now means the first PR that
does close an issue has a rule to follow instead of inventing one.
- Placement is a trailing line at the end of `## Summary`, not the bottom
of the body, because `## Manual Verification` is always last.
- `Fixes`/`Resolves` noted as equivalent; `Refs #N` for a related issue
that should stay open.
- Guidance calls out that the keyword must be repeated per issue, since a
bare `#43` after a comma links without closing.
- Lives in the template's HTML comments, so a PR with no associated issue
leaves no stray `Closes #` behind.
Also drops an unverified claim that the GitHub mirror pre-loads the
template; PRs are opened on Gitea, and the mirror is push-only.
Codifies the PR body format the repo has converged on across its first
122 pull requests, which until now lived only as an implicit pattern
agents and contributors had to reverse-engineer from prior PRs.
- **`.github/PULL_REQUEST_TEMPLATE.md`** — `## Summary`, optional `##`
sections, then `## Manual Verification` as a checkbox list. Gitea falls
back to `.github/` when `.gitea/` is absent, and the GitHub mirror reads
the same path, so one file covers both.
- **`CONTRIBUTING.md`** — new "Pull request descriptions" subsection under
Branching & Pull Requests, documenting the same three-part structure.
Section names were chosen by frequency across all 122 PRs: `## Summary`
(86) and `## Manual Verification` (63) are the clear majority. The later
`## Manual Verification Checklist` (12, PRs 90-113), `## Test plan` (11)
and `## Verification` (3, PRs 121-122) variants are drift away from that
baseline, not a newer standard, so the template restores the dominant
form. Checkboxes ship unchecked but are meant to be checked before the
PR opens, matching the 585-to-12 ratio of `[x]` to `[ ]` in merged PRs.
The heading scan matches a keyword contained in a heading, so a plural
could never reach a singular heading: `customization` found "7.
CUSTOMIZATION" and `customizations` found nothing at all. The index
lookup was exact-match, so it could not cover the gap either without an
alias per word.
Both now try the keyword as typed first, then its singular/plural
forms. Each variant is tried against every heading before the next one
is considered, so a loose plural cannot beat an exact hit further down
the document.
config-help pipes the manual through bat, which dims the backticks and
leaves the span content the same colour as the prose around it -- so a
delimiter carried no information and every span cost the reader two
literal characters. 1236 of them across the document.
Each span is now rendered bold and the delimiters dropped, in every
branch of the viewer chain:
- ov + bat, and ov alone, style the spans (bat's output wraps each
backtick in its own SGR sequence; raw Markdown is matched directly)
- bat alone flattens them on the way in instead, because bat escapes
any SGR sequence handed to it as input
- less and cat style them, less gaining -R to render the result
- man -l needs nothing; pandoc consumed the backticks at build time
Both substitutions are line-preserving, so the tail-slice that opens
the pager on a requested section still lands on it.
Two shapes reach a reader as literal punctuation rather than markup:
A backtick inside a four-space block. The block is verbatim in every
renderer -- pandoc sets it monospace, prettify() fences it for the site
-- so the backtick is a character on the page. Twelve such lines are
cleaned; each sat at the start of its column, so the alignment of the
C5 capture table and the component summaries is unchanged.
A span split over a line break. Markdown pairs it happily, but
config-help pairs backticks one line at a time, so `fish-deps\nupdate`
showed both halves. The sentence is reflowed.
Both are now enforced, the first by test_concat_section_five_stays_
verbatim and the second by test_concat_code_spans_never_straddle_a_line.
A token was typeset by whichever pipeline happened to render it: the
site marked tmux and local.fish through codespans, while the man page
and config-help marked only what the SSOT had backticked by hand. Run
the same pass in build_concat() so prose is marked identically wherever
it is rendered (549 -> 662 spans in the concat).
codespans now treats a four-space block as code. The site never meets
one -- prettify() has already turned it into a fence by then -- but the
concat keeps the indented form pandoc wants, and its contents are
verbatim: without this the table of contents alone would come out with
ov, bat, less and cat wrapped inside a code block. Section 5 is
unaffected for the same reason; its entries arrive as indented blocks
that pandoc already sets in a monospace font.
test_codespans_is_site_only asserted the opposite guarantee and was
passing only because its example, -r/--resume, sits inside one of those
newly-protected blocks. It is replaced by tests for what is now true:
indented blocks stay verbatim, prose spans reach the concat, and
section 5 carries no backticks.
The man page is left for CI to regenerate; pandoc is not needed to
build the concat.
CONTRIBUTING states doc-headers are written as plain text -- the header
is read as-is by config-help, by funcsave, and by anyone opening the
file, and docs/codespans.py adds the site's inline code spans at render
time. 22 files had drifted from that, carrying 41 hand-written spans
that reached config-help and the man page as literal backtick
characters inside an otherwise verbatim block.
The one span whose content ended in a space is requoted rather than
dropped, so "read> " keeps reading as a prompt string.
Cover the ~50 previously-unindexed headings (function reference entries
like play-media, steam-dl, bd-pull, cffetch/ffetch, config-toggle, dops,
rand_string, fish_prompt/fish_mode_prompt/fish_right_prompt, lD, mv, p,
fast/fast-cli, gip4/gip6, fzf-update, sponge_filter_secrets,
fzf_configure_bindings, ld; the C1-C6 component sub-category headings
filesystem/network/monitor/shell-tools/dev-tools,
plugin-management/pkg-wrappers/venv/telemetry/sync,
key-bindings/environment/prompt, terminal-abbrs/window-mgmt/
notifications/history-logs/pkg-upgrade, terminal-capture/
multiplexer-capture/pkg-logs, first-run/greeting-message; plus
Config Variables' "Other", the Integrations "Scrollback History", the
dependency catalog's Optional/Terminal Emulators tiers, the C0/always
override note, and "Reading the source directly").
Expanded aliases for existing headings: play-media also reachable as
video/audio/mpv/vlc, steam-dl as steam, cffetch/ffetch share fetch/
fastfetch/neofetch/sysinfo, rand_string as random/password/randomword,
gip4/gip6 as ipv4/ipv6, dops as docker-ps, ld as lazydocker, bd-pull as
beads, fast-cli as speedtest, sponge_filter_secrets as secret-filter.
Fixed two pre-existing duplicate normalized keys (key-bindings/
keybindings and man-page/manpage both collapsed to a single entry) and
repointed config-toggle from the generic config-settings heading to its
own dedicated "### config-toggle" heading now that one exists.
Judgment calls on ambiguous/repeated heading text (left unindexed, or
indexed via a distinguishing keyword instead of the literal name):
- "### Sub-categories" repeats 7 times (once per opinionated-components
overview and once per C1-C6 page); an index entry pointing at that
text always resolves to the first occurrence, so none of the 7 are
indexed under that name. Each parent (C1-C6, and the Minimal Mode
section) already has its own keyword.
- "### search" appears twice: the pkg search subcommand (kept, existing
`search` key) and the C1 command-shadow sub-category. The C1 one
isn't separately reachable for the same reason.
- "## Integrations" appears twice (Section 1 integrations writeup and
the Dependency Catalog's Integrations tier table); only the first
(already indexed) is reachable.
- Where a C1-C6 sub-category heading text collides after normalization
with an existing higher-value keyword (key-bindings, network,
notifications, venv), the new entry uses a distinguishing keyword
(bindings-toggle, network-shadow, notifications-toggle, venv-toggle)
instead of contesting the existing one.
- `prompt` now points at the fuller "## Prompt and Theme" section
rather than the one-line C3 sub-category blurb; the latter is
reachable via `prompt-toggle`.
The test/build-docs jobs target a self-hosted runner (racknerd-mini)
that only exists on the Gitea instance. When GitHub re-runs this same
workflow on the mirror, those jobs sit queued forever with no matching
runner, so the commit never gets a completed status.
Gate both jobs to skip when github.server_url is github.com, and add a
trivial github-mirror job (runs-on: ubuntu-latest, which GitHub does
provide) that only runs on the mirror, so the check completes instead
of hanging.
Function doc-headers are authored as plain text -- `config-help`,
`funcsave` and anyone opening the `.fish` file read them as-is -- so they
carry no backticks. The site inherited that and rendered `-a/--all` and
`__fish_config_op_aliases` as ordinary prose.
docs/codespans.py adds the spans at render time, as the last step of
prettify(), so only the site sees them; build_concat() (man page,
config-help) is byte-for-byte unchanged.
Recognised shapes: flags and flag pairs, `$vars`, SCREAMING_SNAKE env
vars, snake_case identifiers, paths and filenames, key chords, command
shadow chains (`ls->eza`), runs of tool names, whole command lines in a
table column of command lines, and known command names -- drawn from the
`_fdc_*` catalog in functions/_fish_deps_catalog.fish, the functions/
listing, and a standard-command list, minus the names that also read as
English.
Fenced blocks, existing code spans, headings, link targets, URLs,
component markup and <FileTree> bodies are passed through untouched, and
every rule bails out rather than guess.
Verified every claim in the Prompt and Theme section against the actual
source and corrected several inaccuracies: the Starship wrapper's missing
C3 gate, the fallback prompt's vi-mode states and segment order, the FZF
theme's real location (conf.d/theme.fish, not integrations/fzf.fish) and
color set, and the Catppuccin theme-switch example using the wrong
fish_config subcommand (choose, not save). The right-prompt Docker-context
example was rewritten to show that it's independent of exit status.
While reformatting, found that any block build-manual.py couldn't classify
as shell/table/tree fell back to plain 4-space markdown indentation, which
silently renders as squashed, unreadable paragraph text on any page that
also contains an <Aside> or <FileTree> — MDX has no indented-code-block
syntax, unlike plain Markdown. This affected 07-customization.mdx plus four
other pages. Fixed the fallback to emit a fenced ```text block instead,
since fences work in both MDX and plain Markdown; this also gives every
affected block Starlight's normal code-block styling instead of a bare grey
slab.
docs/fish-config.md is intentionally left stale here — CI regenerates and
auto-commits it from docs/manual/** on push to main.
List every file in functions/ and completions/ inline under the home
page's file tree instead of a one-line summary. The listing is read
live off disk during --site generation, so it never needs manual
upkeep, and only affects the Starlight build — the plain-text
manual/man page (--concat) still renders the compact summary.
Documents the branching/PR workflow, commit conventions, fish function
doc-header and colored --help conventions, the docs generation
pipeline, testing, and the secrets/machine-config placement rule, so
these practices live somewhere durable instead of only in commit
history and conversation memory.
Add the established c_head/c_cmd/c_flag/c_dim/c_arg color scheme to
--help (or usage-on-error) output in play-media and 13 other functions
that lacked it or used an ad hoc scheme: bkg, detach, replay, p, y,
spark, wake-lock, open-url, repo-open, dng2avif, dockup, fish-deps
(__fish_deps_help), and scrub.
Also tweak the standard itself:
- c_cmd now uses plain `set_color --bold` instead of `--bold white`,
so the command name adapts to the terminal's foreground instead of
forcing white text that washes out on light-background themes.
Applied across all functions already using the pattern.
- jobrunner's reset variable renamed from c_rst to c_reset to match
the naming used everywhere else.
A missing trailing backslash after the Thumbs.db:encryptable pattern
terminated the `set -l aggressive_patterns` array early. The AI-tool
patterns (.gemini*, .claude*, .antigravity*, .remember*) were then
executed as a bogus command instead of being appended to the array,
so scrub -a never actually purged them, and every scrub invocation
printed a spurious "Unknown command" error.
Fuzzy-filters audio/video files under the current directory via fd,
previews them with thumbnails pulled from the freedesktop thumbnail
cache (or ffprobe metadata as a fallback), and plays the selection
through the best available player: --player flag, $play_media_player,
xdg-mime default, then a short mpv/vlc fallback list. Adds mpv/vlc as
optional fish-deps entries.
The only other cd left in the workflow mixed two working directories
in one step: docs/build-manual.py --site runs from the repo root, then
npm ci/astro build need docs/site. Split into two steps so each can
use working-directory instead, keeping the whole file cd-free and
consistent with the Cloudflare deploy step's fix.
Gitea/GitHub Actions auto-groups a multi-line run: script under
"Run <first line>". With `cd docs/site` as the first line, the
collapsed log tree showed that instead of the actual wrangler deploy
command. Moving the directory change to the step's working-directory
key drops cd from the script entirely, so the group label now reflects
the command that's actually running.
workflow_dispatch already ran the whole pipeline manually, but there
was no way to fire just one job (e.g. re-run docs generation without
re-running the fish test suite) the way the old standalone
build-docs.yml let you. Add a job choice input (all/test/build-docs,
defaulting to all) and gate each job on it via `if:`, while leaving
the push-triggered path's needs: test gating untouched.
Two more sources of CI noise/fragility alongside the debconf frontend
fix: fish's install pulls in man-db/groff-base/xsel as recommends,
triggering a slow mandb rebuild for tooling nothing here uses;
--no-install-recommends skips that. apt-get update had no retry
policy, so a transient blip against the PPA mirror failed the whole
job; -o Acquire::Retries=3 gives it a few chances first.
apt-get install was probing for a Dialog then Readline debconf frontend
before falling back to Teletype on the non-interactive CI runner,
adding noise and failed-negotiation log lines to every run. Passing
DEBIAN_FRONTEND=noninteractive directly on the sudo command line (env
vars set via step-level `env:` don't survive sudo's env_reset) skips
the negotiation and goes straight to the frontend that actually works
here.