Skip to content

Commit 228d553

Browse files
committed
release: add update-workflow-branches command and TeamCity scripts
This commit adds automation to update the GitHub Actions workflow file (.github/workflows/update_releases.yaml) when new release branches are created. Changes: - New command: `release update-workflow-branches` that auto-detects the latest release branch via git ls-remote and adds it to the workflow matrix - TeamCity wrapper and implementation scripts to run this command and create PRs - Command preserves YAML formatting and is idempotent The command can be run manually or via TeamCity automation to keep the workflow file in sync with active release branches. Epic: None Release note: None
1 parent bbc06e5 commit 228d553

File tree

5 files changed

+337
-0
lines changed

5 files changed

+337
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
3+
# Copyright 2025 The Cockroach Authors.
4+
#
5+
# Use of this software is governed by the CockroachDB Software License
6+
# included in the /LICENSE file.
7+
8+
9+
set -euo pipefail
10+
11+
dir="$(dirname $(dirname $(dirname $(dirname $(dirname $(dirname "${0}"))))))"
12+
13+
source "$dir/teamcity-support.sh" # For $root
14+
source "$dir/teamcity-bazel-support.sh" # For run_bazel
15+
16+
tc_start_block "Run Update Workflow Branches Release Phase"
17+
BAZEL_SUPPORT_EXTRA_DOCKER_ARGS="-e DRY_RUN -e GH_TOKEN" \
18+
run_bazel build/teamcity/internal/cockroach/release/process/update_workflow_branches_impl.sh
19+
tc_end_block "Run Update Workflow Branches Release Phase"
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bash
2+
3+
# Copyright 2025 The Cockroach Authors.
4+
#
5+
# Use of this software is governed by the CockroachDB Software License
6+
# included in the /LICENSE file.
7+
8+
9+
set -xeuo pipefail
10+
11+
dry_run=true
12+
# override dev defaults with production values
13+
if [[ -z "${DRY_RUN:-}" ]] ; then
14+
echo "Setting production values"
15+
dry_run=false
16+
fi
17+
18+
# run git fetch in order to get all remote branches
19+
git fetch --tags -q origin
20+
21+
# install gh CLI tool
22+
curl -fsSL -o /tmp/gh.tar.gz https://github.com/cli/cli/releases/download/v2.32.1/gh_2.32.1_linux_amd64.tar.gz
23+
echo "5c9a70b6411cc9774f5f4e68f9227d5d55ca0bfbd00dfc6353081c9b705c8939 /tmp/gh.tar.gz" | sha256sum -c -
24+
tar --strip-components 1 -xf /tmp/gh.tar.gz
25+
export PATH=$PWD/bin:$PATH
26+
27+
# Build the release tool
28+
bazel build --config=crosslinux //pkg/cmd/release
29+
30+
# Run the update-workflow-branches command and capture output
31+
echo "Running release update-workflow-branches command..."
32+
OUTPUT=$($(bazel info --config=crosslinux bazel-bin)/pkg/cmd/release/release_/release update-workflow-branches 2>&1)
33+
echo "$OUTPUT"
34+
35+
# Extract the branch name from output (line like "Latest release branch: release-26.1")
36+
RELEASE_BRANCH=$(echo "$OUTPUT" | grep "Latest release branch:" | sed 's/Latest release branch: //')
37+
38+
if [[ -z "$RELEASE_BRANCH" ]]; then
39+
echo "ERROR: Could not determine release branch from command output"
40+
exit 1
41+
fi
42+
43+
# Check if any changes were made
44+
if git diff --quiet .github/workflows/update_releases.yaml; then
45+
echo "No changes to workflow file, nothing to commit"
46+
exit 0
47+
fi
48+
49+
echo "Changes detected in workflow file"
50+
51+
if [[ "$dry_run" == "true" ]]; then
52+
echo "DRY RUN: Would create PR with the following changes:"
53+
git diff .github/workflows/update_releases.yaml
54+
exit 0
55+
fi
56+
57+
# Set git user for commits
58+
export GIT_AUTHOR_NAME="Justin Beaver"
59+
export GIT_COMMITTER_NAME="Justin Beaver"
60+
export GIT_AUTHOR_EMAIL="[email protected]"
61+
export GIT_COMMITTER_EMAIL="[email protected]"
62+
63+
# Create a branch for the PR
64+
BRANCH_NAME="update-workflow-branches-$(date +%Y%m%d-%H%M%S)"
65+
git checkout -b "$BRANCH_NAME"
66+
67+
# Commit the changes
68+
git add .github/workflows/update_releases.yaml
69+
git commit -m "workflows: run \`update_releases\` on \`$RELEASE_BRANCH\`
70+
71+
Epic: None
72+
Release note: None
73+
Release justification: non-production (release infra) change."
74+
75+
# Push the branch
76+
git push origin "$BRANCH_NAME"
77+
78+
# Create the pull request
79+
gh pr create \
80+
--repo cockroachdb/cockroach \
81+
--base master \
82+
--head "$BRANCH_NAME" \
83+
--title "workflows: run \`update_releases\` on \`$RELEASE_BRANCH\`" \
84+
--body "Epic: None
85+
Release note: None
86+
Release justification: non-production (release infra) change."
87+
88+
echo "Pull request created successfully"

