Skip to content

Commit 2a5ddd6

Browse files
GHA-163 Add Lock Branch Action to manage branch protection settings (#81)
1 parent a8ef462 commit 2a5ddd6

13 files changed

Lines changed: 1191 additions & 0 deletions

.github/workflows/test-all.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ jobs:
4141

4242
test-update-rule-metadata:
4343
uses: ./.github/workflows/test-update-rule-metadata.yml
44+
45+
test-lock-branch:
46+
uses: ./.github/workflows/test-lock-branch.yml
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Test Lock Branch Action
2+
3+
on:
4+
workflow_call:
5+
pull_request:
6+
paths:
7+
- 'lock-branch/**'
8+
- '.github/workflows/test-lock-branch.yml'
9+
push:
10+
branches:
11+
- branch-*
12+
paths:
13+
- 'lock-branch/**'
14+
- '.github/workflows/test-lock-branch.yml'
15+
workflow_dispatch:
16+
17+
jobs:
18+
unit-tests:
19+
name: Run Unit Tests
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@v4
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
28+
with:
29+
python-version: '3.10'
30+
cache: 'pip'
31+
cache-dependency-path: lock-branch/requirements.txt
32+
33+
- name: Install dependencies
34+
run: |
35+
cd lock-branch
36+
pip install -r requirements.txt
37+
pip install pytest pytest-cov
38+
39+
- name: Run unit tests
40+
run: |
41+
cd lock-branch
42+
python -m pytest test_utils.py test_lock_branch.py test_notify_slack.py -v --cov=utils --cov=lock_branch --cov=notify_slack --cov-report=term-missing

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
This is a collection of reusable GitHub Actions for automating SonarSource analyzer releases. Actions handle Jira integration (tickets, versions, release notes), GitHub releases, cross-repository updates, and Slack notifications.
88

9+
## Branching
10+
11+
**Important:** Changes must always be made on a feature branch, never directly on `master`.
12+
- If on `master`, create a new branch using the format: `ab/<feature-name>` (e.g., `ab/add-slack-notifications`)
13+
- The prefix `ab` represents the developer's initials (first letter of first name + first letter of last name)
14+
- Adapt `<feature-name>` based on the task/prompt (use lowercase, hyphen-separated)
15+
- If already on a feature branch, do not create a new branch—continue working on the current branch
16+
917
## Testing
1018

19+
**Important:** When making any code changes, always check if there are related tests that need to be updated. Always run the tests after making changes to ensure nothing is broken.
20+
1121
### Run all tests (CI)
1222
Tests run automatically via GitHub Actions. To trigger manually:
1323
- Push to `master` runs `.github/workflows/test-all.yml`

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ A centralized collection of reusable GitHub Actions designed to streamline and a
1111
* [**Get Jira Release Notes**](get-jira-release-notes/README.md): Fetches Jira release notes and generates the release notes URL for a given project and version.
1212
* [**Get Jira Version**](get-jira-version/README.md): Extracts a Jira-compatible version number from a release version by formatting it appropriately for Jira.
1313
* [**Get Release Version**](get-release-version/README.md): Extracts the release version from the repox status on a specified branch.
14+
* [**Lock Branch**](lock-branch/README.md): Locks or unlocks a branch by modifying the `lock_branch` setting in branch protection rules.
1415
* [**Notify Slack on Failure**](notify-slack/README.md): Sends a Slack notification when a job fails.
1516
* [**Publish GitHub Release**](publish-github-release/README.md): Publishes a GitHub Release with notes fetched from Jira or provided directly.
1617
* [**Release Jira Version**](release-jira-version/README.md): Releases a Jira version and creates the next one.
1718
* [**Update Analyzer**](update-analyzer/README.md): Updates an analyzer version in SonarQube or SonarCloud and creates a pull request.
1819
* [**Update Release Ticket Status**](update-release-ticket-status/README.md): Updates the status of a Jira release ticket and can change its assignee.
1920
* [**Update Rule Metadata**](update-rule-metadata/README.md): Automates updating rule metadata across all supported languages using the rule-api tooling.
21+
22+
## Available Workflows
23+
2024
* [**Automated Release Workflow**](docs/AUTOMATED_RELEASE.md): Orchestrates the end-to-end release across Jira and GitHub, with optional integration tickets and analyzer PRs.
2125

2226
## Development

lock-branch/README.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Lock Branch Action
2+
3+
This GitHub Action locks or unlocks a branch by modifying the `lock_branch` setting in branch protection rules.
4+
5+
## Description
6+
7+
The action modifies branch protection settings to:
8+
- **Freeze (lock)**: Prevent all pushes and merges to the branch
9+
- **Unfreeze (unlock)**: Allow normal operations on the branch
10+
11+
This is useful for temporarily locking a branch during release processes.
12+
13+
## Prerequisites
14+
15+
- Branch protection must be enabled on the target branch (or will be created with minimal settings)
16+
- GitHub token with `administration:write` permissions
17+
18+
## Inputs
19+
20+
| Input | Description | Required | Default |
21+
|-------|-------------|----------|---------|
22+
| `branch` | The branch name to lock/unlock | Yes | - |
23+
| `freeze` | Set to `true` to lock (freeze), `false` to unlock (unfreeze) | Yes | - |
24+
| `slack-channel` | Slack channel to notify about state changes | No | - |
25+
| `github-token` | GitHub token with admin permissions | No | From Vault |
26+
| `slack-token` | Slack token for notifications | No | From Vault |
27+
28+
## Outputs
29+
30+
| Output | Description |
31+
|--------|-------------|
32+
| `previous-state` | The previous lock state (`true`/`false`) |
33+
| `current-state` | The current lock state (`true`/`false`) |
34+
| `branch` | The branch that was modified |
35+
36+
## Usage
37+
38+
### Lock (freeze) a branch
39+
40+
```yaml
41+
- name: Freeze master branch
42+
uses: SonarSource/release-github-actions/lock-branch@v1
43+
with:
44+
branch: master
45+
freeze: true
46+
slack-channel: '#releases'
47+
```
48+
49+
### Unlock (unfreeze) a branch
50+
51+
```yaml
52+
- name: Unfreeze master branch
53+
uses: SonarSource/release-github-actions/lock-branch@v1
54+
with:
55+
branch: master
56+
freeze: false
57+
slack-channel: '#releases'
58+
```
59+
60+
### Use in automated release workflow
61+
62+
```yaml
63+
jobs:
64+
freeze-branch:
65+
runs-on: ubuntu-latest
66+
permissions:
67+
id-token: write
68+
steps:
69+
- uses: SonarSource/release-github-actions/lock-branch@v1
70+
with:
71+
branch: ${{ inputs.branch }}
72+
freeze: true
73+
slack-channel: ${{ inputs.slack-channel }}
74+
75+
# ... release steps ...
76+
77+
unfreeze-branch:
78+
needs: [release-steps]
79+
if: always()
80+
runs-on: ubuntu-latest
81+
permissions:
82+
id-token: write
83+
steps:
84+
- uses: SonarSource/release-github-actions/lock-branch@v1
85+
with:
86+
branch: ${{ inputs.branch }}
87+
freeze: false
88+
slack-channel: ${{ inputs.slack-channel }}
89+
```
90+
91+
## Behavior
92+
93+
### Locking a branch
94+
- Sets `lock_branch: true` in branch protection settings
95+
- Prevents all pushes and merges to the branch
96+
- Preserves all other existing branch protection settings
97+
98+
### Unlocking a branch
99+
- Sets `lock_branch: false` in branch protection settings
100+
- Allows normal push and merge operations
101+
- Preserves all other existing branch protection settings
102+
103+
### Idempotent operation
104+
- If the branch is already in the requested state, no changes are made
105+
- The action will succeed and report the current state
106+
107+
### No existing protection
108+
- If the branch has no existing protection, minimal protection is created with just the lock setting
109+
- Existing protection settings are always preserved when updating
110+
111+
## Slack Notifications
112+
113+
When `slack-channel` is provided, the action sends a notification with:
114+
- Lock/unlock icon indicating the action taken
115+
- Branch name and repository
116+
- Link to the workflow run
117+
118+
## Error Handling
119+
120+
The action will fail if:
121+
- GitHub authentication fails
122+
- Branch protection cannot be updated (e.g., insufficient permissions)
123+
- API request fails
124+
125+
The action will succeed with a warning if:
126+
- No branch protection exists (will create minimal protection with lock)
127+
- Branch is already in the requested state

lock-branch/action.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: 'Lock Branch'
2+
description: 'Locks or unlocks a branch by modifying the lock_branch setting in branch protection rules.'
3+
4+
inputs:
5+
branch:
6+
description: 'The branch name to lock/unlock'
7+
required: true
8+
freeze:
9+
description: 'Set to true to lock (freeze) the branch, false to unlock (unfreeze)'
10+
required: true
11+
slack-channel:
12+
description: 'Optional Slack channel to notify about the state change'
13+
required: false
14+
github-token:
15+
description: 'GitHub token with admin permissions (defaults to Vault)'
16+
required: false
17+
slack-token:
18+
description: 'Slack token for notifications (defaults to Vault)'
19+
required: false
20+
21+
outputs:
22+
branch:
23+
description: 'The branch that was modified'
24+
value: ${{ steps.run_python_script.outputs.branch }}
25+
previous-state:
26+
description: 'The previous lock state of the branch (true/false)'
27+
value: ${{ steps.run_python_script.outputs.previous_state }}
28+
current-state:
29+
description: 'The current lock state of the branch (true/false)'
30+
value: ${{ steps.run_python_script.outputs.current_state }}
31+
32+
runs:
33+
using: 'composite'
34+
steps:
35+
- name: Get Secrets from Vault
36+
id: secrets
37+
if: ${{ !inputs.github-token || (!inputs.slack-token && inputs.slack-channel) }}
38+
uses: SonarSource/vault-action-wrapper@v3
39+
with:
40+
secrets: |
41+
development/github/token/{REPO_OWNER_NAME_DASH}-lock token | GITHUB_LOCK_TOKEN;
42+
development/kv/data/slack token | SLACK_TOKEN;
43+
44+
- name: Set up Python
45+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
46+
with:
47+
python-version: '3.10'
48+
49+
- name: Install dependencies
50+
shell: bash
51+
run: pip install -r ${{ github.action_path }}/requirements.txt
52+
53+
- name: Lock/Unlock Branch
54+
id: run_python_script
55+
shell: bash
56+
env:
57+
GITHUB_TOKEN: ${{ inputs.github-token || fromJSON(steps.secrets.outputs.vault).GITHUB_LOCK_TOKEN }}
58+
INPUT_BRANCH: ${{ inputs.branch }}
59+
INPUT_FREEZE: ${{ inputs.freeze }}
60+
run: |
61+
python ${{ github.action_path }}/lock_branch.py \
62+
--branch "$INPUT_BRANCH" \
63+
--freeze "$INPUT_FREEZE" \
64+
--repository "${{ github.repository }}" \
65+
>> $GITHUB_OUTPUT
66+
67+
- name: Send Slack Notification
68+
if: ${{ inputs.slack-channel != '' }}
69+
shell: bash
70+
env:
71+
SLACK_TOKEN: ${{ inputs.slack-token || fromJSON(steps.secrets.outputs.vault).SLACK_TOKEN }}
72+
INPUT_CHANNEL: ${{ inputs.slack-channel }}
73+
INPUT_BRANCH: ${{ inputs.branch }}
74+
INPUT_FREEZE: ${{ inputs.freeze }}
75+
run: |
76+
python ${{ github.action_path }}/notify_slack.py \
77+
--channel "$INPUT_CHANNEL" \
78+
--branch "$INPUT_BRANCH" \
79+
--repository "${{ github.repository }}" \
80+
--freeze "$INPUT_FREEZE" \
81+
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

0 commit comments

Comments
 (0)