Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/scripts/renovate-changelog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Called by Renovate postUpgradeTasks. Appends a single bullet under the
# `## [Unreleased]` section of CHANGELOG.md so the auto-release workflow has
# content to promote into a tagged release after the Renovate PR merges to main.
set -euo pipefail

BRANCH="${1:-renovate/unknown}"
TITLE="${2:-Update dependencies}"

# Skip if CHANGELOG.md was already touched this run (grouped PRs fire the hook
# more than once; we only want one bullet per PR).
if git diff --cached --name-only | grep -qx CHANGELOG.md; then
exit 0
fi
if git diff --name-only | grep -qx CHANGELOG.md; then
exit 0
fi

ENTRY="- Renovate: ${TITLE} (\`${BRANCH}\`)"

python3 - "$ENTRY" <<'PY'
import re, sys, pathlib
entry = sys.argv[1]
p = pathlib.Path("CHANGELOG.md")
text = p.read_text(encoding="utf-8")

pattern = re.compile(r"(## \[Unreleased\]\n)(.*?)(\n## \[)", re.DOTALL)
m = pattern.search(text)
if not m:
# No [Unreleased] section yet — insert one after the intro paragraph
# (the first blank line after the H1 header).
head, _, rest = text.partition("\n\n")
text = f"{head}\n\n## [Unreleased]\n\n{entry}\n\n{rest}"
else:
body = m.group(2).rstrip("\n")
sep = "\n" if body else ""
text = pattern.sub(
lambda _m: f"{_m.group(1)}{body}{sep}{entry}\n{_m.group(3)}",
text, count=1,
)

p.write_text(text, encoding="utf-8")
PY
117 changes: 117 additions & 0 deletions .github/workflows/auto-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# After a Renovate PR merges into `main`, promote the `[Unreleased]` section
# of CHANGELOG.md to the next patch version, commit, tag, push, and back-merge
# main into develop so the branches stay in sync.
#
# Required secrets:
# RELEASE_TOKEN PAT or GitHub App token with contents:write + workflows.
# Needed for the tag push to trigger github_build_release.yml
# (the default GITHUB_TOKEN cannot trigger other workflows)
# and for the back-merge push to `develop`.
#
# Gating: the existing pr.yaml is required by branch protection, so by the time
# a PR is merged into main, CI has already passed. We do not re-wait for checks
# on the merge commit (it has no associated check runs after a squash-merge).

name: Auto Release on Renovate Merge

on:
pull_request:
types: [closed]
branches: [main]

permissions:
contents: read

jobs:
release:
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.user.login == 'renovate[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout main with full history
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}

- name: Compute next patch version
id: version
run: |
latest=$(git tag -l '[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1)
: "${latest:=0.0.0}"
IFS=. read -r maj min pat <<<"$latest"
next="${maj}.${min}.$((pat + 1))"
{
echo "current=$latest"
echo "next=$next"
echo "date=$(date -u +%Y-%m-%d)"
} >>"$GITHUB_OUTPUT"
echo "Will release: $latest -> $next"

- name: Bail if [Unreleased] is empty
id: gate
run: |
body=$(awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md \
| grep -v '^[[:space:]]*$' || true)
if [ -z "$body" ]; then
echo "Nothing under [Unreleased]; skipping release."
echo "skip=true" >>"$GITHUB_OUTPUT"
else
echo "skip=false" >>"$GITHUB_OUTPUT"
fi

- name: Promote [Unreleased] -> ${{ steps.version.outputs.next }}
if: steps.gate.outputs.skip != 'true'
env:
VER: ${{ steps.version.outputs.next }}
DATE: ${{ steps.version.outputs.date }}
run: |
python3 - <<'PY'
import os, re, pathlib
ver = os.environ["VER"]
date = os.environ["DATE"]
p = pathlib.Path("CHANGELOG.md")
text = p.read_text(encoding="utf-8")
text = re.sub(
r"## \[Unreleased\]\n",
f"## [Unreleased]\n\n## [{ver}] - {date}\n",
text, count=1,
)
p.write_text(text, encoding="utf-8")
PY

- name: Commit on main, tag, push
if: steps.gate.outputs.skip != 'true'
env:
VER: ${{ steps.version.outputs.next }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git commit -m "release: ${VER}"
git tag -a "${VER}" -m "Release ${VER}"
git push origin main
git push origin "${VER}"

- name: Back-merge main into develop
if: steps.gate.outputs.skip != 'true'
env:
VER: ${{ steps.version.outputs.next }}
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
git fetch origin develop
git checkout -B develop origin/develop
if git merge --no-ff main -m "chore: back-merge hotfix ${VER} into develop"; then
git push origin develop
echo "Back-merged ${VER} to develop."
else
git merge --abort || true
gh pr create \
--repo "${GITHUB_REPOSITORY}" \
--base develop \
--head main \
--title "Back-merge hotfix ${VER} into develop" \
--body "Automated back-merge from \`main\` conflicted with \`develop\`. Resolve and merge to keep branches in sync after release ${VER}."
fi
16 changes: 9 additions & 7 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ jobs:
docker compose exec -e XDEBUG_MODE=coverage phpfpm bin/console --env=test doctrine:migrations:migrate --no-interaction --quiet
docker compose exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/phpunit --coverage-clover=coverage/unit.xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/unit.xml
fail_ci_if_error: true
flags: unittests
# Codecov upload disabled on the fork — no CODECOV_TOKEN secret.
# Re-enable when promoting to upstream.
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v5
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# files: ./coverage/unit.xml
# fail_ci_if_error: true
# flags: unittests

fixtures:
runs-on: ubuntu-latest
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/renovate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Self-hosted Renovate runner. Runs on a schedule and on demand.
# Self-hosted is required because postUpgradeTasks (which writes CHANGELOG.md
# inline with the dep bump) is disabled on the Mend Renovate SaaS app.
#
# Required secrets:
# RENOVATE_TOKEN PAT or GitHub App token with repo write + workflows.
# Pushes by the default GITHUB_TOKEN don't re-trigger Actions,
# so a PAT/App token is needed to make CI re-run on
# Renovate's branch updates.

name: Renovate

on:
schedule:
- cron: "0 11 * * 1-5" # weekdays 11:00 UTC = 13:00 Copenhagen (CEST) / 12:00 (CET)
workflow_dispatch:
inputs:
logLevel:
description: Renovate log level
type: choice
default: info
options: [debug, info, warn, error]
dryRun:
description: Dry-run (no PRs)
type: choice
default: "null"
options: ["null", "full"]

permissions:
contents: read

concurrency:
group: renovate
cancel-in-progress: false

jobs:
renovate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

# PHP/Composer are needed so Renovate can resolve composer.lock
# updates inside its container.
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
tools: composer:v2
coverage: none

- name: Run Renovate
uses: renovatebot/github-action@v43
with:
configurationFile: renovate.json
token: ${{ secrets.RENOVATE_TOKEN }}
renovate-version: latest
env:
LOG_LEVEL: ${{ inputs.logLevel || 'info' }}
RENOVATE_DRY_RUN: ${{ inputs.dryRun || 'null' }}
RENOVATE_REPOSITORIES: ${{ github.repository }}
# Allow the changelog script to run from postUpgradeTasks.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^bash \\.github/scripts/renovate-changelog\\.sh"]'
RENOVATE_ALLOW_POST_UPGRADE_COMMAND_TEMPLATING: "true"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ phpstan.neon

.twig-cs-fixer.cache
.playwright-mcp

# Working docs not meant to ship
RENOVATE_PLAN.md
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

## [1.11.0] - 2026-05-19

Expand Down
134 changes: 134 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommitsDisabled",
":maintainLockFilesWeekly",
"schedule:weekdays"
],
"baseBranches": ["main", "develop"],
"timezone": "Europe/Copenhagen",
"labels": ["dependencies", "renovate"],
"prConcurrentLimit": 5,
"prHourlyLimit": 2,
"rangeStrategy": "bump",
"rebaseWhen": "behind-base-branch",
"platformAutomerge": true,
"ignoreUnstable": true,
"respectLatest": true,

"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security", "dependencies"],
"automerge": true,
"automergeType": "pr",
"minimumReleaseAge": "0 days",
"prPriority": 10,
"commitMessagePrefix": "security:",
"matchBaseBranches": ["main"]
},
"osvVulnerabilityAlerts": true,

