#!/bin/sh
# script/cibuild: run the CI build. The Dockerfile runs the checks
# (gofmt, config verify, lint, test), so a successful build implies a
# green repo. The Gitea workflow runs this on push.
#
# Only if the checks actually ran, though, and a green `docker build` is
# no evidence that they did:
#
# 1. On an unchanged tree every layer comes from cache and the build
#    exits 0 in under a second having executed nothing. Hence the two
#    --no-cache-filter flags.
#
# 2. BuildKit silently ignores a --no-cache-filter naming a stage that
#    does not exist, so a rename restores that green no-op.
#
# 3. BuildKit builds only the final stage's dependency graph. A stage
#    nothing references is never built, and with no --target the final
#    stage is whichever is last in the file — so appending a stage, or
#    moving a reference into a stage that is itself unreachable, drops
#    the checks out of the build while every reference to them is still
#    there to read.
#
# Rather than predict any of that from the Dockerfile's text, this asks
# the finished build what it did: script/assert-step-ran requires that
# the log contain a lint step and a test step that ran, and that each
# wrote the line its own tool writes on success. Every trap above ends
# with the step absent from the log or served from cache, and both fail
# that assertion. See that script for what it does not cover.
#
# 4. A step that ran is not a step that saw the repo. .dockerignore, or
#    a COPY narrower than the tree, removes files from the build
#    context, and the linter then reports `0 issues.` over what is left
#    while `go test ./...` never compiles the package that went missing.
#    So each source-consuming stage inventories what reached it, and
#    script/assert-context-complete compares that inventory against the
#    git index — a source .dockerignore cannot reach.
set -eu

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"

dockerfile=Dockerfile
lint_stage=lint
test_stage=builder

# `ok <pkg> 1.234s`: go test prints `(cached)` in place of the duration
# when it replays a result, so requiring the duration requires a package
# that was really exercised.
test_ran='^ok[[:space:]]+[^[:space:]]+[[:space:]]+[0-9]+[.][0-9]+s'

main() {
    cd "$ROOT"

    tmp="$(mktemp -d "${TMPDIR:-/tmp}/$("$SCRIPT_DIR/projectname")-cibuild.XXXXXX")"
    trap 'rm -rf "$tmp"' EXIT INT TERM

    # --progress=plain is load-bearing: the assertions below read the
    # steps' own output out of this log, and the tty renderer discards
    # all but the tail of it. The exit status travels through a file
    # because a pipeline's status is the last command's; `set +e` is
    # what lets the recording line run at all, since errexit would
    # otherwise abandon the subshell on a failing build. A missing file
    # reads as failure.
    (
        set +e
        docker build \
            --progress=plain \
            --no-cache-filter="$lint_stage" \
            --no-cache-filter="$test_stage" \
            -f "$dockerfile" . 2>&1
        echo "$?" >"$tmp/status"
    ) | tee "$tmp/build.log"

    status="$(cat "$tmp/status" 2>/dev/null || echo 1)"
    [ "${status:-1}" -eq 0 ] || exit "${status:-1}"

    script/assert-step-ran "$tmp/build.log" "$lint_stage" \
        '^RUN golangci-lint run' '^0 issues[.]$' 'the linter'
    script/assert-step-ran "$tmp/build.log" "$test_stage" \
        '^RUN go test' "$test_ran" 'the tests'

    # Both stages, separately: each has its own COPY, so an intact
    # context in one is no evidence about the other.
    script/repo-source-manifest >"$tmp/expected"
    script/assert-context-complete "$tmp/build.log" "$lint_stage" "$tmp/expected"
    script/assert-context-complete "$tmp/build.log" "$test_stage" "$tmp/expected"
}

main "$@"
