Skip to content

Commit 5d896a7

Browse files
Copilotpetesramek
andauthored
feat: add bump-version workflow, merge-to-main, branch locking, and source-branch validation
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/4418acb6-8233-47f4-9b0f-9f6f4de0fb09 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent d7bee6d commit 5d896a7

5 files changed

Lines changed: 277 additions & 3 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: 'Lock branch'
2+
author: 'Pete Sramek'
3+
description: 'Apply branch protection to prevent direct pushes. Requires PRs with at least one approval.'
4+
inputs:
5+
branch:
6+
description: 'Branch name to lock.'
7+
required: true
8+
9+
runs:
10+
using: composite
11+
steps:
12+
- name: 'Lock branch ${{ inputs.branch }}'
13+
shell: bash
14+
env:
15+
GH_TOKEN: ${{ github.token }}
16+
run: |
17+
gh api --method PUT /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection \
18+
--input - << 'EOF'
19+
{
20+
"required_status_checks": null,
21+
"enforce_admins": false,
22+
"required_pull_request_reviews": {
23+
"dismiss_stale_reviews": true,
24+
"require_code_owner_reviews": false,
25+
"required_approving_review_count": 1
26+
},
27+
"restrictions": null,
28+
"allow_force_pushes": false,
29+
"allow_deletions": false,
30+
"lock_branch": false
31+
}
32+
EOF
33+
echo "🔒 Branch '${{ inputs.branch }}' is now protected." >> $GITHUB_STEP_SUMMARY
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: 'Unlock branch'
2+
author: 'Pete Sramek'
3+
description: 'Remove branch protection to allow a workflow to push directly. Always re-lock after the operation.'
4+
inputs:
5+
branch:
6+
description: 'Branch name to unlock.'
7+
required: true
8+
9+
runs:
10+
using: composite
11+
steps:
12+
- name: 'Unlock branch ${{ inputs.branch }}'
13+
shell: bash
14+
env:
15+
GH_TOKEN: ${{ github.token }}
16+
run: |
17+
gh api --method DELETE /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection || true
18+
echo "🔓 Branch protection removed from '${{ inputs.branch }}'." >> $GITHUB_STEP_SUMMARY

