Replaces #559, which conflicted. Model: opus-5-5
This commit is contained in:
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
# 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. make build, make dev
|
||||
# and the Dockerfile's lint, test and build stages run this first.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
tar -xzOf 3p/alpinejs-csp-3.14.9.tgz package/dist/cdn.min.js \
|
||||
>static/js/alpine.min.js
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+14
-12
@@ -3,16 +3,15 @@
|
||||
# this repo. Idempotent: every install is guarded by a check so already
|
||||
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
||||
# or apk (detected in that order); assumes NOTHING is present (not git,
|
||||
# make, or go). golangci-lint is deliberately not installed: linting runs
|
||||
# only in docker, via script/lint and Dockerfile.lint. Finishes by running
|
||||
# script/fetch-assets, which installs the hash-pinned third-party browser
|
||||
# assets the repo does not commit.
|
||||
# make, or go). golangci-lint, node, ESLint and prettier are deliberately
|
||||
# not installed: they run only in docker, via script/lint and script/fmt.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
PKGMGR=""
|
||||
SUDO=""
|
||||
APT_UPDATED=""
|
||||
|
||||
detect_pkgmgr() {
|
||||
[ -n "$PKGMGR" ] && return 0
|
||||
@@ -41,7 +40,14 @@ pkg_install() {
|
||||
detect_pkgmgr
|
||||
case "$PKGMGR" in
|
||||
nix) nix-env -iA "nixpkgs.$1" ;;
|
||||
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
|
||||
apt)
|
||||
# Package lists may be empty (fresh images); refresh once per run.
|
||||
if [ -z "$APT_UPDATED" ]; then
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
APT_UPDATED=1
|
||||
fi
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
|
||||
;;
|
||||
brew) brew install "$3" ;;
|
||||
apk) apk add --no-cache "$4" ;;
|
||||
esac
|
||||
@@ -62,18 +68,14 @@ 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 needs 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 requires it" >&2
|
||||
echo "bootstrap: docker not found; script/test, script/lint, script/fmt and script/css require it" >&2
|
||||
fi
|
||||
|
||||
go mod download
|
||||
|
||||
# Third-party browser assets are not committed; fetch and verify them
|
||||
# so a fresh clone can build and test.
|
||||
if missing curl; then pkg_install curl curl curl curl; fi
|
||||
"$ROOT/script/fetch-assets"
|
||||
|
||||
echo "bootstrap complete"
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -1,7 +1,8 @@
|
||||
#!/bin/sh
|
||||
# script/check: run all checks (test, lint, fmt-check). Our own
|
||||
# extension to scripts-to-rule-them-all. Must not modify any files.
|
||||
# Generic: usually needs no adaptation.
|
||||
# script/check: run all checks (test, lint, fmt-check, css-check). Our own
|
||||
# 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
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
@@ -10,6 +11,7 @@ main() {
|
||||
"$SCRIPT_DIR/test"
|
||||
"$SCRIPT_DIR/lint"
|
||||
"$SCRIPT_DIR/fmt-check"
|
||||
"$SCRIPT_DIR/css-check"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -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
|
||||
# (make fmt-check, lint, 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 "$@"
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
# 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. --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 --no-cache \
|
||||
--target css-output --output type=local,dest=static/css .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+15
@@ -0,0 +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. --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 --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.
|
||||
#
|
||||
# .dockerignore excludes .git/, so the builder stage cannot derive the
|
||||
# version itself. It is resolved here, where the checkout is, and passed
|
||||
# in as a build arg; without it the image would stamp itself "unknown".
|
||||
# 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")" .
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/bin/sh
|
||||
# script/fetch-assets: download the third-party browser assets the web UI
|
||||
# ships and install them under static/. Minified bundles are not committed
|
||||
# (REPO_POLICIES.md: no build artifacts in version control), so the build
|
||||
# fetches them here. Every download is verified against a hardcoded sha256
|
||||
# before it is installed, and any mismatch aborts. Idempotent: an asset
|
||||
# already present with its pinned hash is left alone.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
# The sha256 of each installed asset lives in static/vendor.sha256, in
|
||||
# sha256sum(1) format, with paths relative to static/. That file is the
|
||||
# single source of truth: this script verifies against it, and
|
||||
# static/vendor_test.go asserts the bytes embedded into the binary match
|
||||
# it, so the hash cannot rot into a value nothing checks.
|
||||
MANIFEST="static/vendor.sha256"
|
||||
|
||||
# Alpine.js 3.14.9, 2026-08-17. Fetched from registry.npmjs.org, the
|
||||
# publisher of record; the jsDelivr and unpkg copies are mirrors of this
|
||||
# same tarball. dist/cdn.min.js is the browser build Alpine publishes for
|
||||
# a <script> tag.
|
||||
ALPINE_VERSION="3.14.9"
|
||||
ALPINE_URL="https://registry.npmjs.org/alpinejs/-/alpinejs-${ALPINE_VERSION}.tgz"
|
||||
# sha256 of alpinejs-3.14.9.tgz
|
||||
ALPINE_TARBALL_SHA256="97dad7c0c81e659cfc8e7700055da9770f8186187cb9a8a76efb57e00d5ce52a"
|
||||
ALPINE_MEMBER="package/dist/cdn.min.js"
|
||||
ALPINE_DEST="js/alpine.min.js"
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$1" | cut -d' ' -f1
|
||||
else
|
||||
shasum -a 256 "$1" | cut -d' ' -f1
|
||||
fi
|
||||
}
|
||||
|
||||
# expected_sha256 <path-relative-to-static>
|
||||
expected_sha256() {
|
||||
awk -v want="$1" '$2 == want { print $1; found = 1 }
|
||||
END { if (!found) exit 1 }' "$ROOT/$MANIFEST"
|
||||
}
|
||||
|
||||
# verify <file> <expected-sha256> <what>
|
||||
verify() {
|
||||
actual="$(sha256_of "$1")"
|
||||
if [ "$actual" != "$2" ]; then
|
||||
echo "fetch-assets: sha256 mismatch for $3" >&2
|
||||
echo " expected: $2" >&2
|
||||
echo " actual: $actual" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# up_to_date <path-relative-to-static> <expected-sha256>
|
||||
up_to_date() {
|
||||
[ -f "$ROOT/static/$1" ] || return 1
|
||||
[ "$(sha256_of "$ROOT/static/$1")" = "$2" ]
|
||||
}
|
||||
|
||||
fetch_alpine() {
|
||||
want="$(expected_sha256 "$ALPINE_DEST")"
|
||||
|
||||
if up_to_date "$ALPINE_DEST" "$want"; then
|
||||
echo "fetch-assets: static/$ALPINE_DEST already at $want"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "fetch-assets: fetching Alpine.js $ALPINE_VERSION from $ALPINE_URL"
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||
curl -fsSL -o "$tmp/alpine.tgz" "$ALPINE_URL"
|
||||
verify "$tmp/alpine.tgz" "$ALPINE_TARBALL_SHA256" "alpinejs-${ALPINE_VERSION}.tgz"
|
||||
tar -xzOf "$tmp/alpine.tgz" "$ALPINE_MEMBER" >"$tmp/alpine.min.js"
|
||||
verify "$tmp/alpine.min.js" "$want" "$ALPINE_MEMBER from alpinejs-${ALPINE_VERSION}.tgz"
|
||||
|
||||
mkdir -p "$(dirname "$ROOT/static/$ALPINE_DEST")"
|
||||
cp "$tmp/alpine.min.js" "$ROOT/static/$ALPINE_DEST"
|
||||
rm -rf "$tmp"
|
||||
trap - EXIT INT TERM
|
||||
echo "fetch-assets: installed static/$ALPINE_DEST ($want)"
|
||||
}
|
||||
|
||||
# Re-check every manifest entry against what is now on disk, so an entry
|
||||
# no script installs fails loudly instead of passing silently.
|
||||
verify_manifest() {
|
||||
while read -r want path; do
|
||||
case "$want" in '' | '#'*) continue ;; esac
|
||||
if [ ! -f "$ROOT/static/$path" ]; then
|
||||
echo "fetch-assets: $MANIFEST lists static/$path, which is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
verify "$ROOT/static/$path" "$want" "static/$path"
|
||||
done <"$ROOT/$MANIFEST"
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
fetch_alpine
|
||||
verify_manifest
|
||||
echo "fetch-assets: all assets in $MANIFEST verified"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+6
-1
@@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
# script/fmt: format all files (writes).
|
||||
# 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, built
|
||||
# with --no-cache like every docker build in script/.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
@@ -10,6 +13,8 @@ main() {
|
||||
if command -v goimports >/dev/null 2>&1; then
|
||||
goimports -w .
|
||||
fi
|
||||
docker build --no-cache \
|
||||
--target markdown-output --output type=local,dest=. .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+3
-1
@@ -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,6 +13,7 @@ main() {
|
||||
gofmt -s -l .
|
||||
exit 1
|
||||
fi
|
||||
docker build --no-cache --target markdown-check --output type=cacheonly .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+13
-45
@@ -1,55 +1,23 @@
|
||||
#!/bin/sh
|
||||
# script/lint: run the linter. golangci-lint is never installed locally: it
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# --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
|
||||
docker build --no-cache \
|
||||
--target lint \
|
||||
-t "$("$SCRIPT_DIR/projectname")-lint" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+10
-25
@@ -1,34 +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. That is 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.
|
||||
# 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"
|
||||
go test -v -race -timeout 90s ./...
|
||||
docker build --no-cache \
|
||||
--target test \
|
||||
-t "$("$SCRIPT_DIR/projectname")-test" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# script/test-browser: run the browser test in internal/server. It runs in
|
||||
# Docker: Dockerfile.browser builds the test and runs it in a digest-pinned
|
||||
# headless browser image, so the host needs no browser.
|
||||
#
|
||||
# --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
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build \
|
||||
-f Dockerfile.browser \
|
||||
--no-cache \
|
||||
--progress=plain \
|
||||
--output=type=cacheonly \
|
||||
.
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+10
-10
@@ -1,24 +1,24 @@
|
||||
#!/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:
|
||||
#
|
||||
# 1. $VERSION, if set and non-empty. This is how the value reaches a
|
||||
# build that cannot derive it: .dockerignore excludes .git/, so the
|
||||
# builder stage has no git metadata and the Dockerfile takes the
|
||||
# value as a build arg instead.
|
||||
# 1. $VERSION, if set and non-empty: an explicit value, such as the
|
||||
# Dockerfile's VERSION build arg.
|
||||
# 2. `git describe --tags --always --dirty` against this checkout. At
|
||||
# a clean tagged commit that is exactly the tag; otherwise it
|
||||
# carries the short SHA, the commit distance when a tag is
|
||||
# reachable, and a -dirty suffix for uncommitted changes.
|
||||
# 3. "unknown", for a tree with no git metadata and no $VERSION -- a
|
||||
# source tarball, or `docker build .` with no --build-arg. That
|
||||
# case must not fail the build and must not name a tag the tree may
|
||||
# not be at, so it names nothing.
|
||||
# source tarball, or a `docker build` with no .git in its context
|
||||
# and no VERSION build arg. That case must not fail the build and
|
||||
# must not name a tag the tree may not be at, so it names nothing.
|
||||
#
|
||||
# The git step insists the enclosing repository is this checkout, not
|
||||
# merely some repository above it: an unpacked tarball sitting inside an
|
||||
|
||||
Reference in New Issue
Block a user