Skip to content

Commit 3289566

Browse files
turegjorupclaude
andcommitted
feat: add Renovate auto-patch and auto-release pipeline
Phase 1 fork validation before promoting to upstream itk-dev/devops_itksites. Patches/security target main as hotfixes with a 7-day soak (0 for CVE alerts); minor/major target develop with Dependency Dashboard approval. After a Renovate merge to main, the [Unreleased] CHANGELOG section is promoted to the next patch version, tagged from main, released by github_build_release.yml, and main is back-merged into develop. See RENOVATE_PLAN.md for the full design and exit criteria. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dcdd479 commit 3289566

6 files changed

Lines changed: 671 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env bash
2+
# Called by Renovate postUpgradeTasks. Appends a single bullet under the
3+
# `## [Unreleased]` section of CHANGELOG.md so the auto-release workflow has
4+
# content to promote into a tagged release after the Renovate PR merges to main.
5+
set -euo pipefail
6+
7+
BRANCH="${1:-renovate/unknown}"
8+
TITLE="${2:-Update dependencies}"
9+
10+
# Skip if CHANGELOG.md was already touched this run (grouped PRs fire the hook
11+
# more than once; we only want one bullet per PR).
12+
if git diff --cached --name-only | grep -qx CHANGELOG.md; then
13+
exit 0
14+
fi
15+
if git diff --name-only | grep -qx CHANGELOG.md; then
16+
exit 0
17+
fi
18+
19+
ENTRY="- Renovate: ${TITLE} (\`${BRANCH}\`)"
20+
21+
python3 - "$ENTRY" <<'PY'
22+
import re, sys, pathlib
23+
entry = sys.argv[1]
24+
p = pathlib.Path("CHANGELOG.md")
25+
text = p.read_text(encoding="utf-8")
26+
27+
pattern = re.compile(r"(## \[Unreleased\]\n)(.*?)(\n## \[)", re.DOTALL)
28+
m = pattern.search(text)
29+
if not m:
30+
# No [Unreleased] section yet — insert one after the intro paragraph
31+
# (the first blank line after the H1 header).
32+
head, _, rest = text.partition("\n\n")
33+
text = f"{head}\n\n## [Unreleased]\n\n{entry}\n\n{rest}"
34+
else:
35+
body = m.group(2).rstrip("\n")
36+
sep = "\n" if body else ""
37+
text = pattern.sub(
38+
lambda _m: f"{_m.group(1)}{body}{sep}{entry}\n{_m.group(3)}",
39+
text, count=1,
40+
)
41+
42+
p.write_text(text, encoding="utf-8")
43+
PY
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# After a Renovate PR merges into `main`, promote the `[Unreleased]` section
2+
# of CHANGELOG.md to the next patch version, commit, tag, push, and back-merge
3+
# main into develop so the branches stay in sync.
4+
#
5+
# Required secrets:
6+
# RELEASE_TOKEN PAT or GitHub App token with contents:write + workflows.
7+
# Needed for the tag push to trigger github_build_release.yml
8+
# (the default GITHUB_TOKEN cannot trigger other workflows)
9+
# and for the back-merge push to `develop`.
10+
#
11+
# Gating: the existing pr.yaml is required by branch protection, so by the time
12+
# a PR is merged into main, CI has already passed. We do not re-wait for checks
13+
# on the merge commit (it has no associated check runs after a squash-merge).
14+
15+
name: Auto Release on Renovate Merge
16+
17+
on:
18+
pull_request:
19+
types: [closed]
20+
branches: [main]
21+
22+
permissions:
23+
contents: read
24+
25+
jobs:
26+
release:
27+
if: >-
28+
github.event.pull_request.merged == true &&
29+
github.event.pull_request.user.login == 'renovate[bot]'
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Checkout main with full history
33+
uses: actions/checkout@v6
34+
with:
35+
ref: main
36+
fetch-depth: 0
37+
token: ${{ secrets.RELEASE_TOKEN }}
38+
39+
- name: Compute next patch version
40+
id: version
41+
run: |
42+
latest=$(git tag -l '[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1)
43+
: "${latest:=0.0.0}"
44+
IFS=. read -r maj min pat <<<"$latest"
45+
next="${maj}.${min}.$((pat + 1))"
46+
{
47+
echo "current=$latest"
48+
echo "next=$next"
49+
echo "date=$(date -u +%Y-%m-%d)"
50+
} >>"$GITHUB_OUTPUT"
51+
echo "Will release: $latest -> $next"
52+
53+
- name: Bail if [Unreleased] is empty
54+
id: gate
55+
run: |
56+
body=$(awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md \
57+
| grep -v '^[[:space:]]*$' || true)
58+
if [ -z "$body" ]; then
59+
echo "Nothing under [Unreleased]; skipping release."
60+
echo "skip=true" >>"$GITHUB_OUTPUT"
61+
else
62+
echo "skip=false" >>"$GITHUB_OUTPUT"
63+
fi
64+
65+
- name: Promote [Unreleased] -> ${{ steps.version.outputs.next }}
66+
if: steps.gate.outputs.skip != 'true'
67+
env:
68+
VER: ${{ steps.version.outputs.next }}
69+
DATE: ${{ steps.version.outputs.date }}
70+
run: |
71+
python3 - <<'PY'
72+
import os, re, pathlib
73+
ver = os.environ["VER"]
74+
date = os.environ["DATE"]
75+
p = pathlib.Path("CHANGELOG.md")
76+
text = p.read_text(encoding="utf-8")
77+
text = re.sub(
78+
r"## \[Unreleased\]\n",
79+
f"## [Unreleased]\n\n## [{ver}] - {date}\n",
80+
text, count=1,
81+
)
82+
p.write_text(text, encoding="utf-8")
83+
PY
84+
85+
- name: Commit on main, tag, push
86+
if: steps.gate.outputs.skip != 'true'
87+
env:
88+
VER: ${{ steps.version.outputs.next }}
89+
run: |
90+
git config user.name "github-actions[bot]"
91+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
92+
git add CHANGELOG.md
93+
git commit -m "release: ${VER}"
94+
git tag -a "${VER}" -m "Release ${VER}"
95+
git push origin main
96+
git push origin "${VER}"
97+
98+
- name: Back-merge main into develop
99+
if: steps.gate.outputs.skip != 'true'
100+
env:
101+
VER: ${{ steps.version.outputs.next }}
102+
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
103+
run: |
104+
git fetch origin develop
105+
git checkout -B develop origin/develop
106+
if git merge --no-ff main -m "chore: back-merge hotfix ${VER} into develop"; then
107+
git push origin develop
108+
echo "Back-merged ${VER} to develop."
109+
else
110+
git merge --abort || true
111+
gh pr create \
112+
--repo "${GITHUB_REPOSITORY}" \
113+
--base develop \
114+
--head main \
115+
--title "Back-merge hotfix ${VER} into develop" \
116+
--body "Automated back-merge from \`main\` conflicted with \`develop\`. Resolve and merge to keep branches in sync after release ${VER}."
117+
fi

.github/workflows/renovate.yaml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Self-hosted Renovate runner. Runs on a schedule and on demand.
2+
# Self-hosted is required because postUpgradeTasks (which writes CHANGELOG.md
3+
# inline with the dep bump) is disabled on the Mend Renovate SaaS app.
4+
#
5+
# Required secrets:
6+
# RENOVATE_TOKEN PAT or GitHub App token with repo write + workflows.
7+
# Pushes by the default GITHUB_TOKEN don't re-trigger Actions,
8+
# so a PAT/App token is needed to make CI re-run on
9+
# Renovate's branch updates.
10+
11+
name: Renovate
12+
13+
on:
14+
schedule:
15+
- cron: "0 5 * * 1-5" # weekdays 05:00 UTC ~= 06/07:00 Aarhus
16+
workflow_dispatch:
17+
inputs:
18+
logLevel:
19+
description: Renovate log level
20+
type: choice
21+
default: info
22+
options: [debug, info, warn, error]
23+
dryRun:
24+
description: Dry-run (no PRs)
25+
type: choice
26+
default: "null"
27+
options: ["null", "full"]
28+
29+
permissions:
30+
contents: read
31+
32+
concurrency:
33+
group: renovate
34+
cancel-in-progress: false
35+
36+
jobs:
37+
renovate:
38+
runs-on: ubuntu-latest
39+
steps:
40+
- uses: actions/checkout@v6
41+
42+
# PHP/Composer are needed so Renovate can resolve composer.lock
43+
# updates inside its container.
44+
- uses: shivammathur/setup-php@v2
45+
with:
46+
php-version: "8.3"
47+
tools: composer:v2
48+
coverage: none
49+
50+
- name: Run Renovate
51+
uses: renovatebot/github-action@v43
52+
with:
53+
configurationFile: renovate.json
54+
token: ${{ secrets.RENOVATE_TOKEN }}
55+
renovate-version: latest
56+
env:
57+
LOG_LEVEL: ${{ inputs.logLevel || 'info' }}
58+
RENOVATE_DRY_RUN: ${{ inputs.dryRun || 'null' }}
59+
RENOVATE_REPOSITORIES: ${{ github.repository }}
60+
# Allow the changelog script to run from postUpgradeTasks.
61+
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^bash \\.github/scripts/renovate-changelog\\.sh"]'
62+
RENOVATE_ALLOW_POST_UPGRADE_COMMAND_TEMPLATING: "true"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
- [#81](https://github.com/itk-dev/devops_itksites/pull/81) 5564: Asset Mapper migration
1111
- Add Symfony Asset Mapper bundle and importmap
12+
- Add Renovate auto-patch + auto-release pipeline (Phase 1 fork validation)
1213

1314
## [1.11.0] - 2026-05-19
1415

0 commit comments

Comments
 (0)