.github/workflows/bump-version.yml

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
name: 'Bump version'
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
bump-type:
7+
type: choice
8+
options:
9+
- 'minor'
10+
- 'major'
11+
description: 'Version bump type. Use ''minor'' to add features (keeps API files), ''major'' for breaking changes (resets API files).'
12+
required: true
13+
14+
permissions:
15+
actions: read
16+
contents: write
17+
pull-requests: write
18+
administration: write
19+
20+
concurrency:
21+
group: bump-version
22+
cancel-in-progress: false
23+
24+
env:
25+
dotnet-sdk-version: '10.x'
26+
27+
jobs:
28+
detect-version:
29+
name: 'Detect current version and calculate next'
30+
runs-on: ubuntu-latest
31+
outputs:
32+
current-version: ${{ steps.detect.outputs.current-version }}
33+
next-version: ${{ steps.calculate.outputs.next-version }}
34+
target-branch: ${{ steps.calculate.outputs.target-branch }}
35+
steps:
36+
- name: 'Checkout main'
37+
uses: actions/checkout@v6
38+
with:
39+
ref: main
40+
fetch-depth: 0
41+
42+
- name: 'Detect highest release branch'
43+
id: detect
44+
run: |
45+
git fetch origin
46+
latest=$(git ls-remote --heads origin 'release/*' | grep -oP 'release/\K\d+\.\d+' | sort -V | tail -1)
47+
if [[ -z "$latest" ]]; then
48+
latest="0.0"
49+
fi
50+
echo "Detected current version: $latest"
51+
echo "current-version=$latest" >> $GITHUB_OUTPUT
52+
53+
- name: 'Calculate next version'
54+
id: calculate
55+
run: |
56+
current="${{ steps.detect.outputs.current-version }}"
57+
major="${current%%.*}"
58+
minor="${current##*.}"
59+
if [[ "${{ inputs.bump-type }}" == "major" ]]; then
60+
next_major=$((major + 1))
61+
next_version="${next_major}.0"
62+
else
63+
next_minor=$((minor + 1))
64+
next_version="${major}.${next_minor}"
65+
fi
66+
echo "Next version: $next_version"
67+
echo "next-version=$next_version" >> $GITHUB_OUTPUT
68+
echo "target-branch=develop/${next_version}" >> $GITHUB_OUTPUT
69+
70+
validate:
71+
name: 'Validate bump'
72+
needs: [detect-version]
73+
runs-on: ubuntu-latest
74+
steps:
75+
- name: 'Validate workflow is running from main'
76+
if: ${{ github.ref_name != 'main' }}
77+
run: |
78+
echo "This workflow must be run from the 'main' branch. Current branch: '${{ github.ref_name }}'."
79+
exit 1
80+
81+
- name: 'Validate target branch does not exist'
82+
run: |
83+
set +e
84+
git ls-remote --exit-code --heads origin "${{ needs.detect-version.outputs.target-branch }}"
85+
if [[ $? -eq 0 ]]; then
86+
echo "Target branch '${{ needs.detect-version.outputs.target-branch }}' already exists."
87+
exit 1
88+
fi
89+
set -e
90+
91+
create-branch:
92+
name: 'Create ${{ needs.detect-version.outputs.target-branch }}'
93+
needs: [detect-version, validate]
94+
runs-on: ubuntu-latest
95+
env:
96+
next-version: ${{ needs.detect-version.outputs.next-version }}
97+
target-branch: ${{ needs.detect-version.outputs.target-branch }}
98+
steps:
99+
- name: 'Checkout main'
100+
uses: actions/checkout@v6
101+
with:
102+
ref: main
103+
fetch-depth: 0
104+
105+
- name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
106+
uses: actions/setup-dotnet@v5
107+
with:
108+
dotnet-version: ${{ env.dotnet-sdk-version }}
109+
110+
- name: 'Configure git'
111+
run: |
112+
git config user.name "$(git log -n 1 --pretty=format:%an)"
113+
git config user.email "$(git log -n 1 --pretty=format:%ae)"
114+
115+
- name: 'Create develop branch from main'
116+
run: |
117+
git checkout -b ${{ env.target-branch }}
118+
119+
- name: 'Reset PublicAPI files for major bump'
120+
if: ${{ inputs.bump-type == 'major' }}
121+
run: |
122+
find . -name "PublicAPI.Shipped.txt" | while read file; do
123+
printf '\xef\xbb\xbf#nullable enable\n' > "$file"
124+
echo "Reset: $file"
125+
done
126+
find . -name "PublicAPI.Unshipped.txt" | while read file; do
127+
printf '\xef\xbb\xbf#nullable enable\n' > "$file"
128+
echo "Reset: $file"
129+
done
130+
131+
- name: 'Commit API file reset'
132+
if: ${{ inputs.bump-type == 'major' }}
133+
run: |
134+
git add .
135+
git diff --staged --quiet || git commit -m "Reset PublicAPI files for major version ${{ env.next-version }}"
136+
137+
- name: 'Push develop branch'
138+
run: |
139+
git push --set-upstream origin ${{ env.target-branch }}
140+
141+
- name: 'Write summary'
142+
run: |
143+
echo "## ✅ Branch created: \`${{ env.target-branch }}\`" >> $GITHUB_STEP_SUMMARY
144+
echo "" >> $GITHUB_STEP_SUMMARY
145+
echo "| | |" >> $GITHUB_STEP_SUMMARY
146+
echo "|---|---|" >> $GITHUB_STEP_SUMMARY
147+
echo "| **Bump type** | ${{ inputs.bump-type }} |" >> $GITHUB_STEP_SUMMARY
148+
echo "| **Previous version** | ${{ needs.detect-version.outputs.current-version }} |" >> $GITHUB_STEP_SUMMARY
149+
echo "| **New version** | ${{ env.next-version }} |" >> $GITHUB_STEP_SUMMARY
150+
echo "| **Target branch** | \`${{ env.target-branch }}\` |" >> $GITHUB_STEP_SUMMARY
151+
if [[ "${{ inputs.bump-type }}" == "major" ]]; then
152+
echo "| **API files** | Reset (major bump) |" >> $GITHUB_STEP_SUMMARY
153+
else
154+
echo "| **API files** | Preserved (minor bump) |" >> $GITHUB_STEP_SUMMARY
155+
fi

.github/workflows/promote-branch.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ permissions:
2121
id-token: write
2222
contents: write
2323
pull-requests: write
24+
administration: write
2425

2526
concurrency:
2627
group: 'promote-branch-${{ inputs.promotion-type }}-${{github.ref_name }}'
@@ -30,6 +31,7 @@ env:
3031
dotnet-sdk-version: '10.x'
3132
is-development-branch: ${{ startsWith(github.ref_name, 'develop') }}
3233
is-maintenance-branch: ${{ startsWith(github.ref_name, 'support') }}
34+
is-preview-branch: ${{ startsWith(github.ref_name, 'preview') }}
3335

