Files
coder/scripts/release-action/github.go
T
Garrett Delfosse b95697a370 ci: rewrite release workflow to be fully GitHub Actions-driven (#25162)
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.
2026-06-04 14:38:48 -04:00

116 lines
3.0 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"golang.org/x/xerrors"
)
// ghOutput runs a gh CLI command and returns trimmed stdout.
func ghOutput(args ...string) (string, error) {
cmd := exec.Command("gh", args...)
out, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// pullRequest holds metadata about a GitHub pull request.
type pullRequest struct {
Number int
Title string
Labels []string
Author string
URL string
}
// pullRequestMap holds PR metadata indexed by PR number.
type pullRequestMap map[int]pullRequest
// ghBuildPullRequestMap builds a map of PR number to metadata by
// querying the GitHub API via the gh CLI for the given PR numbers.
func ghBuildPullRequestMap(prNumbers []int) pullRequestMap {
m := make(pullRequestMap)
for _, prNum := range prNumbers {
out, err := ghOutput("pr", "view", fmt.Sprintf("%d", prNum),
"--repo", fmt.Sprintf("%s/%s", owner, repo),
"--json", "number,labels,author")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: failed to fetch PR #%d metadata: %v\n", prNum, err)
continue
}
var result struct {
Number int `json:"number"`
Labels []struct {
Name string `json:"name"`
} `json:"labels"`
Author struct {
Login string `json:"login"`
} `json:"author"`
}
if err := json.Unmarshal([]byte(out), &result); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: failed to parse PR #%d metadata: %v\n", prNum, err)
continue
}
var labels []string
for _, l := range result.Labels {
labels = append(labels, l.Name)
}
m[result.Number] = pullRequest{
Number: result.Number,
Labels: labels,
Author: result.Author.Login,
}
}
return m
}
// checkOpenPRs verifies that no pull requests are open against the
// given branch. If any are found, it returns an error listing them
// with instructions to merge or close before releasing.
func checkOpenPRs(branch string) error {
out, err := ghOutput("pr", "list",
"--repo", fmt.Sprintf("%s/%s", owner, repo),
"--base", branch,
"--state", "open",
"--json", "number,title,author,url",
"--limit", "100")
if err != nil {
return xerrors.Errorf("failed to list open PRs for branch %s: %w", branch, err)
}
var rawPRs []struct {
Number int `json:"number"`
Title string `json:"title"`
Author struct {
Login string `json:"login"`
} `json:"author"`
URL string `json:"url"`
}
if err := json.Unmarshal([]byte(out), &rawPRs); err != nil {
return xerrors.Errorf("failed to parse open PRs response: %w", err)
}
if len(rawPRs) == 0 {
return nil
}
var b strings.Builder
_, _ = fmt.Fprintf(&b, "found %d open pull request(s) targeting %s that must be merged or closed before releasing:\n\n", len(rawPRs), branch)
for _, pr := range rawPRs {
_, _ = fmt.Fprintf(&b, " - #%d: %s (by @%s)\n %s\n", pr.Number, pr.Title, pr.Author.Login, pr.URL)
}
_, _ = fmt.Fprintf(&b, "\nMerge or close these pull requests, then re-run the release workflow.")
return xerrors.New(b.String())
}