ci(gitea): gate scripts/ with shellcheck and the selftests
This repo had eight scripts, six selftest suites and nothing that ran any of
them. It is a composite action consumed at `@v1` — a moving tag — by three
repos' CI, so a bad edit here reaches zemyna, emowheel and lublub at once and
is discovered by whichever of them builds next.
One job: `shellcheck -x --source-path=scripts scripts/*.sh`, then
`bash scripts/selftest.sh`. `-x` follows the `. cache-lib.sh` every script
sources, which is where most of the logic being checked lives; without it
shellcheck reports SC1091 and analyses each file with a hole in it.
The full suite, not `--fast`: `hardlink-clone-selftest.sh` and
`restore-mtimes-selftest.sh` are the two that drive a real Cargo rather than a
fixture, and selftest.sh's own header says `--fast` is for iterating, not for
signing off a change. So the job installs a stable toolchain. The scratch
workspaces they build use path dependencies only, so nothing reaches
crates.io, and the job references no credentials at all.
Modelled on daniel/gitdan's workflow, including the two clauses of the
draft-skip guard (`github.event_name != 'pull_request' ||
!github.event.pull_request.draft` — the first is what stops the second from
also skipping pushes to `main`, where there is no `pull_request` context) and
the per-commit concurrency group for `push`.
No `container.volumes:` entry, deliberately: the suites want a cold target dir
every run, since "did this rebuild?" is exactly what restore-mtimes-selftest
asks. This job takes no share of the shared CI cache disk budget.
Turning the gate on surfaced three pre-existing findings, all fixed here
rather than suppressed or waived:
- `restore-mtimes-selftest.sh` SC2038: `find | xargs touch` -> `-print0 | xargs -0`.
- `hardlink-clone-selftest.sh` SC2295: `${f#$base_fix/}` -> `${f#"$base_fix"/}`.
- `cache-lib.sh` SC2016: a per-line disable with the rationale. The single
quotes are load-bearing — the program is for the inner shell, where `$f` is
its loop variable, `$rc` its accumulator and `$$` its pid — so this is the
documented-false-positive case, not a stopgap.
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
# Spelled out only to keep `ready_for_review` in the list — naming any type
|
||||||
|
# replaces the whole default set, so the other three have to be restated.
|
||||||
|
# It is inert on this instance (draft state here is the `WIP:` title
|
||||||
|
# prefix, so un-drafting is a title edit and raises no
|
||||||
|
# `ready_for_review` action) and costs nothing.
|
||||||
|
#
|
||||||
|
# The consequence, which is the part that bites: the `if:` guard below is
|
||||||
|
# evaluated when a run is CREATED, and un-drafting creates no run. A PR
|
||||||
|
# opened as a draft keeps its skip decision until something else produces
|
||||||
|
# one. Push an empty commit after un-WIP'ing.
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
# gitdan-ci runs four repos' CI on two capacity slots, and the compiler-backed
|
||||||
|
# suites below are multi-minute. A superseded run costs a slot in front of
|
||||||
|
# somebody's build, so drop it.
|
||||||
|
#
|
||||||
|
# `push` groups on `github.sha` rather than `github.ref`: a constant per-branch
|
||||||
|
# group is what let this Gitea (1.26.0) cancel two of daniel/gitdan's merge
|
||||||
|
# runs outright while `cancel-in-progress` was gated away from `push` entirely
|
||||||
|
# — see the long note in that repo's ci.yaml for the evidence. Giving every
|
||||||
|
# commit its own group leaves that behaviour nothing to act on.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
selftest:
|
||||||
|
name: shellcheck + selftests
|
||||||
|
# Two clauses, both load-bearing. The second skips draft PRs: Gitea sets
|
||||||
|
# draft:true when the title starts with `WIP:`, so work-in-progress pushes
|
||||||
|
# cost the shared runner nothing until the PR is un-WIP'd. The first is
|
||||||
|
# what keeps that from also skipping pushes to `main` — a `push` event has
|
||||||
|
# no `pull_request` context, so `github.event.pull_request.draft` is empty
|
||||||
|
# there and the negation alone would be unreliable. Never drop it.
|
||||||
|
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
# Deliberately no `container.volumes:` entry, unlike every repo that
|
||||||
|
# CONSUMES this action. The suites below build throwaway workspaces under
|
||||||
|
# `mktemp -d` and want a cold target dir every time — a persistent cache
|
||||||
|
# would make "did this run rebuild?" unanswerable, which is the question
|
||||||
|
# restore-mtimes-selftest.sh exists to ask. So this job takes no share of
|
||||||
|
# the shared CI cache disk budget.
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install shellcheck
|
||||||
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: shellcheck
|
||||||
|
|
||||||
|
# `hardlink-clone-selftest.sh` and `restore-mtimes-selftest.sh` drive a
|
||||||
|
# real Cargo against a real scratch workspace — they are the only things
|
||||||
|
# here that verify the hardlink-aliasing and mtime-freshness behaviour
|
||||||
|
# against the compiler rather than against a fixture, and selftest.sh's
|
||||||
|
# own header says `--fast` is for iterating, not for signing off a
|
||||||
|
# change. So CI installs a toolchain and runs the full set.
|
||||||
|
#
|
||||||
|
# The scratch workspaces use path dependencies only, so nothing here
|
||||||
|
# reaches crates.io.
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
# Every script, including the suites themselves. `-x` follows the
|
||||||
|
# `. cache-lib.sh` each one sources, which is where most of the logic
|
||||||
|
# being checked actually lives; without it shellcheck reports SC1091 and
|
||||||
|
# analyses each file with a hole in it.
|
||||||
|
- name: shellcheck
|
||||||
|
run: shellcheck -x --source-path=scripts scripts/*.sh
|
||||||
|
|
||||||
|
# One command, not six: selftest.sh is the entry point a developer runs,
|
||||||
|
# so a suite added there is gated here without a matching edit in this
|
||||||
|
# file.
|
||||||
|
- name: Selftests
|
||||||
|
run: bash scripts/selftest.sh
|
||||||
@@ -445,10 +445,25 @@ not automatic.
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/selftest.sh # everything (~1 min; needs cargo)
|
shellcheck -x --source-path=scripts scripts/*.sh
|
||||||
|
bash scripts/selftest.sh # everything (needs cargo)
|
||||||
bash scripts/selftest.sh --fast # fixture-only suites, no compiler
|
bash scripts/selftest.sh --fast # fixture-only suites, no compiler
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both run in CI — `.gitea/workflows/ci.yaml`, one job, on pushes to `main` and
|
||||||
|
on PRs that were non-draft when the run was created. It installs shellcheck
|
||||||
|
and a stable Rust toolchain and references no credentials; the scratch
|
||||||
|
workspaces the compiler-backed suites build use path dependencies only, so
|
||||||
|
nothing reaches crates.io. It runs the full suite rather than `--fast`,
|
||||||
|
because the two compiler-backed suites are the ones that check this scheme
|
||||||
|
against real Cargo instead of against a fixture. Draft (`WIP:`-titled) PRs
|
||||||
|
skip it, and un-drafting does **not** un-skip them — the guard is evaluated
|
||||||
|
when a run is created and un-drafting creates none, so push an empty commit
|
||||||
|
after un-WIP'ing.
|
||||||
|
|
||||||
|
This repo is consumed by three other repos' CI at `@v1`, a moving tag, so a
|
||||||
|
change here reaches all of them at once. That is what the gate is for.
|
||||||
|
|
||||||
| suite | covers |
|
| suite | covers |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `hardlink-clone-selftest.sh` | that a build in a clone cannot mutate its source — with a control proving a raw `cp -al` does. Needs a real compiler. |
|
| `hardlink-clone-selftest.sh` | that a build in a clone cannot mutate its source — with a control proving a raw `cp -al` does. Needs a real compiler. |
|
||||||
|
|||||||
@@ -210,6 +210,9 @@ _unshare_files() {
|
|||||||
# The inner shell propagates a failure of any individual copy-and-rename out
|
# The inner shell propagates a failure of any individual copy-and-rename out
|
||||||
# through xargs (which exits 123 if any invocation exits 1-125), so a
|
# through xargs (which exits 123 if any invocation exits 1-125), so a
|
||||||
# partially-unshared tree is reported rather than silently accepted.
|
# partially-unshared tree is reported rather than silently accepted.
|
||||||
|
# shellcheck disable=SC2016 # the quoted program is for the INNER shell: $f
|
||||||
|
# is its loop variable, $rc its accumulator and $$ its pid. Expanding any of
|
||||||
|
# them here is what the single quotes exist to prevent.
|
||||||
find "$@" -links +1 -print0 2>/dev/null |
|
find "$@" -links +1 -print0 2>/dev/null |
|
||||||
xargs -0 -r -n 64 bash -c 'rc=0; for f; do cp -p -- "$f" "$f.unshare.$$" && mv -f -- "$f.unshare.$$" "$f" || rc=1; done; exit $rc' _
|
xargs -0 -r -n 64 bash -c 'rc=0; for f; do cp -p -- "$f" "$f.unshare.$$" && mv -f -- "$f.unshare.$$" "$f" || rc=1; done; exit $rc' _
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ hardlink_clone_into "$base_fix" "$clone_fix" "selftest" || fail "hardlink_clone_
|
|||||||
# rebuild would prove nothing — the rebuild replaces those files anyway.
|
# rebuild would prove nothing — the rebuild replaces those files anyway.
|
||||||
shared=0; unshared=0
|
shared=0; unshared=0
|
||||||
while IFS= read -r f; do
|
while IFS= read -r f; do
|
||||||
rel="${f#$base_fix/}"
|
rel="${f#"$base_fix"/}"
|
||||||
[ -e "$clone_fix/$rel" ] || continue
|
[ -e "$clone_fix/$rel" ] || continue
|
||||||
if [ "$(stat -c '%i' "$f")" = "$(stat -c '%i' "$clone_fix/$rel")" ]; then
|
if [ "$(stat -c '%i' "$f")" = "$(stat -c '%i' "$clone_fix/$rel")" ]; then
|
||||||
case "$rel" in
|
case "$rel" in
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ assert_log_lacks() {
|
|||||||
# every run — simulate that before each restore-mtimes.sh pass, exactly as
|
# every run — simulate that before each restore-mtimes.sh pass, exactly as
|
||||||
# CI would see it, so this test exercises the script the same way CI does.
|
# CI would see it, so this test exercises the script the same way CI does.
|
||||||
stamp_checkout_now() {
|
stamp_checkout_now() {
|
||||||
find . -path ./.git -prune -o -type f -print | xargs touch
|
find . -path ./.git -prune -o -type f -print0 | xargs -0 touch
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "=== building scratch workspace ==="
|
echo "=== building scratch workspace ==="
|
||||||
|
|||||||
Reference in New Issue
Block a user