Skip to content

Commit b688f8b

Browse files
authored
backtracking (#195)
1 parent 8a6bf93 commit b688f8b

3 files changed

Lines changed: 77 additions & 26 deletions

File tree

.github/actions/github/branch-protection/lock/action.yml

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: 'Lock branch'
22
author: 'Pete Sramek'
3-
description: 'Apply branch protection to prevent direct pushes. Requires PRs with at least one approval.'
3+
description: 'Apply branch protection to prevent direct pushes. Requires PRs with configurable approval count; admins can optionally bypass all restrictions.'
44
inputs:
55
branch:
66
description: 'Branch name to lock.'
@@ -9,7 +9,23 @@ inputs:
99
description: 'GitHub token with administration:write (repo admin) permission. Use a PAT; GITHUB_TOKEN cannot call the branch protection API.'
1010
required: true
1111
lock-branch:
12-
description: 'When true, sets lock_branch to prevent even PR merges (use during automated operations). When false (default), only direct pushes are blocked; PRs with required reviews can still be merged.'
12+
description: 'When true, sets lock_branch to prevent even PR merges (use during automated operations). When false (default), only direct pushes are blocked; PRs can still be merged.'
13+
required: false
14+
default: 'false'
15+
required-approving-review-count:
16+
description: 'Number of approving reviews required before a PR can be merged. Set to 0 to require PRs without requiring approvals.'
17+
required: false
18+
default: '1'
19+
dismiss-stale-reviews:
20+
description: 'When true, approved reviews are dismissed when new commits are pushed to the branch.'
21+
required: false
22+
default: 'true'
23+
bypass-admins:
24+
description: 'When true, repository admins are exempt from all branch protection rules (enforce_admins is disabled). When false (default), admins are also subject to the rules.'
25+
required: false
26+
default: 'false'
27+
skip-pull-request-reviews:
28+
description: 'When true, sets required_pull_request_reviews to null (removes PR review requirement). Use temporarily before automated merges so the merge API is not blocked. When false (default), PR reviews are required as configured.'
1329
required: false
1430
default: 'false'
1531

@@ -21,23 +37,32 @@ runs:
2137
env:
2238
GH_TOKEN: ${{ inputs.token }}
2339
run: |
24-
if ! gh api --method PUT /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection \
25-
--input - << 'EOF'
26-
{
27-
"required_status_checks": null,
28-
"enforce_admins": false,
29-
"required_pull_request_reviews": {
30-
"dismiss_stale_reviews": true,
31-
"require_code_owner_reviews": false,
32-
"required_approving_review_count": 1
33-
},
34-
"restrictions": null,
35-
"allow_force_pushes": false,
36-
"allow_deletions": false,
37-
"lock_branch": ${{ inputs.lock-branch }}
38-
}
39-
EOF
40-
then
40+
ENFORCE_ADMINS=true
41+
if [ '${{ inputs.bypass-admins }}' = 'true' ]; then
42+
ENFORCE_ADMINS=false
43+
fi
44+
45+
PAYLOAD=$(jq -n \
46+
--argjson review_count '${{ inputs.required-approving-review-count }}' \
47+
--argjson dismiss_stale '${{ inputs.dismiss-stale-reviews }}' \
48+
--argjson enforce_admins "$ENFORCE_ADMINS" \
49+
--argjson lock_branch '${{ inputs.lock-branch }}' \
50+
--argjson skip_reviews '${{ inputs.skip-pull-request-reviews }}' \
51+
'{
52+
"required_status_checks": null,
53+
"enforce_admins": $enforce_admins,
54+
"required_pull_request_reviews": (if $skip_reviews then null else {
55+
"dismiss_stale_reviews": $dismiss_stale,
56+
"require_code_owner_reviews": false,
57+
"required_approving_review_count": $review_count
58+
} end),
59+
"restrictions": null,
60+
"allow_force_pushes": false,
61+
"allow_deletions": false,
62+
"lock_branch": $lock_branch
63+
}')
64+
65+
if ! echo "$PAYLOAD" | gh api --method PUT /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection --input -; then
4166
echo "::error::Failed to apply branch protection to '${{ inputs.branch }}'. Ensure the token has 'administration: write' permission and the branch exists."
4267
exit 1
4368
fi

.github/actions/github/branch-protection/unlock/action.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ inputs:
88
token:
99
description: 'GitHub token with administration:write (repo admin) permission. Use a PAT; GITHUB_TOKEN cannot call the branch protection API.'
1010
required: true
11+
fail-on-error:
12+
description: 'When true, the step fails if branch protection cannot be removed. When false (default), errors are silently ignored.'
13+
required: false
14+
default: 'false'
1115

1216
runs:
1317
using: composite
@@ -17,5 +21,9 @@ runs:
1721
env:
1822
GH_TOKEN: ${{ inputs.token }}
1923
run: |
20-
gh api --method DELETE /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection || true
24+
if [ '${{ inputs.fail-on-error }}' = 'true' ]; then
25+
gh api --method DELETE /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection
26+
else
27+
gh api --method DELETE /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection || true
28+
fi
2129
echo "🔓 Branch protection removed from '${{ inputs.branch }}'." >> $GITHUB_STEP_SUMMARY

.github/workflows/backtrack.yml

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ jobs:
8787
token: ${{ secrets.GH_ADMIN_TOKEN }}
8888
lock-branch: 'true'
8989

90+
- name: 'Unlock preview branch for merge'
91+
if: ${{ steps.targets.outputs.preview-branch != '' }}
92+
uses: './.github/actions/github/branch-protection/lock'
93+
with:
94+
branch: ${{ steps.targets.outputs.preview-branch }}
95+
token: ${{ secrets.GH_ADMIN_TOKEN }}
96+
lock-branch: 'false'
97+
skip-pull-request-reviews: 'true'
98+
9099
- name: 'Backtrack: merge ${{ github.base_ref }} into ${{ steps.targets.outputs.preview-branch }}'
91100
if: ${{ steps.targets.outputs.preview-branch != '' }}
92101
env:
@@ -97,6 +106,15 @@ jobs:
97106
--field head="${{ github.base_ref }}" \
98107
--field commit_message="Backtrack: merge ${{ github.base_ref }} into ${{ steps.targets.outputs.preview-branch }}"
99108
109+
- name: 'Unlock develop branch for merge'
110+
if: ${{ steps.check-develop.outputs.exists == 'true' }}
111+
uses: './.github/actions/github/branch-protection/lock'
112+
with:
113+
branch: ${{ steps.targets.outputs.develop-branch }}
114+
token: ${{ secrets.GH_ADMIN_TOKEN }}
115+
lock-branch: 'false'
116+
skip-pull-request-reviews: 'true'
117+
100118
- name: 'Backtrack: merge ${{ steps.targets.outputs.merge-source }} into ${{ steps.targets.outputs.develop-branch }}'
101119
if: ${{ steps.check-develop.outputs.exists == 'true' }}
102120
env:
@@ -109,27 +127,27 @@ jobs:
109127
110128
- name: 'Restore protection: base branch'
111129
if: ${{ always() && steps.lock-base.outcome == 'success' }}
112-
uses: './.github/actions/github/branch-protection/lock'
130+
uses: './.github/actions/github/branch-protection/unlock'
113131
with:
114132
branch: ${{ github.base_ref }}
115133
token: ${{ secrets.GH_ADMIN_TOKEN }}
116-
lock-branch: 'false'
134+
fail-on-error: 'true'
117135

118136
- name: 'Restore protection: preview branch'
119137
if: ${{ always() && steps.lock-preview.outcome == 'success' }}
120-
uses: './.github/actions/github/branch-protection/lock'
138+
uses: './.github/actions/github/branch-protection/unlock'
121139
with:
122140
branch: ${{ steps.targets.outputs.preview-branch }}
123141
token: ${{ secrets.GH_ADMIN_TOKEN }}
124-
lock-branch: 'false'
142+
fail-on-error: 'true'
125143

126144
- name: 'Restore protection: develop branch'
127145
if: ${{ always() && steps.lock-develop.outcome == 'success' }}
128-
uses: './.github/actions/github/branch-protection/lock'
146+
uses: './.github/actions/github/branch-protection/unlock'
129147
with:
130148
branch: ${{ steps.targets.outputs.develop-branch }}
131149
token: ${{ secrets.GH_ADMIN_TOKEN }}
132-
lock-branch: 'false'
150+
fail-on-error: 'true'
133151

134152
- name: 'Write backtrack summary'
135153
if: ${{ always() }}

0 commit comments

Comments
 (0)