"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 6am on Monday"],
"automerge": true,
"automergeType": "pr",
"commitMessageAction": "Refresh",
"matchBaseBranches": ["main"]
},

"composer": { "enabled": true, "ignorePlatformReqs": [] },
"npm": { "enabled": true },
"github-actions": { "enabled": true, "pinDigests": true },
"docker-compose": { "enabled": true, "pinDigests": true },
"dockerfile": { "enabled": true, "pinDigests": true },

"packageRules": [
{
"description": "Hotfix: auto-merge patches/digests/lockfile on main after 7-day soak",
"matchBaseBranches": ["main"],
"matchUpdateTypes": [
"patch",
"pin",
"digest",
"lockFileMaintenance"
],
"automerge": true,
"automergeType": "pr",
"automergeStrategy": "squash",
"minimumReleaseAge": "7 days"
},
{
"description": "Normal flow: minor/major on develop, dashboard approval, never auto-merge",
"matchBaseBranches": ["develop"],
"matchUpdateTypes": ["minor", "major"],
"dependencyDashboardApproval": true,
"automerge": false
},
{
"description": "Suppress minor/major against main (those belong on develop)",
"matchBaseBranches": ["main"],
"matchUpdateTypes": ["minor", "major"],
"enabled": false
},
{
"description": "Suppress patches against develop (they reach develop via back-merge after release)",
"matchBaseBranches": ["develop"],
"matchUpdateTypes": [
"patch",
"pin",
"digest",
"lockFileMaintenance"
],
"enabled": false
},
{
"description": "Group Symfony patches into one PR on main",
"matchBaseBranches": ["main"],
"matchPackagePatterns": ["^symfony/"],
"matchUpdateTypes": ["patch"],
"groupName": "symfony (patch)"
},
{
"description": "Group Doctrine patches on main",
"matchBaseBranches": ["main"],
"matchPackagePatterns": ["^doctrine/"],
"matchUpdateTypes": ["patch"],
"groupName": "doctrine (patch)"
},
{
"description": "Group api-platform patches on main",
"matchBaseBranches": ["main"],
"matchPackagePatterns": ["^api-platform/"],
"matchUpdateTypes": ["patch"],
"groupName": "api-platform (patch)"
},
{
"description": "Never auto-bump the PHP platform requirement",
"matchBaseBranches": ["develop"],
"matchPackageNames": ["php"],
"rangeStrategy": "in-range-only",
"automerge": false,
"dependencyDashboardApproval": true
},
{
"description": "GitHub Actions: pin digests, auto-merge digest/patch on main",
"matchBaseBranches": ["main"],
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["digest", "patch"],
"automerge": true
}
],

"postUpdateOptions": ["composerUpdateAllDependencies"],

"postUpgradeTasks": {
"commands": [
"bash .github/scripts/renovate-changelog.sh \"{{{branchName}}}\" \"{{{prTitle}}}\""
],
"fileFilters": ["CHANGELOG.md"],
"executionMode": "branch"
}
}
Loading