go, gradle, lint: small shared workflows #10
No files matched your search
@@ -0,0 +1,190 @@
|
||||
# Shared Go CI: run the standard Go gates in the official golang:<version>-bookworm image.
|
||||
#
|
||||
# any push -> go mod verify, gofmt, go vet, golangci-lint (optional), go test, commands
|
||||
# tag v* -> + release-commands, then the artifacts files and a SHA256SUMS on the tag's Gitea release
|
||||
#
|
||||
# The toolchain is the image's: GOTOOLCHAIN=local, so a go.mod that needs a newer Go fails instead of
|
||||
# downloading one. golangci-lint is a pinned, checksum-verified release binary and reads the repository's
|
||||
# own .golangci.yml.
|
||||
name: go
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
go-version:
|
||||
description: Go version; selects the golang:<version>-bookworm image
|
||||
type: string
|
||||
default: '1.26'
|
||||
runner:
|
||||
type: string
|
||||
default: container-build
|
||||
working-directory:
|
||||
description: Directory with go.mod; every step runs there
|
||||
type: string
|
||||
default: '.'
|
||||
gofmt:
|
||||
description: Fail when any tracked .go file is not gofmt-clean
|
||||
type: boolean
|
||||
default: true
|
||||
golangci-lint:
|
||||
description: Run the pinned golangci-lint (v2 configuration)
|
||||
type: boolean
|
||||
default: false
|
||||
race:
|
||||
description: Run the tests with the race detector
|
||||
type: boolean
|
||||
default: false
|
||||
test-args:
|
||||
description: Extra go test arguments (for example -coverprofile=cover.out)
|
||||
type: string
|
||||
default: ''
|
||||
commands:
|
||||
description: Shell commands, one per line, run after the tests (for example a coverage gate)
|
||||
type: string
|
||||
default: ''
|
||||
release-commands:
|
||||
description: Shell commands, one per line, run on v* tags after every gate passed (build the binaries)
|
||||
type: string
|
||||
default: ''
|
||||
artifacts:
|
||||
description: Files or globs, one per line, attached with a SHA256SUMS to the Gitea release on v* tags; empty = no release
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GOLANGCI_LINT_VERSION: 2.15.0
|
||||
GOLANGCI_LINT_SHA256: 9b7fe6a6bf6334b98decf8d86b11415ea7889afdabdd1959fdce23af2a9ebcea
|
||||
|
||||
jobs:
|
||||
go:
|
||||
runs-on: ${{ inputs.runner }}
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: golang:${{ inputs.go-version }}-bookworm
|
||||
volumes:
|
||||
- /var/lib/gitea-job-workspace:${{ github.workspace }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
env:
|
||||
HOME: /tmp
|
||||
CI: 'true'
|
||||
GOPATH: /tmp/go
|
||||
GOCACHE: /tmp/go-build
|
||||
GOTOOLCHAIN: local
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
|
||||
- name: Set up
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
go version
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: gofmt
|
||||
if: inputs.gofmt
|
||||
run: |
|
||||
set -euo pipefail
|
||||
unformatted=$(git ls-files -z -- '*.go' | xargs -0 -r gofmt -l)
|
||||
if [[ -n $unformatted ]]; then echo "Not gofmt-clean (run gofmt -w):"; echo "$unformatted"; exit 1; fi
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: golangci-lint
|
||||
if: inputs.golangci-lint
|
||||
run: |
|
||||
set -euo pipefail
|
||||
name=golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64
|
||||
(
|
||||
mkdir -p /tmp/tools && cd /tmp/tools
|
||||
curl -fsSLo lint.tgz "https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/$name.tar.gz"
|
||||
echo "$GOLANGCI_LINT_SHA256 lint.tgz" | sha256sum -c -
|
||||
tar -xzf lint.tgz --strip-components=1 "$name/golangci-lint" && rm lint.tgz
|
||||
)
|
||||
/tmp/tools/golangci-lint version
|
||||
/tmp/tools/golangci-lint run ./...
|
||||
|
||||
- name: go test
|
||||
env:
|
||||
RACE: ${{ inputs.race }}
|
||||
TEST_ARGS: ${{ inputs.test-args }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
args=()
|
||||
[[ $RACE == true ]] && args+=(-race)
|
||||
# Word splitting is intended: TEST_ARGS is a list of go test flags.
|
||||
# shellcheck disable=SC2206
|
||||
args+=($TEST_ARGS)
|
||||
go test "${args[@]}" ./...
|
||||
|
||||
- name: Run commands
|
||||
if: inputs.commands != ''
|
||||
env:
|
||||
COMMANDS: ${{ inputs.commands }}
|
||||
run: |
|
||||
printf '%s\n' "$COMMANDS" > /tmp/commands.sh
|
||||
bash -exo pipefail /tmp/commands.sh
|
||||
|
||||
- name: Release commands
|
||||
if: inputs.release-commands != '' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
|
||||
env:
|
||||
COMMANDS: ${{ inputs.release-commands }}
|
||||
run: |
|
||||
printf '%s\n' "$COMMANDS" > /tmp/release-commands.sh
|
||||
bash -exo pipefail /tmp/release-commands.sh
|
||||
|
||||
- name: Release
|
||||
if: inputs.artifacts != '' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
|
||||
env:
|
||||
ARTIFACTS: ${{ inputs.artifacts }}
|
||||
# The job token creates this repository's release (contents: write); no secret needed.
|
||||
TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
files=()
|
||||
while IFS= read -r pattern; do
|
||||
[[ -z ${pattern// } ]] && continue
|
||||
# Glob expansion is intended.
|
||||
# shellcheck disable=SC2206
|
||||
matches=($pattern)
|
||||
[[ -f ${matches[0]} ]] || { echo "No file matches $pattern"; exit 1; }
|
||||
files+=("${matches[@]}")
|
||||
done <<< "$ARTIFACTS"
|
||||
sums=/tmp/SHA256SUMS
|
||||
for f in "${files[@]}"; do (cd "$(dirname "$f")" && sha256sum "$(basename "$f")"); done > "$sums"
|
||||
cat "$sums"
|
||||
|
||||
api="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
auth=(-H "Authorization: token $TOKEN")
|
||||
rel=/tmp/release.json
|
||||
code=$(curl -sS -o "$rel" -w '%{http_code}' "${auth[@]}" "$api/releases/tags/$TAG")
|
||||
if [[ $code == 404 ]]; then
|
||||
prerelease=false; [[ $TAG == *-* ]] && prerelease=true
|
||||
# Another job may create the same release concurrently: fall back to reading it.
|
||||
curl -fsS "${auth[@]}" -H 'Content-Type: application/json' -o "$rel" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"prerelease\":$prerelease}" "$api/releases" \
|
||||
|| curl -fsS "${auth[@]}" -o "$rel" "$api/releases/tags/$TAG"
|
||||
elif [[ $code != 200 ]]; then
|
||||
echo "release lookup for $TAG returned HTTP $code"; exit 1
|
||||
fi
|
||||
# Gitea serialises a release as {"id":N,...,"assets":[{"id":N,"name":"..."},...]}.
|
||||
release=$(grep -o '^{"id":[0-9]*' "$rel" | cut -d: -f2)
|
||||
[[ -n $release ]] || { echo "cannot read the release id"; cat "$rel"; exit 1; }
|
||||
upload() {
|
||||
# Re-runs replace an asset of the same name instead of failing.
|
||||
{ grep -o "\"id\":[0-9]*,\"name\":\"${2//./\\.}\"" "$rel" || true; } | cut -d, -f1 | cut -d: -f2 | while read -r old; do
|
||||
curl -fsS -X DELETE "${auth[@]}" "$api/releases/$release/assets/$old"
|
||||
done
|
||||
curl -fsS -o /dev/null "${auth[@]}" -F "attachment=@$1" "$api/releases/$release/assets?name=$2"
|
||||
echo "attached $2 to release $TAG"
|
||||
}
|
||||
for f in "${files[@]}"; do upload "$f" "$(basename "$f")"; done
|
||||
upload "$sums" SHA256SUMS
|
||||
@@ -0,0 +1,147 @@
|
||||
# Shared Gradle CI: run the repository's Gradle wrapper in the official gradle:jdk<java> image (Ubuntu,
|
||||
# Eclipse Temurin, git); the image's own Gradle is not used.
|
||||
#
|
||||
# any push -> verify the wrapper, commands, then each Gradle line in tasks
|
||||
# tag v* -> + the artifacts files and a SHA256SUMS on the tag's Gitea release (for example plugin zips)
|
||||
#
|
||||
# Wrapper check: gradle-wrapper.jar must be the official jar of the distributionUrl version, the
|
||||
# distribution must come from services.gradle.org, and distributionSha256Sum must be set to the official
|
||||
# checksum (Gradle then verifies the download itself). Fix a failure with
|
||||
# ./gradlew wrapper --gradle-version <v> --gradle-distribution-sha256-sum <sha256>
|
||||
name: gradle
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tasks:
|
||||
description: Gradle invocations, one per line (arguments to ./gradlew); every line must pass
|
||||
type: string
|
||||
default: build
|
||||
java-version:
|
||||
description: JDK major version; selects the gradle:jdk<version> image (17, 21, 25)
|
||||
type: string
|
||||
default: '21'
|
||||
runner:
|
||||
type: string
|
||||
default: container-build
|
||||
commands:
|
||||
description: Shell commands, one per line, run before Gradle
|
||||
type: string
|
||||
default: ''
|
||||
artifacts:
|
||||
description: Files or globs, one per line, attached with a SHA256SUMS to the Gitea release on v* tags; empty = no release
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
gradle:
|
||||
runs-on: ${{ inputs.runner }}
|
||||
timeout-minutes: 60
|
||||
container:
|
||||
image: gradle:jdk${{ inputs.java-version }}
|
||||
volumes:
|
||||
- /var/lib/gitea-job-workspace:${{ github.workspace }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
HOME: /tmp
|
||||
CI: 'true'
|
||||
GRADLE_USER_HOME: /tmp/.gradle
|
||||
GRADLE_OPTS: -Dorg.gradle.daemon=false
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
|
||||
- name: Verify the Gradle wrapper
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
java -version
|
||||
props=gradle/wrapper/gradle-wrapper.properties
|
||||
[[ -x ./gradlew && -f gradle/wrapper/gradle-wrapper.jar && -f $props ]] || { echo "./gradlew, gradle-wrapper.jar or $props is missing (or gradlew is not executable)"; exit 1; }
|
||||
url=$(sed -n 's/^distributionUrl=//p' "$props" | tr -d '\r' | sed 's/\\:/:/g')
|
||||
re='^https://services\.gradle\.org/distributions/gradle-([0-9][0-9A-Za-z.-]*)-(bin|all)\.zip$'
|
||||
[[ $url =~ $re ]] || { echo "distributionUrl must be an official services.gradle.org distribution: $url"; exit 1; }
|
||||
version=${BASH_REMATCH[1]}
|
||||
official=https://services.gradle.org/distributions
|
||||
jar=$(curl -fsSL "$official/gradle-$version-wrapper.jar.sha256")
|
||||
echo "$jar gradle/wrapper/gradle-wrapper.jar" | sha256sum -c - \
|
||||
|| { echo "gradle-wrapper.jar is not the official Gradle $version wrapper jar; run ./gradlew wrapper"; exit 1; }
|
||||
dist=$(curl -fsSL "${url}.sha256")
|
||||
pinned=$(sed -n 's/^distributionSha256Sum=//p' "$props" | tr -d '\r')
|
||||
[[ $pinned == "$dist" ]] || { echo "distributionSha256Sum must be the official checksum $dist (found '${pinned}')"; exit 1; }
|
||||
echo "Gradle wrapper $version verified"
|
||||
|
||||
- name: Run commands
|
||||
if: inputs.commands != ''
|
||||
env:
|
||||
COMMANDS: ${{ inputs.commands }}
|
||||
run: |
|
||||
printf '%s\n' "$COMMANDS" > /tmp/commands.sh
|
||||
bash -exo pipefail /tmp/commands.sh
|
||||
|
||||
- name: Gradle
|
||||
env:
|
||||
TASKS: ${{ inputs.tasks }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while IFS= read -r line; do
|
||||
[[ -z ${line// } ]] && continue
|
||||
echo "::group::./gradlew $line"
|
||||
# Word splitting is intended: each line is a list of Gradle arguments.
|
||||
# shellcheck disable=SC2086
|
||||
./gradlew --console=plain $line
|
||||
echo "::endgroup::"
|
||||
done <<< "$TASKS"
|
||||
|
||||
- name: Release
|
||||
if: inputs.artifacts != '' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
|
||||
env:
|
||||
ARTIFACTS: ${{ inputs.artifacts }}
|
||||
# The job token creates this repository's release (contents: write); no secret needed.
|
||||
TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
files=()
|
||||
while IFS= read -r pattern; do
|
||||
[[ -z ${pattern// } ]] && continue
|
||||
# Glob expansion is intended.
|
||||
# shellcheck disable=SC2206
|
||||
matches=($pattern)
|
||||
[[ -f ${matches[0]} ]] || { echo "No file matches $pattern"; exit 1; }
|
||||
files+=("${matches[@]}")
|
||||
done <<< "$ARTIFACTS"
|
||||
sums=/tmp/SHA256SUMS
|
||||
for f in "${files[@]}"; do (cd "$(dirname "$f")" && sha256sum "$(basename "$f")"); done > "$sums"
|
||||
cat "$sums"
|
||||
|
||||
api="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
auth=(-H "Authorization: token $TOKEN")
|
||||
rel=/tmp/release.json
|
||||
code=$(curl -sS -o "$rel" -w '%{http_code}' "${auth[@]}" "$api/releases/tags/$TAG")
|
||||
if [[ $code == 404 ]]; then
|
||||
prerelease=false; [[ $TAG == *-* ]] && prerelease=true
|
||||
# Another job may create the same release concurrently: fall back to reading it.
|
||||
curl -fsS "${auth[@]}" -H 'Content-Type: application/json' -o "$rel" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"prerelease\":$prerelease}" "$api/releases" \
|
||||
|| curl -fsS "${auth[@]}" -o "$rel" "$api/releases/tags/$TAG"
|
||||
elif [[ $code != 200 ]]; then
|
||||
echo "release lookup for $TAG returned HTTP $code"; exit 1
|
||||
fi
|
||||
# Gitea serialises a release as {"id":N,...,"assets":[{"id":N,"name":"..."},...]}.
|
||||
release=$(grep -o '^{"id":[0-9]*' "$rel" | cut -d: -f2)
|
||||
[[ -n $release ]] || { echo "cannot read the release id"; cat "$rel"; exit 1; }
|
||||
upload() {
|
||||
# Re-runs replace an asset of the same name instead of failing.
|
||||
{ grep -o "\"id\":[0-9]*,\"name\":\"${2//./\\.}\"" "$rel" || true; } | cut -d, -f1 | cut -d: -f2 | while read -r old; do
|
||||
curl -fsS -X DELETE "${auth[@]}" "$api/releases/$release/assets/$old"
|
||||
done
|
||||
curl -fsS -o /dev/null "${auth[@]}" -F "attachment=@$1" "$api/releases/$release/assets?name=$2"
|
||||
echo "attached $2 to release $TAG"
|
||||
}
|
||||
for f in "${files[@]}"; do upload "$f" "$(basename "$f")"; done
|
||||
upload "$sums" SHA256SUMS
|
||||
@@ -0,0 +1,256 @@
|
||||
# Shared repository hygiene checks. Every check is off until its input is set; each tool is pinned and
|
||||
# checksum- or digest-verified, and reads the repository's own configuration (.shellcheckrc, .yamllint,
|
||||
# ruff.toml, .markdownlint-cli2.*, .ansible-lint).
|
||||
#
|
||||
# File inputs are git pathspec globs, one per line, matched against tracked files: `*.sh` is the top
|
||||
# level only, `**/*.sh` any depth, `ops/**/*.sh` under ops/. A glob that matches nothing fails the check.
|
||||
# `exclude` globs are removed from every file check. git-crypt files (filter=git-crypt) are ciphertext
|
||||
# in CI and always skipped. markdownlint takes markdownlint-cli2 globs instead (exclude becomes #glob).
|
||||
name: lint
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
runner:
|
||||
type: string
|
||||
default: container-build
|
||||
exclude:
|
||||
description: Globs, one per line, removed from every file check (vendored or generated files)
|
||||
type: string
|
||||
default: ''
|
||||
shellcheck:
|
||||
description: Shell script globs for shellcheck
|
||||
type: string
|
||||
default: ''
|
||||
yamllint:
|
||||
description: YAML globs for yamllint
|
||||
type: string
|
||||
default: ''
|
||||
json:
|
||||
description: JSON globs that must parse (python -m json.tool)
|
||||
type: string
|
||||
default: ''
|
||||
ruff:
|
||||
description: Python globs for ruff check
|
||||
type: string
|
||||
default: ''
|
||||
tofu-fmt:
|
||||
description: OpenTofu/Terraform globs (*.tf, *.tfvars) for tofu fmt -check
|
||||
type: string
|
||||
default: ''
|
||||
tofu-validate:
|
||||
description: Root module directories, one per line, for tofu init -backend=false and tofu validate
|
||||
type: string
|
||||
default: ''
|
||||
markdownlint:
|
||||
description: Markdown globs for markdownlint-cli2 (its own glob syntax; config ignores apply)
|
||||
type: string
|
||||
default: ''
|
||||
ansible-lint:
|
||||
description: Arguments to ansible-lint (paths or options; '.' lints the repository); empty = off
|
||||
type: string
|
||||
default: ''
|
||||
commands:
|
||||
description: Extra shell commands, one per line, run after the checks in the lint image (Node, Python 3, git)
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SHELLCHECK_VERSION: v0.11.0
|
||||
SHELLCHECK_SHA256: 8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198
|
||||
TOFU_VERSION: 1.13.2
|
||||
TOFU_SHA256: ac05d9952662c7db28dd57e93f0a68018837bfdf9342384ec0987977c16a9645
|
||||
RUFF_VERSION: 0.17.1
|
||||
RUFF_SHA256: a293ac937715ae6fb6b64da86761598379e6149f40fbc00fde9ba8a2a562ac65
|
||||
UV_VERSION: 0.13.6
|
||||
MARKDOWNLINT_CLI2_VERSION: 0.24.0
|
||||
MARKDOWNLINT_CLI2_BEFORE: '2026-10-22'
|
||||
UV_SHA256: f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
if: inputs.shellcheck != '' || inputs.yamllint != '' || inputs.json != '' || inputs.ruff != '' || inputs.tofu-fmt != '' || inputs.tofu-validate != '' || inputs.markdownlint != '' || inputs.commands != ''
|
||||
runs-on: ${{ inputs.runner }}
|
||||
timeout-minutes: 20
|
||||
container:
|
||||
image: node:22-bookworm
|
||||
volumes:
|
||||
- /var/lib/gitea-job-workspace:${{ github.workspace }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
HOME: /tmp
|
||||
CI: 'true'
|
||||
EXCLUDE: ${{ inputs.exclude }}
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
|
||||
- name: Set up
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
mkdir -p /tmp/tools
|
||||
echo /tmp/tools >> "$GITHUB_PATH"
|
||||
# pick-files GLOBS OUT: the tracked files matching GLOBS, minus EXCLUDE and git-crypt files, NUL-separated.
|
||||
cat > /tmp/tools/pick-files <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
specs=()
|
||||
while IFS= read -r p; do [[ -n ${p// } ]] && specs+=(":(glob)$p"); done <<< "$1"
|
||||
while IFS= read -r p; do [[ -n ${p// } ]] && specs+=(":(glob,exclude)$p"); done <<< "${EXCLUDE:-}"
|
||||
git ls-files -z -- "${specs[@]}" | git check-attr -z --stdin filter |
|
||||
while IFS= read -r -d '' f && IFS= read -r -d '' _ && IFS= read -r -d '' v; do
|
||||
[[ $v == git-crypt ]] || printf '%s\0' "$f"
|
||||
done > "$2"
|
||||
[[ -s $2 ]] || { echo "No tracked file matches: ${1//$'\n'/ }"; exit 1; }
|
||||
echo "$(tr -cd '\0' < "$2" | wc -c) files"
|
||||
EOF
|
||||
chmod +x /tmp/tools/pick-files
|
||||
|
||||
- name: shellcheck
|
||||
if: inputs.shellcheck != ''
|
||||
env:
|
||||
GLOBS: ${{ inputs.shellcheck }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSLo /tmp/sc.txz "https://github.com/koalaman/shellcheck/releases/download/$SHELLCHECK_VERSION/shellcheck-$SHELLCHECK_VERSION.linux.x86_64.tar.xz"
|
||||
echo "$SHELLCHECK_SHA256 /tmp/sc.txz" | sha256sum -c -
|
||||
tar -xJf /tmp/sc.txz -C /tmp/tools --strip-components=1 "shellcheck-$SHELLCHECK_VERSION/shellcheck"
|
||||
shellcheck --version | sed -n 2p
|
||||
pick-files "$GLOBS" /tmp/files
|
||||
xargs -0 shellcheck < /tmp/files
|
||||
|
||||
- name: yamllint
|
||||
if: inputs.yamllint != ''
|
||||
env:
|
||||
GLOBS: ${{ inputs.yamllint }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSLo /tmp/uv.tgz "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-x86_64-unknown-linux-gnu.tar.gz"
|
||||
echo "$UV_SHA256 /tmp/uv.tgz" | sha256sum -c -
|
||||
tar -xzf /tmp/uv.tgz -C /tmp/tools --strip-components=1
|
||||
# Hash-locked for the image's Python 3.11.
|
||||
cat > /tmp/yamllint.txt <<'EOF'
|
||||
yamllint==1.38.0 --hash=sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220
|
||||
pathspec==1.1.1 --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
|
||||
pyyaml==6.0.3 --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d
|
||||
EOF
|
||||
uv venv -q --python /usr/bin/python3 /tmp/yamllint
|
||||
uv pip install -q --python /tmp/yamllint/bin/python --require-hashes --no-deps --only-binary :all: -r /tmp/yamllint.txt
|
||||
/tmp/yamllint/bin/yamllint --version
|
||||
pick-files "$GLOBS" /tmp/files
|
||||
xargs -0 /tmp/yamllint/bin/yamllint < /tmp/files
|
||||
|
||||
- name: JSON
|
||||
if: inputs.json != ''
|
||||
env:
|
||||
GLOBS: ${{ inputs.json }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pick-files "$GLOBS" /tmp/files
|
||||
status=0
|
||||
while IFS= read -r -d '' f; do
|
||||
python3 -m json.tool "$f" > /dev/null || { echo "invalid JSON: $f"; status=1; }
|
||||
done < /tmp/files
|
||||
exit $status
|
||||
|
||||
- name: ruff
|
||||
if: inputs.ruff != ''
|
||||
env:
|
||||
GLOBS: ${{ inputs.ruff }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSLo /tmp/ruff.tgz "https://github.com/astral-sh/ruff/releases/download/$RUFF_VERSION/ruff-x86_64-unknown-linux-gnu.tar.gz"
|
||||
echo "$RUFF_SHA256 /tmp/ruff.tgz" | sha256sum -c -
|
||||
tar -xzf /tmp/ruff.tgz -C /tmp/tools --strip-components=1
|
||||
ruff --version
|
||||
pick-files "$GLOBS" /tmp/files
|
||||
xargs -0 ruff check --no-cache < /tmp/files
|
||||
|
||||
- name: Install OpenTofu
|
||||
if: inputs.tofu-fmt != '' || inputs.tofu-validate != ''
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSLo /tmp/tofu.tgz "https://github.com/opentofu/opentofu/releases/download/v$TOFU_VERSION/tofu_${TOFU_VERSION}_linux_amd64.tar.gz"
|
||||
echo "$TOFU_SHA256 /tmp/tofu.tgz" | sha256sum -c -
|
||||
tar -xzf /tmp/tofu.tgz -C /tmp/tools tofu
|
||||
tofu version
|
||||
|
||||
- name: tofu fmt
|
||||
if: inputs.tofu-fmt != ''
|
||||
env:
|
||||
GLOBS: ${{ inputs.tofu-fmt }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pick-files "$GLOBS" /tmp/files
|
||||
xargs -0 tofu fmt -check -diff < /tmp/files
|
||||
|
||||
- name: tofu validate
|
||||
if: inputs.tofu-validate != ''
|
||||
env:
|
||||
DIRS: ${{ inputs.tofu-validate }}
|
||||
TF_PLUGIN_CACHE_DIR: /tmp/tofu-plugins
|
||||
TF_IN_AUTOMATION: 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$TF_PLUGIN_CACHE_DIR"
|
||||
while IFS= read -r dir; do
|
||||
[[ -z ${dir// } ]] && continue
|
||||
echo "::group::tofu validate $dir"
|
||||
tofu -chdir="$dir" init -backend=false -input=false -no-color > /tmp/init.log || { cat /tmp/init.log; exit 1; }
|
||||
tofu -chdir="$dir" validate -no-color
|
||||
echo "::endgroup::"
|
||||
done <<< "$DIRS"
|
||||
|
||||
- name: markdownlint
|
||||
if: inputs.markdownlint != ''
|
||||
env:
|
||||
GLOBS: ${{ inputs.markdownlint }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Exact version; --before freezes the dependency tree to its release day; npm checks every
|
||||
# package's integrity hash; no install scripts run.
|
||||
npm install --prefix /tmp/mdl --no-save --no-audit --no-fund --ignore-scripts \
|
||||
--before "$MARKDOWNLINT_CLI2_BEFORE" "markdownlint-cli2@$MARKDOWNLINT_CLI2_VERSION" > /dev/null
|
||||
args=()
|
||||
while IFS= read -r p; do [[ -n ${p// } ]] && args+=("$p"); done <<< "$GLOBS"
|
||||
while IFS= read -r p; do [[ -n ${p// } ]] && args+=("#$p"); done <<< "$EXCLUDE"
|
||||
/tmp/mdl/node_modules/.bin/markdownlint-cli2 "${args[@]}"
|
||||
|
||||
- name: Run commands
|
||||
if: inputs.commands != ''
|
||||
env:
|
||||
COMMANDS: ${{ inputs.commands }}
|
||||
run: |
|
||||
printf '%s\n' "$COMMANDS" > /tmp/commands.sh
|
||||
bash -exo pipefail /tmp/commands.sh
|
||||
|
||||
ansible-lint:
|
||||
if: inputs.ansible-lint != ''
|
||||
runs-on: ${{ inputs.runner }}
|
||||
timeout-minutes: 20
|
||||
container:
|
||||
image: ghcr.io/ansible/community-ansible-dev-tools:v26.10.1@sha256:13c96bd005f156fb48ac9913545f3b56b265ba20824b0be7ef33fba1125c6b1d
|
||||
volumes:
|
||||
- /var/lib/gitea-job-workspace:${{ github.workspace }}
|
||||
env:
|
||||
HOME: /tmp
|
||||
ARGS: ${{ inputs.ansible-lint }}
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
|
||||
- name: ansible-lint
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
# Ansible ignores ./ansible.cfg in a world-writable directory; name it explicitly.
|
||||
if [[ -f ansible.cfg ]]; then export ANSIBLE_CONFIG="$PWD/ansible.cfg"; fi
|
||||
ansible-lint --version
|
||||
# Word splitting is intended: ARGS is a list of ansible-lint arguments.
|
||||
# shellcheck disable=SC2086
|
||||
ansible-lint $ARGS
|
||||
@@ -210,6 +210,110 @@ jobs:
|
||||
session, so `xcodebuild test` cannot reach `testmanagerd` (SwiftPM `swift test`
|
||||
works), and the host has no `git-lfs`.
|
||||
|
||||
## Go
|
||||
|
||||
`.gitea/workflows/build.yaml` (see `KofTwentyTwo/license-tool`):
|
||||
|
||||
```yaml
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['v*']
|
||||
jobs:
|
||||
go:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/go.yaml@v1
|
||||
with:
|
||||
golangci-lint: true
|
||||
race: true
|
||||
release-commands: scripts/build-release.sh "${GITHUB_REF_NAME#v}"
|
||||
artifacts: dist/*
|
||||
```
|
||||
|
||||
Every push runs `go mod verify`, `gofmt -l` (tracked files), `go vet ./...`,
|
||||
optionally golangci-lint, and `go test ./...`, in `golang:<go-version>-bookworm`
|
||||
with `GOTOOLCHAIN=local` (a `go.mod` that needs a newer Go fails, nothing is downloaded).
|
||||
|
||||
| Input | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `go-version` | `'1.26'` | Selects the official `golang:<version>-bookworm` image |
|
||||
| `gofmt` | `true` | Fail when a tracked `.go` file is not gofmt-clean |
|
||||
| `golangci-lint` | `false` | Pinned, sha256-verified golangci-lint v2 with the repository's `.golangci.yml` |
|
||||
| `race` | `false` | `go test -race` |
|
||||
| `test-args` | `''` | Extra `go test` flags (for example a coverage profile) |
|
||||
| `commands` | `''` | Extra gates after the tests, one per line (for example a coverage threshold) |
|
||||
| `release-commands` | `''` | Run on `v*` tags after every gate (cross-compile into `dist/`) |
|
||||
| `artifacts` | `''` | Files or globs; on `v*` tags they and a `SHA256SUMS` go on the tag's Gitea release (job token; re-runs replace same-named assets) |
|
||||
| `working-directory` | `.` | Directory with `go.mod` |
|
||||
| `runner` | `container-build` | Runner label |
|
||||
|
||||
## Gradle
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
gradle:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/gradle.yaml@v1
|
||||
with:
|
||||
java-version: '17'
|
||||
tasks: build
|
||||
artifacts: build/distributions/*.zip
|
||||
```
|
||||
|
||||
Runs `./gradlew` in `gradle:jdk<java-version>` (the image's own Gradle is unused).
|
||||
Before Gradle runs, the wrapper is verified: `distributionUrl` must be a
|
||||
`services.gradle.org` distribution, `gradle-wrapper.jar` must be that version's
|
||||
official jar, and `distributionSha256Sum` must be set to the official checksum so
|
||||
Gradle verifies the download. Fix a failure with
|
||||
`./gradlew wrapper --gradle-version <v> --gradle-distribution-sha256-sum <sha256>`.
|
||||
|
||||
| Input | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `tasks` | `build` | Gradle invocations, one per line |
|
||||
| `java-version` | `'21'` | JDK major version (17, 21, 25) |
|
||||
| `commands` | `''` | Shell commands run before Gradle |
|
||||
| `artifacts` | `''` | Files or globs; on `v*` tags they and a `SHA256SUMS` go on the tag's Gitea release |
|
||||
| `runner` | `container-build` | Runner label |
|
||||
|
||||
Android projects use `android.yaml`.
|
||||
|
||||
## Lint
|
||||
|
||||
Repository hygiene for scripts, configuration and infrastructure code. Every
|
||||
check is off until its input is set.
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
lint:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/lint.yaml@v1
|
||||
with:
|
||||
shellcheck: 'ops/**/*.sh'
|
||||
tofu-fmt: '**/*.tf'
|
||||
# vendor/: third-party scripts copied verbatim, not ours to restyle
|
||||
exclude: |
|
||||
vendor/**
|
||||
```
|
||||
|
||||
| Input | Check |
|
||||
| --- | --- |
|
||||
| `shellcheck` | shellcheck (`.shellcheckrc` applies) |
|
||||
| `yamllint` | yamllint (`.yamllint*` applies) |
|
||||
| `json` | every file parses (`python3 -m json.tool`) |
|
||||
| `ruff` | `ruff check` (`ruff.toml` / `pyproject.toml` applies) |
|
||||
| `tofu-fmt` | `tofu fmt -check -diff` |
|
||||
| `tofu-validate` | directories, one per line: `tofu init -backend=false` and `tofu validate` (no state, no secrets) |
|
||||
| `markdownlint` | markdownlint-cli2 globs (`.markdownlint-cli2.*` applies) |
|
||||
| `ansible-lint` | arguments to ansible-lint (`.` = the repository; `.ansible-lint` applies), in its own job |
|
||||
| `commands` | extra shell commands in the lint image (Node 22, Python 3, git, curl) |
|
||||
| `exclude` | globs removed from every file check |
|
||||
|
||||
File inputs are git pathspec globs over tracked files (`*.sh` top level only,
|
||||
`**/*.sh` any depth); a glob that matches nothing fails. git-crypt files are
|
||||
ciphertext in CI and always skipped. Tools are pinned: shellcheck, OpenTofu, ruff
|
||||
and uv by sha256; yamllint hash-locked; ansible-lint by image digest;
|
||||
markdownlint-cli2 by exact version with its dependency tree frozen to the release
|
||||
day (`npm --before`, integrity-checked). Exclude only vendored or generated files,
|
||||
with a comment saying why; fix findings in your own files.
|
||||
|
||||
## Releasing
|
||||
|
||||
| To publish | Do this |
|
||||
|
||||
Reference in new issue
Block a user