Retire Branch #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Retire Branch | |
| # Removes Dependabot entries for a branch that is no longer maintained and | |
| # locks the branch so no further commits can be pushed to it. | |
| # | |
| # Requires a token with: | |
| # contents:write — to update dependabot.yml on the default branch | |
| # administration:write — to set branch protection / lock the branch | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| repo: | |
| description: 'Repository (format: org/repo-name)' | |
| required: true | |
| type: string | |
| branch: | |
| description: 'Branch to retire' | |
| required: true | |
| type: string | |
| token: | |
| description: 'GitHub token with contents:write and administration:write on the target repository. Falls back to GH_ACTIONS_REPO_TOKEN.' | |
| required: false | |
| type: string | |
| default: '' | |
| workflow_call: | |
| inputs: | |
| repo: | |
| description: 'Repository (format: org/repo-name)' | |
| required: true | |
| type: string | |
| branch: | |
| description: 'Branch to retire' | |
| required: true | |
| type: string | |
| secrets: | |
| token: | |
| description: 'GitHub token with contents:write and administration:write on the target repository. Falls back to GH_ACTIONS_REPO_TOKEN.' | |
| required: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| retire: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # ----------------------------------------------------------------------- | |
| # 1. Remove Dependabot entries targeting the retiring branch. | |
| # Dependabot configuration lives on the default branch of the repo. | |
| # ----------------------------------------------------------------------- | |
| - name: Remove Dependabot entries for retiring branch | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }} | |
| run: | | |
| REPO="${{ inputs.repo }}" | |
| BRANCH="${{ inputs.branch }}" | |
| echo "=== Remove Dependabot Entries ===" | |
| echo "Repository: $REPO" | |
| echo "Branch: $BRANCH" | |
| echo "" | |
| # Clone the default branch (where dependabot.yml lives). | |
| git clone --depth 1 \ | |
| "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" _retire_repo | |
| cd _retire_repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| DEPBOT_FILE="" | |
| if [[ -f ".github/dependabot.yml" ]]; then | |
| DEPBOT_FILE=".github/dependabot.yml" | |
| elif [[ -f ".github/dependabot.yaml" ]]; then | |
| DEPBOT_FILE=".github/dependabot.yaml" | |
| fi | |
| if [[ -z "$DEPBOT_FILE" ]]; then | |
| echo "No dependabot.yml found on the default branch — nothing to remove." | |
| else | |
| echo "Found $DEPBOT_FILE" | |
| BEFORE=$(yq e '.updates | length' "$DEPBOT_FILE") | |
| yq e "del(.updates[] | select(.target-branch == \"${BRANCH}\"))" -i "$DEPBOT_FILE" | |
| AFTER=$(yq e '.updates | length' "$DEPBOT_FILE") | |
| REMOVED=$((BEFORE - AFTER)) | |
| if [[ $REMOVED -eq 0 ]]; then | |
| echo "No Dependabot entries found for branch '${BRANCH}' — nothing to remove." | |
| else | |
| echo "Removed ${REMOVED} Dependabot entr$([ $REMOVED -eq 1 ] && echo 'y' || echo 'ies') targeting '${BRANCH}'." | |
| if [[ $AFTER -eq 0 ]]; then | |
| echo "No entries remain — removing ${DEPBOT_FILE}." | |
| rm "$DEPBOT_FILE" | |
| fi | |
| git add . | |
| git commit -m "Remove Dependabot entries for retired branch ${BRANCH}" | |
| git push origin HEAD | |
| echo "Dependabot configuration updated." | |
| fi | |
| fi | |
| # ----------------------------------------------------------------------- | |
| # 2. Lock the branch so no further commits can be pushed to it. | |
| # Existing branch protection rules are preserved; only lock_branch | |
| # is added/set to true. | |
| # ----------------------------------------------------------------------- | |
| - name: Lock branch | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }} | |
| # Python script to merge lock_branch=true into existing branch protection settings. | |
| # Stored here as an env var to avoid unindented heredoc content breaking YAML parsing. | |
| LOCK_SCRIPT: | | |
| import json | |
| with open('/tmp/existing_protection.json') as f: | |
| p = json.load(f) | |
| body = {} | |
| rsc = p.get('required_status_checks') | |
| if rsc: | |
| entry = {'strict': rsc.get('strict', False)} | |
| if 'checks' in rsc: | |
| entry['checks'] = rsc['checks'] | |
| else: | |
| entry['contexts'] = rsc.get('contexts', []) | |
| body['required_status_checks'] = entry | |
| else: | |
| body['required_status_checks'] = None | |
| ea = p.get('enforce_admins', {}) | |
| body['enforce_admins'] = ea.get('enabled', False) if isinstance(ea, dict) else bool(ea) | |
| rprr = p.get('required_pull_request_reviews') | |
| if rprr: | |
| rev = { | |
| 'dismiss_stale_reviews': rprr.get('dismiss_stale_reviews', False), | |
| 'require_code_owner_reviews': rprr.get('require_code_owner_reviews', False), | |
| 'required_approving_review_count': rprr.get('required_approving_review_count', 0), | |
| } | |
| if rprr.get('dismissal_restrictions'): | |
| dr = rprr['dismissal_restrictions'] | |
| rev['dismissal_restrictions'] = { | |
| 'users': [u['login'] for u in dr.get('users', [])], | |
| 'teams': [t['slug'] for t in dr.get('teams', [])], | |
| } | |
| if rprr.get('bypass_pull_request_allowances'): | |
| bpa = rprr['bypass_pull_request_allowances'] | |
| rev['bypass_pull_request_allowances'] = { | |
| 'users': [u['login'] for u in bpa.get('users', [])], | |
| 'teams': [t['slug'] for t in bpa.get('teams', [])], | |
| 'apps': [a['slug'] for a in bpa.get('apps', [])], | |
| } | |
| body['required_pull_request_reviews'] = rev | |
| else: | |
| body['required_pull_request_reviews'] = None | |
| r = p.get('restrictions') | |
| body['restrictions'] = { | |
| 'users': [u['login'] for u in r.get('users', [])], | |
| 'teams': [t['slug'] for t in r.get('teams', [])], | |
| 'apps': [a['slug'] for a in r.get('apps', [])], | |
| } if r else None | |
| for flag in ( | |
| 'required_linear_history', | |
| 'allow_force_pushes', | |
| 'allow_deletions', | |
| 'block_creations', | |
| 'required_conversation_resolution', | |
| 'allow_fork_syncing', | |
| ): | |
| val = p.get(flag, {}) | |
| body[flag] = val.get('enabled', False) if isinstance(val, dict) else bool(val) | |
| body['lock_branch'] = True | |
| print(json.dumps(body)) | |
| run: | | |
| REPO="${{ inputs.repo }}" | |
| BRANCH="${{ inputs.branch }}" | |
| echo "=== Lock Branch ===" | |
| echo "Repository: $REPO" | |
| echo "Branch: $BRANCH" | |
| echo "" | |
| # Fetch any existing branch protection settings. | |
| EXISTING=$(gh api "repos/${REPO}/branches/${BRANCH}/protection" 2>/dev/null) || EXISTING='' | |
| if [[ -z "$EXISTING" ]]; then | |
| echo "No existing branch protection. Creating minimal protection with lock_branch=true." | |
| PAYLOAD='{"required_status_checks":null,"enforce_admins":false,"required_pull_request_reviews":null,"restrictions":null,"lock_branch":true}' | |
| else | |
| echo "Existing protection found. Merging lock_branch=true into current settings." | |
| # Write to a temp file so Python can consume it cleanly. | |
| echo "$EXISTING" > /tmp/existing_protection.json | |
| PAYLOAD=$(python3 -c "$LOCK_SCRIPT") | |
| fi | |
| echo "$PAYLOAD" \ | |
| | gh api "repos/${REPO}/branches/${BRANCH}/protection" \ | |
| --method PUT \ | |
| --header "Content-Type: application/json" \ | |
| --input - | |
| echo "" | |
| echo "Branch '${BRANCH}' is now locked — no further commits can be pushed to it." |