Skip to content

Renovate

Renovate #719

Workflow file for this run

name: Renovate
# Self-hosted Renovate runner. Replaces the Mend-hosted scheduler so we can tick
# more frequently than ~4h and clear the dependency backlog within the existing
# automerge windows. See PLA-48 for the throughput rationale;
# .github/renovate.json5 is still the source of truth for package rules,
# schedule, and automergeSchedule. This workflow adds the live GitHub open-PR
# cap because Renovate's config-level limits are not enough to express "run
# maintenance, but create no more PRs once 10 are open."
on:
schedule:
# Wake the runner only inside the windows where Renovate is actually
# allowed to open or maintain PRs (see `schedule` in renovate.json5),
# plus a single weekday daytime tick to keep vulnerability-alert state
# fresh in the Dependency Dashboard. Ticks outside those windows aren't
# no-ops in practice — each one is a 20-30min full extract — so the
# previous `17 * * * *` was burning ~17h/day of Actions compute for no
# merge benefit.
#
# :17-past-the-hour offset avoids top-of-hour GH Actions scheduler
# contention, which was dropping roughly every other tick on `0 * * * *`.
# Hourly cadence inside active windows roughly matches Ghost's CI
# duration, giving each merge a fresh rebase + green CI window.
#
# Times are UTC; matches renovate.json5's `schedule` block.
# Weekday early-morning window (Mon-Sat 00:00-05:59 UTC). Last tick is
# 03:47 rather than the end of the window: Actions has been delivering
# these up to ~70min late, so a later cron lands outside the window and
# wastes the freshest-CI opportunity of the night.
- cron: '47 0-3 * * 1-6'
# Weekday evening window (Mon-Fri 23:00-23:59 UTC)
- cron: '17 23 * * 1-5'
# Weekend - every 2h is plenty; no automerge urgency, just batch creation
- cron: '17 */2 * * 0,6'
# Weekday daytime CVE pickup tick (Mon-Fri 14:17 UTC)
- cron: '17 14 * * 1-5'
workflow_dispatch:
inputs:
ignoreSchedule:
description: 'Ignore Renovate schedule for this manual run'
required: false
default: false
type: boolean
logLevel:
description: 'Renovate log level for this manual run'
required: false
default: 'info'
type: choice
options: [info, debug, trace]
concurrency:
group: renovate
cancel-in-progress: false
permissions:
contents: read
jobs:
renovate:
# Never run on forks: both `schedule` and `workflow_dispatch` can fire on a
# fork that has Actions enabled, and this job mints the Renovate GitHub App
# token. Matches the fork guard used across the rest of the repo's
# privileged workflows.
if: github.repository == 'TryGhost/Ghost'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Get GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.TRYGHOST_RENOVATE_APP_ID }}
private-key: ${{ secrets.TRYGHOST_RENOVATE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: Ghost
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
# Enforce a live cap on open Renovate PRs while still letting Renovate
# maintain and automerge the PRs that already exist.
#
# Why this lives outside renovate.json5:
# - prConcurrentLimit/branchConcurrentLimit are useful guardrails, but
# Renovate computes them from the branches in the current run rather
# than by asking GitHub for every open Renovate PR.
# - vulnerability-alert PRs can bypass Renovate's normal branch, PR,
# hourly, and schedule limits.
#
# Below the cap, restrict PR creation to the number of slots left. At or
# above the cap, or when RENOVATE_MAINTENANCE_ONLY=true, force dashboard
# approval for new branches/PRs. Existing PRs keep updating and merging
# on the normal schedule. That gives us "rebase and merge the 10, but
# create no more."
#
# Deliberately does NOT force `updateNotScheduled: true` — that overrides
# renovate.json5's `false` and puts a full-fleet force-push in the middle
# of the workday, which is exactly what #28207 removed.
- name: Configure Renovate PR cap
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_CAP: ${{ vars.RENOVATE_OPEN_PR_CAP || '10' }}
MAINTENANCE_ONLY: ${{ vars.RENOVATE_MAINTENANCE_ONLY || 'false' }}
IGNORE_SCHEDULE: ${{ github.event_name == 'workflow_dispatch' && inputs.ignoreSchedule }}
run: |
set -euo pipefail
if ! [[ "$PR_CAP" =~ ^[0-9]+$ ]]; then
echo "::warning::RENOVATE_OPEN_PR_CAP must be a non-negative integer; falling back to 10."
PR_CAP=10
fi
# Exclude `needs:review` PRs: Renovate cannot merge them itself, so
# counting them lets a handful of parked updates hold the cap shut
# forever — the cap is only released by Renovate merging something.
open_count=$(gh pr list \
--repo "${{ github.repository }}" \
--author "app/tryghost-renovate" \
--state open \
--limit 100 \
--json number,labels \
--jq '[.[] | select(any(.labels[].name; . == "needs:review") | not)] | length')
echo "Renovate has $open_count open PRs it can merge itself (cap: $PR_CAP)"
if [ "$MAINTENANCE_ONLY" = "true" ]; then
force='{"dependencyDashboardApproval":true,"prCreation":"approval","vulnerabilityAlerts":{"dependencyDashboardApproval":false}}'
echo "::notice::RENOVATE_MAINTENANCE_ONLY=true. Running in maintenance-only mode: existing PRs may update/automerge, new PRs require dashboard approval."
elif [ "$open_count" -ge "$PR_CAP" ]; then
force='{"dependencyDashboardApproval":true,"prCreation":"approval","vulnerabilityAlerts":{"dependencyDashboardApproval":false}}'
echo "::notice::Renovate is at or above the open PR cap. Running in maintenance-only mode: existing PRs may update/automerge, new PRs require dashboard approval."
else
remaining=$((PR_CAP - open_count))
force="{\"prHourlyLimit\":$remaining}"
if [ "$IGNORE_SCHEDULE" = "true" ]; then
force="{\"schedule\":null,\"automergeSchedule\":null,\"prHourlyLimit\":$remaining}"
fi
echo "Renovate may create up to $remaining PR(s) in this run."
fi
echo "RENOVATE_FORCE=$force" >> "$GITHUB_ENV"
- name: Self-hosted Renovate
uses: renovatebot/github-action@e09d604f8f803bb527bd8321ed5be06c460b8682 # v46.2.2
with:
token: ${{ steps.app-token.outputs.token }}
env:
LOG_LEVEL: ${{ inputs.logLevel || 'info' }}
RENOVATE_REPOSITORY_CACHE: enabled
RENOVATE_REPOSITORIES: TryGhost/Ghost
# Ghost is already onboarded via Mend; don't open an onboarding PR.
RENOVATE_ONBOARDING: 'false'
# pnpm ≥ 11.13 records a pnpmfileChecksum in pnpm-lock.yaml whenever a
# .pnpmfile is present. With allowScripts=false (the default) Renovate
# regenerates lockfiles with `pnpm install --ignore-pnpmfile`, which
# drops the checksum, and CI's frozen install then rejects the lockfile
# (ERR_PNPM_LOCKFILE_CONFIG_MISMATCH). allowScripts=true makes Renovate
# honour the pnpmfile; dependency install scripts stay blocked because
# the repo-level `ignoreScripts` default (true) keeps --ignore-scripts.
RENOVATE_ALLOW_SCRIPTS: 'true'
# Allowlist the changeset-generation postUpgradeTask (renovate.json5).
# Renovate refuses to run any post-upgrade command that isn't matched
# by one of these regexes — arbitrary command execution is opt-in. Both
# the dep install and the generator command must be matched.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^pnpm install --no-frozen-lockfile --filter @internal/scripts --prod --ignore-scripts$", "^node scripts/generate-changeset\\.js$"]'