Files
shared-actions/.github/workflows/renovate-autoupdate.yaml
T
lukaszraczylo aca729f8f7 fix(autoupdate): skip major updates by default
A repo with a backlog of majors got one PR per major on the first run.
Replace include-major with major-updates: ignore (default), separate or
batch. Ignore uses a version cap, because rules that disable by update
type have no effect.
2026-10-03 16:47:43 +01:00

340 lines
13 KiB
YAML

name: Renovate Autoupdate
# Renovate batches every Go and npm/pnpm/yarn update into one PR, the tests run
# on that branch, and the PR merges only when they pass. Replaces go-autoupdate.
# A renovate.json in the calling repository adds to these defaults.
on:
workflow_call:
inputs:
go-version:
description: "Go version to use"
required: false
type: string
default: ">=1.24"
go-working-directory:
description: "Directory that holds go.mod"
required: false
type: string
default: "."
node-version:
description: "Node.js version to use"
required: false
type: string
default: "22"
node-working-directory:
description: "Directory that holds package.json"
required: false
type: string
default: "."
node-scripts:
description: "Comma-separated package.json scripts to run, in order. A script that does not exist is skipped."
required: false
type: string
default: "lint,typecheck,test,build"
require-node-tests:
description: "Fail when a Node project has no test script, so an untested update never merges"
required: false
type: boolean
default: true
major-updates:
description: "What to do with major updates: 'ignore' skips them, 'separate' opens one PR per major that never merges automatically, 'batch' puts them in the batch."
required: false
type: string
default: "ignore"
minimum-release-age:
description: "Skip releases younger than this, for example '2 days'"
required: false
type: string
default: "2 days"
renovate-version:
description: "Renovate version for the action"
required: false
type: string
default: "44"
commit-subject:
description: "Squash commit subject. Keep it free of release keywords such as 'major' or 'breaking', which semver-generator matches."
required: false
type: string
default: "chore(deps): update dependencies"
admin-merge:
description: "Merge with --admin, which bypasses branch protection. The workflow token cannot do this, so leave it off unless you pass a token that can."
required: false
type: boolean
default: false
release-workflow:
description: "Workflow file to dispatch after the merge, for example release.yaml. It must have a workflow_dispatch trigger and the caller needs actions: write. Empty means no dispatch."
required: false
type: string
default: ""
lfs:
description: "Enable Git LFS checkout (for repos with large files)"
required: false
type: boolean
default: false
runner:
description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest."
required: false
type: string
default: ""
concurrency:
group: renovate-autoupdate-${{ github.repository }}
cancel-in-progress: false
env:
BATCH_BRANCH: deps-autoupdate/batch
jobs:
renovate:
name: Update dependencies
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 20
permissions:
contents: write
pull-requests: write
outputs:
pr-number: ${{ steps.find-pr.outputs.pr-number }}
steps:
- name: Write Renovate config
env:
MIN_AGE: ${{ inputs.minimum-release-age }}
MAJOR_UPDATES: ${{ inputs.major-updates }}
CONFIG_FILE: ${{ runner.temp }}/renovate-autoupdate-config.json
run: |
jq -n \
--arg repo "$GITHUB_REPOSITORY" \
--arg age "$MIN_AGE" \
--arg branch_prefix "${BATCH_BRANCH%%/*}/" \
--arg majors "$MAJOR_UPDATES" \
'
{
platform: "github",
repositories: [$repo],
onboarding: false,
requireConfig: "optional",
branchPrefix: $branch_prefix,
enabledManagers: ["gomod", "npm"],
dependencyDashboard: false,
semanticCommits: "enabled",
semanticCommitType: "chore",
semanticCommitScope: "deps",
minimumReleaseAge: $age,
prConcurrentLimit: 0,
prHourlyLimit: 0,
recreateWhen: "always",
postUpdateOptions: ["gomodTidy"],
statusCheckNames: {
artifactError: null,
configValidation: null,
mergeConfidence: null,
minimumReleaseAge: null
},
packageRules: [
({
matchManagers: ["gomod", "npm"],
groupName: "dependencies",
groupSlug: "batch"
} + (if $majors == "batch" then {} else {matchUpdateTypes: ["minor", "patch", "pin", "pinDigest", "digest"]} end)),
{matchManagers: ["gomod"], matchDepTypes: ["indirect"], enabled: true},
{matchPackageNames: ["typescript"], allowedVersions: "<7"}
]
# Rules that disable by update type are ignored, but a version cap works.
+ (if $majors == "ignore" then [{matchManagers: ["gomod", "npm"], allowedVersions: "<{{add major 1}}.0.0"}] else [] end)
}' > "$CONFIG_FILE"
jq . "$CONFIG_FILE"
- name: Run Renovate
uses: renovatebot/[email protected]
with:
configurationFile: ${{ runner.temp }}/renovate-autoupdate-config.json
token: ${{ github.token }}
renovate-version: ${{ inputs.renovate-version }}
env:
LOG_LEVEL: info
- name: Find the batch PR
id: find-pr
env:
GH_TOKEN: ${{ github.token }}
run: |
number=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BATCH_BRANCH" --state open --json number --jq '.[0].number // empty')
echo "pr-number=$number" >> "$GITHUB_OUTPUT"
if [ -z "$number" ]; then
echo "No dependency updates to test."
else
echo "Batch PR: #$number"
fi
test:
name: Test the update
needs: renovate
if: needs.renovate.outputs.pr-number != ''
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 45
permissions:
contents: read
pull-requests: write
outputs:
head-sha: ${{ steps.checkout-sha.outputs.sha }}
steps:
- name: Checkout the batch branch
uses: actions/checkout@v6
with:
ref: ${{ env.BATCH_BRANCH }}
token: ${{ github.token }}
lfs: ${{ inputs.lfs }}
- name: Record the tested commit
id: checkout-sha
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Run workflow prepare script
shell: bash
run: |
if [ -f "./workflow-prepare.sh" ]; then
chmod +x ./workflow-prepare.sh
./workflow-prepare.sh
fi
- name: Detect project types
id: detect
env:
GO_DIR: ${{ inputs.go-working-directory }}
NODE_DIR: ${{ inputs.node-working-directory }}
run: |
go=false
node=false
pm=npm
if [ -f "$GO_DIR/go.mod" ]; then go=true; fi
if [ -f "$NODE_DIR/package.json" ]; then
node=true
if [ -f "$NODE_DIR/pnpm-lock.yaml" ]; then pm=pnpm
elif [ -f "$NODE_DIR/yarn.lock" ]; then pm=yarn
fi
fi
if [ "$go" = false ] && [ "$node" = false ]; then
echo "::error::Neither $GO_DIR/go.mod nor $NODE_DIR/package.json exists, so there is nothing to test."
exit 1
fi
{
echo "go=$go"
echo "node=$node"
echo "pm=$pm"
} >> "$GITHUB_OUTPUT"
- name: Setup Go
if: steps.detect.outputs.go == 'true'
uses: actions/setup-go@v6
with:
go-version: ${{ inputs.go-version }}
check-latest: true
cache: true
cache-dependency-path: ${{ inputs.go-working-directory }}/go.sum
- name: Build and test Go
if: steps.detect.outputs.go == 'true'
working-directory: ${{ inputs.go-working-directory }}
run: |
go mod verify
go build ./...
go test -race -cover ./...
- name: Setup Node.js
if: steps.detect.outputs.node == 'true'
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
- name: Install Node dependencies
if: steps.detect.outputs.node == 'true'
working-directory: ${{ inputs.node-working-directory }}
env:
PM: ${{ steps.detect.outputs.pm }}
run: |
# The lockfile must match what Renovate wrote: install without updating it.
case "$PM" in
pnpm | yarn)
command -v corepack >/dev/null || npm install --global corepack
corepack enable "$PM"
;;
esac
case "$PM" in
pnpm) pnpm install --frozen-lockfile ;;
yarn) yarn install --immutable ;;
npm)
if [ -f package-lock.json ]; then npm ci; else npm install; fi
;;
esac
- name: Run Node scripts
if: steps.detect.outputs.node == 'true'
working-directory: ${{ inputs.node-working-directory }}
env:
PM: ${{ steps.detect.outputs.pm }}
SCRIPTS: ${{ inputs.node-scripts }}
REQUIRE_TESTS: ${{ inputs.require-node-tests }}
run: |
if [ "$REQUIRE_TESTS" = true ] && ! jq -e '.scripts.test' package.json >/dev/null; then
echo "::error::package.json has no test script. Add one, or set require-node-tests to false."
exit 1
fi
IFS=',' read -r -a scripts <<<"$SCRIPTS"
for script in "${scripts[@]}"; do
script=$(echo "$script" | xargs)
[ -n "$script" ] || continue
if jq -e --arg s "$script" '.scripts[$s]' package.json >/dev/null; then
echo "::group::$PM run $script"
"$PM" run "$script"
echo "::endgroup::"
else
echo "::notice::No '$script' script, skipping."
fi
done
- name: Report the failure on the PR
if: failure()
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ needs.renovate.outputs.pr-number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" \
--body "The dependency update failed its tests, so it was not merged. See $RUN_URL. Renovate retries on the next run."
merge:
name: Merge the update
needs: [renovate, test]
if: needs.test.result == 'success'
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 10
permissions:
actions: write
contents: write
pull-requests: write
steps:
- name: Squash-merge the PR
env:
# A push made with github.token does not trigger workflows, so the release is dispatched below.
GH_TOKEN: ${{ github.token }}
PR: ${{ needs.renovate.outputs.pr-number }}
HEAD_SHA: ${{ needs.test.outputs.head-sha }}
SUBJECT: ${{ inputs.commit-subject }}
ADMIN: ${{ inputs.admin-merge && 'true' || '' }}
run: |
# --match-head-commit refuses to merge anything newer than the commit that was tested.
gh pr merge "$PR" --repo "$GITHUB_REPOSITORY" --squash --delete-branch \
--match-head-commit "$HEAD_SHA" \
--subject "$SUBJECT" \
--body "Automated dependency update. All tests passed." \
${ADMIN:+--admin}
- name: Dispatch the release workflow
if: inputs.release-workflow != ''
env:
GH_TOKEN: ${{ github.token }}
RELEASE_WORKFLOW: ${{ inputs.release-workflow }}
run: |
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
gh workflow run "$RELEASE_WORKFLOW" --repo "$GITHUB_REPOSITORY" --ref "$branch"