3436
jobs:
3537
versioning:
@@ -159,10 +161,15 @@ jobs:
159161
run: |
160162
echo "Invalid promotion type ${{ inputs.promotion-type }}"
161163
exit 1
162-
- name: 'Validate current branch'
163-
if: ${{ (env.is-development-branch == 'false') && (env.is-maintenance-branch == 'false') }}
164+
- name: 'Validate source branch for preview promotion'
165+
if: ${{ env.promotion-type == 'preview' && (env.is-development-branch == 'false') && (env.is-maintenance-branch == 'false') }}
164166
run: |
165-
echo "Invalid current branch '${{ github.ref_name }}'"
167+
echo "Preview promotion requires a 'develop/**' or 'support/**' source branch. Current branch: '${{ github.ref_name }}'"
168+
exit 1
169+
- name: 'Validate source branch for release promotion'
170+
if: ${{ env.promotion-type == 'release' && env.is-preview-branch == 'false' }}
171+
run: |
172+
echo "Release promotion requires a 'preview/**' source branch. Current branch: '${{ github.ref_name }}'"
166173
exit 1
167174
- name: 'Validate default and current branch'
168175
if: ${{ env.base-branch == env.current-branch }}
@@ -206,6 +213,11 @@ jobs:
206213
git switch ${{ needs.workflow-variables.outputs.base-branch }}
207214
git checkout -b ${{ needs.workflow-variables.outputs.target-branch }} origin/${{ needs.workflow-variables.outputs.target-branch }} || git checkout -b ${{ needs.workflow-variables.outputs.target-branch }}
208215
git push --set-upstream origin ${{ needs.workflow-variables.outputs.target-branch }}
216+
- name: 'Lock target branch'
217+
if: ${{ needs.workflow-variables.outputs.target-branch-exists == 'false' }}
218+
uses: './.github/actions/github/branch-protection/lock'
219+
with:
220+
branch: ${{ needs.workflow-variables.outputs.target-branch }}
209221
- name: 'Create PR: "Promote ${{ env.current-branch }} to ${{ env.target-branch }}"'
210222
if: ${{ needs.workflow-variables.outputs.pull-request-exists == 'false' }}
211223
env:

.github/workflows/release.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,59 @@ jobs:
257257
release-version: ${{ env.release-version }}
258258
is-preview: ${{ env.is-preview }}
259259
notes-start-tag: ${{ steps.determine-notes-start-tag.outputs.notes-start-tag }}
260+
261+
merge-to-main:
262+
name: 'Merge ${{ github.ref_name }} into main'
263+
needs: [workflow-variables, release, versioning]
264+
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
265+
runs-on: ubuntu-latest
266+
env:
267+
GH_TOKEN: ${{ github.token }}
268+
current-branch: ${{ github.ref_name }}
269+
current-version: ${{ needs.versioning.outputs.friendly-version }}
270+
steps:
271+
- name: 'Checkout ${{ github.head_ref || github.ref }}'
272+
uses: actions/checkout@v6
273+
with:
274+
fetch-depth: 0
275+
276+
- name: 'Detect if current branch is the latest release'
277+
id: detect-latest
278+
run: |
279+
git fetch origin
280+
latest_version=$(git ls-remote --heads origin 'release/*' | grep -oP 'release/\K\d+\.\d+' | sort -V | tail -1)
281+
echo "Latest release branch version: $latest_version"
282+
echo "Current version: ${{ env.current-version }}"
283+
if [[ "$latest_version" == "${{ env.current-version }}" ]]; then
284+
echo "is-latest=true" >> $GITHUB_OUTPUT
285+
else
286+
echo "is-latest=false" >> $GITHUB_OUTPUT
287+
fi
288+
289+
- name: 'Check if PR to main already exists'
290+
id: check-pr
291+
if: ${{ steps.detect-latest.outputs.is-latest == 'true' }}
292+
run: |
293+
pr_count=$(gh pr list --head "${{ env.current-branch }}" --base main --state open --limit 1 --json id --jq '. | length')
294+
if [[ $pr_count -gt 0 ]]; then
295+
echo "pr-exists=true" >> $GITHUB_OUTPUT
296+
else
297+
echo "pr-exists=false" >> $GITHUB_OUTPUT
298+
fi
299+
300+
- name: 'Create PR: Merge ${{ env.current-branch }} into main'
301+
if: ${{ steps.detect-latest.outputs.is-latest == 'true' && steps.check-pr.outputs.pr-exists == 'false' }}
302+
run: |
303+
gh pr create \
304+
--title "Merge ${{ env.current-branch }} into main" \
305+
--fill \
306+
--base main \
307+
--head "${{ env.current-branch }}"
308+
309+
- name: 'Write merge summary'
310+
run: |
311+
if [[ "${{ steps.detect-latest.outputs.is-latest }}" == "true" ]]; then
312+
echo "✅ PR created to merge **${{ env.current-branch }}** into **main**." >> $GITHUB_STEP_SUMMARY
313+
else
314+
echo "⏭️ Skipped merge to main: **${{ env.current-branch }}** is not the highest release branch." >> $GITHUB_STEP_SUMMARY
315+
fi

0 commit comments

Comments
 (0)