mirror of
https://github.com/lukaszraczylo/shared-actions.git
synced 2026-11-03 07:44:04 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97b4c0ec7e | ||
|
|
413f9c3064 | ||
|
|
fe70d4c707 | ||
|
|
ac32cb03c4 | ||
|
|
a6a9c55bdf | ||
|
|
9a042ecafd | ||
|
|
5390791174 | ||
|
|
4f53b0593e | ||
|
|
c9823f320e | ||
|
|
5bc07d18ae | ||
|
|
9533a6518f | ||
|
|
f9b22ef8d9 | ||
|
|
6b47cbd317 | ||
|
|
cce6b9d88c | ||
|
|
5cf1402555 | ||
|
|
72ef4441eb |
@@ -20,6 +20,10 @@ inputs:
|
||||
description: "GoReleaser Pro license key (for split/merge)"
|
||||
required: false
|
||||
default: ""
|
||||
timeout:
|
||||
description: "GoReleaser timeout for the whole step (Go duration, e.g. 60m). The continue command defaults to 30m, which cuts off slow multi-architecture docker builds."
|
||||
required: false
|
||||
default: "60m"
|
||||
homebrew-tap-token:
|
||||
description: "Homebrew tap token (optional)"
|
||||
required: false
|
||||
@@ -59,7 +63,7 @@ runs:
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
args: release --clean --timeout ${{ inputs.timeout }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
HOMEBREW_TAP_TOKEN: ${{ inputs.homebrew-tap-token }}
|
||||
@@ -71,7 +75,7 @@ runs:
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: "~> v2"
|
||||
args: release --clean --split
|
||||
args: release --clean --split --timeout ${{ inputs.timeout }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
GORELEASER_KEY: ${{ inputs.goreleaser-key }}
|
||||
@@ -83,7 +87,7 @@ runs:
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: "~> v2"
|
||||
args: continue --merge
|
||||
args: continue --merge --timeout ${{ inputs.timeout }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
GORELEASER_KEY: ${{ inputs.goreleaser-key }}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
name: "Preflight"
|
||||
description: "Fail early when a required secret, env var or repository access is missing"
|
||||
|
||||
inputs:
|
||||
require:
|
||||
description: "Comma-separated names that must be set and non-empty in the step env"
|
||||
required: false
|
||||
default: ""
|
||||
goreleaser-scan:
|
||||
description: "Also require every .Env.NAME referenced by the GoReleaser config"
|
||||
required: false
|
||||
default: "true"
|
||||
repo-access:
|
||||
description: "Comma-separated owner/repo list the token must have push access to"
|
||||
required: false
|
||||
default: ""
|
||||
repo-token-env:
|
||||
description: "Name of the env var that holds the token for repo-access"
|
||||
required: false
|
||||
default: "HOMEBREW_TAP_TOKEN"
|
||||
|
||||
release-write:
|
||||
description: "Probe that GITHUB_TOKEN can push to this repository (needed to create tags and releases)"
|
||||
required: false
|
||||
default: "false"
|
||||
ghcr-push:
|
||||
description: "Probe push access to ghcr.io: true, false, or auto (only when the GoReleaser config builds ghcr.io images)"
|
||||
required: false
|
||||
default: "false"
|
||||
ghcr-image:
|
||||
description: "Image path (owner/name) for the ghcr probe. Default: first ghcr.io path in the GoReleaser config, else the repository"
|
||||
required: false
|
||||
default: ""
|
||||
ghcr-token-env:
|
||||
description: "Name of the env var that holds the ghcr token"
|
||||
required: false
|
||||
default: "GITHUB_TOKEN"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Check prerequisites
|
||||
shell: bash
|
||||
env:
|
||||
PF_REQUIRE: ${{ inputs.require }}
|
||||
PF_SCAN: ${{ inputs.goreleaser-scan }}
|
||||
PF_REPOS: ${{ inputs.repo-access }}
|
||||
PF_TOKEN_ENV: ${{ inputs.repo-token-env }}
|
||||
PF_RELEASE_WRITE: ${{ inputs.release-write }}
|
||||
PF_GHCR: ${{ inputs.ghcr-push }}
|
||||
PF_GHCR_IMAGE: ${{ inputs.ghcr-image }}
|
||||
PF_GHCR_TOKEN_ENV: ${{ inputs.ghcr-token-env }}
|
||||
run: |
|
||||
missing=()
|
||||
add_missing() { # name reason
|
||||
case " ${missing[*]-} " in *" $1 "*) return ;; esac
|
||||
missing+=("$1")
|
||||
echo "::error title=Missing $1::$2"
|
||||
}
|
||||
is_set() { [ -n "${!1:-}" ]; }
|
||||
|
||||
IFS=',' read -r -a required <<< "$PF_REQUIRE"
|
||||
for name in "${required[@]-}"; do
|
||||
name="${name// /}"
|
||||
[ -z "$name" ] && continue
|
||||
is_set "$name" || add_missing "$name" "Required by this workflow but empty. Set the repository or organisation secret $name and make sure the caller passes 'secrets: inherit'."
|
||||
done
|
||||
|
||||
if [ "$PF_SCAN" = "true" ]; then
|
||||
for cfg in .goreleaser.yaml .goreleaser.yml goreleaser.yaml goreleaser.yml; do
|
||||
[ -f "$cfg" ] || continue
|
||||
echo "Scanning $cfg for .Env references"
|
||||
while IFS= read -r ref; do
|
||||
name="${ref#.Env.}"
|
||||
is_set "$name" || add_missing "$name" "$cfg references .Env.$name but it is not available to the GoReleaser step. GoReleaser would fail at the end of the run."
|
||||
done < <(grep -vE '^[[:space:]]*#' "$cfg" | grep -oE '\.Env\.[A-Za-z_][A-Za-z0-9_]*' | sort -u)
|
||||
break
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$PF_REPOS" ]; then
|
||||
token="${!PF_TOKEN_ENV:-}"
|
||||
if [ -z "$token" ]; then
|
||||
add_missing "$PF_TOKEN_ENV" "repo-access needs this token but it is empty."
|
||||
else
|
||||
IFS=',' read -r -a repos <<< "$PF_REPOS"
|
||||
for repo in "${repos[@]}"; do
|
||||
repo="${repo// /}"
|
||||
[ -z "$repo" ] && continue
|
||||
body="$(mktemp)"
|
||||
code="$(curl -sS -o "$body" -w '%{http_code}' \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"${GITHUB_API_URL:-https://api.github.com}/repos/$repo")" || code="000"
|
||||
if [ "$code" != "200" ]; then
|
||||
add_missing "access:$repo" "$PF_TOKEN_ENV cannot read $repo (HTTP $code). Check the token scope, expiry and repository access."
|
||||
elif ! grep -qE '"push": *true' "$body"; then
|
||||
add_missing "access:$repo" "$PF_TOKEN_ENV can read $repo but has no push access."
|
||||
fi
|
||||
rm -f "$body"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$PF_RELEASE_WRITE" = "true" ]; then
|
||||
out="$(git push --dry-run origin HEAD:refs/heads/preflight-probe 2>&1)" && rc=0 || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if printf '%s' "$out" | grep -qiE 'denied|403|401|not permitted|could not read Username'; then
|
||||
add_missing "contents:write" "The token cannot push to this repository, so it cannot create the tag or release. Grant 'contents: write' in the caller workflow permissions and check the repository Actions settings. Git said: $(printf '%s' "$out" | tail -n 1)"
|
||||
else
|
||||
echo "::warning title=Push probe inconclusive::$(printf '%s' "$out" | tail -n 1)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
ghcr_wanted="$PF_GHCR"
|
||||
cfg_found=""
|
||||
for cfg in .goreleaser.yaml .goreleaser.yml goreleaser.yaml goreleaser.yml; do
|
||||
[ -f "$cfg" ] && { cfg_found="$cfg"; break; }
|
||||
done
|
||||
if [ "$ghcr_wanted" = "auto" ]; then
|
||||
ghcr_wanted=false
|
||||
if [ -n "$cfg_found" ] && grep -qE '^(dockers|dockers_v2|docker_manifests):' "$cfg_found" && grep -q 'ghcr\.io' "$cfg_found"; then
|
||||
ghcr_wanted=true
|
||||
fi
|
||||
fi
|
||||
if [ "$ghcr_wanted" = "true" ]; then
|
||||
gtoken="${!PF_GHCR_TOKEN_ENV:-}"
|
||||
image="$PF_GHCR_IMAGE"
|
||||
if [ -z "$image" ] && [ -n "$cfg_found" ]; then
|
||||
image="$(grep -oE 'ghcr\.io/[A-Za-z0-9._/-]+' "$cfg_found" | head -n 1 | sed 's|^ghcr\.io/||')"
|
||||
fi
|
||||
image="${image:-$GITHUB_REPOSITORY}"
|
||||
image="$(printf '%s' "$image" | tr '[:upper:]' '[:lower:]')"
|
||||
if [ -z "$gtoken" ]; then
|
||||
add_missing "$PF_GHCR_TOKEN_ENV" "ghcr push needs this token but it is empty."
|
||||
else
|
||||
tbody="$(mktemp)"
|
||||
code="$(curl -sS -o "$tbody" -w '%{http_code}' -u "${GITHUB_ACTOR:-token}:$gtoken" \
|
||||
"https://ghcr.io/token?service=ghcr.io&scope=repository:$image:push,pull")" || code="000"
|
||||
btoken="$(sed -n 's/.*"token" *: *"\([^"]*\)".*/\1/p' "$tbody" | head -n 1)"
|
||||
rm -f "$tbody"
|
||||
if [ "$code" = "200" ] && [ -n "$btoken" ]; then
|
||||
ucode="$(curl -sS -o /dev/null -w '%{http_code}' -X POST -H "Authorization: Bearer $btoken" \
|
||||
"https://ghcr.io/v2/$image/blobs/uploads/")" || ucode="000"
|
||||
case "$ucode" in
|
||||
202) ;;
|
||||
401|403) add_missing "packages:write" "The token cannot push ghcr.io/$image (HTTP $ucode). Grant 'packages: write' in the caller workflow permissions, and for an existing package allow this repository under Package settings, Manage Actions access." ;;
|
||||
*) echo "::warning title=ghcr probe inconclusive::upload session for $image returned HTTP $ucode" ;;
|
||||
esac
|
||||
elif [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
||||
add_missing "packages:write" "ghcr.io refused the token for $image (HTTP $code). Grant 'packages: write' in the caller workflow permissions."
|
||||
else
|
||||
echo "::warning title=ghcr probe inconclusive::token request for $image returned HTTP $code"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${#missing[@]}" -gt 0 ]; then
|
||||
echo "Preflight failed: ${missing[*]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Preflight passed"
|
||||
@@ -19,11 +19,22 @@ on:
|
||||
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: autoupdate-${{ github.repository }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
autoupdate:
|
||||
name: Update dependencies and test
|
||||
runs-on: ubuntu-latest
|
||||
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: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -43,12 +54,26 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- name: Update dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
go get -u ./...
|
||||
# Indirect modules can move ahead of their parents (for example
|
||||
# kube-openapi ahead of apimachinery), which breaks the build.
|
||||
update_all() { go get -u ./... && go mod tidy && go build ./...; }
|
||||
update_direct() {
|
||||
local mods=()
|
||||
while IFS= read -r m; do mods+=("$m"); done < <(go list -m -f '{{if and (not .Indirect) (not .Main)}}{{.Path}}@latest{{end}}' all)
|
||||
if [ "${#mods[@]}" -gt 0 ]; then go get "${mods[@]}"; fi
|
||||
go mod tidy
|
||||
}
|
||||
if ! update_all; then
|
||||
echo "::warning title=Full update failed::Falling back to direct dependencies only"
|
||||
git checkout -- go.mod go.sum
|
||||
update_direct
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: go test -race -cover ./...
|
||||
@@ -67,7 +92,7 @@ jobs:
|
||||
if: steps.changes.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN || github.token }}
|
||||
commit-message: "Update go.mod and go.sum"
|
||||
title: "Update Go dependencies"
|
||||
body: |
|
||||
@@ -82,4 +107,4 @@ jobs:
|
||||
if: steps.create-pr.outputs.pull-request-number
|
||||
run: gh pr merge --squash --delete-branch --admin "${{ steps.create-pr.outputs.pull-request-number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN || github.token }}
|
||||
|
||||
+133
-59
@@ -24,16 +24,58 @@ on:
|
||||
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: ""
|
||||
|
||||
# Caller must declare these permissions:
|
||||
# permissions:
|
||||
# contents: read
|
||||
# pull-requests: write
|
||||
# security-events: write
|
||||
|
||||
concurrency:
|
||||
group: go-pr-${{ github.repository }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pre:
|
||||
name: Detect duplicate run
|
||||
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: 5
|
||||
outputs:
|
||||
skip: ${{ steps.dup.outputs.skip }}
|
||||
steps:
|
||||
# A push to a branch with an open PR also fires pull_request; run only the PR one.
|
||||
- name: Skip push run when a PR is open
|
||||
id: dup
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
let skip = false;
|
||||
if (context.eventName === 'push') {
|
||||
try {
|
||||
const { data } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}`,
|
||||
state: 'open',
|
||||
});
|
||||
skip = data.length > 0;
|
||||
} catch (e) {
|
||||
core.warning(`PR lookup failed, running checks: ${e.message}`);
|
||||
}
|
||||
}
|
||||
core.setOutput('skip', String(skip));
|
||||
if (skip) core.notice('Open PR found for this branch; checks run on the pull_request event.');
|
||||
static-analysis:
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
needs: pre
|
||||
if: needs.pre.outputs.skip != 'true'
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -58,6 +100,14 @@ jobs:
|
||||
- name: Install staticcheck
|
||||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
|
||||
- name: Register problem matcher
|
||||
shell: bash
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/go-problem-matcher.json" <<'JSON'
|
||||
{"problemMatcher":[{"owner":"go-lint","severity":"error","pattern":[{"regexp":"^(?:vet: )?([^\\s:]+\\.go):(\\d+):(\\d+): (.+)$","file":1,"line":2,"column":3,"message":4}]}]}
|
||||
JSON
|
||||
echo "::add-matcher::$RUNNER_TEMP/go-problem-matcher.json"
|
||||
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
@@ -66,7 +116,10 @@ jobs:
|
||||
|
||||
security-scan:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
needs: pre
|
||||
if: needs.pre.outputs.skip != 'true'
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -123,7 +176,10 @@ jobs:
|
||||
|
||||
gosec:
|
||||
name: Gosec SARIF
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
needs: pre
|
||||
if: needs.pre.outputs.skip != 'true'
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
@@ -164,7 +220,10 @@ jobs:
|
||||
|
||||
goreleaser-check:
|
||||
name: GoReleaser Config Validation
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
needs: pre
|
||||
if: needs.pre.outputs.skip != 'true'
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -196,7 +255,10 @@ jobs:
|
||||
|
||||
codeql:
|
||||
name: CodeQL Analysis
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
needs: pre
|
||||
if: needs.pre.outputs.skip != 'true'
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
@@ -233,7 +295,10 @@ jobs:
|
||||
|
||||
test:
|
||||
name: Tests & Coverage
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
needs: pre
|
||||
if: needs.pre.outputs.skip != 'true'
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
coverage: ${{ steps.coverage.outputs.coverage }}
|
||||
steps:
|
||||
@@ -272,70 +337,79 @@ jobs:
|
||||
- name: Calculate coverage
|
||||
id: coverage
|
||||
run: |
|
||||
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||
COVERAGE=$(go tool cover -func=coverage.out | awk '/^total:/ {print $3}' | sed 's/%//')
|
||||
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
|
||||
echo "Total Coverage: $COVERAGE%"
|
||||
|
||||
coverage-report:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: always() && needs.test.result == 'success'
|
||||
report:
|
||||
name: PR Checks Report
|
||||
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')) }}
|
||||
needs: [pre, static-analysis, security-scan, gosec, goreleaser-check, codeql, test]
|
||||
if: ${{ !cancelled() && needs.pre.outputs.skip != 'true' }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Comment coverage on PR
|
||||
if: github.event_name == 'pull_request'
|
||||
- name: Summarise, comment and gate
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
NEEDS: ${{ toJSON(needs) }}
|
||||
THRESHOLD: ${{ inputs.coverage-threshold }}
|
||||
with:
|
||||
script: |
|
||||
const coverage = '${{ needs.test.outputs.coverage }}';
|
||||
const threshold = ${{ inputs.coverage-threshold }};
|
||||
const coverageNum = parseFloat(coverage);
|
||||
const hasThreshold = threshold > 0;
|
||||
const meetsThreshold = !hasThreshold || coverageNum >= threshold;
|
||||
const emoji = meetsThreshold ? '✅' : '⚠️';
|
||||
const status = hasThreshold ? (meetsThreshold ? 'meets' : 'below') + ' threshold' : 'reported';
|
||||
|
||||
let body = `## ${emoji} Test Coverage Report\n\n| Metric | Value |\n|--------|-------|\n| **Total Coverage** | ${coverage}% |`;
|
||||
if (hasThreshold) {
|
||||
body += `\n| **Threshold** | ${threshold}% |\n| **Status** | ${emoji} Coverage ${status} |`;
|
||||
const needs = JSON.parse(process.env.NEEDS);
|
||||
const threshold = Number(process.env.THRESHOLD);
|
||||
const labels = {
|
||||
'static-analysis': 'Static analysis (vet, staticcheck)',
|
||||
'security-scan': 'Security scan (TruffleHog, govulncheck)',
|
||||
'gosec': 'Gosec',
|
||||
'goreleaser-check': 'GoReleaser config',
|
||||
'codeql': 'CodeQL',
|
||||
'test': 'Tests (race) & coverage',
|
||||
};
|
||||
const icon = { success: '✅', failure: '❌', cancelled: '🚫', skipped: '⏭️' };
|
||||
const failed = [];
|
||||
let body = '<!-- go-pr-report -->\n## PR checks\n\n| Check | Result |\n|---|---|\n';
|
||||
for (const [key, label] of Object.entries(labels)) {
|
||||
const result = needs[key]?.result ?? 'skipped';
|
||||
if (result === 'failure' || result === 'cancelled') failed.push(label);
|
||||
body += `| ${label} | ${icon[result] ?? result} ${result} |\n`;
|
||||
}
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
const coverage = parseFloat(needs.test?.outputs?.coverage);
|
||||
const hasCoverage = !Number.isNaN(coverage);
|
||||
if (hasCoverage) {
|
||||
body += `\n**Total coverage:** ${coverage}%`;
|
||||
if (threshold > 0) {
|
||||
const ok = coverage >= threshold;
|
||||
body += ` (threshold ${threshold}%: ${ok ? '✅ met' : '❌ below'})`;
|
||||
if (!ok) failed.push(`Coverage ${coverage}% below ${threshold}%`);
|
||||
}
|
||||
body += '\n';
|
||||
}
|
||||
body += `\n[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
|
||||
|
||||
await core.summary.addRaw(body, true).write();
|
||||
|
||||
if (context.eventName === 'pull_request') {
|
||||
try {
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('Test Coverage Report')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
comment_id: botComment.id,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: body
|
||||
});
|
||||
const existing = comments.find(c => c.user.type === 'Bot' &&
|
||||
(c.body.includes('<!-- go-pr-report -->') || c.body.includes('Test Coverage Report')));
|
||||
const args = { owner: context.repo.owner, repo: context.repo.repo, body };
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ ...args, comment_id: existing.id });
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: body
|
||||
});
|
||||
await github.rest.issues.createComment({ ...args, issue_number: context.issue.number });
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Could not post PR comment (fork PR or missing pull-requests: write): ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
- name: Check coverage threshold
|
||||
if: inputs.coverage-threshold > 0
|
||||
run: |
|
||||
COVERAGE=${{ needs.test.outputs.coverage }}
|
||||
THRESHOLD=${{ inputs.coverage-threshold }}
|
||||
echo "Coverage: $COVERAGE%"
|
||||
echo "Threshold: $THRESHOLD%"
|
||||
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
|
||||
echo "❌ Coverage $COVERAGE% is below threshold $THRESHOLD%"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Coverage $COVERAGE% meets threshold $THRESHOLD%"
|
||||
if (failed.length) core.setFailed(`Failed: ${failed.join('; ')}`);
|
||||
|
||||
@@ -60,6 +60,26 @@ on:
|
||||
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: ""
|
||||
preflight:
|
||||
description: "Fail early when a required secret, env var or GoReleaser .Env reference is missing"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
preflight-repos:
|
||||
description: "Comma-separated owner/repo list the token must have push access to (e.g. a Homebrew tap or helm-charts repo)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
release-timeout:
|
||||
description: "GoReleaser timeout for the release step (Go duration)"
|
||||
required: false
|
||||
type: string
|
||||
default: "60m"
|
||||
outputs:
|
||||
version:
|
||||
description: "The calculated version (without v prefix)"
|
||||
@@ -68,10 +88,43 @@ on:
|
||||
description: "The version tag (with v prefix)"
|
||||
value: ${{ jobs.version.outputs.version_tag }}
|
||||
|
||||
concurrency:
|
||||
group: release-cgo-${{ github.repository }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
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: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: inputs.preflight
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Check secrets and access
|
||||
if: inputs.preflight
|
||||
uses: lukaszraczylo/shared-actions/.github/actions/preflight@main
|
||||
with:
|
||||
require: GITHUB_TOKEN,GORELEASER_KEY
|
||||
release-write: "true"
|
||||
ghcr-push: auto
|
||||
repo-access: ${{ inputs.preflight-repos }}
|
||||
repo-token-env: GITHUB_TOKEN
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_PRO }}
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: preflight
|
||||
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: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -95,8 +148,10 @@ jobs:
|
||||
|
||||
frontend:
|
||||
name: Build Frontend
|
||||
needs: preflight
|
||||
if: inputs.node-enabled && inputs.node-build-script != ''
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -132,10 +187,11 @@ jobs:
|
||||
version:
|
||||
name: Calculate Version
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
outputs:
|
||||
version: ${{ steps.semver.outputs.version }}
|
||||
version_tag: ${{ steps.semver.outputs.version_tag }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -170,6 +226,7 @@ jobs:
|
||||
matrix:
|
||||
include: ${{ fromJson(inputs.platforms) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -223,6 +280,7 @@ jobs:
|
||||
uses: lukaszraczylo/shared-actions/.github/actions/goreleaser@main
|
||||
with:
|
||||
version-tag: ${{ needs.version.outputs.version_tag }}
|
||||
timeout: ${{ inputs.release-timeout }}
|
||||
mode: split
|
||||
cgo-enabled: "1"
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -242,11 +300,12 @@ jobs:
|
||||
always() &&
|
||||
needs.version.result == 'success' &&
|
||||
needs.build.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -301,6 +360,7 @@ jobs:
|
||||
uses: lukaszraczylo/shared-actions/.github/actions/goreleaser@main
|
||||
with:
|
||||
version-tag: ${{ needs.version.outputs.version_tag }}
|
||||
timeout: ${{ inputs.release-timeout }}
|
||||
mode: merge
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goreleaser-key: ${{ secrets.GORELEASER_PRO }}
|
||||
|
||||
@@ -18,6 +18,11 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
qemu-image:
|
||||
description: "binfmt image that registers QEMU for multi-arch Docker builds"
|
||||
required: false
|
||||
type: string
|
||||
default: "tonistiigi/binfmt:qemu-v8.1.5"
|
||||
docker-registry:
|
||||
description: "Docker registry to push to"
|
||||
required: false
|
||||
@@ -33,6 +38,26 @@ on:
|
||||
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: ""
|
||||
preflight:
|
||||
description: "Fail early when a required secret, env var or GoReleaser .Env reference is missing"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
preflight-repos:
|
||||
description: "Comma-separated owner/repo list the token must have push access to (e.g. a Homebrew tap or helm-charts repo)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
release-timeout:
|
||||
description: "GoReleaser timeout for the release step (Go duration)"
|
||||
required: false
|
||||
type: string
|
||||
default: "60m"
|
||||
outputs:
|
||||
version:
|
||||
description: "The calculated version (without v prefix)"
|
||||
@@ -41,10 +66,42 @@ on:
|
||||
description: "The version tag (with v prefix)"
|
||||
value: ${{ jobs.version.outputs.version_tag }}
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.repository }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
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: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: inputs.preflight
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Check secrets and access
|
||||
if: inputs.preflight
|
||||
uses: lukaszraczylo/shared-actions/.github/actions/preflight@main
|
||||
with:
|
||||
require: GITHUB_TOKEN
|
||||
release-write: "true"
|
||||
ghcr-push: ${{ inputs.docker-enabled && inputs.docker-registry == 'ghcr.io' && 'true' || 'false' }}
|
||||
repo-access: ${{ inputs.preflight-repos }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: preflight
|
||||
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: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -67,10 +124,11 @@ jobs:
|
||||
version:
|
||||
name: Calculate Version
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
outputs:
|
||||
version: ${{ steps.semver.outputs.version }}
|
||||
version_tag: ${{ steps.semver.outputs.version_tag }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -96,11 +154,12 @@ jobs:
|
||||
name: Release
|
||||
needs: version
|
||||
if: needs.version.outputs.version_tag != ''
|
||||
runs-on: ubuntu-latest
|
||||
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')) }}
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -133,6 +192,8 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
if: inputs.docker-enabled
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
image: ${{ inputs.qemu-image }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: inputs.docker-enabled
|
||||
@@ -150,6 +211,7 @@ jobs:
|
||||
uses: lukaszraczylo/shared-actions/.github/actions/goreleaser@main
|
||||
with:
|
||||
version-tag: ${{ needs.version.outputs.version_tag }}
|
||||
timeout: ${{ inputs.release-timeout }}
|
||||
mode: full
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
homebrew-tap-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
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
|
||||
include-major:
|
||||
description: "Put major updates in the batch. When false, each major update gets its own PR that is never merged automatically."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
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 }}
|
||||
INCLUDE_MAJOR: ${{ inputs.include-major }}
|
||||
CONFIG_FILE: ${{ runner.temp }}/renovate-autoupdate-config.json
|
||||
run: |
|
||||
jq -n \
|
||||
--arg repo "$GITHUB_REPOSITORY" \
|
||||
--arg age "$MIN_AGE" \
|
||||
--arg branch_prefix "${BATCH_BRANCH%%/*}/" \
|
||||
--argjson include_major "$INCLUDE_MAJOR" \
|
||||
'
|
||||
{
|
||||
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 $include_major then {} else {matchUpdateTypes: ["minor", "patch", "pin", "pinDigest", "digest"]} end)),
|
||||
{matchManagers: ["gomod"], matchDepTypes: ["indirect"], enabled: true},
|
||||
{matchPackageNames: ["typescript"], allowedVersions: "<7"}
|
||||
]
|
||||
}' > "$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"
|
||||
@@ -4,6 +4,21 @@ Reusable workflows and composite actions for Go projects.
|
||||
|
||||
## Reusable Workflows
|
||||
|
||||
### Runner selection
|
||||
|
||||
Every reusable workflow accepts `runner` (label, or a JSON object to pick a runner group):
|
||||
|
||||
```yaml
|
||||
with:
|
||||
runner: self-hosted
|
||||
# or
|
||||
runner: '{"group":"ci-linux","labels":["x64"]}'
|
||||
```
|
||||
|
||||
Resolution order: `inputs.runner`, repository/org variable `SHARED_ACTIONS_RUNNER`, `ubuntu-latest`.
|
||||
Set the variable once to move every consumer. In `go-release-cgo.yaml` the build matrix keeps its
|
||||
per-platform `os`; `runner` covers the other jobs. Runner groups exist only for organisations and enterprises.
|
||||
|
||||
### `go-release.yaml`
|
||||
|
||||
Standard Go release workflow using GoReleaser.
|
||||
@@ -57,6 +72,11 @@ jobs:
|
||||
|
||||
Pull request checks: tests, linting, security scans.
|
||||
|
||||
- `go vet` / `staticcheck` findings show as inline annotations on the PR diff; gosec and CodeQL post to code scanning.
|
||||
- The `report` job posts one sticky comment with every check's result and coverage, and fails if any check fails or coverage is below `coverage-threshold`. Require `PR Checks Report` in branch protection.
|
||||
- A push to a branch with an open PR skips the duplicate run; stale runs on the same branch are cancelled.
|
||||
- Fork PRs cannot receive the comment; the report still lands in the job summary.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
pr-checks:
|
||||
@@ -68,7 +88,7 @@ jobs:
|
||||
|
||||
### `go-autoupdate.yaml`
|
||||
|
||||
Automatic dependency updates.
|
||||
Automatic dependency updates for Go. See `renovate-autoupdate.yaml` for the Renovate-based replacement that also covers npm, pnpm and yarn.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
@@ -80,6 +100,75 @@ jobs:
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
### `renovate-autoupdate.yaml`
|
||||
|
||||
Daily dependency updates for Go and npm/pnpm/yarn projects, built on Renovate. It replaces `go-autoupdate.yaml`.
|
||||
|
||||
1. Renovate puts every minor and patch update (direct and indirect Go modules, npm, pnpm and yarn packages) in one PR on the branch `deps-autoupdate/batch`.
|
||||
2. The workflow checks out that branch and runs the tests: `go build`, `go test -race -cover` for Go, and the `package.json` scripts for Node.
|
||||
3. If everything passes, it squash-merges the PR. If a test fails, the PR stays open with a comment and Renovate retries on the next run.
|
||||
|
||||
```yaml
|
||||
name: Update dependencies
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
permissions:
|
||||
actions: write # only to dispatch release-workflow
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/renovate-autoupdate.yaml@main
|
||||
with:
|
||||
go-version: ">=1.24"
|
||||
release-workflow: release.yaml
|
||||
```
|
||||
|
||||
The workflow finds `go.mod` and `package.json` itself and tests what exists. A repo with both, such as a Go backend with a Vue frontend, gets both tested in one run.
|
||||
|
||||
**Inputs:**
|
||||
| Input | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `go-version` | `>=1.24` | Go version |
|
||||
| `go-working-directory` | `.` | Directory that holds `go.mod` |
|
||||
| `node-version` | `22` | Node.js version |
|
||||
| `node-working-directory` | `.` | Directory that holds `package.json` |
|
||||
| `node-scripts` | `lint,typecheck,test,build` | Scripts to run in order. A missing script is skipped. |
|
||||
| `require-node-tests` | `true` | Fail when there is no `test` script, so an untested update never merges |
|
||||
| `include-major` | `false` | Put major updates in the batch. When `false`, each major gets its own PR that is never merged automatically. |
|
||||
| `minimum-release-age` | `2 days` | Skip releases younger than this |
|
||||
| `renovate-version` | `44` | Renovate version |
|
||||
| `commit-subject` | `chore(deps): update dependencies` | Squash commit subject |
|
||||
| `release-workflow` | | Workflow file to dispatch after the merge, for example `release.yaml`. It needs a `workflow_dispatch` trigger. Empty means no dispatch. |
|
||||
| `admin-merge` | `false` | Merge with `--admin`. The workflow token cannot bypass branch protection. |
|
||||
| `lfs` | `false` | Git LFS checkout |
|
||||
| `runner` | | Runner label or runner group, as in the other workflows |
|
||||
|
||||
**Package manager.** pnpm is used when `pnpm-lock.yaml` exists, yarn for `yarn.lock`, otherwise npm. The install step never changes the lockfile (`--frozen-lockfile`, `--immutable`, `npm ci`), so a lockfile that does not match `package.json` fails the run. pnpm and yarn run through Corepack, so set `packageManager` in `package.json`.
|
||||
|
||||
**Per-project rules.** Put a `renovate.json` in the calling repository. Renovate reads it and merges it with the defaults, so no extra input is needed:
|
||||
|
||||
```json
|
||||
{
|
||||
"ignoreDeps": ["some/package"],
|
||||
"packageRules": [{ "matchPackageNames": ["vite"], "enabled": false }]
|
||||
}
|
||||
```
|
||||
|
||||
The defaults already cap `typescript` below 7, which breaks `vue-tsc`.
|
||||
|
||||
**Token.** No secret is needed. Everything runs inside the calling repository with the workflow's own token, which needs `contents: write` and `pull-requests: write`. Two repository settings matter:
|
||||
|
||||
- Settings, Actions, General: tick "Allow GitHub Actions to create and approve pull requests". Without it Renovate cannot open the PR.
|
||||
- A push made with the workflow token does not trigger other workflows, so a release workflow that runs on `push` will not start after the merge. Set `release-workflow` to dispatch it explicitly, and grant `actions: write`.
|
||||
|
||||
**Commit message.** The squash commit uses a fixed subject and body without the words `major`, `minor` or `breaking`, because `semver-generator` matches release keywords in commit messages.
|
||||
|
||||
## Composite Actions
|
||||
|
||||
### `actions/go-test`
|
||||
@@ -134,6 +223,31 @@ Run GoReleaser with mode support.
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
### `actions/preflight`
|
||||
|
||||
Fail in seconds when a prerequisite is missing, instead of at the end of a long run. Reads names from the step `env`, so map secrets there.
|
||||
|
||||
```yaml
|
||||
- uses: lukaszraczylo/shared-actions/.github/actions/preflight@main
|
||||
with:
|
||||
require: GITHUB_TOKEN,NPM_TOKEN # must be non-empty
|
||||
repo-access: lukaszraczylo/helm-charts # token needs push access
|
||||
repo-token-env: HOMEBREW_TAP_TOKEN
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
```
|
||||
|
||||
Optional probes use the real token, so they catch caller `permissions:` that are too narrow:
|
||||
- `release-write: "true"` runs `git push --dry-run`, which fails when the token cannot push (no tag or release possible).
|
||||
- `ghcr-push: true|auto` asks ghcr.io for a push token and opens a blob upload session (no package is created). `auto` probes only when the config has `dockers`/`dockers_v2`/`docker_manifests` and mentions `ghcr.io`. The image is the first `ghcr.io/...` path in the config, else the repository (`ghcr-image` overrides).
|
||||
- A probe that cannot reach a verdict (network error, odd status) warns and does not fail.
|
||||
|
||||
It also scans `.goreleaser.y*ml` and requires every `{{ .Env.NAME }}` (comment lines ignored) to be set. All problems are listed in one run.
|
||||
|
||||
`go-release.yaml` and `go-release-cgo.yaml` run it in a first `preflight` job before tests. Set `preflight: false` to skip it, and `preflight-repos` to check access to a tap or helm-charts repo. An empty secret usually means the caller omitted `secrets: inherit`.
|
||||
|
||||
### `actions/rolling-release`
|
||||
|
||||
Create/update a rolling release tag.
|
||||
|
||||
Reference in New Issue
Block a user