Re-vendor the shared files from sneak/prompts at dd4027b (closes #504)
check / check (push) Failing after 3s
check / check (push) Failing after 3s
Fetches the shared files unchanged from sneak/prompts commit dd4027b and adds .prettierignore; .dockerignore keeps this repository's anchored host artifacts at its end. Linting moves into the Dockerfile's lint phase on the golangci-lint v2.14.0 image, which also runs the js-lint stage, and Dockerfile.lint is gone. Tests move into a test phase on the golang bookworm image. script/lint, test, docker and cibuild are the model scripts, every docker build in script/ passes --no-cache, and the .ci-fingerprint barrier is gone. The workflow no longer calls script/ci-mark-superseded, so it and its tests are removed. make build passes -trimpath and -s -w. Model: opus-5-5
This commit is contained in:
+2
-2
@@ -2,8 +2,8 @@
|
||||
# script/assets: extract Alpine.js from its npm package tarball, committed
|
||||
# in 3p/, to static/js/alpine.min.js, where go:embed reads it. The package
|
||||
# is @alpinejs/csp, Alpine's build for pages whose Content-Security-Policy
|
||||
# forbids eval. The extracted file is not committed. script/test, make
|
||||
# build and make dev run this first.
|
||||
# forbids eval. The extracted file is not committed. make build, make dev
|
||||
# and the Dockerfile's lint, test and build stages run this first.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
+3
-3
@@ -60,10 +60,10 @@ main() {
|
||||
if missing go; then pkg_install go golang go go; fi
|
||||
|
||||
# Not installed here: docker is platform-specific and out of scope for a
|
||||
# package-manager bootstrap, but script/lint, script/fmt and script/css
|
||||
# need it.
|
||||
# package-manager bootstrap, but script/test, script/lint, script/fmt and
|
||||
# script/css need it.
|
||||
if missing docker; then
|
||||
echo "bootstrap: docker not found; script/lint, script/fmt and script/css require it" >&2
|
||||
echo "bootstrap: docker not found; script/test, script/lint, script/fmt and script/css require it" >&2
|
||||
fi
|
||||
|
||||
go mod download
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
# script/check: run all checks (test, lint, fmt-check, css-check). Our own
|
||||
# extension to scripts-to-rule-them-all.
|
||||
# Writes only the ignored static/js/alpine.min.js, through script/test.
|
||||
# extension to scripts-to-rule-them-all. test, lint and css-check are
|
||||
# Docker builds; fmt-check runs gofmt on the host. Writes nothing.
|
||||
# Generic, apart from css-check.
|
||||
set -eu
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/bin/sh
|
||||
# script/ci-mark-superseded: record an honest status on commits whose CI
|
||||
# run Gitea cancelled because a newer commit landed on the same branch.
|
||||
# Gitea writes `failure` / "Has been cancelled" for such a run, which
|
||||
# reads as a test result on a commit nothing ever tested. Cancellation is
|
||||
# unconditional server-side for push events, so the superseding run
|
||||
# rewrites those statuses to `failure` with a description that says the
|
||||
# commit was never tested. `skipped` cannot be used: Gitea's combined
|
||||
# status folds `skipped` into `success`, so a never-tested commit would
|
||||
# report green. Genuine failures and successes are never touched.
|
||||
#
|
||||
# Called by the Gitea Actions workflow, which supplies GITHUB_API_URL,
|
||||
# GITHUB_REPOSITORY, GITHUB_SHA, GITHUB_WORKFLOW, GITHUB_JOB,
|
||||
# GITHUB_EVENT_NAME and GITEA_TOKEN. ANCESTOR_LIMIT (default 20) caps how
|
||||
# far back the walk looks; a value that is set but not a positive integer
|
||||
# aborts rather than silently disabling the walk.
|
||||
set -eu
|
||||
|
||||
SUPERSEDED_DESC='Superseded by a newer commit; never tested'
|
||||
|
||||
# Gitea builds the commit-status context as
|
||||
# "<workflow name> / <job name> (<event>)", so derive it rather than
|
||||
# hardcoding the result.
|
||||
#
|
||||
# The derivation is deliberately not byte-exact with Gitea's own rule and
|
||||
# must not be "fixed" into a silent fallback. Gitea uses the job's `name:`
|
||||
# (falling back to the job id) and the workflow's `name:` (falling back to
|
||||
# the workflow filename), while the runner exports GITHUB_JOB as the job
|
||||
# *id* and GITHUB_WORKFLOW as the parsed workflow `name:`. So giving the
|
||||
# job a display `name:`, or dropping the workflow's `name:`, makes the
|
||||
# derived context stop matching --- and require_own_context below then
|
||||
# turns every push red with a message. That loud failure is the point
|
||||
# (https://git.eeqj.de/sneak/webhooker/issues/147 item 2); guessing at a
|
||||
# fallback would restore the silent no-op it replaced.
|
||||
context() {
|
||||
printf '%s / %s (%s)' \
|
||||
"$GITHUB_WORKFLOW" "$GITHUB_JOB" "$GITHUB_EVENT_NAME"
|
||||
}
|
||||
|
||||
# ANCESTOR_LIMIT is a documented knob, so a value that is set but
|
||||
# unusable must fail loudly instead of defaulting
|
||||
# (https://git.eeqj.de/sneak/webhooker/issues/80). Passing it straight to
|
||||
# git would print `fatal: not an integer` into a discarded exit status
|
||||
# and mark nothing.
|
||||
ancestor_limit() {
|
||||
# `-` and not `:-`: an explicitly empty value is set-but-unusable
|
||||
# config, so it aborts like any other bad value rather than silently
|
||||
# running at the default.
|
||||
_limit="${ANCESTOR_LIMIT-20}"
|
||||
case "$_limit" in
|
||||
'' | *[!0-9]* | 0*)
|
||||
echo "ANCESTOR_LIMIT must be a positive integer," \
|
||||
"got '${_limit}'" >&2
|
||||
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$_limit"
|
||||
}
|
||||
|
||||
# The status Gitea created for this very job proves which context string
|
||||
# it uses. If the derived one is missing, the workflow or the job was
|
||||
# renamed and the match below would silently stop firing, restoring the
|
||||
# false-red bug with no signal. Fail loudly instead.
|
||||
require_own_context() {
|
||||
if ! _body="$(curl -sf --retry 3 --retry-delay 2 --max-time 30 \
|
||||
"${1}/commits/${GITHUB_SHA}/status")"; then
|
||||
echo "cannot read commit statuses for ${GITHUB_SHA}" >&2
|
||||
return 1
|
||||
fi
|
||||
_found="$(printf '%s' "$_body" | jq -r '(.statuses // [])[].context')"
|
||||
if printf '%s\n' "$_found" | grep -qxF "$2"; then
|
||||
return 0
|
||||
fi
|
||||
echo "no commit status with context '${2}' on ${GITHUB_SHA}:" >&2
|
||||
echo "workflow or job renamed? contexts present:" >&2
|
||||
printf '%s\n' "$_found" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Latest status for our context on a commit, as "state|description".
|
||||
# The read is retried and bounded, and a read that still fails aborts the
|
||||
# step: a laundered commit that cannot be read is not the same as one
|
||||
# with nothing to do, and piping curl into jq would discard the
|
||||
# difference.
|
||||
status_of() {
|
||||
if ! _sbody="$(curl -sf --retry 3 --retry-delay 2 --max-time 30 \
|
||||
"${1}/commits/${2}/status")"; then
|
||||
echo "cannot read commit statuses for ${2}" >&2
|
||||
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$_sbody" | jq -r --arg c "$3" \
|
||||
'[(.statuses // [])[] | select(.context == $c)][0] // empty
|
||||
| "\(.status)|\(.description)"'
|
||||
}
|
||||
|
||||
mark_superseded() {
|
||||
curl -sf -X POST "${1}/statuses/${2}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$(jq -nc --arg c "$3" --arg d "$SUPERSEDED_DESC" \
|
||||
'{context: $c, state: "failure", description: $d}')" \
|
||||
>/dev/null
|
||||
}
|
||||
|
||||
main() {
|
||||
_api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
|
||||
_ctx="$(context)"
|
||||
|
||||
_limit="$(ancestor_limit)"
|
||||
|
||||
require_own_context "$_api" "$_ctx"
|
||||
|
||||
# A shallow clone cannot resolve the parent, so it looks exactly like
|
||||
# a root commit to rev-parse below and would exit 0 having walked
|
||||
# nothing (or, at depth > 1, only the ancestors that happen to be
|
||||
# present). The workflow checks out with `fetch-depth: 0`; verify
|
||||
# that here rather than depend on it silently.
|
||||
if [ "$(git rev-parse --is-shallow-repository)" = 'true' ]; then
|
||||
echo "shallow repository: the ancestor walk needs full history" >&2
|
||||
|
||||
return 1
|
||||
fi
|
||||
|
||||
# A root commit legitimately has no ancestors and is not an error.
|
||||
# A SHA this repository does not have lands here too, since its
|
||||
# parent is equally unresolvable, but require_own_context above has
|
||||
# already aborted on the 404 for it. The walk itself carries no
|
||||
# `|| true`, so a rev-list failure aborts.
|
||||
if ! git rev-parse -q --verify "${GITHUB_SHA}^" >/dev/null; then
|
||||
echo "no ancestor of ${GITHUB_SHA} to check"
|
||||
|
||||
return 0
|
||||
fi
|
||||
|
||||
_walk="$(git rev-list --max-count="$_limit" "${GITHUB_SHA}^")"
|
||||
|
||||
for _sha in $_walk; do
|
||||
_latest="$(status_of "$_api" "$_sha" "$_ctx")"
|
||||
# A run that was cancelled, or one an earlier revision of this
|
||||
# script laundered into `skipped`. Anything else stands.
|
||||
case "$_latest" in
|
||||
'failure|Has been cancelled' | "skipped|${SUPERSEDED_DESC}") ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
mark_superseded "$_api" "$_sha" "$_ctx"
|
||||
echo "marked superseded: ${_sha}"
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+19
-6
@@ -1,15 +1,28 @@
|
||||
#!/bin/sh
|
||||
# script/cibuild: run the CI build. The Dockerfile runs the checks (the
|
||||
# gofmt check, golangci-lint, the stylesheet check, ESLint, the Markdown
|
||||
# check, make test), so a successful build implies a green repo. Generic:
|
||||
# needs no adaptation. The Gitea workflow runs this on push.
|
||||
# script/cibuild: run the CI build. It bootstraps first: a CI runner
|
||||
# checks out and runs this and nothing else, and script/fmt-check runs
|
||||
# the formatter on the host, which a pristine checkout cannot do.
|
||||
# --no-cache for the same reason as script/docker: the gate phases the
|
||||
# final stage depends on are RUN steps, and a cached one is a check that
|
||||
# did not run.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build .
|
||||
"$SCRIPT_DIR/bootstrap"
|
||||
"$SCRIPT_DIR/check"
|
||||
# Own line: a failing command substitution inside an argument does
|
||||
# not trip `set -e`, so the inline form degrades silently to an
|
||||
# empty constant. The VERSION build argument takes precedence over
|
||||
# the version a build stage derives from the .git in the context.
|
||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
docker build --no-cache \
|
||||
--build-arg VERSION="$version" \
|
||||
-t "$("$SCRIPT_DIR/projectname")" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+4
-2
@@ -2,14 +2,16 @@
|
||||
# script/css: regenerate static/css/tailwind.css (writes). tailwindcss is
|
||||
# never installed locally: it runs in docker, at the version and sha256
|
||||
# pinned in the Dockerfile's stylesheet stages, which also say what the
|
||||
# stylesheet is generated from.
|
||||
# stylesheet is generated from. --no-cache, as on every docker build in
|
||||
# script/, so the stylesheet is generated rather than taken from the cache.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build --target css-output --output type=local,dest=static/css .
|
||||
docker build --no-cache \
|
||||
--target css-output --output type=local,dest=static/css .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+3
-2
@@ -1,14 +1,15 @@
|
||||
#!/bin/sh
|
||||
# script/css-check: fail when static/css/tailwind.css differs from what
|
||||
# script/css would generate (read-only). The comparison is the Dockerfile's
|
||||
# css-check stage, which the image build runs too.
|
||||
# css-check stage, which the image build runs too. --no-cache because a
|
||||
# cached check is a check that did not run.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build --target css-check --output type=cacheonly .
|
||||
docker build --no-cache --target css-check --output type=cacheonly .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+11
-7
@@ -1,10 +1,8 @@
|
||||
#!/bin/sh
|
||||
# script/docker: build the Docker image tagged with the project name.
|
||||
# The tag comes from script/projectname.
|
||||
#
|
||||
# The version script/version resolves here goes in as the VERSION build
|
||||
# arg, which takes precedence over what the build would derive from the
|
||||
# .git in its context.
|
||||
# Identical in all repos; the tag comes from script/projectname.
|
||||
# --no-cache because the gate phases the final stage depends on are RUN
|
||||
# steps, and a cached one is a check that did not run.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
@@ -12,8 +10,14 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build \
|
||||
--build-arg VERSION="$("$SCRIPT_DIR/version")" \
|
||||
# Own line: a failing command substitution inside an argument does
|
||||
# not trip `set -e`, so the inline form degrades silently to an
|
||||
# empty constant. The VERSION build argument takes precedence over
|
||||
# the version a build stage derives from the .git in the context.
|
||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
docker build --no-cache \
|
||||
--build-arg VERSION="$version" \
|
||||
-t "$("$SCRIPT_DIR/projectname")" .
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1,7 +1,8 @@
|
||||
#!/bin/sh
|
||||
# script/fmt: format all files (writes): the Go code with gofmt and
|
||||
# goimports, the Markdown with prettier. prettier is never installed
|
||||
# locally: it runs in docker, in the Dockerfile's Markdown stages.
|
||||
# locally: it runs in docker, in the Dockerfile's Markdown stages, built
|
||||
# with --no-cache like every docker build in script/.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
@@ -12,7 +13,8 @@ main() {
|
||||
if command -v goimports >/dev/null 2>&1; then
|
||||
goimports -w .
|
||||
fi
|
||||
docker build --target markdown-output --output type=local,dest=. .
|
||||
docker build --no-cache \
|
||||
--target markdown-output --output type=local,dest=. .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
# script/fmt-check: check formatting (read-only). Same scope as
|
||||
# script/fmt, but fails instead of writing.
|
||||
# script/fmt, but fails instead of writing. --no-cache because a cached
|
||||
# check is a check that did not run.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
@@ -12,7 +13,7 @@ main() {
|
||||
gofmt -s -l .
|
||||
exit 1
|
||||
fi
|
||||
docker build --target markdown-check --output type=cacheonly .
|
||||
docker build --no-cache --target markdown-check --output type=cacheonly .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+13
-61
@@ -1,71 +1,23 @@
|
||||
#!/bin/sh
|
||||
# script/lint: run the linters, golangci-lint over the Go code and then
|
||||
# ESLint over static/js/. Neither is ever installed locally.
|
||||
# script/lint: run the linter. Linting is a phase of the Dockerfile and
|
||||
# this builds that phase alone; the linter is never installed or run on
|
||||
# a developer host, where a shared result cache and a host-global lock
|
||||
# make its answer untrustworthy.
|
||||
#
|
||||
# golangci-lint runs via docker only, one way, everywhere — script/lint builds
|
||||
# Dockerfile.lint, which COPYs the repo into the pinned golangci-lint image
|
||||
# and lints as a build step. This works even when the docker daemon is remote
|
||||
# and bind mounts are impossible, and it removes the host linter's shared
|
||||
# cache, which has attributed other checkouts' findings to this one.
|
||||
#
|
||||
# --no-cache-filter=lint forces the lint stage to re-execute on every run; a
|
||||
# cached lint stage exits 0 in under a second having linted nothing. The deps
|
||||
# stage keeps its cache, so module downloads are not repeated.
|
||||
# --progress=plain keeps the linter's own output visible on success, so a
|
||||
# passing run shows the issue count rather than nothing.
|
||||
# --output=type=cacheonly leaves no image behind to clean up.
|
||||
#
|
||||
# docker silently ignores --no-cache-filter for a stage name that does not
|
||||
# match, so a rename or a typo would restore the cached false green with no
|
||||
# warning and a fast exit 0. The flag is therefore not trusted: the build
|
||||
# output is teed to a log and a run is only a pass if golangci-lint's own
|
||||
# summary line ("N issues." / "N issues:") is in it. No summary, no lint,
|
||||
# whatever the exit code says.
|
||||
# The phase is not the last stage in the file, so it is built only when
|
||||
# --target names it. --no-cache because a cached lint layer is a lint
|
||||
# that did not run. The tag makes each build replace the previous image
|
||||
# instead of leaving a dangling one behind.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
log="$(mktemp -t webhooker-lint.XXXXXXXX)"
|
||||
rcfile="$(mktemp -t webhooker-lint-rc.XXXXXXXX)"
|
||||
trap 'rm -f "$log" "$rcfile"' EXIT INT TERM
|
||||
|
||||
# The pipeline's status is tee's, and POSIX sh has no pipefail, so the
|
||||
# build's status travels via a file. Output still streams live.
|
||||
{
|
||||
docker build \
|
||||
-f Dockerfile.lint \
|
||||
--no-cache-filter=lint \
|
||||
--progress=plain \
|
||||
--output=type=cacheonly \
|
||||
. 2>&1 && echo 0 >"$rcfile" || echo $? >"$rcfile"
|
||||
} | tee "$log" >&2
|
||||
|
||||
rc="$(cat "$rcfile")"
|
||||
[ "$rc" -eq 0 ] || exit "$rc"
|
||||
|
||||
if ! grep -qE '[0-9]+ issues[.:]' "$log"; then
|
||||
echo "script/lint: golangci-lint printed no summary line; the linter" >&2
|
||||
echo " did not run. Check that the stage named in --no-cache-filter" >&2
|
||||
echo " still matches a stage in Dockerfile.lint." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ESLint runs in the Dockerfile's js-lint stage, which the image build
|
||||
# runs too. It prints nothing on a pass, so there is no summary to look
|
||||
# for. Instead the stage is named once, for both flags: --target fails
|
||||
# on a name that matches no stage, so a rename cannot leave
|
||||
# --no-cache-filter silently ignored. The js-deps stage, which installs
|
||||
# ESLint, keeps its cache, so ESLint is not downloaded again.
|
||||
js_stage=js-lint
|
||||
docker build \
|
||||
--target "$js_stage" \
|
||||
--no-cache-filter="$js_stage" \
|
||||
--progress=plain \
|
||||
--output=type=cacheonly \
|
||||
.
|
||||
docker build --no-cache \
|
||||
--target lint \
|
||||
-t "$("$SCRIPT_DIR/projectname")-lint" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+10
-75
@@ -1,84 +1,19 @@
|
||||
#!/bin/sh
|
||||
# script/test: run the test suite.
|
||||
#
|
||||
# -timeout is applied by `go test` per package, not to the run as a whole, so
|
||||
# it only has to clear the slowest single package. When this budget was set
|
||||
# that was internal/handlers, measured in a cache-defeated builder stage on the
|
||||
# 48-core shared build host (2026-08-18); load- and host-dependent, not
|
||||
# invariants:
|
||||
#
|
||||
# 16.9s host load 5-20, GOMAXPROCS 48
|
||||
# 45.9s / 47.3s / 49.0s three runs at deliberate host load 31-73
|
||||
# 30.6s / 39.7s host load 5-20, GOMAXPROCS 6 / 4
|
||||
# 67.3s / 97.5s host load 5-20, GOMAXPROCS 2 / 1
|
||||
# 67.3s GOMAXPROCS 4 at deliberate host load 52-68
|
||||
#
|
||||
# The old 30s budget was breached by every loaded run and by every GOMAXPROCS
|
||||
# at or below 6; at GOMAXPROCS 4 it failed outright ("panic: test timed out
|
||||
# after 30s"), reproduced on 33e4fa4 with no other change.
|
||||
#
|
||||
# 90s matches the org-wide backstop in REPO_POLICIES.md and is sized here
|
||||
# against the figures above: the worst case under native parallelism is 49.0s,
|
||||
# and the compound GOMAXPROCS-4-under-load case at 67.3s sits at 75% of it.
|
||||
# The one figure above 90s is GOMAXPROCS 1, a synthetic core floor rather than
|
||||
# a condition CI runs under. If a CPU-limited runner ever puts a real run near
|
||||
# 67s, that is the datum to revisit the org figure with.
|
||||
#
|
||||
# Those figures predate tests hashing the admin password at 1 MB instead of
|
||||
# 64 MB (https://git.eeqj.de/sneak/webhooker/pulls/404). After that change, in
|
||||
# a cache-defeated build at host load 44-109 (2026-10-02), internal/handlers
|
||||
# took 8.5s and the slowest package was internal/database at 15.8s. Once its
|
||||
# retention tests seeded 50 rows per insert instead of 500
|
||||
# (https://git.eeqj.de/sneak/webhooker/issues/198), internal/database took
|
||||
# 7.3s and the slowest package was internal/handlers at 8.1s to 10.0s, at host
|
||||
# load 25-48 (2026-10-02).
|
||||
#
|
||||
# -p 4 -parallel 8 keep the run under 2 GB of memory: at most four test
|
||||
# binaries build or run at once, each with at most eight parallel tests. Under
|
||||
# -race every test binary and every link costs a few hundred MB, so the
|
||||
# defaults (one per core) add up to several GB on a many-core host.
|
||||
#
|
||||
# The first run has no -v: go test then prints one result line per package,
|
||||
# with its coverage, and for a package that fails, everything its tests wrote,
|
||||
# application log lines included. Verbose output from the whole suite passes
|
||||
# the 2 MiB at which the Docker build cuts off each step's log, so on a failure
|
||||
# only the tests that failed run again, with -v. The script exits 1 after that
|
||||
# rerun whatever its result: the first run already showed the suite is broken.
|
||||
# script/test: run the test suite. Testing is a phase of the Dockerfile
|
||||
# and this builds that phase alone, on the same terms as script/lint:
|
||||
# --target because a phase that is not the last stage is built only when
|
||||
# named, --no-cache because a cached test layer is a test that did not
|
||||
# run, and a tag so each build replaces the previous image.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
"$ROOT/script/assets"
|
||||
|
||||
log="$(mktemp -t webhooker-test.XXXXXXXX)"
|
||||
rcfile="$(mktemp -t webhooker-test-rc.XXXXXXXX)"
|
||||
trap 'rm -f "$log" "$rcfile"' EXIT INT TERM
|
||||
|
||||
# The pipeline's status is tee's, and POSIX sh has no pipefail, so go
|
||||
# test's status travels via a file. Output still streams live.
|
||||
{
|
||||
go test -race -cover -p 4 -parallel 8 -timeout 90s ./... 2>&1 \
|
||||
&& echo 0 >"$rcfile" || echo $? >"$rcfile"
|
||||
} | tee "$log"
|
||||
if [ "$(cat "$rcfile")" -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# go test reports a failed test as a line starting "--- FAIL: TestName"
|
||||
# (a failed subtest's line is indented, and reruns with its parent), and
|
||||
# a failed package as "FAIL<tab>package/path<tab>...". A failure that
|
||||
# names no test, such as a build error or a timeout, is already shown in
|
||||
# full above, so there is nothing to rerun.
|
||||
tests="$(awk '/^--- FAIL: / { print $3 }' "$log" | paste -s -d '|' -)"
|
||||
packages="$(awk '/^FAIL\t/ { print $2 }' "$log")"
|
||||
if [ -n "$tests" ]; then
|
||||
echo "--- Rerunning the failed tests with -v for details ---"
|
||||
go test -race -v -p 4 -parallel 8 -timeout 90s \
|
||||
-run "^($tests)\$" $packages || true
|
||||
fi
|
||||
exit 1
|
||||
docker build --no-cache \
|
||||
--target test \
|
||||
-t "$("$SCRIPT_DIR/projectname")-test" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+2
-3
@@ -3,8 +3,7 @@
|
||||
# Docker: Dockerfile.browser builds the test and runs it in a digest-pinned
|
||||
# headless browser image, so the host needs no browser.
|
||||
#
|
||||
# --no-cache-filter=browser runs the test again even when nothing changed;
|
||||
# it must name the stage in Dockerfile.browser that runs it.
|
||||
# --no-cache because a cached test layer is a test that did not run.
|
||||
# --output=type=cacheonly leaves no image behind to clean up.
|
||||
set -eu
|
||||
|
||||
@@ -14,7 +13,7 @@ main() {
|
||||
cd "$ROOT"
|
||||
docker build \
|
||||
-f Dockerfile.browser \
|
||||
--no-cache-filter=browser \
|
||||
--no-cache \
|
||||
--progress=plain \
|
||||
--output=type=cacheonly \
|
||||
.
|
||||
|
||||
+5
-3
@@ -1,9 +1,11 @@
|
||||
#!/bin/sh
|
||||
# script/version: output the version string the binary is stamped with.
|
||||
# Our own extension to scripts-to-rule-them-all. The Makefile's build
|
||||
# target and script/docker both take the value from here, so a `make
|
||||
# build` binary and a `make docker` image built from the same checkout
|
||||
# report the same thing.
|
||||
# and version targets take the value from here, and the Dockerfile's
|
||||
# build stage calls them. script/docker and script/cibuild run the same
|
||||
# `git describe` on the host and pass the result in as $VERSION, so a
|
||||
# `make build` binary and a `make docker` image built from the same
|
||||
# checkout report the same thing.
|
||||
#
|
||||
# Order of precedence:
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user