mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: remove releaser v2 flow and drop v1 naming (#27421)
## Summary Removes the GitHub Actions-driven releaser **v2** pipeline so the interactive release wizard is the only release path, and drops the `v1` naming now that it is the sole implementation. ## Changes - Delete `.github/workflows/tag-and-release.yaml` (the v2 workflow). - Delete `scripts/releaser/v2/`. - Move `scripts/releaser/v1/` into `scripts/releaser/` as `package main`. - Rewrite `scripts/releaser/main.go` to a single wizard command: drop the `--legacy` flag and the v2 `rc`/`branch`/`release` subcommands and hidden CI compat commands. `--dry-run` is preserved. - Update `scripts/release.sh` to run `go run ./scripts/releaser "$@"` (no `--legacy`). The legacy `release.yaml` workflow (triggered by `scripts/release.sh`) is unchanged and remains the release pipeline. ## Validation - `go build ./scripts/releaser/...` - `go test ./scripts/releaser/...` - `go vet` + `golangci-lint run ./scripts/releaser/...` - `gofmt -l` clean > [!NOTE] > The GPG signing key check removal is handled in a stacked follow-up PR based on this branch. <details> <summary>Implementation plan</summary> - v2 flow = `scripts/releaser/v2/` + `.github/workflows/tag-and-release.yaml` (uses `go run ./scripts/releaser prepare-release|generate-notes`). `v2` was imported only by `main.go`; the workflow was referenced nowhere else. - v1 flow = interactive wizard in `scripts/releaser/v1/`, reached via `--legacy`, driving `release.yaml` (triggered by `scripts/release.sh`). - No `docs/` referenced the releaser tool or these workflows. - Steps: delete the v2 workflow and package; move `v1/*` up to `scripts/releaser/` (`package main`, including test files); rewrite `main.go` to a single wizard command; update `release.sh`. </details> --- Generated by Coder Agents on behalf of @f0ssel.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// commitEntry represents a single non-merge commit.
|
||||
type commitEntry struct {
|
||||
SHA string
|
||||
FullSHA string
|
||||
Title string
|
||||
PRCount int // 0 if no PR number found
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
var prNumRe = regexp.MustCompile(`\(#(\d+)\)`)
|
||||
|
||||
// cherryPickPRRe matches cherry-pick bot titles like
|
||||
// "chore: foo bar (cherry-pick #42) (#43)".
|
||||
var cherryPickPRRe = regexp.MustCompile(`\(cherry-pick #(\d+)\)\s*\(#\d+\)$`)
|
||||
|
||||
// commitLog returns non-merge commits in the given range, filtering
|
||||
// out left-side commits (already in the base) and deduplicating
|
||||
// cherry-picks using git's --cherry-mark.
|
||||
func commitLog(commitRange string) ([]commitEntry, error) {
|
||||
// Use --left-right --cherry-mark to identify equivalent
|
||||
// (cherry-picked) commits and left-side-only commits.
|
||||
out, err := gitOutput("log", "--no-merges", "--left-right", "--cherry-mark",
|
||||
"--pretty=format:%m %ct %h %H %s", commitRange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Collect cherry-pick equivalent commits (marked with '=') so
|
||||
// we can skip duplicates. We keep only the right-side version.
|
||||
seen := make(map[string]bool)
|
||||
|
||||
var entries []commitEntry
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// Format: %m %ct %h %H %s
|
||||
// mark timestamp shortSHA fullSHA title...
|
||||
parts := strings.SplitN(line, " ", 5)
|
||||
if len(parts) < 5 {
|
||||
continue
|
||||
}
|
||||
mark := parts[0]
|
||||
ts, _ := strconv.ParseInt(parts[1], 10, 64)
|
||||
shortSHA := parts[2]
|
||||
fullSHA := parts[3]
|
||||
title := parts[4]
|
||||
|
||||
// Skip left-side commits (already in the old version).
|
||||
if mark == "<" {
|
||||
continue
|
||||
}
|
||||
// Skip cherry-pick equivalents that we've already seen
|
||||
// (marked '=' by --cherry-mark).
|
||||
if mark == "=" {
|
||||
if seen[title] {
|
||||
continue
|
||||
}
|
||||
seen[title] = true
|
||||
}
|
||||
|
||||
// Normalize cherry-pick bot titles:
|
||||
// "chore: foo (cherry-pick #42) (#43)" → "chore: foo (#42)"
|
||||
if m := cherryPickPRRe.FindStringSubmatch(title); m != nil {
|
||||
title = title[:cherryPickPRRe.FindStringIndex(title)[0]] + "(#" + m[1] + ")"
|
||||
}
|
||||
|
||||
e := commitEntry{
|
||||
SHA: shortSHA,
|
||||
FullSHA: fullSHA,
|
||||
Title: title,
|
||||
Timestamp: ts,
|
||||
}
|
||||
if m := prNumRe.FindStringSubmatch(e.Title); m != nil {
|
||||
e.PRCount, _ = strconv.Atoi(m[1])
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
|
||||
// Sort by conventional commit prefix, then by timestamp
|
||||
// (matching the bash script's sort -k3,3 -k1,1n).
|
||||
sort.SliceStable(entries, func(i, j int) bool {
|
||||
pi := commitSortPrefix(entries[i].Title)
|
||||
pj := commitSortPrefix(entries[j].Title)
|
||||
if pi != pj {
|
||||
return pi < pj
|
||||
}
|
||||
return entries[i].Timestamp < entries[j].Timestamp
|
||||
})
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// commitSortPrefix extracts the first word of a title for sorting.
|
||||
func commitSortPrefix(title string) string {
|
||||
idx := strings.IndexAny(title, " (:")
|
||||
if idx < 0 {
|
||||
return title
|
||||
}
|
||||
return title[:idx]
|
||||
}
|
||||
|
||||
// humanizedAreas maps conventional commit scopes to human-readable area
|
||||
// names. Order matters: more specific prefixes must come first so that
|
||||
// the first partial match wins.
|
||||
var humanizedAreas = []struct {
|
||||
Prefix string
|
||||
Area string
|
||||
}{
|
||||
{"agent/agentssh", "Agent SSH"},
|
||||
{"coderd/database", "Database"},
|
||||
{"enterprise/audit", "Auditing"},
|
||||
{"enterprise/cli", "CLI"},
|
||||
{"enterprise/coderd", "Server"},
|
||||
{"enterprise/dbcrypt", "Database"},
|
||||
{"enterprise/derpmesh", "Networking"},
|
||||
{"enterprise/provisionerd", "Provisioner"},
|
||||
{"enterprise/tailnet", "Networking"},
|
||||
{"enterprise/wsproxy", "Workspace Proxy"},
|
||||
{"agent", "Agent"},
|
||||
{"cli", "CLI"},
|
||||
{"coderd", "Server"},
|
||||
{"codersdk", "SDK"},
|
||||
{"docs", "Documentation"},
|
||||
{"enterprise", "Enterprise"},
|
||||
{"examples", "Examples"},
|
||||
{"helm", "Helm"},
|
||||
{"install.sh", "Installer"},
|
||||
{"provisionersdk", "SDK"},
|
||||
{"provisionerd", "Provisioner"},
|
||||
{"provisioner", "Provisioner"},
|
||||
{"pty", "CLI"},
|
||||
{"scaletest", "Scale Testing"},
|
||||
{"site", "Dashboard"},
|
||||
{"support", "Support"},
|
||||
{"tailnet", "Networking"},
|
||||
}
|
||||
|
||||
// conventionalPrefixRe extracts prefix, scope, and rest from a
|
||||
// conventional commit title. Does NOT match breaking "!" suffix —
|
||||
// those titles are left as-is (matching bash behavior).
|
||||
var conventionalPrefixRe = regexp.MustCompile(`^([a-z]+)(\((.+)\))?:\s*(.*)$`)
|
||||
|
||||
// humanizeTitle converts a conventional commit title to a
|
||||
// human-readable form, e.g. "feat(site): add bar" → "Dashboard: Add bar".
|
||||
func humanizeTitle(title string) string {
|
||||
m := conventionalPrefixRe.FindStringSubmatch(title)
|
||||
if m == nil {
|
||||
return title
|
||||
}
|
||||
scope := m[3] // may be empty
|
||||
rest := m[4]
|
||||
if rest == "" {
|
||||
return title
|
||||
}
|
||||
// Capitalize the first letter of the rest.
|
||||
rest = strings.ToUpper(rest[:1]) + rest[1:]
|
||||
|
||||
if scope == "" {
|
||||
return rest
|
||||
}
|
||||
|
||||
// Look up scope in humanizedAreas (first partial match wins).
|
||||
for _, ha := range humanizedAreas {
|
||||
if strings.HasPrefix(scope, ha.Prefix) {
|
||||
return ha.Area + ": " + rest
|
||||
}
|
||||
}
|
||||
// Scope not found in map — return as-is.
|
||||
return title
|
||||
}
|
||||
|
||||
// breakingCommitRe matches conventional commit "!:" breaking changes.
|
||||
var breakingCommitRe = regexp.MustCompile(`^[a-zA-Z]+(\(.+\))?!:`)
|
||||
|
||||
// categorizeCommit determines the release note section for a commit.
|
||||
// The priority order matches the bash script: breaking title first,
|
||||
// then labels (breaking, security, experimental), then prefix.
|
||||
func categorizeCommit(title string, labels []string) string {
|
||||
// Check breaking title first (matches bash behavior).
|
||||
if breakingCommitRe.MatchString(title) {
|
||||
return "breaking"
|
||||
}
|
||||
|
||||
// Label-based categorization.
|
||||
for _, l := range labels {
|
||||
if l == "release/breaking" {
|
||||
return "breaking"
|
||||
}
|
||||
if l == "security" {
|
||||
return "security"
|
||||
}
|
||||
if l == "release/experimental" {
|
||||
return "experimental"
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the conventional commit prefix (e.g. "feat", "fix(scope)").
|
||||
prefixRe := regexp.MustCompile(`^([a-z]+)(\(.+\))?[!]?:`)
|
||||
m := prefixRe.FindStringSubmatch(title)
|
||||
if m == nil {
|
||||
return "other"
|
||||
}
|
||||
|
||||
validPrefixes := []string{
|
||||
"feat", "fix", "docs", "refactor", "perf",
|
||||
"test", "build", "ci", "chore", "revert",
|
||||
}
|
||||
for _, p := range validPrefixes {
|
||||
if m[1] == p {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return "other"
|
||||
}
|
||||
Reference in New Issue
Block a user