From 32895668c105529986ee51c1436e236097ca1bb8 Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 21 May 2026 11:49:39 +0200 Subject: [PATCH 1/5] 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) --- .github/scripts/renovate-changelog.sh | 43 ++++ .github/workflows/auto-release.yaml | 117 ++++++++++ .github/workflows/renovate.yaml | 62 +++++ CHANGELOG.md | 1 + RENOVATE_PLAN.md | 314 ++++++++++++++++++++++++++ renovate.json | 134 +++++++++++ 6 files changed, 671 insertions(+) create mode 100755 .github/scripts/renovate-changelog.sh create mode 100644 .github/workflows/auto-release.yaml create mode 100644 .github/workflows/renovate.yaml create mode 100644 RENOVATE_PLAN.md create mode 100644 renovate.json diff --git a/.github/scripts/renovate-changelog.sh b/.github/scripts/renovate-changelog.sh new file mode 100755 index 0000000..5f4ab6d --- /dev/null +++ b/.github/scripts/renovate-changelog.sh @@ -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 diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml new file mode 100644 index 0000000..59c472b --- /dev/null +++ b/.github/workflows/auto-release.yaml @@ -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 diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml new file mode 100644 index 0000000..7bc78fe --- /dev/null +++ b/.github/workflows/renovate.yaml @@ -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 5 * * 1-5" # weekdays 05:00 UTC ~= 06/07:00 Aarhus + 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" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d3c760..83a4725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/RENOVATE_PLAN.md b/RENOVATE_PLAN.md new file mode 100644 index 0000000..89ade1e --- /dev/null +++ b/RENOVATE_PLAN.md @@ -0,0 +1,314 @@ +# Plan: Renovate auto-patch + auto-release — fork test, then upstream + +## Objective + +Validate a Renovate + auto-release pipeline on the **fork** (`turegjorup/devops_itksites`), then re-implement the proven setup on **upstream** (`itk-dev/devops_itksites`). + +When complete, the pipeline will: + +1. Open grouped patch-update PRs (Composer/npm/GitHub Actions/Docker) **against `main` as hotfixes**, run the existing `pr.yaml` checks, and **auto-merge** when green. +2. Open security-advisory PRs (the `composer audit` set) against `main` with **0-day soak** so they ship as soon as CI passes. +3. After each merge to `main`, promote the `[Unreleased]` section of `CHANGELOG.md` to the next **patch** version, tag from `main`, let `github_build_release.yml` build the GitHub Release, then **back-merge `main` → `develop`** so the branches stay in sync. +4. Continue to route **minor/major** updates against `develop` with Dependency Dashboard approval (no auto-merge). + +## Why a fork test first + +- The pipeline writes commits, opens PRs, creates tags, publishes releases, and force-syncs `main` ↔ `develop`. Each of those is observable to the upstream team and to anyone subscribed to upstream releases. +- The repo already has a real git-flow with `hotfix/*`, `release/*` and `main`/`develop`. Renovate needs to slot into it cleanly — any misconfiguration produces tag noise, spurious PRs, or broken back-merges that are painful to clean up in a shared repo. +- The fork is a near-identical copy of upstream (same `pr.yaml`, same `changelog.yaml`, same `github_build_release.yml`, same composer.lock), so behavior should transfer 1:1 with only secret/owner substitutions. + +## Branch state (verified) + +| Branch | Origin (fork) | Upstream | HEAD | +| --- | --- | --- | --- | +| `develop` | ✅ | ✅ | `614c76e` (fork is ahead with renovate lock-file maintenance PRs) | +| `main` | ✅ (just pushed) | ✅ | `4402754` (Merge `release/1.11.0`) | + +Highest semver tag on upstream: **`1.11.0`**. First auto-release on the fork should produce `1.11.1`. + +## Context (why this is worth doing now) + +Running `composer audit` against `composer.lock` flags the May 20, 2026 coordinated Symfony + Twig disclosure: + +| Package | Locked | Fixed in | +| --- | --- | --- | +| `symfony/http-kernel`, `security-http`, `runtime`, etc. (v8.0.x) | v8.0.8–8.0.11 | **8.0.12** | +| `twig/twig` | v3.25.0 | **3.26.0** | + +Relevant CVEs: `CVE-2026-45075`, `CVE-2026-45065`, `CVE-2026-46626`, `CVE-2026-46633`, `CVE-2026-24425`, plus the HtmlSanitizer / Mailer / Notifier batch. All patch-level — exactly the class this pipeline auto-merges. + +## Architecture decisions (apply to both fork and upstream) + +- **Self-hosted Renovate** via `renovatebot/github-action` on a schedule. Self-hosted is required because `postUpgradeTasks` (the bit that writes `CHANGELOG.md` in the same commit as the dep bump) is disabled on Mend's hosted app. +- **Two base branches.** `baseBranches: ["main", "develop"]`. Patches/security/digest/lockFileMaintenance route to `main`; minor/major route to `develop`. `matchBaseBranches` in `packageRules` enforces the split. +- **Patch-only auto-merge.** Minor and major bumps open PRs against `develop` but require Dependency Dashboard approval. `php` package never auto-bumps. +- **CHANGELOG gate.** The repo's `changelog.yaml` workflow fails any PR that doesn't touch `CHANGELOG.md`. Rather than fork that workflow, Renovate appends an `[Unreleased]` bullet via `postUpgradeTasks` so every Renovate PR satisfies the gate. +- **Two PATs** so pushes re-trigger Actions (the default `GITHUB_TOKEN` doesn't): `RENOVATE_TOKEN` (Renovate's pushes to `renovate/*` branches) and `RELEASE_TOKEN` (the tag push that fires `github_build_release.yml`, plus the back-merge to `develop`). +- **Soak time:** 7 days for normal patches, 0 days for security advisories. The conservative window catches yanked releases and follow-up advisories; CVE-flagged patches still ship immediately via the vuln-alert path. + +## Deliverables (same files in both phases) + +``` +renovate.json +.github/workflows/renovate.yaml +.github/workflows/auto-release.yaml +.github/scripts/renovate-changelog.sh +``` + +--- + +# Phase 1 — Validate on the fork (`turegjorup/devops_itksites`) + +Goal: run the full pipeline against the fork's own `main` and `develop` until a clean Renovate PR → auto-merge → tag → GitHub Release → back-merge cycle completes end-to-end, twice (once for a regular patch, once for a security advisory). + +## Step 1.1 — Branch & baseline (already partly done) + +`main` has been fetched from upstream and pushed to origin (`4402754`). Now: + +```bash +git checkout develop && git pull origin develop +git checkout -b feat/renovate-auto-patch +``` + +Confirm no existing `renovate.json`, `.renovaterc*`, or `dependabot.yml` on develop. If `dependabot.yml` exists, delete it in this PR — Renovate replaces it. + +## Step 1.2 — Create `renovate.json` at repo root + +Key sections: + +- `extends`: `config:recommended`, `:dependencyDashboard`, `:semanticCommitsDisabled`, `:maintainLockFilesWeekly`, `schedule:weekdays`. +- `baseBranches: ["main", "develop"]`, `timezone: "Europe/Copenhagen"`. +- `vulnerabilityAlerts`: `automerge: true`, `minimumReleaseAge: "0 days"`, `prPriority: 10`, `commitMessagePrefix: "security:"`, `matchBaseBranches: ["main"]`. Also enable `osvVulnerabilityAlerts: true`. Security advisories always land on `main` so they ship as a hotfix release. +- `lockFileMaintenance`: weekly Monday morning, auto-merge, `matchBaseBranches: ["main"]`. +- `packageRules`: + - **Patch/pin/digest/lockFileMaintenance on `main`** → `matchBaseBranches: ["main"]`, `matchUpdateTypes: ["patch", "pin", "digest", "lockFileMaintenance"]`, `automerge: true`, `minimumReleaseAge: "7 days"`. The hotfix path. + - **Minor/major on `develop`** → `matchBaseBranches: ["develop"]`, `matchUpdateTypes: ["minor", "major"]`, `dependencyDashboardApproval: true`, `automerge: false`. The normal release path. + - **Suppression rules** so updates don't open against the wrong base: + - `matchBaseBranches: ["main"]` + `matchUpdateTypes: ["minor", "major"]` → `enabled: false`. + - `matchBaseBranches: ["develop"]` + `matchUpdateTypes: ["patch", "pin", "digest", "lockFileMaintenance"]` → `enabled: false` (patches reach develop via the back-merge). + - Group `^symfony/`, `^doctrine/`, `^api-platform/` patches each into one PR (apply on `main`). + - `php` package → `matchBaseBranches: ["develop"]`, `rangeStrategy: "in-range-only"`, no auto-merge. + - GitHub Actions → pin digests, auto-merge digest/patch on `main`. +- `postUpgradeTasks`: `bash .github/scripts/renovate-changelog.sh {{{branchName}}} {{{prTitle}}}`, `fileFilters: ["CHANGELOG.md"]`, `executionMode: "branch"`. + +## Step 1.3 — Create `.github/scripts/renovate-changelog.sh` + +Idempotent script that appends one bullet under `## [Unreleased]`: + +- `set -euo pipefail`. +- Exit 0 if `CHANGELOG.md` is already staged or modified in the working tree (prevents duplicate bullets when `postUpgradeTasks` fires multiple times for a grouped PR). +- Python heredoc splices `- Renovate: (\`\`)` into the `[Unreleased]` section. If no `[Unreleased]` section exists, create one after the intro paragraph. + +```bash +chmod +x .github/scripts/renovate-changelog.sh +``` + +Local validation: + +```bash +.github/scripts/renovate-changelog.sh renovate/symfony-patch "Update symfony (patch)" +git diff CHANGELOG.md # expect one new bullet under [Unreleased] +git checkout -- CHANGELOG.md +``` + +## Step 1.4 — Create `.github/workflows/renovate.yaml` + +- Triggers: `schedule` (weekdays 05:00 UTC) + `workflow_dispatch` with `logLevel` and `dryRun` inputs. +- `concurrency.group: renovate`, `cancel-in-progress: false`. +- Set up PHP 8.4 + Composer (needed so `composer update` works inside Renovate's container). +- `renovatebot/github-action@v43` with `token: ${{ secrets.RENOVATE_TOKEN }}`. +- Critical env vars for `postUpgradeTasks`: + - `RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^bash \\.github/scripts/renovate-changelog\\.sh"]'` + - `RENOVATE_ALLOW_POST_UPGRADE_COMMAND_TEMPLATING: "true"` +- `RENOVATE_REPOSITORIES: ${{ github.repository }}` — this means the same workflow file works unmodified on both fork and upstream; it always operates on its own repo. + +## Step 1.5 — Create `.github/workflows/auto-release.yaml` + +Trigger: `pull_request.closed` on `main`, gated by `github.event.pull_request.merged == true && github.event.pull_request.user.login == 'renovate[bot]'`. + +Steps: + +1. `actions/checkout@v6` with `ref: main`, `fetch-depth: 0`, `token: ${{ secrets.RELEASE_TOKEN }}`. +2. `lewagon/wait-on-check-action@v1.4.0` waiting on the merge commit's required checks (`PHP Unit tests`, `PHPStan`, `Validate Doctrine Schema`, `Load fixtures`, `Build assets`). +3. Compute next patch: `git tag -l '[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1`, bump the patch component. On the fork, the first run produces `1.11.1`. +4. Gate: if `[Unreleased]` is empty (`awk` extraction returns nothing), `skip=true` and stop. +5. Promote `[Unreleased]` → `[] - ` via Python heredoc. +6. `git commit -m "release: "`, `git tag -a `, push both to `main`. `github_build_release.yml` fires on the `*.*.*` tag. +7. **Back-merge `main` → `develop`:** + - `git fetch origin develop && git checkout develop` + - `git merge --no-ff main -m "chore: back-merge hotfix into develop"` + - On conflict: abort, then `gh pr create --base develop --head main --title "Back-merge hotfix "`. Do not force-push. + - Otherwise push develop with `RELEASE_TOKEN`. + +## Step 1.6 — Local validation before pushing + +```bash +python3 -c "import json; json.load(open('renovate.json'))" +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/renovate.yaml'))" +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/auto-release.yaml'))" +bash -n .github/scripts/renovate-changelog.sh +``` + +If Taskfile defines them, also: + +```bash +task lint +``` + +## Step 1.7 — Fork-side manual setup + +Done by **you (turegjorup)** as the fork owner before the pipeline runs: + +1. **Repository secrets on `turegjorup/devops_itksites`:** + - `RENOVATE_TOKEN` — a PAT on your account scoped to the fork with `contents:write`, `pull-requests:write`, `workflows`. For the test phase this can be a classic PAT or fine-grained PAT; a GitHub App is overkill. + - `RELEASE_TOKEN` — PAT with `contents:write`, `workflows` for the tag push + back-merge. +2. **Branch protection on `main` and `develop` (fork)** — match what you'll later configure on upstream: + - Required status checks: `PHP Unit tests`, `PHPStan`, `Validate Doctrine Schema`, `Load fixtures`, `Build assets`, `Changelog`, `composer-validate`, `composer-normalized`, `composer-audit`. + - Require branches up to date before merging. + - **Allow auto-merge** at the repo level. + - Allow `RELEASE_TOKEN` identity to direct-push (for back-merges); fallback to PR if blocked. +3. **(Optional) CODEOWNERS exemption** for `renovate[bot]` if you mirror upstream's CODEOWNERS. + +## Step 1.8 — Commit & open PR against the fork's `develop` + +```bash +git add renovate.json .github/workflows/renovate.yaml .github/workflows/auto-release.yaml .github/scripts/renovate-changelog.sh +# changelog.yaml will gate this PR — add a bullet to [Unreleased] +$EDITOR CHANGELOG.md +git add CHANGELOG.md +git commit -m "Add Renovate auto-patch + auto-release pipeline" +git push -u origin feat/renovate-auto-patch +gh pr create --repo turegjorup/devops_itksites --base develop --fill +``` + +Merge it once the existing `pr.yaml` checks pass. (No release fires from this — it lands on `develop`, not `main`.) + +## Step 1.9 — Promote the pipeline files to `main` on the fork + +The pipeline only takes effect for hotfix PRs once the workflow files and `renovate.json` exist on `main`. Two options: + +- **Quick:** cherry-pick the four pipeline files from develop to main with a manual PR (`gh pr create --base main --head feat/renovate-pipeline-main`). Safe because these are tooling files; no app behavior changes. +- **Slow:** wait for the next `release/*` → `main` cut to carry them across. + +Use the quick path for the fork test so the validation cycle isn't blocked on a release. + +## Step 1.10 — First-run verification on the fork + +1. **Dry-run.** Actions → Renovate → Run workflow with `dryRun: full`, `logLevel: debug`. Confirm logs show: + - Config parses cleanly. + - Patch candidates for `symfony/*` (8.0.8/9/10/11 → 8.0.12) and `twig/twig` (3.25.0 → 3.26.0) are scoped to `main`. + - Minor/major candidates (if any) scoped to `develop`. + - `postUpgradeTasks` listed as allowed. +2. **Live run.** Re-run with `dryRun: null`. Dependency Dashboard issue appears; first PRs open within minutes. +3. **First patch PR (against `main`):** + - `CHANGELOG.md` has exactly one new bullet. + - `pr.yaml`, `composer-audit`, `Changelog` all pass. + - PR auto-merges after `minimumReleaseAge` (security PRs immediately, regular patches after 7 days — use a security PR for the first test to skip the soak). +4. **Auto-release cycle:** + - `auto-release.yaml` runs after merge, computes `1.11.1`, promotes `[Unreleased]`, tags from `main`, pushes. + - `github_build_release.yml` builds the GitHub Release on the fork. +5. **Back-merge:** verify the release commit reached `develop` (push or PR). + +## Step 1.11 — Exit criteria for Phase 1 + +Move to Phase 2 only when **all of these** are observed on the fork: + +- One **security** PR (e.g. a Symfony 8.0.x patch) → auto-merged with 0-day soak → released → back-merged. +- One **regular patch** PR → auto-merged after 7-day soak → released → back-merged. (To avoid blocking the Phase 1 sign-off on a real 7-day wait, temporarily set `minimumReleaseAge: "0 days"` for this single test run, confirm the merge fires, then restore `"7 days"` before going to upstream.) +- One **grouped patch** PR (Symfony or Doctrine cluster) → auto-merged correctly without partial-update artifacts. +- One **minor or major** candidate appeared on the Dependency Dashboard against `develop` and did **not** auto-merge. +- A **back-merge conflict** scenario has been exercised at least once (synthesize one if it doesn't happen naturally) and the PR-fallback worked. +- The CHANGELOG ended each cycle correctly: `[Unreleased]` empty, `[1.11.x] - ` populated. + +Document any deviations or required tweaks in `RENOVATE_TEST_NOTES.md` (created during this phase, not promoted to upstream). + +## Step 1.12 — Cleanup before Phase 2 + +After the fork test is signed off: + +- Optionally delete the test releases (`1.11.1`, `1.11.2`, …) from the fork's Releases page to keep the fork's tag history tidy. The tags themselves can stay. +- Keep the `Renovate` Actions workflow enabled on the fork so it continues to track patches as a low-stakes early-warning system. + +--- + +# Phase 2 — Re-implement on upstream (`itk-dev/devops_itksites`) + +Goal: port the validated files to upstream with the minimum possible delta. The smaller the diff between fork and upstream, the cheaper the review and the lower the risk. + +## Step 2.1 — Sync fork's pipeline state to a fresh branch + +```bash +git fetch upstream develop +git checkout -b feat/renovate-auto-patch upstream/develop +git checkout feat/renovate-auto-patch -- \ + renovate.json \ + .github/workflows/renovate.yaml \ + .github/workflows/auto-release.yaml \ + .github/scripts/renovate-changelog.sh +# OR, if working from the merged develop on the fork: +# git checkout feat/renovate-auto-patch-fork -- +``` + +No content changes should be needed — `RENOVATE_REPOSITORIES: ${{ github.repository }}` makes the workflow portable. + +## Step 2.2 — Upstream-only manual setup (request from itk-dev admins) + +Mirror the fork's manual setup, but on upstream: + +1. **Repository secrets on `itk-dev/devops_itksites`:** + - `RENOVATE_TOKEN` — a **GitHub App** installation token. Create a dedicated app (e.g. `renovate-itkdev`) on the itk-dev org with `contents:write`, `pull-requests:write`, `workflows:write` permissions; install it on `devops_itksites`; store the App ID + private key as `RENOVATE_APP_ID` / `RENOVATE_APP_PRIVATE_KEY` secrets; the workflow exchanges them for an installation token at runtime via `actions/create-github-app-token`. The App identity (e.g. `renovate-itkdev[bot]`) is what shows up in commits and audit logs — no personnel coupling. + - `RELEASE_TOKEN` — same GitHub App token, or a separate App if you want auditing separation between Renovate pushes and release pushes. Needs `contents:write`, `workflows:write`. +2. **Branch protection on `main` and `develop`** — identical rule sets to the fork (status checks, up-to-date, auto-merge enabled, RELEASE_TOKEN allowed to push). +3. **CODEOWNERS** — if upstream has CODEOWNERS, either exempt `renovate[bot]` or accept that each PR needs a human approval before auto-merge. + +## Step 2.3 — PR + handoff + +```bash +$EDITOR CHANGELOG.md # add bullet under [Unreleased] +git add -A +git commit -m "Add Renovate auto-patch + auto-release pipeline" +git push -u upstream feat/renovate-auto-patch +gh pr create --repo itk-dev/devops_itksites --base develop --fill +``` + +PR body should link to: + +- This plan. +- The fork's successful Renovate run history (Actions → Renovate). +- The fork's first auto-release (`turegjorup/devops_itksites` Releases → `1.11.1`). + +Reviewers can verify the diff is small and that all the behavior they're approving has already been observed running on the fork. + +## Step 2.4 — Promote the files to upstream `main` + +After the PR merges to `upstream/develop`, the same promotion question from Step 1.9 applies upstream. Recommend the same quick path: a small, scoped PR cherry-picking the four pipeline files from `develop` to `main`. + +## Step 2.5 — First-run verification on upstream + +Same as Step 1.10, but on upstream. Watch the first cycle closely — security PR first (0-day soak) so feedback comes quickly. + +--- + +## Rollback + +Applies to both fork and upstream: + +- **Stop new PRs:** disable the `Renovate` workflow in the Actions tab. +- **Disable auto-release:** delete `.github/workflows/auto-release.yaml`. `github_build_release.yml` still works manually. +- **Disable auto-merge globally:** set top-level `automerge: false` in `renovate.json`. Renovate keeps opening PRs but waits for human merge. +- **Revert a bad back-merge:** the back-merge is `--no-ff`, so `git revert -m 1 ` cleanly undoes it on `develop` without touching the release on `main`. + +## Resolved decisions + +| Decision | Choice | Where it lives | +| --- | --- | --- | +| Upstream auth | **GitHub App** (`renovate-itkdev`-style). Personal PAT acceptable on the fork only. | Step 2.2 | +| Non-security soak window | **7 days.** Vuln-alert path overrides to 0 days for CVEs. | Step 1.2 patch package rule, Step 1.7 fork-side fallback | +| Version source for auto-release | **Git tags.** Highest `X.Y.Z` → bump patch. | Step 1.5 step 3 | +| Back-merge granularity | **Single grouped back-merge** per release, no per-package splitting. | Step 1.5 step 7 | + +## Pre-flight checklist (do before Phase 1 first-run) + +- [ ] Verify `CHANGELOG.md` on `main` does **not** already contain a `## [1.11.1]` header — if it does, either delete it or bump the auto-release seed to `1.11.2`. (Tag-based versioning will collide otherwise.) +- [ ] Confirm `composer.lock` is committed and matches `composer.json` (Renovate's `composer update` runs against the lock). +- [ ] Confirm `task lint` (or equivalent) passes locally on the four new files. diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..afa00a6 --- /dev/null +++ b/renovate.json @@ -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" + } +} From 1df789b7195c97de454dd3cb5af4406d725344fa Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 21 May 2026 11:53:35 +0200 Subject: [PATCH 2/5] chore: run Renovate at 12:00 Europe/Copenhagen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cron is in UTC so the run drifts ±1h between DST and standard time. Picked 10:00 UTC since DST (CEST, UTC+2) covers ~7 of 12 months in Denmark, so the workflow hits noon Aarhus during the larger part of the year and 11:00 in winter. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 7bc78fe..fb220e2 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -12,7 +12,7 @@ name: Renovate on: schedule: - - cron: "0 5 * * 1-5" # weekdays 05:00 UTC ~= 06/07:00 Aarhus + - cron: "0 10 * * 1-5" # weekdays 10:00 UTC = 12:00 Copenhagen (CEST) / 11:00 (CET) workflow_dispatch: inputs: logLevel: From 36ae460ae85dcf09c3e2c65f7c61529a7d07ee3d Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 21 May 2026 12:52:06 +0200 Subject: [PATCH 3/5] chore: untrack RENOVATE_PLAN.md Working doc, kept locally via .gitignore. Removing from git also fixes the markdown-lint failures it was causing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 + RENOVATE_PLAN.md | 314 ----------------------------------------------- 2 files changed, 3 insertions(+), 314 deletions(-) delete mode 100644 RENOVATE_PLAN.md diff --git a/.gitignore b/.gitignore index 5fb8656..39d1d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ phpstan.neon .twig-cs-fixer.cache .playwright-mcp + +# Working docs not meant to ship +RENOVATE_PLAN.md diff --git a/RENOVATE_PLAN.md b/RENOVATE_PLAN.md deleted file mode 100644 index 89ade1e..0000000 --- a/RENOVATE_PLAN.md +++ /dev/null @@ -1,314 +0,0 @@ -# Plan: Renovate auto-patch + auto-release — fork test, then upstream - -## Objective - -Validate a Renovate + auto-release pipeline on the **fork** (`turegjorup/devops_itksites`), then re-implement the proven setup on **upstream** (`itk-dev/devops_itksites`). - -When complete, the pipeline will: - -1. Open grouped patch-update PRs (Composer/npm/GitHub Actions/Docker) **against `main` as hotfixes**, run the existing `pr.yaml` checks, and **auto-merge** when green. -2. Open security-advisory PRs (the `composer audit` set) against `main` with **0-day soak** so they ship as soon as CI passes. -3. After each merge to `main`, promote the `[Unreleased]` section of `CHANGELOG.md` to the next **patch** version, tag from `main`, let `github_build_release.yml` build the GitHub Release, then **back-merge `main` → `develop`** so the branches stay in sync. -4. Continue to route **minor/major** updates against `develop` with Dependency Dashboard approval (no auto-merge). - -## Why a fork test first - -- The pipeline writes commits, opens PRs, creates tags, publishes releases, and force-syncs `main` ↔ `develop`. Each of those is observable to the upstream team and to anyone subscribed to upstream releases. -- The repo already has a real git-flow with `hotfix/*`, `release/*` and `main`/`develop`. Renovate needs to slot into it cleanly — any misconfiguration produces tag noise, spurious PRs, or broken back-merges that are painful to clean up in a shared repo. -- The fork is a near-identical copy of upstream (same `pr.yaml`, same `changelog.yaml`, same `github_build_release.yml`, same composer.lock), so behavior should transfer 1:1 with only secret/owner substitutions. - -## Branch state (verified) - -| Branch | Origin (fork) | Upstream | HEAD | -| --- | --- | --- | --- | -| `develop` | ✅ | ✅ | `614c76e` (fork is ahead with renovate lock-file maintenance PRs) | -| `main` | ✅ (just pushed) | ✅ | `4402754` (Merge `release/1.11.0`) | - -Highest semver tag on upstream: **`1.11.0`**. First auto-release on the fork should produce `1.11.1`. - -## Context (why this is worth doing now) - -Running `composer audit` against `composer.lock` flags the May 20, 2026 coordinated Symfony + Twig disclosure: - -| Package | Locked | Fixed in | -| --- | --- | --- | -| `symfony/http-kernel`, `security-http`, `runtime`, etc. (v8.0.x) | v8.0.8–8.0.11 | **8.0.12** | -| `twig/twig` | v3.25.0 | **3.26.0** | - -Relevant CVEs: `CVE-2026-45075`, `CVE-2026-45065`, `CVE-2026-46626`, `CVE-2026-46633`, `CVE-2026-24425`, plus the HtmlSanitizer / Mailer / Notifier batch. All patch-level — exactly the class this pipeline auto-merges. - -## Architecture decisions (apply to both fork and upstream) - -- **Self-hosted Renovate** via `renovatebot/github-action` on a schedule. Self-hosted is required because `postUpgradeTasks` (the bit that writes `CHANGELOG.md` in the same commit as the dep bump) is disabled on Mend's hosted app. -- **Two base branches.** `baseBranches: ["main", "develop"]`. Patches/security/digest/lockFileMaintenance route to `main`; minor/major route to `develop`. `matchBaseBranches` in `packageRules` enforces the split. -- **Patch-only auto-merge.** Minor and major bumps open PRs against `develop` but require Dependency Dashboard approval. `php` package never auto-bumps. -- **CHANGELOG gate.** The repo's `changelog.yaml` workflow fails any PR that doesn't touch `CHANGELOG.md`. Rather than fork that workflow, Renovate appends an `[Unreleased]` bullet via `postUpgradeTasks` so every Renovate PR satisfies the gate. -- **Two PATs** so pushes re-trigger Actions (the default `GITHUB_TOKEN` doesn't): `RENOVATE_TOKEN` (Renovate's pushes to `renovate/*` branches) and `RELEASE_TOKEN` (the tag push that fires `github_build_release.yml`, plus the back-merge to `develop`). -- **Soak time:** 7 days for normal patches, 0 days for security advisories. The conservative window catches yanked releases and follow-up advisories; CVE-flagged patches still ship immediately via the vuln-alert path. - -## Deliverables (same files in both phases) - -``` -renovate.json -.github/workflows/renovate.yaml -.github/workflows/auto-release.yaml -.github/scripts/renovate-changelog.sh -``` - ---- - -# Phase 1 — Validate on the fork (`turegjorup/devops_itksites`) - -Goal: run the full pipeline against the fork's own `main` and `develop` until a clean Renovate PR → auto-merge → tag → GitHub Release → back-merge cycle completes end-to-end, twice (once for a regular patch, once for a security advisory). - -## Step 1.1 — Branch & baseline (already partly done) - -`main` has been fetched from upstream and pushed to origin (`4402754`). Now: - -```bash -git checkout develop && git pull origin develop -git checkout -b feat/renovate-auto-patch -``` - -Confirm no existing `renovate.json`, `.renovaterc*`, or `dependabot.yml` on develop. If `dependabot.yml` exists, delete it in this PR — Renovate replaces it. - -## Step 1.2 — Create `renovate.json` at repo root - -Key sections: - -- `extends`: `config:recommended`, `:dependencyDashboard`, `:semanticCommitsDisabled`, `:maintainLockFilesWeekly`, `schedule:weekdays`. -- `baseBranches: ["main", "develop"]`, `timezone: "Europe/Copenhagen"`. -- `vulnerabilityAlerts`: `automerge: true`, `minimumReleaseAge: "0 days"`, `prPriority: 10`, `commitMessagePrefix: "security:"`, `matchBaseBranches: ["main"]`. Also enable `osvVulnerabilityAlerts: true`. Security advisories always land on `main` so they ship as a hotfix release. -- `lockFileMaintenance`: weekly Monday morning, auto-merge, `matchBaseBranches: ["main"]`. -- `packageRules`: - - **Patch/pin/digest/lockFileMaintenance on `main`** → `matchBaseBranches: ["main"]`, `matchUpdateTypes: ["patch", "pin", "digest", "lockFileMaintenance"]`, `automerge: true`, `minimumReleaseAge: "7 days"`. The hotfix path. - - **Minor/major on `develop`** → `matchBaseBranches: ["develop"]`, `matchUpdateTypes: ["minor", "major"]`, `dependencyDashboardApproval: true`, `automerge: false`. The normal release path. - - **Suppression rules** so updates don't open against the wrong base: - - `matchBaseBranches: ["main"]` + `matchUpdateTypes: ["minor", "major"]` → `enabled: false`. - - `matchBaseBranches: ["develop"]` + `matchUpdateTypes: ["patch", "pin", "digest", "lockFileMaintenance"]` → `enabled: false` (patches reach develop via the back-merge). - - Group `^symfony/`, `^doctrine/`, `^api-platform/` patches each into one PR (apply on `main`). - - `php` package → `matchBaseBranches: ["develop"]`, `rangeStrategy: "in-range-only"`, no auto-merge. - - GitHub Actions → pin digests, auto-merge digest/patch on `main`. -- `postUpgradeTasks`: `bash .github/scripts/renovate-changelog.sh {{{branchName}}} {{{prTitle}}}`, `fileFilters: ["CHANGELOG.md"]`, `executionMode: "branch"`. - -## Step 1.3 — Create `.github/scripts/renovate-changelog.sh` - -Idempotent script that appends one bullet under `## [Unreleased]`: - -- `set -euo pipefail`. -- Exit 0 if `CHANGELOG.md` is already staged or modified in the working tree (prevents duplicate bullets when `postUpgradeTasks` fires multiple times for a grouped PR). -- Python heredoc splices `- Renovate: (\`\`)` into the `[Unreleased]` section. If no `[Unreleased]` section exists, create one after the intro paragraph. - -```bash -chmod +x .github/scripts/renovate-changelog.sh -``` - -Local validation: - -```bash -.github/scripts/renovate-changelog.sh renovate/symfony-patch "Update symfony (patch)" -git diff CHANGELOG.md # expect one new bullet under [Unreleased] -git checkout -- CHANGELOG.md -``` - -## Step 1.4 — Create `.github/workflows/renovate.yaml` - -- Triggers: `schedule` (weekdays 05:00 UTC) + `workflow_dispatch` with `logLevel` and `dryRun` inputs. -- `concurrency.group: renovate`, `cancel-in-progress: false`. -- Set up PHP 8.4 + Composer (needed so `composer update` works inside Renovate's container). -- `renovatebot/github-action@v43` with `token: ${{ secrets.RENOVATE_TOKEN }}`. -- Critical env vars for `postUpgradeTasks`: - - `RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^bash \\.github/scripts/renovate-changelog\\.sh"]'` - - `RENOVATE_ALLOW_POST_UPGRADE_COMMAND_TEMPLATING: "true"` -- `RENOVATE_REPOSITORIES: ${{ github.repository }}` — this means the same workflow file works unmodified on both fork and upstream; it always operates on its own repo. - -## Step 1.5 — Create `.github/workflows/auto-release.yaml` - -Trigger: `pull_request.closed` on `main`, gated by `github.event.pull_request.merged == true && github.event.pull_request.user.login == 'renovate[bot]'`. - -Steps: - -1. `actions/checkout@v6` with `ref: main`, `fetch-depth: 0`, `token: ${{ secrets.RELEASE_TOKEN }}`. -2. `lewagon/wait-on-check-action@v1.4.0` waiting on the merge commit's required checks (`PHP Unit tests`, `PHPStan`, `Validate Doctrine Schema`, `Load fixtures`, `Build assets`). -3. Compute next patch: `git tag -l '[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1`, bump the patch component. On the fork, the first run produces `1.11.1`. -4. Gate: if `[Unreleased]` is empty (`awk` extraction returns nothing), `skip=true` and stop. -5. Promote `[Unreleased]` → `[] - ` via Python heredoc. -6. `git commit -m "release: "`, `git tag -a `, push both to `main`. `github_build_release.yml` fires on the `*.*.*` tag. -7. **Back-merge `main` → `develop`:** - - `git fetch origin develop && git checkout develop` - - `git merge --no-ff main -m "chore: back-merge hotfix into develop"` - - On conflict: abort, then `gh pr create --base develop --head main --title "Back-merge hotfix "`. Do not force-push. - - Otherwise push develop with `RELEASE_TOKEN`. - -## Step 1.6 — Local validation before pushing - -```bash -python3 -c "import json; json.load(open('renovate.json'))" -python3 -c "import yaml; yaml.safe_load(open('.github/workflows/renovate.yaml'))" -python3 -c "import yaml; yaml.safe_load(open('.github/workflows/auto-release.yaml'))" -bash -n .github/scripts/renovate-changelog.sh -``` - -If Taskfile defines them, also: - -```bash -task lint -``` - -## Step 1.7 — Fork-side manual setup - -Done by **you (turegjorup)** as the fork owner before the pipeline runs: - -1. **Repository secrets on `turegjorup/devops_itksites`:** - - `RENOVATE_TOKEN` — a PAT on your account scoped to the fork with `contents:write`, `pull-requests:write`, `workflows`. For the test phase this can be a classic PAT or fine-grained PAT; a GitHub App is overkill. - - `RELEASE_TOKEN` — PAT with `contents:write`, `workflows` for the tag push + back-merge. -2. **Branch protection on `main` and `develop` (fork)** — match what you'll later configure on upstream: - - Required status checks: `PHP Unit tests`, `PHPStan`, `Validate Doctrine Schema`, `Load fixtures`, `Build assets`, `Changelog`, `composer-validate`, `composer-normalized`, `composer-audit`. - - Require branches up to date before merging. - - **Allow auto-merge** at the repo level. - - Allow `RELEASE_TOKEN` identity to direct-push (for back-merges); fallback to PR if blocked. -3. **(Optional) CODEOWNERS exemption** for `renovate[bot]` if you mirror upstream's CODEOWNERS. - -## Step 1.8 — Commit & open PR against the fork's `develop` - -```bash -git add renovate.json .github/workflows/renovate.yaml .github/workflows/auto-release.yaml .github/scripts/renovate-changelog.sh -# changelog.yaml will gate this PR — add a bullet to [Unreleased] -$EDITOR CHANGELOG.md -git add CHANGELOG.md -git commit -m "Add Renovate auto-patch + auto-release pipeline" -git push -u origin feat/renovate-auto-patch -gh pr create --repo turegjorup/devops_itksites --base develop --fill -``` - -Merge it once the existing `pr.yaml` checks pass. (No release fires from this — it lands on `develop`, not `main`.) - -## Step 1.9 — Promote the pipeline files to `main` on the fork - -The pipeline only takes effect for hotfix PRs once the workflow files and `renovate.json` exist on `main`. Two options: - -- **Quick:** cherry-pick the four pipeline files from develop to main with a manual PR (`gh pr create --base main --head feat/renovate-pipeline-main`). Safe because these are tooling files; no app behavior changes. -- **Slow:** wait for the next `release/*` → `main` cut to carry them across. - -Use the quick path for the fork test so the validation cycle isn't blocked on a release. - -## Step 1.10 — First-run verification on the fork - -1. **Dry-run.** Actions → Renovate → Run workflow with `dryRun: full`, `logLevel: debug`. Confirm logs show: - - Config parses cleanly. - - Patch candidates for `symfony/*` (8.0.8/9/10/11 → 8.0.12) and `twig/twig` (3.25.0 → 3.26.0) are scoped to `main`. - - Minor/major candidates (if any) scoped to `develop`. - - `postUpgradeTasks` listed as allowed. -2. **Live run.** Re-run with `dryRun: null`. Dependency Dashboard issue appears; first PRs open within minutes. -3. **First patch PR (against `main`):** - - `CHANGELOG.md` has exactly one new bullet. - - `pr.yaml`, `composer-audit`, `Changelog` all pass. - - PR auto-merges after `minimumReleaseAge` (security PRs immediately, regular patches after 7 days — use a security PR for the first test to skip the soak). -4. **Auto-release cycle:** - - `auto-release.yaml` runs after merge, computes `1.11.1`, promotes `[Unreleased]`, tags from `main`, pushes. - - `github_build_release.yml` builds the GitHub Release on the fork. -5. **Back-merge:** verify the release commit reached `develop` (push or PR). - -## Step 1.11 — Exit criteria for Phase 1 - -Move to Phase 2 only when **all of these** are observed on the fork: - -- One **security** PR (e.g. a Symfony 8.0.x patch) → auto-merged with 0-day soak → released → back-merged. -- One **regular patch** PR → auto-merged after 7-day soak → released → back-merged. (To avoid blocking the Phase 1 sign-off on a real 7-day wait, temporarily set `minimumReleaseAge: "0 days"` for this single test run, confirm the merge fires, then restore `"7 days"` before going to upstream.) -- One **grouped patch** PR (Symfony or Doctrine cluster) → auto-merged correctly without partial-update artifacts. -- One **minor or major** candidate appeared on the Dependency Dashboard against `develop` and did **not** auto-merge. -- A **back-merge conflict** scenario has been exercised at least once (synthesize one if it doesn't happen naturally) and the PR-fallback worked. -- The CHANGELOG ended each cycle correctly: `[Unreleased]` empty, `[1.11.x] - ` populated. - -Document any deviations or required tweaks in `RENOVATE_TEST_NOTES.md` (created during this phase, not promoted to upstream). - -## Step 1.12 — Cleanup before Phase 2 - -After the fork test is signed off: - -- Optionally delete the test releases (`1.11.1`, `1.11.2`, …) from the fork's Releases page to keep the fork's tag history tidy. The tags themselves can stay. -- Keep the `Renovate` Actions workflow enabled on the fork so it continues to track patches as a low-stakes early-warning system. - ---- - -# Phase 2 — Re-implement on upstream (`itk-dev/devops_itksites`) - -Goal: port the validated files to upstream with the minimum possible delta. The smaller the diff between fork and upstream, the cheaper the review and the lower the risk. - -## Step 2.1 — Sync fork's pipeline state to a fresh branch - -```bash -git fetch upstream develop -git checkout -b feat/renovate-auto-patch upstream/develop -git checkout feat/renovate-auto-patch -- \ - renovate.json \ - .github/workflows/renovate.yaml \ - .github/workflows/auto-release.yaml \ - .github/scripts/renovate-changelog.sh -# OR, if working from the merged develop on the fork: -# git checkout feat/renovate-auto-patch-fork -- -``` - -No content changes should be needed — `RENOVATE_REPOSITORIES: ${{ github.repository }}` makes the workflow portable. - -## Step 2.2 — Upstream-only manual setup (request from itk-dev admins) - -Mirror the fork's manual setup, but on upstream: - -1. **Repository secrets on `itk-dev/devops_itksites`:** - - `RENOVATE_TOKEN` — a **GitHub App** installation token. Create a dedicated app (e.g. `renovate-itkdev`) on the itk-dev org with `contents:write`, `pull-requests:write`, `workflows:write` permissions; install it on `devops_itksites`; store the App ID + private key as `RENOVATE_APP_ID` / `RENOVATE_APP_PRIVATE_KEY` secrets; the workflow exchanges them for an installation token at runtime via `actions/create-github-app-token`. The App identity (e.g. `renovate-itkdev[bot]`) is what shows up in commits and audit logs — no personnel coupling. - - `RELEASE_TOKEN` — same GitHub App token, or a separate App if you want auditing separation between Renovate pushes and release pushes. Needs `contents:write`, `workflows:write`. -2. **Branch protection on `main` and `develop`** — identical rule sets to the fork (status checks, up-to-date, auto-merge enabled, RELEASE_TOKEN allowed to push). -3. **CODEOWNERS** — if upstream has CODEOWNERS, either exempt `renovate[bot]` or accept that each PR needs a human approval before auto-merge. - -## Step 2.3 — PR + handoff - -```bash -$EDITOR CHANGELOG.md # add bullet under [Unreleased] -git add -A -git commit -m "Add Renovate auto-patch + auto-release pipeline" -git push -u upstream feat/renovate-auto-patch -gh pr create --repo itk-dev/devops_itksites --base develop --fill -``` - -PR body should link to: - -- This plan. -- The fork's successful Renovate run history (Actions → Renovate). -- The fork's first auto-release (`turegjorup/devops_itksites` Releases → `1.11.1`). - -Reviewers can verify the diff is small and that all the behavior they're approving has already been observed running on the fork. - -## Step 2.4 — Promote the files to upstream `main` - -After the PR merges to `upstream/develop`, the same promotion question from Step 1.9 applies upstream. Recommend the same quick path: a small, scoped PR cherry-picking the four pipeline files from `develop` to `main`. - -## Step 2.5 — First-run verification on upstream - -Same as Step 1.10, but on upstream. Watch the first cycle closely — security PR first (0-day soak) so feedback comes quickly. - ---- - -## Rollback - -Applies to both fork and upstream: - -- **Stop new PRs:** disable the `Renovate` workflow in the Actions tab. -- **Disable auto-release:** delete `.github/workflows/auto-release.yaml`. `github_build_release.yml` still works manually. -- **Disable auto-merge globally:** set top-level `automerge: false` in `renovate.json`. Renovate keeps opening PRs but waits for human merge. -- **Revert a bad back-merge:** the back-merge is `--no-ff`, so `git revert -m 1 ` cleanly undoes it on `develop` without touching the release on `main`. - -## Resolved decisions - -| Decision | Choice | Where it lives | -| --- | --- | --- | -| Upstream auth | **GitHub App** (`renovate-itkdev`-style). Personal PAT acceptable on the fork only. | Step 2.2 | -| Non-security soak window | **7 days.** Vuln-alert path overrides to 0 days for CVEs. | Step 1.2 patch package rule, Step 1.7 fork-side fallback | -| Version source for auto-release | **Git tags.** Highest `X.Y.Z` → bump patch. | Step 1.5 step 3 | -| Back-merge granularity | **Single grouped back-merge** per release, no per-package splitting. | Step 1.5 step 7 | - -## Pre-flight checklist (do before Phase 1 first-run) - -- [ ] Verify `CHANGELOG.md` on `main` does **not** already contain a `## [1.11.1]` header — if it does, either delete it or bump the auto-release seed to `1.11.2`. (Tag-based versioning will collide otherwise.) -- [ ] Confirm `composer.lock` is committed and matches `composer.json` (Renovate's `composer update` runs against the lock). -- [ ] Confirm `task lint` (or equivalent) passes locally on the four new files. From 36abadab649a73d47ca0330b65bf77c208b08faf Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 21 May 2026 12:53:18 +0200 Subject: [PATCH 4/5] chore: disable Codecov upload on the fork The fork has no CODECOV_TOKEN secret, so the upload step fails with fail_ci_if_error: true and takes the whole PHP Unit tests check down with it. Comment the step out here; restore it (or supply a token) when promoting to upstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pr.yaml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 5625c95..b7b2436 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -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 From 292ba8664bf668abe276613b4555312282a87848 Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 21 May 2026 12:58:44 +0200 Subject: [PATCH 5/5] 13 --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index fb220e2..69a1f1f 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -12,7 +12,7 @@ name: Renovate on: schedule: - - cron: "0 10 * * 1-5" # weekdays 10:00 UTC = 12:00 Copenhagen (CEST) / 11:00 (CET) + - cron: "0 11 * * 1-5" # weekdays 11:00 UTC = 13:00 Copenhagen (CEST) / 12:00 (CET) workflow_dispatch: inputs: logLevel: