From a8961b7d63dba2db3a68b46cbe542fa2752b0b41 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Mon, 15 Jun 2026 09:45:16 +0200 Subject: [PATCH 1/2] ci: Add a scheduled prune for stale deploy branches The deploy job commits one __deploy____ branch of generated RTL per push; these accumulated to 100+ refs and polluted the GitLab CI mirror (git push --mirror re-syncs every branch, churning pipelines). Add a weekly workflow that keeps the latest release deploy branch and any mainline deploy newer than 60 days, and prunes the rest. Feature-branch deploys are always pruned. Allow-listed to __deploy__ refs with a dry-run default on manual runs. Nothing references these branches; downstream Bender deps pin the vX.Y.Z tag, which is unaffected. --- .github/workflows/prune-deploy-branches.yml | 101 ++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/workflows/prune-deploy-branches.yml diff --git a/.github/workflows/prune-deploy-branches.yml b/.github/workflows/prune-deploy-branches.yml new file mode 100644 index 00000000..512061c4 --- /dev/null +++ b/.github/workflows/prune-deploy-branches.yml @@ -0,0 +1,101 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# Author: +# - Daniel Keller + +name: prune-deploy-branches + +on: + schedule: + - cron: '17 4 * * 1' + workflow_dispatch: + inputs: + dry_run: + description: 'List deletions without deleting' + type: boolean + default: true + +permissions: + contents: write + +concurrency: + group: prune-deploy-branches + cancel-in-progress: false + +jobs: + prune: + if: github.repository == 'pulp-platform/iDMA' + runs-on: ubuntu-latest + env: + # Scheduled runs prune for real; manual runs honour the dry_run input. + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} + RETENTION_DAYS: '60' + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Identify the latest release deploy branch + id: keep + run: | + set -euo pipefail + latest_tag="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' | grep -vE -- '-' | sort -V | tail -n1)" + keep_branch="" + if [ -n "$latest_tag" ]; then + # The vX.Y.Z tag sits on the deploy commit; its parent is the -src commit, + # whose short hash names the deploy branch __deploy____master. + src7="$(git rev-parse --short=7 "${latest_tag}^" 2>/dev/null || true)" + [ -n "$src7" ] && keep_branch="__deploy__${src7}__master" + fi + echo "keep_branch=${keep_branch}" >> "$GITHUB_OUTPUT" + echo "latest release: ${latest_tag:-} | protected deploy branch: ${keep_branch:-}" + + - name: Prune deploy branches + env: + KEEP_BRANCH: ${{ steps.keep.outputs.keep_branch }} + run: | + set -euo pipefail + repo="${GITHUB_REPOSITORY}" + now="$(date -u +%s)" + cutoff=$(( now - RETENTION_DAYS * 86400 )) + kept=0; deleted=0; total=0 + echo "mode: $([ "$DRY_RUN" = 'true' ] && echo DRY-RUN || echo DELETE) | cutoff: $(date -u -d "@${cutoff}" '+%Y-%m-%d')" + + gh api --paginate "/repos/${repo}/branches?per_page=100" --jq '.[].name' > branches.txt + + while IFS= read -r br; do + # Hard allow-list: only __deploy__ refs are ever touchable. + case "$br" in __deploy__*) : ;; *) continue ;; esac + total=$((total+1)) + + # Keep the latest release's deploy branch unconditionally. + if [ -n "$KEEP_BRANCH" ] && [ "$br" = "$KEEP_BRANCH" ]; then + echo "KEEP (latest release) $br"; kept=$((kept+1)); continue + fi + + target="${br#__deploy__*__}" + sha="$(gh api "/repos/${repo}/branches/${br}" --jq '.commit.sha')" + cepoch="$(date -u -d "$(gh api "/repos/${repo}/commits/${sha}" --jq '.commit.committer.date')" +%s)" + age=$(( (now - cepoch) / 86400 )) + + # Feature-branch deploys are pure CI cruft: always prune. Mainline deploys + # follow the retention window. + if [ "$target" = "master" ] || [ "$target" = "devel" ]; then + if [ "$cepoch" -ge "$cutoff" ]; then + echo "KEEP (${age}d, mainline) $br"; kept=$((kept+1)); continue + fi + fi + + if [ "$DRY_RUN" = 'true' ]; then + echo "WOULD-DELETE (${age}d) $br" + else + gh api -X DELETE "/repos/${repo}/git/refs/heads/${br}" || echo " (already gone) $br" + echo "DELETED (${age}d) $br" + fi + deleted=$((deleted+1)) + done < branches.txt + + echo "scanned __deploy__: $total | kept: $kept | pruned: $deleted" From e00dd38f17445e00e3d3f7e691a8cbc166084219 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Mon, 15 Jun 2026 09:52:32 +0200 Subject: [PATCH 2/2] ci: Harden deploy-branch prune against deleting the release branch Address review: (1) refuse to prune if no release tag is found instead of falling through and deleting the (>60-day-old) latest release branch when KEEP_BRANCH is empty; (2) derive the kept branch hash with `rev-parse | cut -c1-7` to exactly match deploy.py's [0:7] (rev-parse --short can extend past 7 on ambiguity and mismatch); (3) skip a branch on any transient API/date failure instead of aborting the whole run under set -e. --- .github/workflows/prune-deploy-branches.yml | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/prune-deploy-branches.yml b/.github/workflows/prune-deploy-branches.yml index 512061c4..7bc8a61b 100644 --- a/.github/workflows/prune-deploy-branches.yml +++ b/.github/workflows/prune-deploy-branches.yml @@ -43,15 +43,15 @@ jobs: run: | set -euo pipefail latest_tag="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' | grep -vE -- '-' | sort -V | tail -n1)" - keep_branch="" - if [ -n "$latest_tag" ]; then - # The vX.Y.Z tag sits on the deploy commit; its parent is the -src commit, - # whose short hash names the deploy branch __deploy____master. - src7="$(git rev-parse --short=7 "${latest_tag}^" 2>/dev/null || true)" - [ -n "$src7" ] && keep_branch="__deploy__${src7}__master" + if [ -z "$latest_tag" ]; then + echo "::error::no vX.Y.Z release tag found; refusing to prune"; exit 1 fi + # The vX.Y.Z tag sits on the deploy commit; its parent is the -src commit, + # whose first 7 hex chars name __deploy____master (deploy.py uses [0:7]). + src7="$(git rev-parse "${latest_tag}^" | cut -c1-7)" + keep_branch="__deploy__${src7}__master" echo "keep_branch=${keep_branch}" >> "$GITHUB_OUTPUT" - echo "latest release: ${latest_tag:-} | protected deploy branch: ${keep_branch:-}" + echo "latest release: ${latest_tag} | protected deploy branch: ${keep_branch}" - name: Prune deploy branches env: @@ -77,8 +77,13 @@ jobs: fi target="${br#__deploy__*__}" - sha="$(gh api "/repos/${repo}/branches/${br}" --jq '.commit.sha')" - cepoch="$(date -u -d "$(gh api "/repos/${repo}/commits/${sha}" --jq '.commit.committer.date')" +%s)" + # Skip (do not delete) on any transient API/parse failure for this branch. + sha="$(gh api "/repos/${repo}/branches/${br}" --jq '.commit.sha')" \ + || { echo "SKIP (api error) $br"; continue; } + cdate="$(gh api "/repos/${repo}/commits/${sha}" --jq '.commit.committer.date')" \ + || { echo "SKIP (api error) $br"; continue; } + cepoch="$(date -u -d "$cdate" +%s)" \ + || { echo "SKIP (bad date) $br"; continue; } age=$(( (now - cepoch) / 86400 )) # Feature-branch deploys are pure CI cruft: always prune. Mainline deploys