mirror of
https://github.com/lukaszraczylo/shared-actions.git
synced 2026-10-12 04:54:48 +00:00
Renovate now prepares branches and opens no PR. Each branch is merged into the default branch locally and tested, one at a time. Only a branch that passes gets a PR, which merges at once, so a failing update never shows up as a PR. Majors default to one branch each. The release is dispatched once, after anything merged.
386 lines
16 KiB
YAML
386 lines
16 KiB
YAML
name: Renovate Autoupdate
|
|
|
|
# Renovate prepares branches with every Go and npm/pnpm/yarn update and opens no PR.
|
|
# Each branch is merged into the default branch and tested. Only when the tests
|
|
# pass does the workflow open a PR and merge it. 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: 'separate' gives each its own branch, tested and merged on its own, 'batch' puts them in the one batch, 'ignore' skips them."
|
|
required: false
|
|
type: string
|
|
default: "separate"
|
|
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"
|
|
release-workflow:
|
|
description: "Workflow file to dispatch after a 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: ""
|
|
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
|
|
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: Prepare the updates
|
|
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: write
|
|
pull-requests: write
|
|
outputs:
|
|
branches: ${{ steps.list.outputs.branches }}
|
|
base-sha: ${{ steps.base.outputs.sha }}
|
|
steps:
|
|
- name: Record the default branch commit
|
|
id: base
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
|
|
echo "sha=$(gh api "repos/$GITHUB_REPOSITORY/commits/$branch" --jq .sha)" >> "$GITHUB_OUTPUT"
|
|
|
|
- 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,
|
|
# Branches only: with no dashboard, no PR is ever opened. The workflow opens it after the tests.
|
|
prCreation: "approval",
|
|
rebaseWhen: "behind-base-branch",
|
|
semanticCommits: "enabled",
|
|
semanticCommitType: "chore",
|
|
semanticCommitScope: "deps",
|
|
minimumReleaseAge: $age,
|
|
prConcurrentLimit: 0,
|
|
prHourlyLimit: 0,
|
|
branchConcurrentLimit: 0,
|
|
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: List the update branches
|
|
id: list
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
prefix="${BATCH_BRANCH%%/*}/"
|
|
all=$(gh api --paginate "repos/$GITHUB_REPOSITORY/branches" --jq '.[].name' | grep -F "$prefix" || true)
|
|
# The batch first, then each major update.
|
|
ordered=$({ printf '%s\n' "$all" | grep -Fx "$BATCH_BRANCH" || true; printf '%s\n' "$all" | grep -vFx "$BATCH_BRANCH" || true; } | sed '/^$/d')
|
|
json=$(printf '%s\n' "$ordered" | jq -R . | jq -sc 'map(select(length > 0))')
|
|
echo "branches=$json" >> "$GITHUB_OUTPUT"
|
|
echo "Update branches: $json"
|
|
|
|
test-and-merge:
|
|
name: Test and merge ${{ matrix.branch }}
|
|
needs: renovate
|
|
if: needs.renovate.outputs.branches != '[]'
|
|
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: 60
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
strategy:
|
|
fail-fast: false
|
|
# One at a time, so each branch is tested on top of the ones merged before it.
|
|
max-parallel: 1
|
|
matrix:
|
|
branch: ${{ fromJSON(needs.renovate.outputs.branches) }}
|
|
env:
|
|
BRANCH: ${{ matrix.branch }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
steps:
|
|
- name: Checkout the default branch
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
lfs: ${{ inputs.lfs }}
|
|
|
|
- name: Merge the update branch locally
|
|
id: merge
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git fetch origin "$BRANCH"
|
|
echo "head=$(git rev-parse FETCH_HEAD)" >> "$GITHUB_OUTPUT"
|
|
echo "subject=$(git log -1 --format=%s FETCH_HEAD)" >> "$GITHUB_OUTPUT"
|
|
if git merge-base --is-ancestor FETCH_HEAD HEAD; then
|
|
echo "::notice::$BRANCH is already in the default branch."
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
elif git merge --no-ff --no-commit FETCH_HEAD; then
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
git merge --abort
|
|
echo "::warning::$BRANCH conflicts with the default branch. Renovate rebases it on the next run."
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Run workflow prepare script
|
|
if: steps.merge.outputs.skip != 'true'
|
|
shell: bash
|
|
run: |
|
|
if [ -f "./workflow-prepare.sh" ]; then
|
|
chmod +x ./workflow-prepare.sh
|
|
./workflow-prepare.sh
|
|
fi
|
|
|
|
- name: Detect project types
|
|
id: detect
|
|
if: steps.merge.outputs.skip != 'true'
|
|
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.merge.outputs.skip != 'true' && 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.merge.outputs.skip != 'true' && 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.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true'
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ inputs.node-version }}
|
|
|
|
- name: Install Node dependencies
|
|
if: steps.merge.outputs.skip != 'true' && 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.merge.outputs.skip != 'true' && 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: Open the PR and merge it
|
|
if: steps.merge.outputs.skip != 'true'
|
|
env:
|
|
HEAD_SHA: ${{ steps.merge.outputs.head }}
|
|
SUBJECT: ${{ steps.merge.outputs.subject }}
|
|
ADMIN: ${{ inputs.admin-merge && 'true' || '' }}
|
|
run: |
|
|
default=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
|
|
pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BRANCH" --state open --json number --jq '.[0].number // empty')
|
|
if [ -z "$pr" ]; then
|
|
url=$(gh pr create --repo "$GITHUB_REPOSITORY" --head "$BRANCH" --base "$default" \
|
|
--title "$SUBJECT" \
|
|
--body "Automated dependency update. The tests passed on the result of merging this branch into $default.")
|
|
pr=${url##*/}
|
|
fi
|
|
# GitHub needs a moment to work out whether the PR can merge.
|
|
for _ in $(seq 1 12); do
|
|
state=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json mergeable --jq .mergeable)
|
|
[ "$state" != UNKNOWN ] && break
|
|
sleep 5
|
|
done
|
|
# --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}
|
|
echo "Merged #$pr ($BRANCH)." >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Report the failure
|
|
if: failure()
|
|
run: |
|
|
echo "The tests failed for \`$BRANCH\`, so no PR was opened. Renovate retries on the next run." >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
release:
|
|
name: Dispatch the release
|
|
needs: [renovate, test-and-merge]
|
|
if: always() && needs.renovate.outputs.branches != '[]' && inputs.release-workflow != ''
|
|
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: read
|
|
steps:
|
|
- name: Dispatch when the default branch moved
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_WORKFLOW: ${{ inputs.release-workflow }}
|
|
BASE_SHA: ${{ needs.renovate.outputs.base-sha }}
|
|
run: |
|
|
# A push made with github.token does not trigger workflows, so dispatch the release explicitly.
|
|
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
|
|
now=$(gh api "repos/$GITHUB_REPOSITORY/commits/$branch" --jq .sha)
|
|
if [ "$now" = "$BASE_SHA" ]; then
|
|
echo "Nothing was merged, so no release."
|
|
exit 0
|
|
fi
|
|
gh workflow run "$RELEASE_WORKFLOW" --repo "$GITHUB_REPOSITORY" --ref "$branch"
|