Contributing to gwm
August 14, 2026 · View on GitHub
Thanks for your interest in gwm — a Rust CLI / TUI for managing git worktrees across projects. This file describes the conventions used here. They mirror the ones used in fiches-pedagogiques-api-rest so the muscle memory is the same.
Table of contents
- About this repository
- Project layout
- Development
- Testing
- Branches
- Commits
- Labels
- Pull Requests
- Merge strategy
- Branch protection
- Releases
About this repository
gwm is a single-binary Rust crate (bin + reusable lib):
- bin
gwm— entry point: dispatches to subcommands (CLI) or opens the TUI. - lib
gwm— modules (aliases,bootstrap,clean,cli,command_log,config,config_cli,daemon,doctor,error,exec,github,gitmoji,history,hooks,issue_templates,json_api,labels,launcher,lifecycle,milestones,multiplexer,naming,pr_templates,presets,review,statusline,sync,templating,trust,tui,workspace,worktree) exposed publicly so integration tests intests/can drive them directly.
It uses git2 (vendored libgit2) for worktree operations and ratatui for the TUI.
Project layout
gwm-cli/
├── Cargo.toml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── docs/ # full documentation tree (README delegates here)
├── examples/
│ ├── gwm.toml.example
│ └── presets/ # embedded `gwm init --preset` bodies
├── src/
│ ├── lib.rs # public re-exports
│ ├── main.rs # bin entry point
│ ├── error.rs
│ ├── config.rs # .gwm.toml parsing
│ ├── config_cli.rs # `gwm config get/set/...` plumbing
│ ├── naming.rs # branch / path conventions
│ ├── worktree.rs # libgit2 worktree ops
│ ├── bootstrap.rs # copies / guards / shell hooks
│ ├── lifecycle.rs # [hooks.*] lifecycle phases
│ ├── hooks.rs # git hook install (commit-msg auto-prefix)
│ ├── trust.rs # TOFU ledger gating bootstrap (issue #95)
│ ├── doctor.rs # 8 health checks for `gwm doctor`
│ ├── github.rs # gh shell-out + issue / PR linking
│ ├── labels.rs # declarative GitHub label set (issue #81)
│ ├── milestones.rs # declarative GitHub milestone set (issue #82)
│ ├── launcher.rs # `l` / `r` TUI launcher resolution (issue #75)
│ ├── multiplexer.rs # tmux / zellij window+split helpers
│ ├── presets.rs # `gwm init --preset` stack registry (issue #37)
│ ├── review.rs # `gwm review <PR#>` (issue #308)
│ ├── sync.rs # `gwm sync` fetch + rebase / merge
│ ├── exec.rs # `gwm exec ... -- <cmd>` fleet runner (issue #313)
│ ├── clean.rs # `gwm clean` artifact reclaim (issue #313)
│ ├── workspace.rs # multi-repo workspace mode (issue #36)
│ ├── daemon.rs # JSON-RPC unix-socket daemon (issue #38)
│ ├── json_api.rs # `--format=json` rendering
│ ├── statusline.rs # `gwm statusline` daemon consumer (issue #309)
│ ├── history.rs # destructive-op journal + `gwm undo`
│ ├── command_log.rs # TUI command log buffer
│ ├── aliases.rs # [aliases] argv expansion
│ ├── gitmoji.rs # branch-type → :shortcode: map
│ ├── templating.rs # placeholder substitution
│ ├── issue_templates.rs# `gwm new` issue templating
│ ├── pr_templates.rs # `gwm pr` body rendering
│ ├── cli.rs # clap subcommands
│ └── tui/
│ ├── mod.rs # event loop
│ ├── app.rs # state
│ ├── ui.rs # rendering
│ ├── keymap.rs # global keymap (rebindable)
│ ├── modal_keymap.rs # per-context modal keys
│ ├── palette.rs # command palette
│ ├── theme.rs # theme presets / roles
│ ├── wt_tree.rs # working-tree file tree
│ ├── commit_graph.rs # recent-commits view
│ └── state/ # per-view state machines
│ ├── async_task.rs
│ ├── command_logs.rs
│ ├── config_panel.rs
│ ├── confirm.rs
│ ├── create_form.rs
│ ├── filter.rs
│ ├── github_fetch.rs
│ ├── link_prompt.rs
│ ├── pty_overlay.rs
│ ├── sidebar.rs
│ └── spinner.rs
└── tests/ # one `*_tests.rs` / `*_integration.rs` per module
├── common/ # shared helpers (init_repo, paths_equal)
├── config_tests.rs
├── naming_tests.rs
├── bootstrap_tests.rs
├── trust_tests.rs # ledger load/save/lookup/record/revoke (issue #95)
├── worktree_integration.rs
├── tui_app_tests.rs
└── cli_binary.rs # assert_cmd end-to-end
All tests live under tests/ — no inline #[cfg(test)] mod tests blocks inside src/.
Development
Prerequisites
- Rust toolchain (stable channel, 1.95+ — the MSRV declared in
Cargo.toml, raised byrusqlite's bundledlibsqlite3-sysand verified on every PR by CI'smsrvjob). - A C compiler (libgit2 is vendored and built from source on first
cargo build).
Build & run
git clone https://github.com/kbrdn1/gwm-cli.git
cd gwm-cli
cargo build # builds bin + lib
cargo run -- list # smoke test the CLI
cargo run # opens the TUI in the current repo
cargo install --path . # install gwm into ~/.cargo/bin
Code style
- Indentation: 2 spaces (matches
fiches-pedagogiquesconvention). - Formatter:
cargo fmt(project usesrustfmtdefaults except indent). - Linter:
cargo clippy -- -D warnings. - Run
cargo fmt && cargo clippybefore opening a PR.
Local hooks (recommended, opt-in)
A POSIX pre-commit script lives under .githooks/. It is not installed automatically — opt in with:
git config core.hooksPath .githooks
Once enabled, two gates run on every git commit:
-
Env-dependent test pre-validation. If staged
tests/*.rshunks reference ambient state (assert_cmd,std::env::var,which::which,dirs::,Command::cargo_bin), the hook re-runs the suite under a stripped PATH:PATH="$(dirname "$(command -v cargo)"):/usr/bin:/bin" cargo testThis catches tests that pass in your rich dev shell but fail on a minimal CI runner — the lesson from PR #43 (three CI round-trips before the suite went green).
-
Local
gwm doctor. If staged paths touch.gwm.toml,src/bootstrap.rs,src/doctor.rs,examples/gwm.toml.example, ortests/{bootstrap,doctor}*, the hook runsgwm doctor. Exit codes follow the doctor contract:Exit Meaning Commit behaviour 0Clean proceeds silently 1Warnings proceeds with advisory 2Errors blocked until resolved If
gwmis not onPATH, the gate prints a skip notice and the commit proceeds — the CIdoctorjob is the safety net.
Both gates short-circuit in O(1) when no staged paths match — contributors who never touch tests or config pay nothing per commit.
Bypass for a single commit you know is safe:
git commit --no-verify
CI runs shellcheck against the hook and a smoke test on every PR — see the hook-smoke job in ci.yml — so a broken hook is caught before it reaches you.
Testing
cargo test # run everything
cargo test --test config_tests # one file
cargo test --test worktree_integration # libgit2 integration
cargo test -- --nocapture # see println from tests
🔴 TDD is mandatory — non-negotiable
Test-Driven Development is the primary contribution rule of this repo. No production code lands without a failing test that pinned the behaviour down first. This is not a guideline, it is a hard merge requirement. PRs that add or change behaviour without tests are sent back, full stop.
The loop is red → green → refactor:
- Red — write a failing test capturing the new behaviour (or the bug you are fixing). Run it. It MUST fail for the right reason (assertion mismatch, not a compile error in unrelated code).
- Green — write the minimum production code that turns the test green. No speculative abstractions.
- Refactor — clean up under green tests. Re-run the suite after each refactor step.
Where the test lives:
- unit logic (config parsing, naming, kebab, guard regex) → tests in the matching
tests/*_tests.rsfile. - disk side effects (file copy, symlink removal, command exec) → use
tempfile::TempDir. - git operations → use
tests/common::init_repo()which gives you a fresh repo onmainwith one commit. - public CLI surface → end-to-end test in
tests/cli_binary.rsviaassert_cmd. - bootstrap stages (copy, guard, no-symlink, command) →
tests/bootstrap_tests.rs. - TUI state transitions → ratatui-free state-machine tests in
tests/tui_app_tests.rs.
Exceptions (must be argued in the PR description)
The bar to skip a test is "observably untestable from the public surface":
- Pure formatting / typo fixes in incidental strings (not asserted anywhere).
- Dependency bumps with no behaviour change (CI green is the test).
- Comment-only changes.
Everything else needs a test. "I tested it manually" is not an exception — codify it as an integration test.
Enforcement
- Reviewers run
git log --stat <branch>..HEAD -- tests/. If the touched module has no companion test diff and the change isn't one of the exceptions above, the PR is blocked. - The
## Testschecklist in the PR template is binding. Do not tickcargo testunless it actually ran green locally. tests/cli_binary.rs::help_prints_subcommandsis the canary — update it whenever a new CLI subcommand is added.
Branches
Main branches:
main— what ships. Direct commits allowed for trivial maintenance (typos, docs, dep bumps). Anything user-visible goes through a PR.- Feature branches:
<type>/#<issue-number>-<short-description>.
Examples: feat/#12-tui-search, fix/#45-locked-worktree-detection, docs/#3-update-readme.
gwm itself uses this exact convention via gwm create feat 12 tui-search.
Commits
Format: <emoji> <type>(<scope>)<!>: <subject> (Gitmoji + Conventional Commits).
Types
| Type | When |
|---|---|
feat | new feature |
fix | bug fix |
hotfix | critical production bug fix |
refactor | code restructuring, no behaviour change |
docs | documentation only |
test | adding / fixing tests |
perf | performance improvement |
chore | repo maintenance (deps, config, scripts) |
ci | CI / GitHub Actions changes |
build | build system, Cargo manifest |
Emojis (Gitmoji)
| Emoji | Type |
|---|---|
| ✨ | feat |
| 🐛 | fix |
| 🚑️ | hotfix |
| 📝 | docs |
| ♻️ | refactor |
| ⚡ | perf |
| ✅ | test |
| 🔧 | chore |
| 🏗️ | build |
| 👷 | ci |
| 🔥 | chore (remove) |
| ⬆️ | chore (bump deps) |
| 🔒 | security |
Scopes (optional, used in this repo)
config, naming, worktree, bootstrap, cli, tui, tests, docs, ci, structure.
Examples
✨ feat(tui): add fuzzy search on worktree list🐛 fix(worktree): handle is_prunable error gracefully🔧 chore(deps): bump ratatui to 0.29♻️ refactor(bootstrap): extract guard-matching into pure fn✅ test(naming): cover unicode descriptions
Breaking changes
Suffix the type with ! and add a BREAKING CHANGE: footer:
✨ feat(config)!: replace `[[bootstrap.copy]]` with `[[steps]]`
BREAKING CHANGE: configs using the old keys must migrate to the new schema.
Signing (preferred)
Commits on a PR should show up as Verified on GitHub. GPG is preferred;
SSH signing is equally accepted (GitHub verifies both the same way).
This is a preference, not a gate: nothing in CI or branch protection enforces it, and a PR will not be rejected for unsigned commits. It is asked for because a signed history is worth having, not because tooling demands it.
Signing a commit and getting it verified are two different things. GitHub
shows Verified only when both hold:
- the public key is registered on your GitHub account (Settings → SSH and GPG keys)
- the committer email matches a uid on the key and a verified email on your account
The second one is what usually bites. A commit signed with a perfectly good key
whose uid does not match the committer email stays Unverified forever. If you
use different user.email values across repos, check before you push:
git config user.email # the committer email git will stamp
gpg --list-secret-keys --keyid-format=long # the uid(s) on your key
To turn signing on for this repo only:
git config user.signingkey <KEY_ID>
git config commit.gpgsign true
# SSH instead of GPG:
git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519.pub
Verify what GitHub actually thinks, which is the only opinion that counts here
(local git log --show-signature can disagree with it, e.g. on a keyring it
cannot read):
gh api /repos/<owner>/<repo>/commits/<sha> \
--jq '.commit.verification | "\(.verified) \(.reason)"' # want: true valid
Labels
See .github/LABELS.md for the full matrix. Quick reference:
- type:
feature,fix,hotfix,docs,test,refactor,chore,perf,ci,build - status:
duplicate,invalid,wontfix - domain:
cli,tui,config,worktree,bootstrap,security,dependencies
Pull Requests
Before opening a PR:
-
cargo fmt -
cargo clippy -- -D warnings -
cargo test(all green) - Commits show as
Verifiedon GitHub (preferred, see Signing) - CHANGELOG.md updated under
## [Unreleased] - If the public CLI changed: the
docs/3.clisection updated (the README is a landing page that delegates todocs/) - If the config schema changed:
examples/gwm.toml.exampleand thedocs/4.configurationsection updated
Use the PR template (.github/PULL_REQUEST_TEMPLATE.md).
Merge strategy
- Never squash. Use a regular merge commit so the atomic commit history (with its
feat/fix/refactorlabels) is preserved onmain. - Never delete the source branch after merge. Keeps traceability and lets us cherry-pick / revert.
gh pr merge <num> --merge # NOT --squash, NOT --delete-branch
Branch protection
main is protected. Nothing reaches it except through a pull request with green
checks, and that includes the maintainer: enforce_admins is on, so
git push origin main is rejected outright and there is no admin override. The
only way to lift it is to disable the protection by hand, which should be a
deliberate, visible act rather than a reflex.
Active rules (read them with gh api repos/kbrdn1/gwm-cli/branches/main/protection):
| Rule | Value |
|---|---|
| Require a pull request | yes, 0 approvals |
| Required status checks | rustfmt, clippy, test (ubuntu-latest), test (macos-latest), test (windows-latest), pre-commit hook smoke, cargo audit |
Require branches up to date (strict) | no |
| Enforce for admins | yes |
| Require linear history | no |
| Force pushes / deletions | blocked |
Three of those are counter-intuitive and are set that way on purpose:
- 0 required approvals, not 1. This is a single-maintainer repo and GitHub forbids approving your own pull request, so requiring one approval would be a permanent lockout. The status checks are the real gate; the PR is the rail that makes sure they run.
- Linear history off. Turning it on would force squash or rebase merges and break Merge strategy. The atomic commit history is the artefact, so merge commits have to stay legal.
gwm doctor (advisory), CodeRabbit and GitGuardian are not required. The first is advisory by design; the other two are third-party and can stop reporting. A required check that never reports blocks the branch forever, so only checks we own and that always run are in the list.
strict is off because main gains a merge commit that dev does not have on
every release; requiring "up to date" would force a back-merge into dev before
each cut, for no added safety since the checks re-run on the PR anyway.
This does not affect releases mechanically: release.yml and pre-release.yml
are triggered by tags, and protection guards branch refs, not tags. It does
change how dev reaches main (see below), and it means a hotfix cannot go
straight to main either (see Step 0):
branch off main, open a PR back into it, let the checks run.
Releases
Versioning is SemVer (MAJOR.MINOR.PATCH), with -rc.N / -alpha.N / -beta.N suffixes for pre-releases cut from dev.
MAJOR→ breaking changeMINOR→ new featurePATCH→ bug fix-rc.N/-alpha.N/-beta.N→ release candidate / alpha / beta cut fromdevbefore promotion tomain
What a "breaking change" actually covers — the published 1.0 compatibility contract (which surfaces are covered by this SemVer promise, which are free to change in a minor/patch, the MSRV policy, and the deprecation process) — lives in Stability & compatibility.
Step 0 — Reconcile open PRs (applies to every tag)
Before any RC or stable cut, run:
gh pr list --state open
Every open PR must be in exactly one of these buckets:
- In the changeset — merged into the source branch (
devfor RCs / stables,mainfor hotfixes) before tagging. - Intentionally deferred — won't make this release, will land in a later one. Note why in the release notes if it was a known candidate.
- Closed as stale — superseded, obsolete, or duplicate. Close with a one-line comment pointing at the supersession.
Skipping this step caused the v0.3.0 cut to ship without three queued feature PRs (#51, #52, #53). Recovery required an immediate v0.4.0 promotion 38 minutes later. Two minutes upfront beats a follow-up release.
Pre-release (from dev)
When dev is ready to be exercised by early adopters before promotion:
- Step 0 first — see above.
- Stay on
dev(do not merge tomainyet). - Write per-RC notes in a new file
changelogs/pre-releases/<version>-rc.N.md— heading# [<version>-rc.N] - YYYY-MM-DD, body describing only the delta against the previous RC (or against the previous stable, forrc.1). One file per RC, not a running log. (Seechangelogs/pre-releases/0.3.0-rc.2.mdfor the expected layout.) - Add the entry to
CHANGELOG.md's## Past releases > ### Pre-releasesindex. - Tag:
git tag -a v0.x.y-rc.N -m "v0.x.y-rc.N" && git push --tags. - GitHub Actions (
pre-release.yml) builds binaries and publishes a prerelease (5 targets — Linux x86_64 + aarch64, macOS Intel + Apple Silicon, Windows x86_64). The release body is populated from the per-RC file via--notes-file changelogs/pre-releases/<version>-rc.N.md(rungh release edit <tag> --notes-file <path>after the workflow if you need to refresh it). - Iterate: subsequent candidates are
v0.x.y-rc.2,v0.x.y-rc.3, …
Stable release (from main)
Once the rc is validated and promoted to main:
-
Step 0 first — see above.
-
Update
Cargo.tomlversion. -
Move the
## [Unreleased]section out ofCHANGELOG.mdinto a new filechangelogs/<version>.md(e.g.changelogs/0.3.0.md), rename its heading to# [<version>] - YYYY-MM-DD, and add a one-line entry at the bottom ofCHANGELOG.md's## Past releasesindex pointing to the new file.CHANGELOG.mdat the root then only carries the next## [Unreleased]section. (Seechangelogs/0.2.0.mdfor the expected layout.) -
Open a PR from
devtomain, wait for the required checks, then merge it with a merge commit (never squash; see Merge strategy).mainis protected: a localgit push origin mainis rejected, including for the maintainer, so there is no direct-merge path.gh pr create --base main --head dev --title "Release v0.x.y" --body "…" gh pr merge <num> --merge # once the 7 checks are green -
Tag the merge commit on
main:git checkout main && git pull && git tag -a v0.x.y -m "v0.x.y" && git push --tags. Tags are not covered by the branch protection, so this push goes through as-is. -
GitHub Actions (
release.yml) builds binaries and publishes the stable release. The release body is populated fromchangelogs/<version>.mdvia--notes-file(rungh release edit v0.x.y --notes-file changelogs/<version>.mdafter the workflow if needed).
⚠️ Finalise the crate identity before the tag. Any change to the crates.io package identity — the
[package] name, or aversionbump — must land in the same commit the tag points at, socargo publishfrom that tag is reproducible. Thev1.0.0tag carriedname = "gwm"; the rename togwm-cli(the namegwmwas already taken on crates.io) landed two commits later, so the publishedgwm-cli@1.0.0is not reachable by checking outv1.0.0. If a rename or identity change is ever needed again, do it in step 2 (alongside theversionbump), before the merge + tag — not after.
Triggering matrix:
| Tag pattern | Workflow | prerelease flag |
|---|---|---|
v0.x.y | release.yml | false |
v0.x.y-rc.N | pre-release.yml | true |
v0.x.y-alpha.N | pre-release.yml | true |
v0.x.y-beta.N | pre-release.yml | true |
Homebrew tap (brew install kbrdn1/tap/gwm)
Stable releases automatically refresh kbrdn1/homebrew-tap (Formula/gwm.rb) via the homebrew-tap-update job in release.yml. Pre-releases (-rc.N / -alpha.N / -beta.N) are filtered out so brew install gwm always tracks the latest stable.
The canonical formula source lives at packaging/homebrew/gwm.rb.template. Edits to the template (new shell completion call, license bump, extra test do block) flow to the tap on the next stable release — no manual sync needed.
One-time bootstrap (maintainer)
The job needs a fine-grained personal access token (PAT) with contents: write scoped to the tap repo. Create it once:
- Generate a PAT at https://github.com/settings/personal-access-tokens/new:
- Resource owner: your user (or the org owning
homebrew-tap). - Repository access: select
kbrdn1/homebrew-taponly. - Permissions: Contents → Read and write. Nothing else.
- Expiration: ≥ 1 year (set a calendar reminder to rotate).
- Resource owner: your user (or the org owning
- Add it as a secret on the
gwm-clirepo:- https://github.com/kbrdn1/gwm-cli/settings/secrets/actions/new
- Name:
HOMEBREW_TAP_TOKEN. Value: the PAT.
- Flip
continue-on-error: truetofalseon thehomebrew-tap-updatejob inrelease.ymlafter the first successful sync — failures should then block the workflow loudly.
Re-running after a failed sync
If the job failed (typically: PAT missing or expired) after the GitHub release already shipped, re-drive the tap refresh without re-tagging:
gh workflow run release.yml --ref <tag> # e.g. v0.5.0
The workflow_dispatch path is gated to the same stable-only condition; rc/alpha/beta will skip the tap step automatically.
Scoop bucket (scoop install gwm)
Stable releases automatically refresh kbrdn1/scoop-gwm (bucket/gwm.json) via the scoop-bucket-update job in release.yml, mirroring the Homebrew tap. Pre-releases are filtered out so scoop install gwm always tracks the latest stable. End users add the bucket once:
scoop bucket add gwm https://github.com/kbrdn1/scoop-gwm
scoop install gwm
The canonical manifest source lives at packaging/scoop/gwm.json.template; the render + Scoop-autoupdate contract is pinned by tests/scoop_manifest_tests.rs. Only the __FOO__ placeholders are substituted at release time — the Scoop $version / $url autoupdate variables are left verbatim so Scoop's maintainer-side checkver/excavator tooling can regenerate the manifest. End users get new versions from scoop update gwm once the scoop-bucket-update job pushes the refreshed bucket/gwm.json, so keep the job green (that is what the client actually pulls).
One-time bootstrap (maintainer)
Same shape as the Homebrew tap:
- Create the
kbrdn1/scoop-gwmrepo (abucket/gwm.json+ README). - Generate a fine-grained PAT scoped to
kbrdn1/scoop-gwmonly, Contents → Read and write. - Add it as the
SCOOP_BUCKET_TOKENsecret ongwm-cli: https://github.com/kbrdn1/gwm-cli/settings/secrets/actions/new. - Flip
continue-on-error: truetofalseon thescoop-bucket-updatejob after the first successful sync.
Re-drive a failed sync the same way: gh workflow run release.yml --ref <tag>.
AUR (yay -S gwm-cli-bin)
This channel is manual, and the package is not ours. gwm-cli-bin was submitted to the AUR on 2026-07-16 by a third-party packager, so we have no push rights on it. There is no aur-publish job in release.yml: one existed briefly, but a job that cannot push is a job that fails silently on every tag, which is worse than no job at all (#430). AUR joins Nixpkgs and aqua as a channel we feed by hand.
End users install with any AUR helper:
yay -S gwm-cli-bin # or: paru -S gwm-cli-bin
gwm-cli-bin is a prebuilt-binary package (downloads the linux-gnu tarball, verifies its sha256, installs the binary + both license texts + bash/zsh/fish completions).
Refreshing the package after a stable release
Render the PKGBUILD from the release's checksums and hand it over:
TAG=v1.2.0 # the stable tag you just pushed
mkdir -p sha aur
gh release download "$TAG" --pattern 'gwm-*-unknown-linux-gnu.tar.gz.sha256' --dir sha
sh .github/scripts/render-aur-pkgbuild.sh \
"${TAG#v}" \
"$(awk '{print \$1}' "sha/gwm-${TAG}-x86_64-unknown-linux-gnu.tar.gz.sha256")" \
"$(awk '{print \$1}' "sha/gwm-${TAG}-aarch64-unknown-linux-gnu.tar.gz.sha256")" \
packaging/aur/PKGBUILD.template \
> aur/PKGBUILD
The script writes to stdout, hence the redirect; aur/ is scratch space, not tracked. The render contract is pinned by tests/aur_pkgbuild_tests.rs, so the output is trustworthy even though nothing in CI consumes it any more. Lint aur/PKGBUILD locally before sending (makepkg + namcap in an archlinux container). The x86_64→$CARCH namcap warning on the arch-suffixed source_* arrays is a known false positive ($CARCH is illegal in an array name).
If co-maintenance of gwm-cli-bin is ever granted, the job can come back: the template, the render script and its tests all survived the removal intact. That conversation is tracked in #430.
winget (winget install kbrdn1.gwm)
This channel is manual. A winget-publish job existed briefly (#381) and was removed in #448: WINGET_TOKEN was never provisioned, so its guard step painted a red job on every stable release run, and the channel is blocked upstream anyway — the initial kbrdn1.gwm manifest PR (microsoft/winget-pkgs#403295) sits on Needs-CLA, and komac update can only update a package that already exists in winget-pkgs. Automation for a channel that cannot publish is a job that can only fail. winget joins the AUR, Nixpkgs and aqua as a channel fed by hand; the absence of the job is pinned by a test in tests/release_workflow_tests.rs.
Unblocking the channel (one-time)
- Sign Microsoft's CLA on microsoft/winget-pkgs#403295 (comment
@microsoft-github-policy-service agree) and get the initial manifest merged. Every submission goes through Microsoft's moderated validation (schema + a Windows sandbox install), which is external to this repo. - Keep the
microsoft/winget-pkgsfork under your account —komacpushes its PR branches there.
Refreshing the package after a stable release
Once the initial manifest is merged, submit each new version with komac (a classic PAT with the public_repo scope — komac's fork + cross-repo-PR flow does not work with fine-grained tokens). The token rule from the removed job still applies to the manual flow: komac is the binary that holds the PAT, so run a pinned, digest-verified komac, not whatever PATH happens to find. Bump the version and digest together, re-deriving the digest yourself (shasum -a 256 <tarball>):
KOMAC_VERSION=2.16.0
KOMAC_SHA256=7d2707fa6210f2789a3702de49fbd150b736dbf426ee0b9bc8e098736f9fd82d # x86_64-unknown-linux-gnu
tarball="komac-${KOMAC_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
gh release download "v${KOMAC_VERSION}" --repo russellbanks/Komac --pattern "$tarball" --dir /tmp/komac
echo "${KOMAC_SHA256} /tmp/komac/${tarball}" | shasum -a 256 -c -
tar -C /tmp/komac -xzf "/tmp/komac/${tarball}"
TAG=v1.3.0 # the stable tag you just pushed
GITHUB_TOKEN=<classic PAT> KOMAC_FORK_OWNER=kbrdn1 \
/tmp/komac/komac update kbrdn1.gwm \
--version "${TAG#v}" \
--urls "https://github.com/kbrdn1/gwm-cli/releases/download/${TAG}/gwm-${TAG}-x86_64-pc-windows-msvc.zip" \
--submit
(On macOS, swap the x86_64-unknown-linux-gnu triple for your platform's komac artifact and re-derive its digest.)
The tag's v prefix is stripped to match the winget PackageVersion, and the manifest keeps the shape of the initial submission (InstallerType: zip, NestedInstallerType: portable). Never submit a pre-release tag.
If the channel is unblocked and the manual flow proves routine, the job can come back — deleting the pin test is the first step of that change, and the removed job (pinned, digest-anchored komac; see the git history of release.yml at #448) is the starting point.
Documentation site (https://gwm-docs.kbrdn.dev)
The published documentation follows main, not dev. Every push to main that touches docs/ or changelogs/ fires docs-sync.yml, which sends a repository_dispatch to kbrdn1/kbrdn-docs; that repo replays the conversion into its Starlight site, commits the drift and deploys.
main is the right trigger precisely because it is only ever reached through a dev → main pull request: what lands there is what was delivered. dev would publish pages describing behaviour nobody can install yet, and a tag trigger would be worse still, since GitHub's v*.*.* glob matches -rc.N too and every release candidate's docs would go live as if they were stable. tests/release_workflow_tests.rs pins both the branch and the watched paths.
Nothing about the site lives in this repo: the conversion script, the Starlight theme and the Cloudflare Pages deploy all belong to kbrdn-docs. This side owns the Markdown sources and the bell.
One asymmetry is worth knowing before you go looking for it: docs/index.md and docs/fr/index.md are not ported. The site has its own hand-written landing pages, which the sync deliberately preserves. Editing either file therefore fires the workflow, produces no drift and finishes green without changing anything online. Every other page under docs/, plus changelogs/, does travel.
One-time bootstrap (maintainer)
Same shape as the Homebrew tap, with a token scoped to the docs repo:
- Generate a fine-grained PAT at https://github.com/settings/personal-access-tokens/new:
- Repository access: select
kbrdn1/kbrdn-docsonly. - Permissions: Contents → Read and write. Nothing else: that is the documented requirement for
POST /repos/{owner}/{repo}/dispatches. - Expiration: ≥ 1 year (set a calendar reminder to rotate).
- Repository access: select
- Add it as the
DOCS_SITE_TOKENsecret ongwm-cli: https://github.com/kbrdn1/gwm-cli/settings/secrets/actions/new. - Land
sync-gwm.ymlon the default branch ofkbrdn-docs. This one is easy to miss because nothing reports it: GitHub only ever runs arepository_dispatchreceiver from the default branch, so while that file sits on a feature branch the API call returns204, this workflow goes green, and nothing syncs. There is no failure anywhere to notice. - Flip
continue-on-error: truetofalseon thenotifyjob indocs-sync.ymlafter the first successful dispatch.
Note that this is a write credential for a private repository, held in a public repository's secrets. It sits at the same trust level as HOMEBREW_TAP_TOKEN and SCOOP_BUCKET_TOKEN, which push to repositories of their own.
The deploy itself needs three more settings, all on kbrdn-docs and none of them here: the CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID secrets, the DEPLOY_ENABLED repository variable, and a Cloudflare Pages project named gwm-docs. Until they exist the sync still runs and commits, but the deploy job skips. The custom domain is attached to the Pages project itself; gwm-docs.pages.dev stays served alongside it and cannot be removed, which is why site: in sites/gwm/astro.config.mjs pins the custom one: that value is what ships in the sitemap and the canonical URLs.
Re-running a missed sync
The dispatch is a single API call with no state, so re-drive it from either end:
gh workflow run docs-sync.yml --ref main # from gwm-cli
gh workflow run sync-gwm.yml --ref main -R kbrdn1/kbrdn-docs # or straight at the site
License
This project is dual-licensed under either the MIT license (LICENSE-MIT) or
the Apache License, Version 2.0 (LICENSE-APACHE), at the user's option.
Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in this project, as defined in the Apache-2.0 license, shall be dual-licensed as above, with no additional terms or conditions.
That sentence is the whole of it: there is no CLA to sign and nothing to send. It exists so the project cannot end up in the state where one file is under a narrower license than the rest, which is what makes a relicense impossible later.