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.
- About this repository
- Project layout
- Development
- Testing
- Branches
- Commits
- Labels
- Pull Requests
- Merge strategy
- Branch protection
- Releases
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.
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/.
- 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).
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- 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.
A POSIX pre-commit script lives under .githooks/. It is not installed automatically — opt in with:
git config core.hooksPath .githooksOnce 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 test
This 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-verifyCI 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.
cargo test # run everything
cargo test --test config_tests # one file
cargo test --test worktree_integration # libgit2 integration
cargo test -- --nocapture # see println from testsTest-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.
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.
- 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.
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.
Format: <emoji> <type>(<scope>)<!>: <subject> (Gitmoji + Conventional Commits).
| 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 |
| Emoji | Type |
|---|---|
| ✨ | feat |
| 🐛 | fix |
| 🚑️ | hotfix |
| 📝 | docs |
| ♻️ | refactor |
| ⚡ | perf |
| ✅ | test |
| 🔧 | chore |
| 🏗️ | build |
| 👷 | ci |
| 🔥 | chore (remove) |
| ⬆️ | chore (bump deps) |
| 🔒 | security |
config, naming, worktree, bootstrap, cli, tui, tests, docs, ci, structure.
✨ 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
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.
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 keyTo 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.pubVerify 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 validSee .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
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).
- 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-branchmain 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),benches,flake version (nix eval), CodeRabbit and GitGuardian are not required. The first is advisory by design; the last 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.benchesandflake version (nix eval)are the odd ones out: they are ours and they do always run, they are simply newer than this table (#634, #672). Promoting them means editing the protection, so until that happens a red one fails the pull request's checks without blocking thedevtomainmerge.
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.
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.
Before any RC or stable cut, run:
gh pr list --state openEvery 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.
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, …
Once the rc is validated and promoted to main:
-
Step 0 first — see above.
-
Update
Cargo.tomlversion, then regenerate the lock, which carries the root package's version too:cargo update --workspace --offline # rewrites only the workspace entrySkipping it costs the next two steps:
git add Cargo.lockadds nothing, and the capture run'scargo build --release --lockedrefuses to start (cannot update the lock file ... because --locked was passed). -
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.) -
Commit the bump, then open the PR. Name the files:
git commit -amwill not pick upchangelogs/<version>.md, which step 3 has just created and git does not track yet, andrelease.ymlhard-fails at tag time without it (see the release-notes rule above). The PR comes before the captures because the capture run needs it open, see step 5.git add Cargo.toml Cargo.lock CHANGELOG.md changelogs/ git commit -m "🔖 chore(release): vX.Y.Z" git push gh pr create --base main --head dev --title "Release v0.x.y" --body "…"
-
Regenerate the doc captures, in the checkout the release is cut from, with step 4 committed and its PR open:
./docs/_capture/generate.sh git add docs && git commit -m "📸 docs(release): regenerate the captures for vX.Y.Z" && git push
One entry point, and the order inside it is three constraints rather than a preference (#631):
- The bump comes first. 17 of the 24 tapes open the TUI, whose header paints a
gwm X.Y.Zchip fromCARGO_PKG_VERSION. A set regenerated before the bump advertises the previous version for the whole life of the release. v1.8.0 shipped that way and v1.10.0 nearly did. - The binary comes from the tree, not from
PATH. The script builds it and putstarget/releasefirst. Without that, agwmleft over from another worktree renders captures that exist, size correctly and show the wrong UI: v1.10.0 came within a commit of publishing a set 175 commits stale.version-stamp.tapechecks that it took: it asks vhs what it actually resolved, both the version and the path, and aborts unless the file is the one cargo just built. The path is the half that matters, since a stale build carrying the same version number answers the version check perfectly. github-linking.tapephotographs a real checkout of this repo. The TUI pane follows the selected row, and that row is the repo's main checkout wherever the tape was launched from (measured: run from a clean linked worktree, the capture came back showing the trunk's branch and the trunk's uncommitted files). So the tape is pointed at the trunk, it runs first, and it runs only when the trunk is clean and its branch has an open PR. That is the whole reason step 4 opens the PR before this step runs: with the steps the other way round, the trunk has no open PR at capture time, the script skips the tape and the published capture silently stays on the previous release. A skip is announced, not silent, but the run still ends on a tick. The corollary is that this capture documents the trunk, which is one more reason to cut the release there rather than in a worktree.
It needs vhs 0.12.1 or later: check
vhs --versionbefore cutting. 0.12.0 runs a tape to the end, exits 0 and writes nothing at all (charmbracelet/vhs#787). Installed here on 2026-09-10, it went unseen until the v1.11.0 cut, where each tape the run reached failed twice, each failure read as a slow write, until the theme gallery's own check stopped the run. The script now stops in its preflight whenversion-stamp.tapelands the stamp, which the shell writes, but not its own gif, which only vhs writes.This stays a local step:
vhs, the Nerd Font and the demo fixture are not on a CI runner, and a workflow committing binaries into the release PR buys nothing over the preflight below.The preflight is
cargo test --test docs_assets_tests. The run writes thegwm --versionit captured with todocs/_capture/captured-version.txt, andcaptures_were_generated_at_the_manifest_versionfails as soon as that stops matchingCargo.toml;the_capture_run_keeps_its_orderpins the sequence above, which was prose until each constraint had bitten once. Both run in CI on the release PR, so the tag cannot be cut from a set that documents another version.If the machine cutting the release has no
vhs, no Nerd Font or no demo fixture, that preflight is a hard block on a protected branch. The escape hatch is to editdocs/_capture/captured-version.txtby hand and say so in the PR: it is one line, it shows up in the diff, and it is the release note admitting the captures still show the previous version. - The bump comes first. 17 of the 24 tapes open the TUI, whose header paints a
-
Wait for the required checks on the PR from step 4, 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 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 |
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.
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.
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.0The workflow_dispatch path is gated to the same stable-only condition; rc/alpha/beta will skip the tap step automatically.
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 gwmThe 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).
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>.
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-bingwm-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).
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/PKGBUILDThe 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.
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.
- 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.
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.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.
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.
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 siteThis 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.