mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user