mirror of
https://github.com/lukaszraczylo/shared-actions.git
synced 2026-10-12 04:54:48 +00:00
feat: validate workflows and actions, and prepare Renovate bumps
scripts/check-actions.sh runs actionlint and checks that every external uses: reference resolves. The validate workflow runs it, then runs the go-test and node-build actions and the node-pr workflow against small fixtures, so a bump of an action they use is exercised before it merges. renovate.json bumps the actions this repository uses. Minor and patch bumps merge themselves once validate is green. Major bumps wait for a person, because every consumer calls these actions at @main. The same script runs as a Renovate post-upgrade task and marks a failing branch so no PR opens.
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
name: Validate
|
||||||
|
|
||||||
|
# Checks that a change to the workflows or composite actions still works. Renovate
|
||||||
|
# opens a PR for each action bump; this workflow is the gate those PRs must pass.
|
||||||
|
# The aggregate job "validate" is the one required status check.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".github/**"
|
||||||
|
- "scripts/**"
|
||||||
|
- "testdata/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: validate-${{ github.head_ref || github.ref_name }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
static:
|
||||||
|
name: Static checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# actionlint, then every external `uses:` reference must resolve.
|
||||||
|
- name: Check workflows and action references
|
||||||
|
run: scripts/check-actions.sh --ci
|
||||||
|
|
||||||
|
selftest-go:
|
||||||
|
name: Self-test go-test action
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# The local path runs the action as it is on this branch, not at @main.
|
||||||
|
- name: Run the go-test action on the fixture module
|
||||||
|
uses: ./.github/actions/go-test
|
||||||
|
with:
|
||||||
|
working-directory: testdata/selftest/go
|
||||||
|
|
||||||
|
selftest-node-build:
|
||||||
|
name: Self-test node-build action
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Run the node-build action on the fixture project
|
||||||
|
uses: ./.github/actions/node-build
|
||||||
|
with:
|
||||||
|
build-script: cd testdata/selftest/node && npm ci && npm run build
|
||||||
|
cache-dependency-path: testdata/selftest/node/package-lock.json
|
||||||
|
|
||||||
|
selftest-node-pr:
|
||||||
|
name: Self-test node-pr workflow
|
||||||
|
uses: ./.github/workflows/node-pr.yaml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
with:
|
||||||
|
working-directory: testdata/selftest/node
|
||||||
|
require-tests: true
|
||||||
|
|
||||||
|
validate:
|
||||||
|
name: validate
|
||||||
|
if: always()
|
||||||
|
needs: [static, selftest-go, selftest-node-build, selftest-node-pr]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Require every check to pass
|
||||||
|
env:
|
||||||
|
RESULTS: ${{ join(needs.*.result, ' ') }}
|
||||||
|
run: |
|
||||||
|
echo "results: $RESULTS"
|
||||||
|
for r in $RESULTS; do
|
||||||
|
if [ "$r" != success ]; then
|
||||||
|
echo "::error::a validation job finished as '$r'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"timezone": "Europe/London",
|
||||||
|
"enabledManagers": ["github-actions"],
|
||||||
|
"semanticCommits": "enabled",
|
||||||
|
"semanticCommitType": "chore",
|
||||||
|
"semanticCommitScope": "deps",
|
||||||
|
"dependencyDashboard": false,
|
||||||
|
"prConcurrentLimit": 0,
|
||||||
|
"prHourlyLimit": 0,
|
||||||
|
"minimumReleaseAge": "3 days",
|
||||||
|
"postUpgradeTasks": {
|
||||||
|
"commands": ["scripts/check-actions.sh --renovate"],
|
||||||
|
"fileFilters": [".validation-failed"],
|
||||||
|
"executionMode": "branch"
|
||||||
|
},
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "This repository's own composite actions are tracked at @main by design.",
|
||||||
|
"matchPackageNames": ["lukaszraczylo/shared-actions"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Minor and patch bumps merge on their own once the validate check is green.",
|
||||||
|
"matchUpdateTypes": ["minor", "patch", "digest", "pin"],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeType": "pr",
|
||||||
|
"platformAutomerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Every consumer calls these actions at @main, so a major bump waits for a human.",
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"automerge": false,
|
||||||
|
"addLabels": ["major"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Static checks for the workflows and composite actions in this repository:
|
||||||
|
# 1. actionlint (schema, expressions, and shell steps through shellcheck when present)
|
||||||
|
# 2. every external `uses:` reference resolves to a real tag or branch
|
||||||
|
#
|
||||||
|
# Usage: scripts/check-actions.sh [--ci | --renovate]
|
||||||
|
# --ci exit 1 when a check fails (default)
|
||||||
|
# --renovate exit 0 and write .validation-failed when a check fails, so the
|
||||||
|
# Renovate runner deletes the branch before a PR can open
|
||||||
|
#
|
||||||
|
# Needs: bash, git, curl, tar, sha256sum or shasum. No other tools: actionlint is
|
||||||
|
# downloaded at a pinned version and checked against a pinned SHA-256.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
mode=--ci
|
||||||
|
case "${1:-}" in
|
||||||
|
"" | --ci) mode=--ci ;;
|
||||||
|
--renovate) mode=--renovate ;;
|
||||||
|
*) echo "usage: $0 [--ci | --renovate]" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ACTIONLINT_VERSION=1.7.12
|
||||||
|
ACTIONLINT_SHA256_AMD64=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
||||||
|
ACTIONLINT_SHA256_ARM64=325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6
|
||||||
|
marker=.validation-failed
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)" || exit 2
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
problem() {
|
||||||
|
echo "::error::$*" >&2
|
||||||
|
fail=1
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256_of() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_actionlint() {
|
||||||
|
local os arch want url tmp
|
||||||
|
case "$(uname -s)" in Linux) os=linux ;; Darwin) os=darwin ;; *) return 1 ;; esac
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64 | amd64) arch=amd64; want=$ACTIONLINT_SHA256_AMD64 ;;
|
||||||
|
aarch64 | arm64) arch=arm64; want=$ACTIONLINT_SHA256_ARM64 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
# The pinned checksums are for the linux builds only.
|
||||||
|
[ "$os" = linux ] || return 1
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${os}_${arch}.tar.gz"
|
||||||
|
curl -fsSL --retry 3 -o "$tmp/a.tgz" "$url" || return 1
|
||||||
|
[ "$(sha256_of "$tmp/a.tgz")" = "$want" ] || { echo "actionlint checksum mismatch" >&2; return 1; }
|
||||||
|
tar -xzf "$tmp/a.tgz" -C "$tmp" actionlint || return 1
|
||||||
|
echo "$tmp/actionlint"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_actionlint() {
|
||||||
|
local bin
|
||||||
|
if command -v actionlint >/dev/null 2>&1; then
|
||||||
|
bin=$(command -v actionlint)
|
||||||
|
else
|
||||||
|
bin=$(install_actionlint) || { problem "cannot install actionlint ${ACTIONLINT_VERSION}"; return; }
|
||||||
|
fi
|
||||||
|
# -shellcheck= disables the shellcheck pass when the binary is missing.
|
||||||
|
local opts=()
|
||||||
|
command -v shellcheck >/dev/null 2>&1 || opts+=(-shellcheck=)
|
||||||
|
# Info and style findings from shellcheck (for example SC2086 on intentional word
|
||||||
|
# splitting) do not fail the gate. Warnings and errors do. Composite actions are
|
||||||
|
# not linted by actionlint 1.7; workflows are.
|
||||||
|
opts+=(-ignore 'shellcheck reported issue.+SC[0-9]+:(info|style):')
|
||||||
|
if ! "$bin" "${opts[@]}" -color .github/workflows/*.y*ml; then
|
||||||
|
problem "actionlint reported problems"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolves every `uses: owner/repo[/path]@ref` that points outside this repository.
|
||||||
|
check_refs() {
|
||||||
|
local files uses ref repo path checked=""
|
||||||
|
files=$(find .github -type f \( -name '*.yml' -o -name '*.yaml' \))
|
||||||
|
uses=$(grep -hoE '^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*[^[:space:]#]+' $files | sed -E 's/^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*//' | sort -u)
|
||||||
|
while IFS= read -r u; do
|
||||||
|
[ -n "$u" ] || continue
|
||||||
|
case "$u" in ./* | docker://*) continue ;; esac
|
||||||
|
repo=${u%%@*}
|
||||||
|
ref=${u#*@}
|
||||||
|
[ "$repo" != "$u" ] || { problem "no @ref in 'uses: $u'"; continue; }
|
||||||
|
# owner/repo/sub/path -> owner/repo
|
||||||
|
path=$(echo "$repo" | cut -d/ -f1,2)
|
||||||
|
# This repository's own composite actions are tracked at @main by design.
|
||||||
|
[ "$path" = lukaszraczylo/shared-actions ] && continue
|
||||||
|
case " $checked " in *" $path@$ref "*) continue ;; esac
|
||||||
|
checked="$checked $path@$ref"
|
||||||
|
if ! git ls-remote --exit-code "https://github.com/$path" "refs/tags/$ref" "refs/heads/$ref" >/dev/null 2>&1; then
|
||||||
|
# A full commit SHA is not listed by ls-remote; accept it when the repository answers at all.
|
||||||
|
if echo "$ref" | grep -qE '^[0-9a-f]{40}$' && git ls-remote --exit-code "https://github.com/$path" HEAD >/dev/null 2>&1; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
problem "'uses: $u' does not resolve: no tag or branch $ref in $path"
|
||||||
|
fi
|
||||||
|
done <<<"$uses"
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f "$marker"
|
||||||
|
run_actionlint
|
||||||
|
check_refs
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
if [ "$mode" = --renovate ]; then
|
||||||
|
echo "validation failed; writing $marker" >&2
|
||||||
|
printf 'actions validation failed\n' >"$marker"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "actions validation passed"
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
// Package selftest is a fixture that the validate workflow runs the go-test action against.
|
||||||
|
package selftest
|
||||||
|
|
||||||
|
// Add returns the sum of a and b.
|
||||||
|
func Add(a, b int) int { return a + b }
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package selftest
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
if got := Add(2, 3); got != 5 {
|
||||||
|
t.Fatalf("Add(2, 3) = %d, want 5", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
module example.com/selftest
|
||||||
|
|
||||||
|
go 1.24
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "selftest",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "selftest",
|
||||||
|
"version": "0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "selftest",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"lint": "node -e \"process.exit(0)\"",
|
||||||
|
"test": "node --test",
|
||||||
|
"build": "node -e \"console.log('built')\""
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
|
||||||
|
test('addition', () => {
|
||||||
|
assert.strictEqual(2 + 3, 5);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user