Files
shared-actions/.github/workflows/renovate-autoupdate.yaml
T

698 lines
32 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:
description: "Update and test the Go module. Turn off for a repo with no Go code."
required: false
type: boolean
default: true
go-working-directory:
description: "Directory that holds go.mod. Only this module is updated and tested."
required: false
type: string
default: "."
node-version:
description: "Node.js version to use"
required: false
type: string
default: "22"
go-mod-tidy:
description: "Run go mod tidy after a Go update. Turn it off for an Encore app: Encore adds imports at build time, so tidy prunes go.sum entries the build needs."
required: false
type: boolean
default: true
go-prepare-command:
description: "Shell commands to run before the Go tests, for example installing a CLI. To put a tool on PATH, append its directory to $GITHUB_PATH."
required: false
type: string
default: ""
go-test-command:
description: "Replaces the default Go build and test commands, for example 'encore test ./...' for an Encore app. It runs in go-working-directory."
required: false
type: string
default: ""
node:
description: "Update and test the Node project. Turn off for a repo with no frontend to test."
required: false
type: boolean
default: true
node-working-directory:
description: "Directory that holds package.json. Only this project is updated and tested."
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"
node-workspace:
description: "The Node project is a pnpm workspace. node-working-directory then holds pnpm-workspace.yaml and pnpm-lock.yaml, and needs no package.json. Dependencies install once there, and node-scripts run in each of node-packages."
required: false
type: boolean
default: false
node-packages:
description: "With node-workspace, comma-separated workspace package directories, relative to node-working-directory. Only these are updated and tested."
required: false
type: string
default: ""
pnpm-version:
description: "pnpm version to use, for a repo whose package.json has no packageManager field. Empty uses the packageManager field, or the newest pnpm."
required: false
type: string
default: ""
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. Give several as a comma-separated list. Each 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
log-level:
description: "Renovate log level: info, debug or trace. Use debug to see why Renovate fails."
required: false
type: string
default: "info"
lfs:
description: "Enable Git LFS checkout (for repos with large files)"
required: false
type: boolean
default: false
npm-registry-scope:
description: "Scope served by a private npm registry, for example '@fortawesome'. Used only when npm-registry-url and the npm-registry-token secret are also set."
required: false
type: string
default: ""
npm-registry-url:
description: "URL of the private npm registry, for example 'https://npm.fontawesome.com/'. Used only when npm-registry-scope and the npm-registry-token secret are also set."
required: false
type: string
default: ""
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: ""
secrets:
npm-registry-token:
description: "Auth token for the private npm registry"
required: false
concurrency:
group: renovate-autoupdate-${{ github.repository }}
cancel-in-progress: false
env:
BATCH_BRANCH: deps-autoupdate/batch
ISSUE_TITLE: Dependency updates failing tests
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
# Renovate reads the repository's issues when it starts. A private repo refuses that without this.
issues: read
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 }}
USE_GO: ${{ inputs.go }}
USE_NODE: ${{ inputs.node }}
GO_DIR: ${{ inputs.go-working-directory }}
NODE_DIR: ${{ inputs.node-working-directory }}
NODE_WORKSPACE: ${{ inputs.node-workspace }}
NODE_PACKAGES: ${{ inputs.node-packages }}
PNPM_VERSION: ${{ inputs.pnpm-version }}
GO_MOD_TIDY: ${{ inputs.go-mod-tidy }}
NPM_SCOPE: ${{ inputs.npm-registry-scope }}
NPM_URL: ${{ inputs.npm-registry-url }}
NPM_TOKEN: ${{ secrets.npm-registry-token }}
CONFIG_FILE: ${{ runner.temp }}/renovate-autoupdate-config.json
run: |
# The token goes to jq through the environment, never argv, and the config print below hides it.
export NPMRC=""
if [ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ]; then
echo "::add-mask::$NPM_TOKEN"
url="${NPM_URL%/}/"
NPMRC=$(printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN")
fi
jq -n \
--arg repo "$GITHUB_REPOSITORY" \
--arg age "$MIN_AGE" \
--arg branch_prefix "${BATCH_BRANCH%%/*}/" \
--arg majors "$MAJOR_UPDATES" \
--argjson use_go "$USE_GO" \
--argjson use_node "$USE_NODE" \
--arg go_dir "$GO_DIR" \
--arg node_dir "$NODE_DIR" \
--argjson node_workspace "$NODE_WORKSPACE" \
--arg node_packages "$NODE_PACKAGES" \
--arg pnpm_version "$PNPM_VERSION" \
--argjson go_tidy "$GO_MOD_TIDY" \
'
# A regex for files directly inside a directory, so only what the tests cover is updated.
def prefix($d): (if $d == "." or $d == "" then "" else ($d | ltrimstr("./") | rtrimstr("/")) + "/" end) | gsub("\\."; "\\.");
def pkgprefix($d; $p): prefix($d) + ($p | ltrimstr("./") | rtrimstr("/") | gsub("\\."; "\\.")) + "/";
# A workspace updates only its listed packages. The root lockfile is rewritten by pnpm with them.
def node_files:
if $node_workspace
then [$node_packages | split(",")[] | gsub("^\\s+|\\s+$"; "") | select(length > 0) | "/^" + pkgprefix($node_dir; .) + "package\\.json$/"]
else ["/^" + prefix($node_dir) + "package\\.json$/"]
end;
{
platform: "github",
repositories: [$repo],
onboarding: false,
requireConfig: "optional",
branchPrefix: $branch_prefix,
enabledManagers: ((if $use_go then ["gomod"] else [] end) + (if $use_node then ["npm"] else [] end)),
# Renovate writes the lockfile with its own pnpm. Use the version the tests install with.
constraints: (if $pnpm_version != "" then {pnpm: $pnpm_version} else {} end),
gomod: {managerFilePatterns: ["/^" + prefix($go_dir) + "go\\.mod$/"]},
npm: {managerFilePatterns: (node_files + ["/^" + prefix($node_dir) + "pnpm-workspace\\.yaml$/"])},
dependencyDashboard: false,
# Branches only: with no dashboard, no PR is ever opened. The workflow opens it after the tests.
prCreation: "approval",
# A PR closed after a merge or by hand must not stop a later update of the same package.
recreateWhen: "always",
rebaseWhen: "behind-base-branch",
semanticCommits: "enabled",
semanticCommitType: "chore",
semanticCommitScope: "deps",
minimumReleaseAge: $age,
prConcurrentLimit: 0,
prHourlyLimit: 0,
branchConcurrentLimit: 0,
# A Go major update changes the import path: rewrite the imports, so the build tests the real update.
postUpdateOptions: ((if $go_tidy then ["gomodTidy"] else [] end) + ["gomodUpdateImportPaths"]),
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},
# The parent module chooses the major version of an indirect dependency.
{matchManagers: ["gomod"], matchDepTypes: ["indirect"], allowedVersions: "<{{add major 1}}.0.0"},
{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)
)
}
| if $ENV.NPMRC != "" then .npmrc = $ENV.NPMRC else . end' > "$CONFIG_FILE"
jq 'if has("npmrc") then .npmrc = "<redacted>" else . end' "$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: ${{ inputs.log-level }}
- 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
issues: 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: Find the default branch
id: default
run: echo "branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)" >> "$GITHUB_OUTPUT"
# Not the triggering ref: a push or a manual run from another branch must still test the default branch.
- name: Checkout the default branch
uses: actions/checkout@v6
with:
ref: ${{ steps.default.outputs.branch }}
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 }}
NODE_WORKSPACE: ${{ inputs.node-workspace }}
NODE_PACKAGES: ${{ inputs.node-packages }}
USE_GO: ${{ inputs.go }}
USE_NODE: ${{ inputs.node }}
run: |
go=false
node=false
pm=npm
if [ "$USE_GO" = true ] && [ -f "$GO_DIR/go.mod" ]; then go=true; fi
if [ "$USE_NODE" = true ] && [ "$NODE_WORKSPACE" = true ]; then
if [ ! -f "$NODE_DIR/pnpm-workspace.yaml" ] || [ ! -f "$NODE_DIR/pnpm-lock.yaml" ]; then
echo "::error::node-workspace needs pnpm-workspace.yaml and pnpm-lock.yaml in $NODE_DIR."
exit 1
fi
IFS=',' read -r -a packages <<<"$NODE_PACKAGES"
count=0
for package in "${packages[@]}"; do
package=$(echo "$package" | xargs)
[ -n "$package" ] || continue
if [ ! -f "$NODE_DIR/$package/package.json" ]; then
echo "::error::node-packages lists $package, but $NODE_DIR/$package/package.json does not exist."
exit 1
fi
count=$((count + 1))
done
if [ "$count" -eq 0 ]; then
echo "::error::node-workspace needs node-packages: the packages to update and test."
exit 1
fi
node=true
pm=pnpm
elif [ "$USE_NODE" = true ] && [ -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: Prepare Go
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.go == 'true' && inputs.go-prepare-command != ''
working-directory: ${{ inputs.go-working-directory }}
env:
GO_PREPARE_COMMAND: ${{ inputs.go-prepare-command }}
run: |
set -o pipefail
bash -c "$GO_PREPARE_COMMAND" 2>&1 | tee -a "$RUNNER_TEMP/test-output.log"
- name: Build and test Go
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.go == 'true'
working-directory: ${{ inputs.go-working-directory }}
env:
GO_TEST_COMMAND: ${{ inputs.go-test-command }}
run: |
set -o pipefail
{
go mod verify
if [ -n "$GO_TEST_COMMAND" ]; then
bash -c "$GO_TEST_COMMAND"
else
go build ./...
go test -race -cover ./...
fi
} 2>&1 | tee -a "$RUNNER_TEMP/test-output.log"
- 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 }}
# The npmrc lives in RUNNER_TEMP, not the checkout, so no git command can pick the token up.
- name: Configure the private npm registry
if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true'
env:
NPM_SCOPE: ${{ inputs.npm-registry-scope }}
NPM_URL: ${{ inputs.npm-registry-url }}
NPM_TOKEN: ${{ secrets.npm-registry-token }}
run: |
[ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ] || exit 0
echo "::add-mask::$NPM_TOKEN"
url="${NPM_URL%/}/"
npmrc="$RUNNER_TEMP/private-registry.npmrc"
(umask 077 && printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN" > "$npmrc")
echo "NPM_CONFIG_USERCONFIG=$npmrc" >> "$GITHUB_ENV"
echo "Using a private npm registry for $NPM_SCOPE."
- 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 }}
PNPM_VERSION: ${{ inputs.pnpm-version }}
run: |
set -o pipefail
{
# 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
if [ "$PM" = pnpm ] && [ -n "$PNPM_VERSION" ]; then
corepack prepare "pnpm@$PNPM_VERSION" --activate
fi
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
} 2>&1 | tee -a "$RUNNER_TEMP/test-output.log"
- 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 }}
NODE_WORKSPACE: ${{ inputs.node-workspace }}
NODE_PACKAGES: ${{ inputs.node-packages }}
run: |
set -o pipefail
log="$RUNNER_TEMP/test-output.log"
# A single project is the one package ".". A workspace is each listed package.
packages=(.)
if [ "$NODE_WORKSPACE" = true ]; then
IFS=',' read -r -a packages <<<"$NODE_PACKAGES"
fi
if [ "$REQUIRE_TESTS" = true ]; then
for package in "${packages[@]}"; do
package=$(echo "$package" | xargs)
[ -n "$package" ] || continue
if ! jq -e '.scripts.test' "$package/package.json" >/dev/null; then
echo "::error::$package/package.json has no test script. Add one, or set require-node-tests to false."
echo "$package/package.json has no test script, so the update was not tested." >>"$log"
exit 1
fi
done
fi
IFS=',' read -r -a scripts <<<"$SCRIPTS"
for script in "${scripts[@]}"; do
script=$(echo "$script" | xargs)
[ -n "$script" ] || continue
for package in "${packages[@]}"; do
package=$(echo "$package" | xargs)
[ -n "$package" ] || continue
where=""
[ "$package" = . ] || where=" ($package)"
if jq -e --arg s "$script" '.scripts[$s]' "$package/package.json" >/dev/null; then
echo "::group::$PM run $script$where"
(cd "$package" && "$PM" run "$script") 2>&1 | tee -a "$log"
echo "::endgroup::"
else
echo "::notice::No '$script' script$where, skipping."
fi
done
done
- name: Mark the tests as passed
if: steps.merge.outputs.skip != 'true'
run: touch "$RUNNER_TEMP/tests-passed"
- 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
if [ "$state" = CONFLICTING ]; then
gh pr close "$pr" --repo "$GITHUB_REPOSITORY" \
--comment "The tests passed, but the branch conflicts with $default. Renovate rebases it on the next run."
echo "::warning::$BRANCH conflicts with $default, so PR #$pr was closed. Renovate rebases it on the next run."
echo "\`$BRANCH\` passed its tests but conflicts with \`$default\`. Renovate rebases it on the next run." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# --match-head-commit refuses to merge anything newer than the commit that was tested.
# GitHub answers 5xx now and then; a failed call may still have merged, so check the state before retrying.
merged=false
for _ in 1 2 3; do
if 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}; then
merged=true
break
fi
sleep 10
[ "$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json state --jq .state)" = MERGED ] && { merged=true; break; }
done
if [ "$merged" != true ]; then
gh pr close "$pr" --repo "$GITHUB_REPOSITORY" --comment "The tests passed, but GitHub refused the merge. See the workflow run. The next run tries again."
exit 1
fi
echo "Merged #$pr ($BRANCH)." >> "$GITHUB_STEP_SUMMARY"
- name: Report the failure
if: failure()
# Best effort: the summary is always written, the issue needs issues: write.
continue-on-error: true
env:
HEAD_SHA: ${{ steps.merge.outputs.head }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
if [ -e "$RUNNER_TEMP/tests-passed" ]; then
echo "The tests passed for \`$BRANCH\`, but the merge failed. See the log above. The next run tries again." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "The tests failed for \`$BRANCH\`, so no PR was opened. Renovate retries on the next run." >> "$GITHUB_STEP_SUMMARY"
log="$RUNNER_TEMP/test-output.log"
esc=$(printf '\033')
marker="<!-- renovate-autoupdate:$BRANCH -->"
body="$RUNNER_TEMP/failure-comment.md"
{
echo "$marker"
echo "### \`$BRANCH\` failed its tests"
echo
echo "Commit \`${HEAD_SHA:0:8}\`, [workflow run]($RUN_URL), $(date -u '+%Y-%m-%d %H:%M') UTC. No PR was opened."
echo
echo "<details open><summary>Test output, last lines</summary>"
echo
echo '`````'
if [ -s "$log" ]; then
sed -E "s/${esc}\[[0-9;]*[A-Za-z]//g" "$log" | tail -c 50000
else
echo "No output was captured. See the workflow run."
fi
echo '`````'
echo
echo "</details>"
} > "$body"
issue=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$ISSUE_TITLE\" in:title" --json number,title \
--jq "[.[] | select(.title == \"$ISSUE_TITLE\")][0].number // empty")
if [ -z "$issue" ]; then
url=$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$ISSUE_TITLE" \
--body "Updates that failed their tests, so no PR was opened. Each comment holds the output for one branch. Fix the cause on the branch or in the code. The workflow retries on every run, merges an update once it passes, and closes this issue when none fail.")
issue=${url##*/}
fi
id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$issue/comments" --paginate \
--jq ".[] | select(.body | contains(\"$marker\")) | .id" | head -n 1)
if [ -n "$id" ]; then
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@"$body" >/dev/null
else
gh issue comment "$issue" --repo "$GITHUB_REPOSITORY" --body-file "$body"
fi
tidy-failures:
name: Tidy the failure issue
needs: [renovate, test-and-merge]
if: always() && needs.renovate.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:
issues: write
contents: read
steps:
- name: Remove fixed updates and close the issue when none fail
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
issue=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$ISSUE_TITLE\" in:title" --json number,title \
--jq "[.[] | select(.title == \"$ISSUE_TITLE\")][0].number // empty")
[ -n "$issue" ] || exit 0
branches=$(gh api --paginate "repos/$GITHUB_REPOSITORY/branches" --jq '.[].name')
remaining=0
while IFS=$'\t' read -r id branch; do
[ -n "$id" ] || continue
if printf '%s\n' "$branches" | grep -Fxq "$branch"; then
remaining=$((remaining + 1))
else
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/comments/$id"
fi
done < <(gh api "repos/$GITHUB_REPOSITORY/issues/$issue/comments" --paginate \
--jq '.[] | select(.body | startswith("<!-- renovate-autoupdate:")) | [.id, (.body | split("\n")[0] | sub("^<!-- renovate-autoupdate:"; "") | sub(" -->$"; ""))] | @tsv')
if [ "$remaining" -eq 0 ]; then
gh issue close "$issue" --repo "$GITHUB_REPOSITORY" --comment "Every dependency update now passes its tests."
fi
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
IFS=',' read -r -a workflows <<<"$RELEASE_WORKFLOW"
for workflow in "${workflows[@]}"; do
workflow=$(echo "$workflow" | xargs)
[ -n "$workflow" ] || continue
gh workflow run "$workflow" --repo "$GITHUB_REPOSITORY" --ref "$branch"
done