go, gradle, lint: small shared workflows #10

Merged
james.maes merged 5 commits from small-workflows into main 2026-11-08 14:56:13 +00:00
4 changed files with 697 additions and 0 deletions

No files matched your search

+190
View File
@@ -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
+147
View File
@@ -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
+256
View File
@@ -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
+104
View File
@@ -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 |