Skip to content

Commit 2d22177

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 2d22177

File tree

5 files changed

+374
-0
lines changed

5 files changed

+374
-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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
# Ensure we're starting from a clean master branch
22+
git checkout master
23+
git reset --hard origin/master
24+
25+
# install gh CLI tool
26+
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
27+
echo "5c9a70b6411cc9774f5f4e68f9227d5d55ca0bfbd00dfc6353081c9b705c8939 /tmp/gh.tar.gz" | sha256sum -c -
28+
tar --strip-components 1 -xf /tmp/gh.tar.gz
29+
export PATH=$PWD/bin:$PATH
30+
31+
# Build the release tool
32+
bazel build --config=crosslinux //pkg/cmd/release
33+
34+
# Run the update-workflow-branches command and capture output
35+
echo "Running release update-workflow-branches command..."
36+
OUTPUT=$($(bazel info --config=crosslinux bazel-bin)/pkg/cmd/release/release_/release update-workflow-branches 2>&1)
37+
echo "$OUTPUT"
38+
39+
# Extract the branch name from output (line like "Latest release branch: release-26.1")
40+
RELEASE_BRANCH=$(echo "$OUTPUT" | grep "Latest release branch:" | sed 's/Latest release branch: //')
41+
42+
if [[ -z "$RELEASE_BRANCH" ]]; then
43+
echo "ERROR: Could not determine release branch from command output"
44+
exit 1
45+
fi
46+
47+
# Check if any changes were made
48+
if git diff --quiet .github/workflows/update_releases.yaml; then
49+
echo "No changes to workflow file, nothing to commit"
50+
exit 0
51+
fi
52+
53+
echo "Changes detected in workflow file"
54+
55+
if [[ "$dry_run" == "true" ]]; then
56+
echo "DRY RUN: Would create PR with the following changes:"
57+
git diff .github/workflows/update_releases.yaml
58+
exit 0
59+
fi
60+
61+
# Set git user for commits
62+
export GIT_AUTHOR_NAME="Justin Beaver"
63+
export GIT_COMMITTER_NAME="Justin Beaver"
64+
export GIT_AUTHOR_EMAIL="[email protected]"
65+
export GIT_COMMITTER_EMAIL="[email protected]"
66+
67+
# Check for GitHub token
68+
if [[ -z "${GH_TOKEN:-}" ]]; then
69+
echo "ERROR: GH_TOKEN environment variable must be set"
70+
exit 1
71+
fi
72+
73+
# Create a branch for the PR
74+
BRANCH_NAME="update-workflow-branches-$(date +%Y%m%d-%H%M%S)"
75+
git checkout -b "$BRANCH_NAME"
76+
77+
# Commit the changes
78+
git add .github/workflows/update_releases.yaml
79+
git commit -m "workflows: run \`update_releases\` on \`$RELEASE_BRANCH\`
80+
81+
Epic: None
82+
Release note: None
83+
Release justification: non-production (release infra) change."
84+
85+
# Push the branch to cockroach-teamcity fork (like update_releases.yaml workflow does)
86+
git push "https://oauth2:${GH_TOKEN}@github.com/cockroach-teamcity/cockroach" "$BRANCH_NAME"
87+
88+
# Create the pull request from the fork
89+
gh pr create \
90+
--repo cockroachdb/cockroach \
91+
--base master \
92+
--head "cockroach-teamcity:$BRANCH_NAME" \
93+
--title "workflows: run \`update_releases\` on \`$RELEASE_BRANCH\`" \
94+
--body "Epic: None
95+
Release note: None
96+
Release justification: non-production (release infra) change."
97+
98+
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: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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(
161+
lines []string,
162+
) (branches []string, start int, end int, indent string, err error) {
163+
start = -1
164+
end = -1
165+
166+
for i, line := range lines {
167+
// Look for "branch:" within the matrix section
168+
if strings.Contains(line, "branch:") && !strings.HasPrefix(strings.TrimSpace(line), "#") {
169+
start = i + 1
170+
// Determine indentation from the next line
171+
if i+1 < len(lines) {
172+
nextLine := lines[i+1]
173+
// Count leading spaces
174+
trimmed := strings.TrimLeft(nextLine, " ")
175+
indent = strings.Repeat(" ", len(nextLine)-len(trimmed))
176+
}
177+
} else if start != -1 && end == -1 {
178+
// Check if this line is still part of branch list
179+
trimmed := strings.TrimSpace(line)
180+
if trimmed == "" {
181+
// Empty line, continue
182+
continue
183+
}
184+
if !strings.HasPrefix(trimmed, "-") {
185+
// Found the end of the branch list
186+
end = i
187+
break
188+
} else {
189+
// Extract branch name from line like '- "release-25.4"'
190+
// Remove leading "- " and quotes
191+
branchLine := strings.TrimSpace(trimmed)
192+
branchLine = strings.TrimPrefix(branchLine, "- ")
193+
branchLine = strings.Trim(branchLine, "\"")
194+
branches = append(branches, branchLine)
195+
}
196+
}
197+
}
198+
199+
if start == -1 {
200+
return nil, 0, 0, "", fmt.Errorf("branch section not found in workflow file")
201+
}
202+
203+
// If we reached end of file, set end to len(lines)
204+
if end == -1 {
205+
end = len(lines)
206+
}
207+
208+
return branches, start, end, indent, nil
209+
}
210+
211+
// writeBranchSection writes the updated branch list back to the workflow file.
212+
func writeBranchSection(
213+
lines []string, branches []string, start int, end int, indent string,
214+
) error {
215+
// Build new lines
216+
var newLines []string
217+
newLines = append(newLines, lines[:start]...)
218+
219+
// Add branch lines
220+
for _, branch := range branches {
221+
newLines = append(newLines, fmt.Sprintf("%s- %q", indent, branch))
222+
}
223+
224+
// Add remaining lines
225+
newLines = append(newLines, lines[end:]...)
226+
227+
// Write back to file using atomic write pattern
228+
// Create temp file in the same directory as target to avoid cross-device link errors
229+
dir := ".github/workflows"
230+
f, err := os.CreateTemp(dir, "update_releases_*.yaml")
231+
if err != nil {
232+
return fmt.Errorf("could not create temporary file: %w", err)
233+
}
234+
tmpName := f.Name()
235+
defer func() {
236+
if err != nil {
237+
_ = os.Remove(tmpName)
238+
}
239+
}()
240+
241+
if _, err = f.WriteString(strings.Join(newLines, "\n")); err != nil {
242+
f.Close()
243+
return fmt.Errorf("error writing to temp file: %w", err)
244+
}
245+
246+
if err = f.Close(); err != nil {
247+
return fmt.Errorf("error closing temp file: %w", err)
248+
}
249+
250+
if err = os.Rename(tmpName, workflowFile); err != nil {
251+
return fmt.Errorf("error moving file to final destination: %w", err)
252+
}
253+
254+
return nil
255+
}

0 commit comments

Comments
 (0)