From 6ec1d534a22b412b29a1eeb4ef72e43f426d03f6 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Fri, 7 Aug 2026 11:17:34 +0200 Subject: [PATCH 1/2] Document the release flow now that main is protected Direct pushes to main are rejected, so the version bump goes through a pull request. That changes the commit the tag has to point at, since merging creates a new one. --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 49968ff..69364f8 100644 --- a/README.md +++ b/README.md @@ -159,35 +159,69 @@ of the three secrets is missing. ### Cutting a release -1. Bump `version` in `main.go`. The tag you push next must match it exactly, or +`main` is protected, so the version bump goes through a pull request. + +1. Bump `version` in `main.go` to `0.1.0`. The tag must match it exactly, or the release workflow refuses to build. -2. Commit and push: +2. Commit it on a branch and push: ```sh + git checkout -b version-0.1.0 git commit -am "Version 0.1.0" - git push + git push -u origin version-0.1.0 + ``` + +3. Open a pull request from that branch, wait for `build` to pass, then merge + it. + +4. Go back to `main` and pull. Merging created a new commit, and the tag has to + point at that one: + + ```sh + git checkout main + git pull + ``` + +5. Check that `main` carries the version you expect: + + ```sh + grep '^const version' main.go ``` -3. Tag that commit and push the tag: +6. Tag and push: ```sh git tag v0.1.0 git push origin v0.1.0 ``` -4. Wait for the release workflow to finish. It builds static binaries for +7. Wait for the release workflow to finish. It builds static binaries for Linux, macOS, and Windows on amd64 and arm64, publishes the GitHub release with checksums, updates the Homebrew tap and the Scoop bucket, pushes to the AUR, and opens a winget pull request. -5. Write the release notes into the release body on GitHub. It starts empty by +8. Write the release notes into the release body on GitHub. It starts empty by design. Follow the shape in CLAUDE.md. -6. Check that the winget pull request opened against `microsoft/winget-pkgs`. +9. Check that the winget pull request opened against `microsoft/winget-pkgs`. It is the one step GoReleaser reports without failing the run, so a failure there is easy to miss. +### Testing the pipeline + +A tag with a prerelease suffix publishes a GitHub prerelease and skips every +package manager, so it exercises the whole workflow without shipping anything. +Bump `version` to `0.0.2-rc1` through a pull request as above, then: + +```sh +git tag v0.0.2-rc1 +git push origin v0.0.2-rc1 +``` + +Tags cannot be deleted or moved, so pick a throwaway patch version rather than +the one you intend to release. + ## Repository layout ```sh From b68d74589171ea5445defc47771368fb365be8e9 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Fri, 7 Aug 2026 11:22:23 +0200 Subject: [PATCH 2/2] Cut the README back to structure and steps Rationale, workflow descriptions, and the token explanations move into CLAUDE.md, which is where anything that explains rather than instructs belongs. Also syncs the coding principles with superstack-next verbatim and notes each repo in the other. --- CLAUDE.md | 56 +++++++++--- README.md | 251 +++++++++++++++++------------------------------------- 2 files changed, 120 insertions(+), 187 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5801ec9..8f70ce1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,11 +3,18 @@ Guidance for working in this repository. This file holds **only** high-level coding and operational principles plus the architectural overview as it takes shape. It must never describe product functionality or per-feature behaviour. -The README.md holds the project-structure overview, the build instructions, -and the release process. +The README.md holds the project-structure overview and the setup instructions +for local development and releasing. -**This repository will be public.** Never commit secrets, tokens, or customer -data: the entire git history ships when the repository opens. +**This repository is public.** Never commit secrets, tokens, or customer data: +the entire git history ships. + +**Built in tandem with the server.** The server lives at +`~/projects/superstack-next` (github.com/siliconwitchery/superstack-next) and +owns the JSON API this binary speaks. A change on either side of the wire +usually implies one on the other, so read its CLAUDE.md before changing +anything that crosses it. The two files share the coding principles below +verbatim; a change to one belongs in both. ## Coding principles @@ -16,7 +23,9 @@ General and meant to be reused verbatim across projects. - **Complete names.** Use descriptive, whole-word names for non-trivial variables (`tuiWidth`, not `boxW`). Short names are acceptable only for receivers, loop indices, `err`, `ok`, and a framework's own idiomatic short - names, which stay reserved for that exact framework type. + names: `w` for an `http.ResponseWriter`, `r` for an `*http.Request`. Keep + those reserved for that exact type: a reader that is not an `*http.Request` + is `reader`, not `r`. - **Breathing room.** Separate a statement that produces a value from the statement that consumes it with a blank line. For example: an assignment, a blank line, then the `if err != nil` check. Group code into readable @@ -49,6 +58,14 @@ General and meant to be reused verbatim across projects. - **Table-driven tests.** Express tests as a table of input → expected cases iterated in a loop, not as repeated near-identical assertions. +## Operational principles + +- Build and run with cgo disabled for a static, dependency-free binary: + `CGO_ENABLED=0 go build`. Run `go vet` and `go test` with `CGO_ENABLED=0` + too, so neither reaches for a C toolchain that need not exist. +- Keep the dependency set small: nothing a distribution's packager would balk + at. + ## Architecture `main.go` is the composition root. It holds one table of sections and commands, @@ -65,14 +82,6 @@ Targets are flags rather than positions. Every command that touches devices takes `--fleet` or `--device`, so an operation that can apply to one device or to many stays one verb instead of splitting into a noun-verb pair per level. -## Operational principles - -- Build and run with cgo disabled for a static, dependency-free binary: - `CGO_ENABLED=0 go build`. Run `go vet` and `go test` with `CGO_ENABLED=0` - too, so neither reaches for a C toolchain that need not exist. -- Keep the dependency set small: nothing a distribution's packager would balk - at. - ## Releases The version lives in one place, `version` in `main.go`. Bump it, then tag the @@ -108,6 +117,20 @@ running `goreleaser release --snapshot` rather than by rebuilding the same targets by hand, so the release workflow is never the first thing to exercise that config. +Every publisher carries `skip_upload: auto`, so a tag with a prerelease suffix +publishes a GitHub release and touches no package manager. That is how the +pipeline gets exercised without shipping. It leaves the four publisher pushes +themselves untested, which only a real tag proves. + +Three credentials sit behind the release, and the workflow's guard checks only +that they are present, never that they work. `TAP_GITHUB_TOKEN` is +fine-grained and scoped to the tap and the bucket. `WINGET_GITHUB_TOKEN` has to +be a classic token, because a fine-grained one cannot reach a repository +outside its resource owner and so cannot open the pull request against +Microsoft. `AUR_KEY` is a passphraseless SSH key registered with an AUR +account. GoReleaser reports a failed winget pull request without failing the +run, so that step needs checking by hand after a release. + The generated changelog is deliberately disabled, so a fresh release starts with an empty body. Write the notes into it afterwards; GoReleaser keeps an existing body and will not overwrite them on a re-run. @@ -139,3 +162,10 @@ security bullet when nothing was fixed. After adding or changing a major feature, re-read this file and update it so the principles and the architectural overview stay accurate. + +Keep the README minimal as the project grows: the structure overview and the +setup steps, nothing else. Numbered imperative steps, self-contained command +blocks, constraints stated bare. No rationale, no explanation of how something +works, no troubleshooting. Anything that explains rather than instructs belongs +in this file, and a step that needs a paragraph to justify it is a sign the +step itself is wrong. diff --git a/README.md b/README.md index 69364f8..4b43c6e 100644 --- a/README.md +++ b/README.md @@ -2,169 +2,126 @@ `superstack` is the command line interface to Superstack: sign in, claim devices, push Lua code, and stream events and logs from your fleet. It is a -single static binary talking to the Superstack server's JSON API. - -## Install - -Every option below needs a published release to exist first. - -### Linux and macOS +single static binary talking to the Superstack server's JSON API. The server is +a separate project; this repo is the CLI only. It is laid out as follows: ```sh -curl -fsSL https://raw.githubusercontent.com/siliconwitchery/superstack-cli/main/install.sh | sh -``` - -To install a specific version instead: - -```sh -curl -fsSL https://raw.githubusercontent.com/siliconwitchery/superstack-cli/main/install.sh | VERSION=1.2.3 sh +├── .github/dependabot.yml # Weekly action and module update PRs +├── .github/workflows # CI on pull requests, release on v* tags +├── .gitignore +├── .goreleaser.yaml # Build matrix and every publishing target +├── CLAUDE.md # Coding principles and architectural overview +├── flake.lock # Pins nixpkgs +├── flake.nix # The superstack package, and the dev shell +├── go.mod +├── install.sh # curl-to-shell installer for Linux and macOS +├── LICENSE +├── main.go # Entry point, version constant, and the command table +├── main_test.go +└── README.md ``` -### macOS, with Homebrew +## Install -```sh -brew install --cask siliconwitchery/tap/superstack -``` +Each option needs a published release. -### Windows, with winget +- **Linux and macOS.** Pin a version by putting `VERSION=1.2.3` on the `sh` + side of the pipe. -```sh -winget install SiliconWitchery.Superstack -``` + ```sh + curl -fsSL https://raw.githubusercontent.com/siliconwitchery/superstack-cli/main/install.sh | sh + ``` -### Windows, with Scoop +- **Homebrew:** -```sh -scoop bucket add siliconwitchery https://github.com/siliconwitchery/scoop-bucket -scoop install superstack -``` + ```sh + brew install --cask siliconwitchery/tap/superstack + ``` -### Arch Linux +- **winget:** -```sh -yay -S superstack-bin -``` + ```sh + winget install SiliconWitchery.Superstack + ``` -### Nix +- **Scoop:** -Superstack is not in nixpkgs yet. Until it is, install it from this flake: + ```sh + scoop bucket add siliconwitchery https://github.com/siliconwitchery/scoop-bucket + scoop install superstack + ``` -```sh -nix profile install github:siliconwitchery/superstack-cli -``` +- **Arch Linux:** -Or run it without installing anything: + ```sh + yay -S superstack-bin + ``` -```sh -nix run github:siliconwitchery/superstack-cli -``` +- **Nix.** Not in nixpkgs yet. -### Any platform, by hand + ```sh + nix profile install github:siliconwitchery/superstack-cli + ``` -Download an archive for your platform from the -[releases page](https://github.com/siliconwitchery/superstack-cli/releases), -unpack it, and move `superstack` somewhere on your `PATH`. +- **Any platform.** Download an archive from the + [releases page](https://github.com/siliconwitchery/superstack-cli/releases), + unpack it, and move `superstack` onto your `PATH`. -## Build +## Local development 1. Install [Nix](https://nixos.org) with flakes enabled. -2. Clone the repository: - - ```sh - git clone git@github.com:siliconwitchery/superstack-cli.git - cd superstack-cli - ``` - -3. Enter the dev shell: +1. Clone the repository and enter the dev shell: ```sh + git clone git@github.com:siliconwitchery/superstack-cli.git ~/projects/superstack-cli + cd ~/projects/superstack-cli nix develop ``` -4. Build and run: +1. Build and run: ```sh go build -o superstack . ./superstack ``` -## Release +## Release setup -### One-time setup +Do everything below once. -Every step here is required before the first release. A release fails if any -of the three secrets is missing. +1. Create public repositories `siliconwitchery/homebrew-tap` and + `siliconwitchery/scoop-bucket`, each with a README. -#### Homebrew and Scoop - -1. Create a public repository `siliconwitchery/homebrew-tap`, ticking "Add a - README file" so it has a default branch. - -2. Create a public repository `siliconwitchery/scoop-bucket`, ticking "Add a - README file" so it has a default branch. +1. Fork `microsoft/winget-pkgs` into `siliconwitchery`. -3. Go to Settings > Developer settings > Personal access tokens > Fine-grained - tokens, and generate a token with: +1. Add a fine-grained token (Settings > Developer settings > Personal access + tokens) as the Actions secret `TAP_GITHUB_TOKEN`: - Resource owner: `siliconwitchery` - - Repository access: only `homebrew-tap` and `scoop-bucket` + - Repository access: `homebrew-tap` and `scoop-bucket` - Permissions: Contents, read and write -4. In this repository, go to Settings > Secrets and variables > Actions > New - repository secret. Name it `TAP_GITHUB_TOKEN` and paste the token in. +1. Add a classic token with the `public_repo` scope as the Actions secret + `WINGET_GITHUB_TOKEN`. A fine-grained token cannot open the pull request. -#### winget - -1. Fork `microsoft/winget-pkgs` into `siliconwitchery`. - -2. Go to Settings > Developer settings > Personal access tokens > Tokens - (classic), and generate a token with the `public_repo` scope. It has to be - a classic token, because a fine-grained token cannot open a pull request - against a repository outside its resource owner. - -3. In this repository, add it as an Actions secret named - `WINGET_GITHUB_TOKEN`. - -#### AUR - -1. Register an account at - [aur.archlinux.org/register](https://aur.archlinux.org/register). - -2. Generate an SSH key with no passphrase: +1. Register at [aur.archlinux.org](https://aur.archlinux.org/register), then: ```sh ssh-keygen -t ed25519 -N "" -f aur_key - ``` - -3. Print the public key and paste it into "SSH Public Key" in your AUR account - settings: - - ```sh - cat aur_key.pub - ``` - -4. Print the private key and add it to this repository as an Actions secret - named `AUR_KEY`: - - ```sh - cat aur_key - ``` - -5. Delete both key files: - - ```sh + cat aur_key.pub # paste into SSH Public Key in your AUR account settings + cat aur_key # add as the Actions secret AUR_KEY rm aur_key aur_key.pub ``` -### Cutting a release +1. Add one ruleset (Settings > Rules > Rulesets) targeting the default branch: + require a pull request with 0 approvals, require the `build` status check, + block force pushes, restrict deletions. Add a second targeting `v*` tags: + block force pushes, restrict deletions. -`main` is protected, so the version bump goes through a pull request. +## Releasing -1. Bump `version` in `main.go` to `0.1.0`. The tag must match it exactly, or - the release workflow refuses to build. - -2. Commit it on a branch and push: +1. Bump `version` in `main.go`, then open and merge a pull request: ```sh git checkout -b version-0.1.0 @@ -172,73 +129,19 @@ of the three secrets is missing. git push -u origin version-0.1.0 ``` -3. Open a pull request from that branch, wait for `build` to pass, then merge - it. - -4. Go back to `main` and pull. Merging created a new commit, and the tag has to - point at that one: - - ```sh - git checkout main - git pull - ``` - -5. Check that `main` carries the version you expect: +1. Merging creates a new commit, and the tag has to point at that one: ```sh + git checkout main && git pull grep '^const version' main.go - ``` - -6. Tag and push: - - ```sh git tag v0.1.0 git push origin v0.1.0 ``` -7. Wait for the release workflow to finish. It builds static binaries for - Linux, macOS, and Windows on amd64 and arm64, publishes the GitHub release - with checksums, updates the Homebrew tap and the Scoop bucket, pushes to - the AUR, and opens a winget pull request. - -8. Write the release notes into the release body on GitHub. It starts empty by - design. Follow the shape in CLAUDE.md. - -9. Check that the winget pull request opened against `microsoft/winget-pkgs`. - It is the one step GoReleaser reports without failing the run, so a failure - there is easy to miss. - -### Testing the pipeline - -A tag with a prerelease suffix publishes a GitHub prerelease and skips every -package manager, so it exercises the whole workflow without shipping anything. -Bump `version` to `0.0.2-rc1` through a pull request as above, then: - -```sh -git tag v0.0.2-rc1 -git push origin v0.0.2-rc1 -``` - -Tags cannot be deleted or moved, so pick a throwaway patch version rather than -the one you intend to release. - -## Repository layout +1. Write the release notes into the empty release body on GitHub, following the + shape in CLAUDE.md. -```sh -├── .github/dependabot.yml # Weekly action and module update PRs -├── .github/workflows # CI on PRs and pushes to main, release on v* tags -├── .gitignore -├── .goreleaser.yaml # Build matrix and every publishing target -├── CLAUDE.md # Coding principles and release conventions -├── flake.lock # Pinned nixpkgs for the package and the dev shell -├── flake.nix # The superstack package, and the dev shell -├── go.mod -├── install.sh # curl-to-shell installer for Linux and macOS -├── LICENSE -├── main.go # Entry point, version constant, and the command table -├── main_test.go -└── README.md -``` +1. Check that the winget pull request opened against `microsoft/winget-pkgs`. -CI runs `goreleaser release --snapshot` on every pull request, so a mistake in -`.goreleaser.yaml` fails there rather than halfway through a tagged release. +A tag carrying a prerelease suffix, `v0.0.2-rc1`, publishes a GitHub prerelease +and skips every package manager. Tags cannot be moved or deleted.