pkg/cmd/release/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ go_library(
1111
"update_helm.go",
1212
"update_releases.go",
1313
"update_versions.go",
14+
"update_workflow.go",
1415
],
1516
importpath = "github.com/cockroachdb/cockroach/pkg/cmd/release",
1617
visibility = ["//visibility:private"],

pkg/cmd/release/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ func main() {
3838
func init() {
3939
rootCmd.AddCommand(updateReleasesTestFilesCmd)
4040
rootCmd.AddCommand(updateVersionsCmd)
41+
rootCmd.AddCommand(updateWorkflowBranchesCmd)
4142
}

pkg/cmd/release/update_workflow.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package main
7+
8+
import (
9+
"fmt"
10+
"log"
11+
"os"
12+
"slices"
13+
"strings"
14+
15+
"github.com/cockroachdb/version"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
const workflowFile = ".github/workflows/update_releases.yaml"
20+
21+
var updateWorkflowBranchesCmd = &cobra.Command{
22+
Use: "update-workflow-branches",
23+
Short: "Update the branch matrix in update_releases.yaml workflow",
24+
Long: "Detects the latest release branch and adds it to the GitHub Actions workflow if not present",
25+
RunE: updateWorkflowBranches,
26+
}
27+
28+
// updateWorkflowBranches is the main command handler.
29+
func updateWorkflowBranches(_ *cobra.Command, _ []string) error {
30+
fmt.Println("Finding latest release branch...")
31+
latestBranch, err := findLatestReleaseBranch()
32+
if err != nil {
33+
return fmt.Errorf("failed to find latest release branch: %w", err)
34+
}
35+
36+
fmt.Printf("Latest release branch: %s\n", latestBranch)
37+
38+
fmt.Println("Updating workflow file...")
39+
if err := addBranchToWorkflow(latestBranch); err != nil {
40+
return fmt.Errorf("failed to update workflow file: %w", err)
41+
}
42+
43+
fmt.Println("Successfully updated workflow file")
44+
return nil
45+
}
46+
47+
// findLatestReleaseBranch detects the latest release branch by querying git remote branches.
48+
func findLatestReleaseBranch() (string, error) {
49+
// Get all release branches
50+
branches, err := listRemoteBranches("release-*")
51+
if err != nil {
52+
return "", fmt.Errorf("failed to list remote branches: %w", err)
53+
}
54+
55+
// Filter out RC branches - we only want major release branches like "release-25.4"
56+
var releaseBranches []string
57+
for _, branch := range branches {
58+
if !strings.Contains(branch, "-rc") {
59+
releaseBranches = append(releaseBranches, branch)
60+
}
61+
}
62+
63+
if len(releaseBranches) == 0 {
64+
return "", fmt.Errorf("no release branches found")
65+
}
66+
67+
// Parse versions and sort
68+
type branchVersion struct {
69+
branch string
70+
version version.Version
71+
}
72+
var versions []branchVersion
73+
74+
for _, branch := range releaseBranches {
75+
// Extract version from "release-X.Y" format
76+
versionStr := strings.TrimPrefix(branch, "release-")
77+
// Add .0 for patch to make it a valid semantic version
78+
v, err := version.Parse("v" + versionStr + ".0")
79+
if err != nil {
80+
log.Printf("WARNING: cannot parse version from branch %s: %v", branch, err)
81+
continue
82+
}
83+
versions = append(versions, branchVersion{branch, v})
84+
}
85+
86+
if len(versions) == 0 {
87+
return "", fmt.Errorf("no valid version branches found")
88+
}
89+
90+
// Sort by version
91+
slices.SortFunc(versions, func(a, b branchVersion) int {
92+
return a.version.Compare(b.version)
93+
})
94+
95+
// Return highest version
96+
return versions[len(versions)-1].branch, nil
97+
}
98+
99+
// addBranchToWorkflow adds the specified branch to the workflow file if not already present.
100+
func addBranchToWorkflow(branch string) error {
101+
// Read the workflow file
102+
rawData, err := os.ReadFile(workflowFile)
103+
if err != nil {
104+
return fmt.Errorf("failed to read workflow file: %w", err)
105+
}
106+
107+
lines := strings.Split(string(rawData), "\n")
108+
109+
// Find the branch section and extract current branches
110+
currentBranches, branchStart, branchEnd, indent, err := parseBranchSection(lines)
111+
if err != nil {
112+
return err
113+
}
114+
115+
// Check if branch already exists
116+
for _, b := range currentBranches {
117+
if b == branch {
118+
fmt.Printf("Branch %s is already in the workflow file\n", branch)
119+
return nil
120+
}
121+
}
122+
123+
// Add the new branch
124+
currentBranches = append(currentBranches, branch)
125+
126+
// Sort branches: master first, then release branches in version order
127+
slices.SortFunc(currentBranches, func(a, b string) int {
128+
if a == "master" {
129+
return -1
130+
}
131+
if b == "master" {
132+
return 1
133+
}
134+
135+
// Compare release versions
136+
aVer := strings.TrimPrefix(a, "release-")
137+
bVer := strings.TrimPrefix(b, "release-")
138+
139+
va, err1 := version.Parse("v" + aVer + ".0")
140+
vb, err2 := version.Parse("v" + bVer + ".0")
141+
142+
// If either fails to parse, fall back to string comparison
143+
if err1 != nil || err2 != nil {
144+
return strings.Compare(a, b)
145+
}
146+
147+
return va.Compare(vb)
148+
})
149+
150+
// Write the updated file
151+
if err := writeBranchSection(lines, currentBranches, branchStart, branchEnd, indent); err != nil {
152+
return err
153+
}
154+
155+
fmt.Printf("Added branch %s to workflow file\n", branch)
156+
return nil
157+
}
158+
159+
// parseBranchSection parses the workflow file to find the branch matrix section.
160+
func parseBranchSection(lines []string) (branches []string, start int, end int, indent string, err error) {
161+
start = -1
162+
end = -1
163+
164+
for i, line := range lines {
165+
// Look for "branch:" within the matrix section
166+
if strings.Contains(line, "branch:") && !strings.HasPrefix(strings.TrimSpace(line), "#") {
167+
start = i + 1
168+
// Determine indentation from the next line
169+
if i+1 < len(lines) {
170+
nextLine := lines[i+1]
171+
// Count leading spaces
172+
trimmed := strings.TrimLeft(nextLine, " ")
173+
indent = strings.Repeat(" ", len(nextLine)-len(trimmed))
174+
}
175+
} else if start != -1 && end == -1 {
176+
// Check if this line is still part of branch list
177+
trimmed := strings.TrimSpace(line)
178+
if trimmed == "" {
179+
// Empty line, continue
180+
continue
181+
}
182+
if !strings.HasPrefix(trimmed, "-") {
183+
// Found the end of the branch list
184+
end = i
185+
break
186+
} else {
187+
// Extract branch name from line like '- "release-25.4"'
188+
// Remove leading "- " and quotes
189+
branchLine := strings.TrimSpace(trimmed)
190+
branchLine = strings.TrimPrefix(branchLine, "- ")
191+
branchLine = strings.Trim(branchLine, "\"")
192+
branches = append(branches, branchLine)
193+
}
194+
}
195+
}
196+
197+
if start == -1 {
198+
return nil, 0, 0, "", fmt.Errorf("branch section not found in workflow file")
199+
}
200+
201+
// If we reached end of file, set end to len(lines)
202+
if end == -1 {
203+
end = len(lines)
204+
}
205+
206+
return branches, start, end, indent, nil
207+
}
208+
209+
// writeBranchSection writes the updated branch list back to the workflow file.
210+
func writeBranchSection(lines []string, branches []string, start int, end int, indent string) error {
211+
// Build new lines
212+
var newLines []string
213+
newLines = append(newLines, lines[:start]...)
214+
215+
// Add branch lines
216+
for _, branch := range branches {
217+
newLines = append(newLines, fmt.Sprintf("%s- %q", indent, branch))
218+
}
219+
220+
// Add remaining lines
221+
newLines = append(newLines, lines[end:]...)
222+
223+
// Write back to file using atomic write pattern
224+
return writeFileIntoRepo(func(f *os.File) error {
225+
_, err := f.WriteString(strings.Join(newLines, "\n"))
226+
return err
227+
}, workflowFile)
228+
}

0 commit comments

Comments
 (0)