Renovate #719
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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$"]' |