ci: rewrite release workflow to be fully GitHub Actions-driven#25162
Conversation
Docs preview📖 View docs preview for |
49ec231 to
1e57c3b
Compare
|
Coder Agent Chat: agent finished or is awaiting input Chat: https://dev.coder.com/chats/0f7abc63-cfc3-4020-ab78-a455672e1cea |
Replace the local interactive release CLI (scripts/releaser/) and legacy shell scripts (scripts/release/) with a non-interactive, CI-oriented Go tool (scripts/release-action/) and move all release initiation into the release.yaml GitHub Actions workflow. Release managers now trigger releases from the GitHub Actions UI: - RC: provide a commit SHA on main, or a release branch name - Create release branch: provide a commit SHA, creates release/x.y and tags the next RC atomically (continuing the RC numbering sequence) - Release: provide a release branch name (e.g. release/2.32) The workflow handles version calculation, tag creation, release notes generation, building, and publishing automatically. New scripts/release-action/ Go CLI with three subcommands: - calculate-version: compute next version tag from git state (JSON output) - generate-notes: produce release notes from commit log + PR metadata - update-docs: modify release calendar and version pragmas The prepare-release job is idempotent: it detects existing tags and branches so failed runs can be safely retried. scripts/version.sh now uses git describe as a fallback so release branches resolve to the correct version series.
Remove the release_branch input. The workflow now uses github.ref_name (the branch selected in the 'Use workflow from' dropdown) to determine context. The calculate-version CLI takes --ref (required) and --commit (optional, defaults to HEAD of ref). This simplifies the UX: - RC: pick main or a release branch, choose rc, done - Release: pick a release branch, choose release, done - Create release branch: pick main, choose create-release-branch, done The tool validates the ref is appropriate for the release type and fails with a clear error if not (e.g. release from main).
- Fix main.go indentation for calculate-version Handler - Add _, _ = to all WriteString/Fprintf calls in notes.go and docs.go - Add nolint:gosec for WriteFile 0o644 permissions in docs.go - Remove unused lookupCommit type from github.go - Rewrite if-else chain to switch in calculate.go - Format markdown tables in CONTRIBUTING.md - Fix peter-evans/create-pull-request SHA to v7.0.8
- Use grouped redirect for GITHUB_OUTPUT writes (SC2129) - Add SC2153 disable for VERSION env var (not a misspelling)
Shellcheck directives don't support inline comments with --. Move the explanation to a separate comment line.
Replace the Go CLI's update-docs subcommand with a call to the existing scripts/update-release-calendar.sh bash script, which regenerates the release calendar from git tags (including ESR support). This eliminates duplicate calendar-update logic and aligns with the approach in #25205. The update-docs job no longer needs Go setup, only Node (for markdown table formatting via make fmt/markdown which the script calls).
0a1cc3f to
1315e89
Compare
| - rc | ||
| release_notes: | ||
| description: Release notes for the publishing the release. This is required to create a release. | ||
| - release | ||
| - create-release-branch |
There was a problem hiding this comment.
How is create-release-branch different from release?
I would suggest changing the options to
- rc
- patch
- minor
These are usually the three release types we do.
There was a problem hiding this comment.
Patch and minor are the same steps to be taken here. RC is from main, minor/patch happen on release branches, and the 3rd action release managers have to take is creating the branch in the first place. You would run create-release-branch at the beginning of code freeze, and then run release at the end for a minor.
There was a problem hiding this comment.
Ideally, the release freeze should be time-based and automatic.
… error handling - Add checkOpenPRs() that blocks releases when open PRs target the release branch, listing each PR with instructions to merge or close. - Call checkOpenPRs() from calculateRelease() and calculateRCFromBranch() so both final releases and branch RCs are gated. - Add warning logging in ghBuildPRMetadataMap() when PR metadata fetch or parse fails, replacing silent continues. - Validate jq outputs in the workflow are non-empty/non-null before setting GITHUB_OUTPUT, failing with ::error:: if any are missing. - Replace 'git tag ... 2>/dev/null || true' with explicit idempotent rev-parse checks in both prepare-release and release job dry-run paths. - Write a release preparation summary table to GITHUB_STEP_SUMMARY.
….yaml
- Add '_, _ =' to all fmt.Fprintf calls to satisfy revive unhandled-error
- Use grouped redirect '{ ... } >> file' for GITHUB_STEP_SUMMARY writes
to satisfy shellcheck SC2129
Restore scripts/releaser/, scripts/release.sh, scripts/release/, and scripts/release_promote_stable.sh so the old interactive release tool remains available as a manual fallback. The new CI-oriented tool lives in scripts/release-action/ as a separate package.
- Remove all dry_run concepts from release.yaml - Rename CalculateResult to ReleaseRequest - Rename factory functions to calculate*ReleaseRequest pattern - Inline determineChannel (just returns "stable") - Rename versionLess to versionIsLess - Replace mustAtoi with strconv.Atoi - Change PRCount int to PRNumbers []int with FindAllStringSubmatch - Move humanizedAreas to top of commit.go - Replace prMetadata with pullRequest type (exported fields) - Rename ghBuildPRMetadataMap to ghBuildPullRequestMap
commitEntry is now a pure git log data structure (SHA, Title, Timestamp). PR number extraction and GitHub API lookups live in notes.go where they are actually consumed for release note generation.
Releases are mainline by default. They become stable only when a newer minor series has a final release (mainline - 1). Restores determineChannel with the correct logic.
…mmand Replace the shell-based publish.sh and sign_with_gpg.sh with a native Go 'publish' subcommand in the release-action tool. The subcommand handles SHA256 checksum generation, GPG signing, and gh release create with the correct flags based on the release channel. The workflow now calls 'go run ./scripts/release-action publish' instead of './scripts/release/publish.sh'.
GPG signing happens during the build step, not at publish time.
- Separate CreateBranchRequest type (embeds ReleaseRequest + BranchName) - Rename calculateReleaseReleaseRequest to createRegularReleaseRequest - Rename calculateCreateBranchReleaseRequest to calculateCreateBranchRequest - Replace Channel string with Stable bool; determineChannel with isStable - Merge openPR into pullRequest type; flatten Author to string - Remove --channel flag from generate-notes (derive RC from version) - Replace --channel in publish with --stable bool flag - Update workflow to use stable output instead of channel
- Remove trailing blank lines in publish.go - Use 0o600 file permissions for checksums file (gosec G306)
These changes belong in a separate PR.
| } | ||
|
|
||
| // gitRun runs a git command with stdout/stderr connected to the | ||
| // terminal. |
There was a problem hiding this comment.
Looks like gitRun tosses all output from the git command. Did you mean to direct the output somewhere?
There was a problem hiding this comment.
P1 [CRF-1] Echoing the broader concern here: scripts/release-action/ has 1339 lines of new Go code with 96 lines of tests (14:1 ratio). Core release logic is untested.
The only tests cover parseVersion, version.String(), and version.IsRC(). At least 16 pure functions that need no git state to test have zero coverage: versionIsLess, findLatestRC, findLatestNonRC, findPreviousTag, filterTagsForSeries, isStable, isHexSHA, humanizeTitle, categorizeCommit, parsePRNumbers, stripPRRef, isDependabot, commitSortPrefix, ReleaseRequest.String, CreateBranchRequest.String.
These implement the version calculation and release note generation logic that determines what gets tagged and published. A bug in versionIsLess or findPreviousTag produces wrong tags in production. (Netero)
🤖
There was a problem hiding this comment.
Added unit tests for all 15 pure functions: versionIsLess, findLatestRC, findLatestNonRC, findPreviousTag, filterTagsForSeries, isStable, isHexSHA, humanizeTitle, categorizeCommit, commitSortPrefix, parsePRNumbers, stripPRRef, isDependabot across calculate_test.go and commit_test.go.
Generated with Coder Agents
There was a problem hiding this comment.
Good catch. Fixed the comment and removed the redundant nil assignments (they're already the zero value). gitRun intentionally discards output; it's only used for exit-code checks like merge-base --is-ancestor.
Generated with Coder Agents
| var rawPRs []struct { | ||
| Number int `json:"number"` | ||
| Title string `json:"title"` | ||
| Author string `json:"author"` |
There was a problem hiding this comment.
Seems like this is an object. Quick test on my machine:
$ gh pr list --json "number,title,author,url" --limit 1
[
{
"author": {
"id": "U_kgDODaQKsg",
"is_bot": false,
"login": "tracyjohnsonux",
"name": "TJ"
},
"number": 25595,
"title": "refactor(site): promote search to full-width sidebar nav item",
"url": "https://github.com/coder/coder/pull/25595"
}
]
There was a problem hiding this comment.
Bug confirmed, thanks for the repro. Fixed to parse author as struct{ Login string } matching gh's actual output.
Generated with Coder Agents
| files+=(./coder_latest_sbom.spdx.json) | ||
| fi | ||
|
|
||
| ./scripts/release/publish.sh \ |
There was a problem hiding this comment.
Planning to remove these old scripts in a follow-up PR?
There was a problem hiding this comment.
Keeping the old scripts in place as a fallback. No deletions in this PR.
Generated with Coder Agents
|
/coder-agents-review |
|
Review posted | Chat Review history
deep-review v0.5.0 | Round 1 | Last posted: Round 1, 4 findings (1 P1, 2 P2, 1 Nit), COMMENT. Review Finding inventoryFindings
Round logRound 1Netero-only. 1 P1, 2 P2, 1 Nit. Reviewed against 9810d29..52fc13f. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
First-pass review (Netero). 1 P1, 2 P2, 1 Nit.
This is a first-pass review only: these are mechanical findings from Netero. The full review panel has not yet reviewed this PR and will review after these findings are addressed.
The release workflow rewrite is a substantial improvement over the shell-script approach. The Go tool is well-structured with clean separation between subcommands, and the workflow cleanly delegates to it. The version calculation logic, tag management, and notes generation are all reasonable.
The main concern is test coverage for release-critical logic. The pure functions that determine what gets tagged and published have no tests, and at least one bug (the Author field mismatch) would have been caught by a basic unit test.
Note: the Author field bug (CRF-2) was also flagged by @zedkipp in an existing review comment.
"The following pure functions have zero test coverage and need no git state to test: versionIsLess, findLatestRC, findLatestNonRC, findPreviousTag, filterTagsForSeries, isStable, isHexSHA, humanizeTitle, categorizeCommit, parsePRNumbers, stripPRRef, isDependabot, commitSortPrefix." Netero
🤖 This review was automatically generated with Coder Agents.
| var rawPRs []struct { | ||
| Number int `json:"number"` | ||
| Title string `json:"title"` | ||
| Author string `json:"author"` |
There was a problem hiding this comment.
P2 [CRF-2] Author is declared as string but gh pr list --json author returns a JSON object {"login": "username"}. Go's json.Unmarshal silently skips type-mismatched fields, so pr.Author is always "". The error message at line 109 renders (by @) instead of (by @username).
Compare with ghBuildPullRequestMap at line 54 in this same file, which correctly uses Author struct { Login string }. Fix: change line 95 to match the struct pattern at line 54 and reference .Author.Login at line 109.
🤖
| GitHub Actions UI. The `prepare-release` job is idempotent and will detect | ||
| the existing tag. | ||
|
|
||
| To test the workflow without publishing, select dry-run. |
There was a problem hiding this comment.
P2 [CRF-3] "To test the workflow without publishing, select dry-run" references a removed feature. The dry_run input was deleted from the workflow in this PR. Either remove this line or describe the current way to test (if one exists).
🤖
| } | ||
|
|
||
| // Extract the conventional commit prefix (e.g. "feat", "fix(scope)"). | ||
| prefixRe := regexp.MustCompile(`^([a-z]+)(\(.+\))?[!]?:`) |
There was a problem hiding this comment.
Nit [CRF-4] regexp.MustCompile inside function body. Every other regex in this package (branchRe, cherryPickPRRe, conventionalPrefixRe, breakingCommitRe, prNumRe, semverRe) is a package-level var compiled once at init. This one recompiles on every call to categorizeCommit. Not a performance concern for a release tool, but it breaks the file's own convention. Move it to a var block.
🤖
- Fix gitRun comment: it discards output, not connects to terminal - Fix Author field in checkOpenPRs: gh returns an object, not string - Add unit tests for all pure functions (versionIsLess, findLatestRC, findLatestNonRC, findPreviousTag, filterTagsForSeries, isStable, isHexSHA, humanizeTitle, categorizeCommit, commitSortPrefix, parsePRNumbers, stripPRRef, isDependabot)
Replace the local interactive release CLI and legacy shell scripts with a non-interactive Go tool (
scripts/release-action/) and a rewrittenrelease.yamlworkflow. Release managers trigger releases from the GitHub Actions UI by selecting a branch, picking a release type (rc,release, orcreate-release-branch), and optionally providing a commit SHA.The Go tool has four subcommands:
calculate-version(computes next version from git state),generate-notes(release notes from commit log and PR metadata),publish(creates GitHub release with checksums), and the workflow handles tag creation, branch creation, building, and downstream publishing.scripts/version.shfallback now usesgit describe(nearest ancestor tag) instead of global latest so dev builds on release branches show the correct version series.