From 61aa9d23837cba43732952fb5292af9f5ac80feb Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Thu, 22 Oct 2026 18:45:10 -0500 Subject: [PATCH 1/3] Add a Claude skill for cutting State Tool releases. Records the release flow (master after the last beta merge-back -> release branch -> beta -> release), the changelog convention, and the post-merge staging, Windows signing, S3 promotion and merge-back steps. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/release-state-tool/SKILL.md | 144 +++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .claude/skills/release-state-tool/SKILL.md diff --git a/.claude/skills/release-state-tool/SKILL.md b/.claude/skills/release-state-tool/SKILL.md new file mode 100644 index 0000000000..8527186b7c --- /dev/null +++ b/.claude/skills/release-state-tool/SKILL.md @@ -0,0 +1,144 @@ +--- +name: release-state-tool +description: Cut a State Tool beta (RC) or stable release from this repo — branch from master after the previous beta was merged back, write the changelog, open the PR into beta (or beta into release), and walk the post-merge staging → Windows signing → S3 promotion → merge-back steps. Use when asked to "cut a beta", "make a release", "ship an RC", "release the state tool", or "promote beta to release". +--- + +# Releasing the State Tool + +A release follows on from the previous one on `master`: + +``` + ┌──────────── merge back after each beta ◀────────────┐ + ▼ │ +master ─▶ release branch (master + changelog) ─PR▶ beta ─PR▶ release + version.txt = X.Y.Z-RCn (beta channel) (stable channel) +``` + +- When a beta ships, its branch is merged back into `master` (`Merge branch 'version/X-Y-Z-RCn'`). + `master` then carries the next RC in `version.txt` (e.g. `0.48.2-RC1` once 0.48.1 has gone out). +- Feature PRs merge to `master` after that point. The next release is **`master` as of now**: the + last merge-back plus everything merged since. +- Branch names use dashes: version `0.48.2-RC1` → `version/0-48-2-RC1`. +- Each RC that ships to beta bumps `n`. An attempt that never reached beta keeps its number. +- Keep unrelated changes, such as edits to this skill, out of the release PR. They go to + `master` on their own branch. + +## Before starting: read the state + +```bash +git fetch origin +for b in master beta release; do echo "== $b: $(git show origin/$b:version.txt)"; git log -1 --format='%h %ad %s' --date=short origin/$b; done +git log --first-parent --format='%h %ad %s' --date=short origin/master | head -15 # find the last "Merge branch 'version/...'" +gh pr list --base beta --state open # stale RC PRs must be closed, not merged by accident +``` + +What the release ships is the first-parent commits on `master` after the last +`Merge branch 'version/...'`. List them: + +```bash +git log --first-parent --oneline ..origin/master +``` + +**Don't use `origin/beta..origin/master` to work out what ships.** The RC branches reach `master` +as merges with different hashes, so that range lists dozens of commits that have already shipped. + +Confirm with the user which version is being cut (`X.Y.Z-RCn`) and which changes it ships +before you push anything. + +## 1. Prepare the release branch + +Work in a separate worktree, not someone's checkout. Branch from `master`: + +```bash +git worktree add ~/work/cli-X-Y-Z -b /X-Y-Z-RCn origin/master +cd ~/work/cli-X-Y-Z +``` + +- `version.txt` must read exactly `X.Y.Z-RCn`. If not, fix it in its own commit ("Update version.txt"). +- **Changelog:** add a `## X.Y.Z` section above the previous release in `changelog.md`, + in Keep-a-Changelog form (`### Added / Changed / Fixed / Security`). Write it from the + bodies of the commits listed above. Describe what users will notice, leave out test and + CI-only commits, and don't describe internals. There is one section per X.Y.Z; later RCs + of the same version edit it. +- The release branch holds the `master` commits plus the changelog commit, and nothing else. +- `version/*` branches are protected, and a direct push is rejected with "protected branch hook + declined". Push the branch under your own name. Whoever has rights can push it as + `version/X-Y-Z-RCn` if the PR-time branch-channel deploy is wanted (see step 2). + +## 2. PR into beta + +```bash +gh pr create --base beta --head /X-Y-Z-RCn --title "X.Y.Z-RCn" --body "" +``` + +`gh pr edit` can fail silently in this repo because of a Projects (classic) error. +To change the title, body or base, use `gh api -X PATCH repos/ActiveState/cli/pulls/` +and read the result back. + +Any PR into `beta` runs the full integration suite on all five OS runners +(`.github/workflows/build.yml`). A branch-channel deploy happens at PR time only when the head +branch is named `version/*`. The release doesn't need that deploy, because the push to `beta` +after merging is what stages it. Red integration tests block the release. Look at a failure +before re-running it; some suites are flaky on macOS and Windows. Fixes land on `master` and the +release branch is rebuilt from it. + +**Merging this PR is the release decision. Do not merge without the user's go-ahead.** + +## 3. After merge: staging + +The push to `beta` runs the Deploy job with `DEPLOY_TO_STAGING=true`. That uploads updates and +installers to `s3://state-tool/staging/update/state/…` and the remote installer to +`staging/remote-installer`. Users see nothing yet. Check that the run on `beta` is green: + +```bash +gh run list -R ActiveState/cli --branch beta --limit 3 +``` + +## 4. Sign Windows binaries + +This is a manual step, and how it is done is not recorded in this repo. Ask the release owner +(historically mitchell-as) and write the procedure here once it is known. + +## 5. Promote staging to production + +Run this from a checkout of the merged `beta`, with AWS credentials for the `state-tool` bucket: + +```bash +state run promote-staging-to-production # interactive y/N prompt +``` + +`scripts/ci/s3-promoter` **moves** staged objects to production. How far it reaches depends on +the `promote-staging-to-production` line in that checkout's `activestate.yaml`: + +- `... s3-promoter/main.go us-east-1 state-tool update/state` (`master` since 589d443b4) moves + only `staging/update/state/`. That covers every channel staged there, so if `release` was + also staged and not yet promoted, it goes live too. `staging/remote-installer` is not moved. +- `... s3-promoter/main.go us-east-1 state-tool` (older branches) moves everything under `staging/`. + +Either way, list what is staged before you run it: + +```bash +aws s3 ls --recursive s3://state-tool/staging/ | head -50 +``` + +## 6. Verify + +```bash +sh <(curl -q https://platform.activestate.com/dl/cli/install.sh) -b beta +~/.local/ActiveState/StateTool/beta/bin/state --version # expect X.Y.Z-RCn +``` + +An existing install can switch with `state update --set-channel beta`. + +## 7. Merge back to master + +Merge the released branch back into `master` (`Merge branch 'version/X-Y-Z-RCn'`), so the changelog +and any RC-only fixes are on `master`. Then bump `master`'s `version.txt` to the next RC +(`X.Y.(Z+1)-RC1`, or `X.Y.Z-RC(n+1)` if another RC of the same version is expected). That +merge-back is the starting point for the next release. + +## Stable release (beta → release) + +Once a beta has soaked, open a PR with `--base release --head beta`, titled `X.Y.Z`. Merging it +stages under `staging/`, and steps 4–6 then run the same way, using `-b release` when you verify. +Close stale `version/*` → `beta` PRs. From 5ebe3b921607e6f26e91f7d960ce9398e810b140 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Fri, 23 Oct 2026 12:54:53 -0500 Subject: [PATCH 2/3] Release skill: rewrite from the 0.48.2-RC1 release. Releases continue the last RC branch with cherry-picks from master; they are not cut from master or beta. Records the protected version branches, the beta sync merge, the CI failures hit on 0.48.2 and their fixes, the Trivy scan gate, CI tooling quirks, and the S3 checks. Steps after the beta merge are marked unverified. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/release-state-tool/SKILL.md | 234 +++++++++++++-------- 1 file changed, 151 insertions(+), 83 deletions(-) diff --git a/.claude/skills/release-state-tool/SKILL.md b/.claude/skills/release-state-tool/SKILL.md index 8527186b7c..17c56ee136 100644 --- a/.claude/skills/release-state-tool/SKILL.md +++ b/.claude/skills/release-state-tool/SKILL.md @@ -1,144 +1,212 @@ --- name: release-state-tool -description: Cut a State Tool beta (RC) or stable release from this repo — branch from master after the previous beta was merged back, write the changelog, open the PR into beta (or beta into release), and walk the post-merge staging → Windows signing → S3 promotion → merge-back steps. Use when asked to "cut a beta", "make a release", "ship an RC", "release the state tool", or "promote beta to release". +description: Cut a State Tool beta (RC) or stable release from this repo — continue the last release's version branch, cherry-pick the master changes it ships, write the changelog, get the version branch green and in sync with beta, open the PR into beta, then stage, sign, promote and verify. Use when asked to "cut a beta", "make a release", "ship an RC", "release the state tool", or "promote beta to release". --- # Releasing the State Tool -A release follows on from the previous one on `master`: +> **Status:** steps 0–6 were worked through for 0.48.2-RC1 (PROD-240). Steps 7–10 (staging, +> Windows signing, promotion, verification, merge-back) come from the CI config and +> `activestate.yaml`, and have **not** been run end to end yet. Confirm them with the release +> owner and update this file after the next release. + +## How the branches fit together ``` - ┌──────────── merge back after each beta ◀────────────┐ - ▼ │ -master ─▶ release branch (master + changelog) ─PR▶ beta ─PR▶ release - version.txt = X.Y.Z-RCn (beta channel) (stable channel) +version/0-48-1-RC4 ──(+ version.txt bump)──▶ version/0-48-2-RC1 ──PR▶ beta ──PR▶ release + ▲ last release ▲ cherry-picks of the master changes + └── merged back into master └── merged back into master after release ``` -- When a beta ships, its branch is merged back into `master` (`Merge branch 'version/X-Y-Z-RCn'`). - `master` then carries the next RC in `version.txt` (e.g. `0.48.2-RC1` once 0.48.1 has gone out). -- Feature PRs merge to `master` after that point. The next release is **`master` as of now**: the - last merge-back plus everything merged since. -- Branch names use dashes: version `0.48.2-RC1` → `version/0-48-2-RC1`. -- Each RC that ships to beta bumps `n`. An attempt that never reached beta keeps its number. -- Keep unrelated changes, such as edits to this skill, out of the release PR. They go to - `master` on their own branch. - -## Before starting: read the state +- **A release continues the previous release's version branch, not `master` and not `beta`.** + After a release, the next `version/X-Y-Z-RC1` is cut from the previous RC's tip plus a + one-line `version.txt` bump, and merged into `master`. The bump commit is "Update version.txt". +- **`master` and the RC line have diverged.** `master` carries unreleased `master`-only work + dating back about a year, plus merge-backs of every RC branch. **Never merge `master` into a + version branch.** That ships all of the unreleased work, and the PR shows close to 100 commits. + Cherry-pick only the changes this release ships. Feature PRs land on `master` as single squash + commits, so `git cherry-pick -x ` works. +- `beta`'s files match the last RC's exactly, but `beta` has one extra commit: the merge commit of + the last `version/* → beta` PR. See step 4. +- Branch names use dashes: version `0.48.2-RC1` → `version/0-48-2-RC1`. Each RC that ships bumps + `n`; an attempt that never reached beta keeps its number. +- `version/*` branches are **protected**: a direct push is rejected ("protected branch hook + declined"), and so is GitHub's "Update branch" ("Changes must be made through a pull request"). + Every change goes through a PR from a personal branch. +- This skill and other unrelated changes stay out of the release. They go to `master` on their + own branch. + +## 0. Read the state ```bash git fetch origin for b in master beta release; do echo "== $b: $(git show origin/$b:version.txt)"; git log -1 --format='%h %ad %s' --date=short origin/$b; done -git log --first-parent --format='%h %ad %s' --date=short origin/master | head -15 # find the last "Merge branch 'version/...'" -gh pr list --base beta --state open # stale RC PRs must be closed, not merged by accident +git branch -r | grep 'version/' | sort -V | tail -3 # last RC branch, and the next one if it exists +git log --first-parent --format='%h %ad %s' --date=short origin/master | head -15 +gh pr list -R ActiveState/cli --base beta --state open # close stale RC PRs so nobody merges them ``` -What the release ships is the first-parent commits on `master` after the last -`Merge branch 'version/...'`. List them: +**What ships** is the first-parent commits on `master` after the last +`Merge branch 'version/...'`: ```bash git log --first-parent --oneline ..origin/master ``` -**Don't use `origin/beta..origin/master` to work out what ships.** The RC branches reach `master` -as merges with different hashes, so that range lists dozens of commits that have already shipped. +Don't use `origin/beta..origin/master` for this. The RC branches reach `master` as merges with +different hashes, so that range lists dozens of commits that have already shipped. -Confirm with the user which version is being cut (`X.Y.Z-RCn`) and which changes it ships -before you push anything. +Confirm the version (`X.Y.Z-RCn`) and the exact list of commits with the user before you push +anything. -## 1. Prepare the release branch +## 1. The version branch -Work in a separate worktree, not someone's checkout. Branch from `master`: +Check that `version/X-Y-Z-RCn` exists and is the last RC tip plus the `version.txt` bump: ```bash -git worktree add ~/work/cli-X-Y-Z -b /X-Y-Z-RCn origin/master -cd ~/work/cli-X-Y-Z +git log --oneline origin/version/..origin/version/X-Y-Z-RCn # expect only "Update version.txt" +git show origin/version/X-Y-Z-RCn:version.txt # exactly X.Y.Z-RCn ``` -- `version.txt` must read exactly `X.Y.Z-RCn`. If not, fix it in its own commit ("Update version.txt"). -- **Changelog:** add a `## X.Y.Z` section above the previous release in `changelog.md`, - in Keep-a-Changelog form (`### Added / Changed / Fixed / Security`). Write it from the - bodies of the commits listed above. Describe what users will notice, leave out test and - CI-only commits, and don't describe internals. There is one section per X.Y.Z; later RCs - of the same version edit it. -- The release branch holds the `master` commits plus the changelog commit, and nothing else. -- `version/*` branches are protected, and a direct push is rejected with "protected branch hook - declined". Push the branch under your own name. Whoever has rights can push it as - `version/X-Y-Z-RCn` if the PR-time branch-channel deploy is wanted (see step 2). +If it doesn't exist, ask someone with rights to create it. You can't push `version/*`. -## 2. PR into beta +## 2. PR the release contents into the version branch + +Work in a private worktree: ```bash -gh pr create --base beta --head /X-Y-Z-RCn --title "X.Y.Z-RCn" --body "" +git worktree add ~/work/cli-X-Y-Z -b /X-Y-Z-RCn origin/version/X-Y-Z-RCn +cd ~/work/cli-X-Y-Z +git cherry-pick -x ... # the agreed master commits, oldest first +git diff --stat origin/version/X-Y-Z-RCn HEAD # only those changes, plus the changelog ``` -`gh pr edit` can fail silently in this repo because of a Projects (classic) error. -To change the title, body or base, use `gh api -X PATCH repos/ActiveState/cli/pulls/` -and read the result back. +- **Changelog:** add a `## X.Y.Z` section above the previous release in `changelog.md` + (`### Added / Changed / Fixed / Security`, Keep-a-Changelog). Write it from the commit bodies. + Describe what users will notice, not internals, and leave out test and CI-only commits. Later RCs of + the same version edit that section rather than adding a new one. +- **Check locally before pushing.** The build needs the generated constants first: + ```bash + go run scripts/constants-generator/main.go -- internal/constants/generated.go # gitignored + go build ./cmd/... + go test -vet=off # CI and `state run test` use -vet=off + ``` + With vet on, the tests fail on old findings unrelated to your change. Off CI, + `internal/condition`, `internal/constants/preprocess` and `pkg/projectfile` also fail locally. +- Open the PR with `--base version/X-Y-Z-RCn`. Squash-merging it is fine. + +## 3. Get CI green + +A personal head branch runs **more** of CI than a `version/*` head does. Check Format and the unit +tests run only when the head isn't `version/*`, so problems surface on the PRs into the version branch. +The Scan job (Trivy) runs after all five OS jobs pass, and **it gates Deploy**. A red Scan means the +push to `beta` would build but never stage. + +Failures seen on 0.48.2, all unrelated to the release's own changes: + +| Symptom | Cause | Fix | +|---|---|---| +| Check Format fails on every OS with an empty log | `check-format`'s `grep -v` exits 1 on an empty diff and `set -e` aborts | `\|\| true` (fixed on the RC line in 0.48.2; `master` still has the bug, but master PRs exit before reaching it) | +| Scan: stdlib and `golang.org/x/net` CVEs | CVEs published since the last release | bump the `go` line in `go.mod` **and** the `build.yml` matrix `go-version`, `go get @`, `go mod tidy`, `go mod vendor`; add a changelog Security line | +| `internal/keypairs` tests fail intermittently after a Go bump | deep struct equality on `*rsa.PrivateKey` internals | compare with `rsa.PrivateKey.Equal` (fixed in 0.48.2) | +| `pkg/sysinfo` `TestPerformance/OSVersion` on `macos-latest` | the cold `sw_vers` call (100 ms–1 s) dominates a 10 ms average | larger budget on darwin (#3866). Before that, it failed 3 of 4 runs | + +Check Scan locally before pushing a Go or dependency bump. Use the same Trivy version as +`build.yml` and pass its `--ignore-unfixed`: -Any PR into `beta` runs the full integration suite on all five OS runners -(`.github/workflows/build.yml`). A branch-channel deploy happens at PR time only when the head -branch is named `version/*`. The release doesn't need that deploy, because the push to `beta` -after merging is what stages it. Red integration tests block the release. Look at a failure -before re-running it; some suites are flaky on macOS and Windows. Fixes land on `master` and the -release branch is rebuilt from it. +```bash +DOCKER_CONFIG=$(mktemp -d) trivy rootfs --scanners vuln --ignore-unfixed --exit-code 1 +``` + +Set `DOCKER_CONFIG` to an empty directory, or the vuln-DB download fails on a credential helper. +Without `--ignore-unfixed`, Trivy also lists GO-2026-5932 (`x/crypto/openpgp` unmaintained), which +has no fix and which CI ignores. -**Merging this PR is the release decision. Do not merge without the user's go-ahead.** +**CI tooling notes:** +- `gh pr checks` in this environment has no `--json`. Use + `gh api repos/ActiveState/cli/commits//check-runs`. +- `gh run view --log` refuses while any job in the run is still going. Use + `gh api repos/ActiveState/cli/actions/jobs//logs`. +- Unit test results are in the "Check if Unit Tests Failed" step, as go test JSON. Search for + `--- FAIL`. +- `gh pr edit` can fail silently because of a Projects (classic) error. Use + `gh api -X PATCH repos/ActiveState/cli/pulls/` and read the result back. -## 3. After merge: staging +## 4. Sync the version branch with beta -The push to `beta` runs the Deploy job with `DEPLOY_TO_STAGING=true`. That uploads updates and -installers to `s3://state-tool/staging/update/state/…` and the remote installer to -`staging/remote-installer`. Users see nothing yet. Check that the run on `beta` is green: +`beta` requires the PR branch to be up to date. The version branch lacks the last release's +`version/* → beta` merge commit, so the release PR shows **BEHIND**. Open a PR from a +personal branch that merges `beta` in. It changes no files. ```bash -gh run list -R ActiveState/cli --branch beta --limit 3 +git switch -c /X-Y-Z-RCn-sync-beta origin/version/X-Y-Z-RCn +git merge --no-ff -m "Merge branch 'beta' into version/X-Y-Z-RCn" origin/beta ``` -## 4. Sign Windows binaries - -This is a manual step, and how it is done is not recorded in this repo. Ask the release owner -(historically mitchell-as) and write the procedure here once it is known. - -## 5. Promote staging to production +**Merge that PR with "Create a merge commit", never squash.** A squash records no ancestry, so +the release PR stays BEHIND. Do this before the release PR's long CI run, because it restarts that run. +If the PR changes no files at all, it hits the Check Format bug above. Fix-up commits can ride +along with it. -Run this from a checkout of the merged `beta`, with AWS credentials for the `state-tool` bucket: +## 5. Release PR into beta ```bash -state run promote-staging-to-production # interactive y/N prompt +gh pr create -R ActiveState/cli --base beta --head version/X-Y-Z-RCn --title "X.Y.Z-RCn" --body "" ``` -`scripts/ci/s3-promoter` **moves** staged objects to production. How far it reaches depends on -the `promote-staging-to-production` line in that checkout's `activestate.yaml`: +It needs a review approval and green CI, including Scan. **Merging it is the release decision. Don't +merge without the user's go-ahead.** -- `... s3-promoter/main.go us-east-1 state-tool update/state` (`master` since 589d443b4) moves - only `staging/update/state/`. That covers every channel staged there, so if `release` was - also staged and not yet promoted, it goes live too. `staging/remote-installer` is not moved. -- `... s3-promoter/main.go us-east-1 state-tool` (older branches) moves everything under `staging/`. +## 6. Before merging: check S3 -Either way, list what is staged before you run it: +Check that staging is empty, so a promote moves only this release, and record what the beta channel +serves now: ```bash -aws s3 ls --recursive s3://state-tool/staging/ | head -50 +aws s3 ls --recursive s3://state-tool/staging/ | head # expect nothing +for p in linux-amd64 windows-amd64 darwin-arm64; do aws s3 cp s3://state-tool/update/state/beta/$p/info.json - | jq -c '{version}'; done ``` -## 6. Verify +`version` reads `X.Y.Z-RCn-SHA<7-char commit>`. Before 0.48.2 it was `0.48.1-SHA5650ae1`. + +## 7. After merge: staging (not yet verified) + +The push to `beta` runs Deploy with `DEPLOY_TO_STAGING=true`. That uploads updates and installers under +`s3://state-tool/staging/update/state/…` and the remote installer under `staging/remote-installer`. +Check the `beta` run is green (`gh run list -R ActiveState/cli --branch beta --limit 3`) and that +staging now holds the new build. + +## 8. Sign Windows binaries (unknown) + +This is a manual step that isn't recorded in the repo. Ask the release owner (historically mitchell-as) +and write the procedure here. + +## 9. Promote (not yet verified) + +Run from a checkout of the merged `beta`, with AWS credentials for `state-tool`: ```bash -sh <(curl -q https://platform.activestate.com/dl/cli/install.sh) -b beta -~/.local/ActiveState/StateTool/beta/bin/state --version # expect X.Y.Z-RCn +state run promote-staging-to-production # interactive y/N ``` -An existing install can switch with `state update --set-channel beta`. +`scripts/ci/s3-promoter` **moves** staged objects. How far it reaches depends on that checkout's +`promote-staging-to-production` line in `activestate.yaml`: +- `... state-tool`, the RC line as of 0.48.x: everything under `staging/`, including the + remote installer and any other staged channel. +- `... state-tool update/state`, `master` since 589d443b4: only `staging/update/state/`. The + remote installer then needs promoting separately. + +## 10. Verify and merge back (not yet verified) -## 7. Merge back to master +```bash +for p in linux-amd64 windows-amd64 darwin-arm64; do aws s3 cp s3://state-tool/update/state/beta/$p/info.json - | jq -c '{version}'; done +sh <(curl -q https://platform.activestate.com/dl/cli/install.sh) -b beta && ~/.local/ActiveState/StateTool/beta/bin/state --version +``` -Merge the released branch back into `master` (`Merge branch 'version/X-Y-Z-RCn'`), so the changelog -and any RC-only fixes are on `master`. Then bump `master`'s `version.txt` to the next RC -(`X.Y.(Z+1)-RC1`, or `X.Y.Z-RC(n+1)` if another RC of the same version is expected). That -merge-back is the starting point for the next release. +Then merge the version branch back into `master` (`Merge branch 'version/X-Y-Z-RCn'`). The next +release's version branch is cut from this RC's tip plus a `version.txt` bump. ## Stable release (beta → release) -Once a beta has soaked, open a PR with `--base release --head beta`, titled `X.Y.Z`. Merging it -stages under `staging/`, and steps 4–6 then run the same way, using `-b release` when you verify. -Close stale `version/*` → `beta` PRs. +Once a beta has soaked, a PR with `--base release --head beta`, titled `X.Y.Z`, as in #3852. Merging it +stages under `staging/`. Steps 8–10 then follow the same way, using `-b release` to verify. From 6c32174f026369b0294ab1410ca57fa42ad2236b Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Mon, 26 Oct 2026 20:44:22 -0500 Subject: [PATCH 3/3] Release skill: staging, TI signing, promote and verify from 0.48.2. --- .claude/skills/release-state-tool/SKILL.md | 112 +++++++++++++++++---- 1 file changed, 91 insertions(+), 21 deletions(-) diff --git a/.claude/skills/release-state-tool/SKILL.md b/.claude/skills/release-state-tool/SKILL.md index 17c56ee136..260837e594 100644 --- a/.claude/skills/release-state-tool/SKILL.md +++ b/.claude/skills/release-state-tool/SKILL.md @@ -5,10 +5,9 @@ description: Cut a State Tool beta (RC) or stable release from this repo — con # Releasing the State Tool -> **Status:** steps 0–6 were worked through for 0.48.2-RC1 (PROD-240). Steps 7–10 (staging, -> Windows signing, promotion, verification, merge-back) come from the CI config and -> `activestate.yaml`, and have **not** been run end to end yet. Confirm them with the release -> owner and update this file after the next release. +> **Status:** steps 0–10 were worked through for the 0.48.2-RC1 beta (PROD-240, 2026-10-23 to +> 10-26), except the merge back into `master`. The stable (beta → release) path hasn't been run +> with this skill yet. ## How the branches fit together @@ -110,6 +109,7 @@ Failures seen on 0.48.2, all unrelated to the release's own changes: | Scan: stdlib and `golang.org/x/net` CVEs | CVEs published since the last release | bump the `go` line in `go.mod` **and** the `build.yml` matrix `go-version`, `go get @`, `go mod tidy`, `go mod vendor`; add a changelog Security line | | `internal/keypairs` tests fail intermittently after a Go bump | deep struct equality on `*rsa.PrivateKey` internals | compare with `rsa.PrivateKey.Equal` (fixed in 0.48.2) | | `pkg/sysinfo` `TestPerformance/OSVersion` on `macos-latest` | the cold `sw_vers` call (100 ms–1 s) dominates a 10 ms average | larger budget on darwin (#3866). Before that, it failed 3 of 4 runs | +| `internal/hail` `TestOpen` on Windows: "The process cannot access the file because it is being used by another process" | the temp dir is still locked at cleanup | flake; re-run the failed job (`gh run rerun --failed`) | Check Scan locally before pushing a Go or dependency bump. Use the same Trivy version as `build.yml` and pass its `--ignore-unfixed`: @@ -157,6 +157,9 @@ gh pr create -R ActiveState/cli --base beta --head version/X-Y-Z-RCn --title "X. It needs a review approval and green CI, including Scan. **Merging it is the release decision. Don't merge without the user's go-ahead.** +#3864 (0.48.2-RC1) was squash-merged, as earlier releases were. So `beta` again has a commit the +next version branch lacks, and the next release repeats step 4. + ## 6. Before merging: check S3 Check that staging is empty, so a promote moves only this release, and record what the beta channel @@ -167,42 +170,109 @@ aws s3 ls --recursive s3://state-tool/staging/ | head # expect nothing for p in linux-amd64 windows-amd64 darwin-arm64; do aws s3 cp s3://state-tool/update/state/beta/$p/info.json - | jq -c '{version}'; done ``` -`version` reads `X.Y.Z-RCn-SHA<7-char commit>`. Before 0.48.2 it was `0.48.1-SHA5650ae1`. +The served `version` is `X.Y.Z-SHA<7-char commit>`. It has **no `-RCn`**: 0.48.2-RC1 shipped as +`0.48.2-SHA89954c6`, under `beta/0.48.2/`. Before that, beta served `0.48.1-SHA5650ae1`. + +## 7. After merge: staging + +The push to `beta` runs Deploy with `DEPLOY_TO_STAGING=true`. Wait for the run to be green +(`gh run list -R ActiveState/cli --branch beta --limit 3`). For 0.48.2 Deploy staged 37 objects: + +- `staging/update/state/beta//info.json` and `staging/update/state/beta///info.json` + (two copies of each platform's `info.json`, both written by `scripts/ci/update-generator`) +- `staging/update/state/beta///state---SHA.{tar.gz,zip}` +- `staging/update/state/beta//state-installer.{tar.gz,zip}`, the `install*.sh/ps1` and + `stop.*` scripts, and `staging/update/state/versions.json` +- `staging/remote-installer/beta/{,0.1.0/}/state-remote-installer[.exe]` + +```bash +aws s3 ls --recursive s3://state-tool/staging/ +``` + +## 8. Sign Windows binaries (TI team) + +The TI team holds the Authenticode certificate (ActiveState Software, Sectigo). File a Linear +issue for them; Linear has no TI team, so it goes on the Product team as a sub-issue of the +release story (0.48.2: PROD-1352, done by Pablo Bleck). Give them: -## 7. After merge: staging (not yet verified) +- the staged update zip's S3 path and its sha256 +- the four exes inside it to sign: `state-install/bin/state.exe`, `state-svc.exe`, `state-exec.exe` + and `state-install/state-installer.exe` -The push to `beta` runs Deploy with `DEPLOY_TO_STAGING=true`. That uploads updates and installers under -`s3://state-tool/staging/update/state/…` and the remote installer under `staging/remote-installer`. -Check the `beta` run is green (`gh run list -R ActiveState/cli --branch beta --limit 3`) and that -staging now holds the new build. +For 0.48.2 TI returned a re-signed zip of the same name in a `sign/` folder next to the original +(`staging/update/state/beta//windows-amd64/sign/`). Earlier releases (0.48.1) also signed +only the update zip: the beta zip, both channels' `state-installer.zip` and the remote installer +were not signed. -## 8. Sign Windows binaries (unknown) +**Check the returned zip** before using it: -This is a manual step that isn't recorded in the repo. Ask the release owner (historically mitchell-as) -and write the procedure here. +```bash +unzip -Z1 unsigned.zip | sort > a; unzip -Z1 signed.zip | sort > b; diff a b # same file list +osslsigncode verify -in state.exe # or check the PE certificate table; all 4 exes signed +# every non-.exe file byte-identical to the unsigned zip +``` + +**Then fix staging.** The updater checks the zip against `info.json`'s `sha256` +(`internal/updater/fetcher.go`, "sha256 did not match"), so a new zip without a new hash breaks +every Windows update: + +1. Save both Windows `info.json` files and keep the unsigned zip locally, so this can be undone. +2. Copy the signed zip over `staging/update/state/beta//windows-amd64/`. +3. Set `.sha256` to the signed zip's hash in **both** `staging/update/state/beta/windows-amd64/info.json` + and `staging/update/state/beta//windows-amd64/info.json` + (`jq --arg s $SHA '.sha256=$s'`, upload with `--content-type application/json`). Read both back. +4. Delete the `sign/` folder once its hash matches the copy at the normal path. The promoter moves + everything under `staging/`, so a leftover `sign/` ends up in production. + +## 9. Promote -## 9. Promote (not yet verified) +The promote changes what every beta user gets, so run it only on the user's go-ahead. Claude Code's +auto mode blocks it as a production deploy, so the user runs it (`! ` at the prompt). -Run from a checkout of the merged `beta`, with AWS credentials for `state-tool`: +Use a clean checkout of the merged `beta`. The promoter imports `internal/condition`, which needs the +gitignored generated constants, or it fails with `undefined: constants.ChannelName`: ```bash -state run promote-staging-to-production # interactive y/N +git worktree add --detach ~/work/cli-beta-promote origin/beta +cd ~/work/cli-beta-promote +go run scripts/constants-generator/main.go -- internal/constants/generated.go +go build -o /dev/null ./scripts/ci/s3-promoter/ # check it compiles first +state run promote-staging-to-production # interactive y/N, then the go run below +# equivalent, without the prompt: +go run scripts/ci/s3-promoter/main.go us-east-1 state-tool ``` -`scripts/ci/s3-promoter` **moves** staged objects. How far it reaches depends on that checkout's -`promote-staging-to-production` line in `activestate.yaml`: +It ends with `Successfully promoted files from staging to production.` An AWS SSO PowerUser +role was enough for it. + +`scripts/ci/s3-promoter` **moves** staged objects (copy, then delete the staging copy). How far +it reaches depends on that checkout's `promote-staging-to-production` line in `activestate.yaml`: - `... state-tool`, the RC line as of 0.48.x: everything under `staging/`, including the remote installer and any other staged channel. - `... state-tool update/state`, `master` since 589d443b4: only `staging/update/state/`. The remote installer then needs promoting separately. -## 10. Verify and merge back (not yet verified) +## 10. Verify and merge back + +Staging must now be empty. Check what the update service serves, which is what installs and +auto-updates read, and that the Windows binaries are signed: ```bash -for p in linux-amd64 windows-amd64 darwin-arm64; do aws s3 cp s3://state-tool/update/state/beta/$p/info.json - | jq -c '{version}'; done -sh <(curl -q https://platform.activestate.com/dl/cli/install.sh) -b beta && ~/.local/ActiveState/StateTool/beta/bin/state --version +aws s3 ls --recursive s3://state-tool/staging/ | wc -l # 0 +for pa in "linux amd64" "linux arm64" "darwin amd64" "darwin arm64" "windows amd64"; do set -- $pa + for src in install update; do + curl -sS "https://platform.activestate.com/sv/state-update/api/v1/info?channel=beta&source=$src&platform=$1&arch=$2" | jq -c '{version,sha256}' + done; done +curl -sSfLO https://state-tool.s3.amazonaws.com/update/state/beta//windows-amd64/ +sha256sum # = the windows info.json sha256 +unzip && osslsigncode verify -in state-install/bin/state.exe # each of the 4 exes +curl -sSfLO https://state-tool.s3.amazonaws.com/update/state/beta//linux-amd64/ +tar xzf && state-install/bin/state --version # Version X.Y.Z-SHA, Channel beta ``` +Running the downloaded binary checks the build without running `install.sh -b beta`, which edits +the user's shell profile and their real install. + Then merge the version branch back into `master` (`Merge branch 'version/X-Y-Z-RCn'`). The next release's version branch is cut from this RC's tip plus a `version.txt` bump.