Skip to content

Commit f4c8520

Browse files
authored
Merge pull request #82 from turegjorup/feat/renovate-auto-patch
Feat/renovate auto patch
2 parents dcdd479 + 292ba86 commit f4c8520

7 files changed

Lines changed: 369 additions & 7 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/pr.yaml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ jobs:
5555
docker compose exec -e XDEBUG_MODE=coverage phpfpm bin/console --env=test doctrine:migrations:migrate --no-interaction --quiet
5656
docker compose exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/phpunit --coverage-clover=coverage/unit.xml
5757
58-
- name: Upload coverage to Codecov
59-
uses: codecov/codecov-action@v5
60-
with:
61-
token: ${{ secrets.CODECOV_TOKEN }}
62-
files: ./coverage/unit.xml
63-
fail_ci_if_error: true
64-
flags: unittests
58+
# Codecov upload disabled on the fork — no CODECOV_TOKEN secret.
59+
# Re-enable when promoting to upstream.
60+
# - name: Upload coverage to Codecov
61+
# uses: codecov/codecov-action@v5
62+
# with:
63+
# token: ${{ secrets.CODECOV_TOKEN }}
64+
# files: ./coverage/unit.xml
65+
# fail_ci_if_error: true
66+
# flags: unittests
6567

6668
fixtures:
6769
runs-on: ubuntu-latest

.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 11 * * 1-5" # weekdays 11:00 UTC = 13:00 Copenhagen (CEST) / 12:00 (CET)
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"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ phpstan.neon
3838

3939
.twig-cs-fixer.cache
4040
.playwright-mcp
41+
42+
# Working docs not meant to ship
43+
RENOVATE_PLAN.md

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

renovate.json

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
{
2+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3+
"extends": [
4+
"config:recommended",
5+
":dependencyDashboard",
6+
":semanticCommitsDisabled",
7+
":maintainLockFilesWeekly",
8+
"schedule:weekdays"
9+
],
10+
"baseBranches": ["main", "develop"],
11+
"timezone": "Europe/Copenhagen",
12+
"labels": ["dependencies", "renovate"],
13+
"prConcurrentLimit": 5,
14+
"prHourlyLimit": 2,
15+
"rangeStrategy": "bump",
16+
"rebaseWhen": "behind-base-branch",
17+
"platformAutomerge": true,
18+
"ignoreUnstable": true,
19+
"respectLatest": true,
20+
21+
"vulnerabilityAlerts": {
22+
"enabled": true,
23+
"labels": ["security", "dependencies"],
24+
"automerge": true,
25+
"automergeType": "pr",
26+
"minimumReleaseAge": "0 days",
27+
"prPriority": 10,
28+
"commitMessagePrefix": "security:",
29+
"matchBaseBranches": ["main"]
30+
},
31+
"osvVulnerabilityAlerts": true,
32+
33+
"lockFileMaintenance": {
34+
"enabled": true,
35+
"schedule": ["before 6am on Monday"],
36+
"automerge": true,
37+
"automergeType": "pr",
38+
"commitMessageAction": "Refresh",
39+
"matchBaseBranches": ["main"]
40+
},
41+
42+
"composer": { "enabled": true, "ignorePlatformReqs": [] },
43+
"npm": { "enabled": true },
44+
"github-actions": { "enabled": true, "pinDigests": true },
45+
"docker-compose": { "enabled": true, "pinDigests": true },
46+
"dockerfile": { "enabled": true, "pinDigests": true },
47+
48+
"packageRules": [
49+
{
50+
"description": "Hotfix: auto-merge patches/digests/lockfile on main after 7-day soak",
51+
"matchBaseBranches": ["main"],
52+
"matchUpdateTypes": [
53+
"patch",
54+
"pin",
55+
"digest",
56+
"lockFileMaintenance"
57+
],
58+
"automerge": true,
59+
"automergeType": "pr",
60+
"automergeStrategy": "squash",
61+
"minimumReleaseAge": "7 days"
62+
},
63+
{
64+
"description": "Normal flow: minor/major on develop, dashboard approval, never auto-merge",
65+
"matchBaseBranches": ["develop"],
66+
"matchUpdateTypes": ["minor", "major"],
67+
"dependencyDashboardApproval": true,
68+
"automerge": false
69+
},
70+
{
71+
"description": "Suppress minor/major against main (those belong on develop)",
72+
"matchBaseBranches": ["main"],
73+
"matchUpdateTypes": ["minor", "major"],
74+
"enabled": false
75+
},
76+
{
77+
"description": "Suppress patches against develop (they reach develop via back-merge after release)",
78+
"matchBaseBranches": ["develop"],
79+
"matchUpdateTypes": [
80+
"patch",
81+
"pin",
82+
"digest",
83+
"lockFileMaintenance"
84+
],
85+
"enabled": false
86+
},
87+
{
88+
"description": "Group Symfony patches into one PR on main",
89+
"matchBaseBranches": ["main"],
90+
"matchPackagePatterns": ["^symfony/"],
91+
"matchUpdateTypes": ["patch"],
92+
"groupName": "symfony (patch)"
93+
},
94+
{
95+
"description": "Group Doctrine patches on main",
96+
"matchBaseBranches": ["main"],
97+
"matchPackagePatterns": ["^doctrine/"],
98+
"matchUpdateTypes": ["patch"],
99+
"groupName": "doctrine (patch)"
100+
},
101+
{
102+
"description": "Group api-platform patches on main",
103+
"matchBaseBranches": ["main"],
104+
"matchPackagePatterns": ["^api-platform/"],
105+
"matchUpdateTypes": ["patch"],
106+
"groupName": "api-platform (patch)"
107+
},
108+
{
109+
"description": "Never auto-bump the PHP platform requirement",
110+
"matchBaseBranches": ["develop"],
111+
"matchPackageNames": ["php"],
112+
"rangeStrategy": "in-range-only",
113+
"automerge": false,
114+
"dependencyDashboardApproval": true
115+
},
116+
{
117+
"description": "GitHub Actions: pin digests, auto-merge digest/patch on main",
118+
"matchBaseBranches": ["main"],
119+
"matchManagers": ["github-actions"],
120+
"matchUpdateTypes": ["digest", "patch"],
121+
"automerge": true
122+
}
123+
],
124+
125+
"postUpdateOptions": ["composerUpdateAllDependencies"],
126+
127+
"postUpgradeTasks": {
128+
"commands": [
129+
"bash .github/scripts/renovate-changelog.sh \"{{{branchName}}}\" \"{{{prTitle}}}\""
130+
],
131+
"fileFilters": ["CHANGELOG.md"],
132+
"executionMode": "branch"
133+
}
134+
}

0 commit comments

Comments
 (0)