mirror of
https://github.com/lukaszraczylo/shared-actions.git
synced 2026-10-12 04:54:48 +00:00
Co-authored-by: tozuo-home-cluster-eso[bot] <287217661+tozuo-home-cluster-eso[bot]@users.noreply.github.com>
583 lines
26 KiB
YAML
583 lines
26 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"
|
|
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
|
|
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
|
|
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 }}
|
|
PNPM_VERSION: ${{ inputs.pnpm-version }}
|
|
GO_MOD_TIDY: ${{ inputs.go-mod-tidy }}
|
|
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" \
|
|
--argjson use_go "$USE_GO" \
|
|
--argjson use_node "$USE_NODE" \
|
|
--arg go_dir "$GO_DIR" \
|
|
--arg node_dir "$NODE_DIR" \
|
|
--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("\\."; "\\.");
|
|
{
|
|
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: ["/^" + prefix($node_dir) + "package\\.json$/", "/^" + 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)
|
|
)
|
|
}' > "$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: ${{ 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 }}
|
|
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 ] && [ -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@v7
|
|
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 }}
|
|
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 }}
|
|
run: |
|
|
set -o pipefail
|
|
log="$RUNNER_TEMP/test-output.log"
|
|
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."
|
|
echo "package.json has no test script, so the update was not tested." >>"$log"
|
|
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" 2>&1 | tee -a "$log"
|
|
echo "::endgroup::"
|
|
else
|
|
echo "::notice::No '$script' script, skipping."
|
|
fi
|
|
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.
|
|
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
|
|
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
|