Skip to content

ci: rewrite release workflow to be fully GitHub Actions-driven#25162

Merged
f0ssel merged 19 commits into
mainfrom
ci/release-workflow-rewrite-v2
Jun 4, 2026
Merged

ci: rewrite release workflow to be fully GitHub Actions-driven#25162
f0ssel merged 19 commits into
mainfrom
ci/release-workflow-rewrite-v2

Conversation

@f0ssel
Copy link
Copy Markdown
Member

@f0ssel f0ssel commented May 11, 2026

Replace the local interactive release CLI and legacy shell scripts with a non-interactive Go tool (scripts/release-action/) and a rewritten release.yaml workflow. Release managers trigger releases from the GitHub Actions UI by selecting a branch, picking a release type (rc, release, or create-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.sh fallback now uses git describe (nearest ancestor tag) instead of global latest so dev builds on release branches show the correct version series.

@github-actions
Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/about/contributing/CONTRIBUTING.md

@f0ssel f0ssel force-pushed the ci/release-workflow-rewrite-v2 branch from 49ec231 to 1e57c3b Compare May 11, 2026 22:47
@github-actions
Copy link
Copy Markdown

Coder Agent Chat: agent finished or is awaiting input

Chat: https://dev.coder.com/chats/0f7abc63-cfc3-4020-ab78-a455672e1cea
Status: waiting

f0ssel added 6 commits May 12, 2026 19:27
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).
@f0ssel f0ssel force-pushed the ci/release-workflow-rewrite-v2 branch from 0a1cc3f to 1315e89 Compare May 12, 2026 19:28
@f0ssel f0ssel requested review from david-fraley and matifali May 12, 2026 19:37
Comment on lines 11 to +13
- rc
release_notes:
description: Release notes for the publishing the release. This is required to create a release.
- release
- create-release-branch
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, the release freeze should be time-based and automatic.

f0ssel added 3 commits May 18, 2026 13:22
… 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.
Comment thread .github/workflows/release.yaml Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/commit.go Outdated
Comment thread scripts/release-action/commit.go Outdated
Comment thread scripts/release-action/commit.go Outdated
Comment thread scripts/release-action/github.go Outdated
f0ssel added 5 commits May 20, 2026 16:45
- 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.
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/calculate.go Outdated
Comment thread scripts/release-action/github.go Outdated
Comment thread scripts/release-action/github.go Outdated
f0ssel added 4 commits May 20, 2026 19:00
- 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.
Comment thread scripts/release-action/git.go Outdated
}

// gitRun runs a git command with stdout/stderr connected to the
// terminal.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like gitRun tosses all output from the git command. Did you mean to direct the output somewhere?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread scripts/release-action/github.go Outdated
var rawPRs []struct {
Number int `json:"number"`
Title string `json:"title"`
Author string `json:"author"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
  }
]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Planning to remove these old scripts in a follow-up PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the old scripts in place as a fallback. No deletions in this PR.

Generated with Coder Agents

@f0ssel
Copy link
Copy Markdown
Member Author

f0ssel commented May 27, 2026

/coder-agents-review

@coder-agents-review
Copy link
Copy Markdown
Contributor

coder-agents-review Bot commented May 27, 2026

Review posted | Chat
Requested: 2026-05-27 00:55 UTC by @f0ssel
Spend: $7.75 / $100.00

Review history
  • R1 (2026-05-27), 1 Nit, 1 P1, 2 P2, COMMENT. Review

deep-review v0.5.0 | Round 1 | 9810d29..52fc13f

Last posted: Round 1, 4 findings (1 P1, 2 P2, 1 Nit), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P1 Open scripts/release-action/ 1339 lines of new Go with 96 lines of tests; ~16 pure functions are trivially testable with zero coverage R1 Netero Yes
CRF-2 P2 Open github.go:95 Author string in checkOpenPRs but gh pr list --json author returns an object; author always empty R1 Netero Yes
CRF-3 P2 Open CONTRIBUTING.md:265 "select dry-run" references a removed workflow option R1 Netero Yes
CRF-4 Nit Open commit.go:205 regexp.MustCompile inside function body; every other regex in this package is a package-level var R1 Netero Yes

Round log

Round 1

Netero-only. 1 P1, 2 P2, 1 Nit. Reviewed against 9810d29..52fc13f.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

Copy link
Copy Markdown
Contributor

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/release-action/github.go Outdated
var rawPRs []struct {
Number int `json:"number"`
Title string `json:"title"`
Author string `json:"author"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]+)(\(.+\))?[!]?:`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
@f0ssel f0ssel merged commit b95697a into main Jun 4, 2026
28 of 31 checks passed
@f0ssel f0ssel deleted the ci/release-workflow-rewrite-v2 branch June 4, 2026 18:38
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 4, 2026
@f0ssel f0ssel added the backport label Jun 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants