bring the repo up to org standards
Adopt the standard tooling: a Makefile of thin shims over a full scripts-to-rule-them-all `script/` set, the canonical `.golangci.yml` vendored byte-identical from `sneak/prompts`, docker-only linting via `Dockerfile.lint`, a `Dockerfile` and Gitea workflow that gate every push, and prettier/editorconfig/dockerignore config. `make build` pointed at a `cmd/dcfinfo` that is not in the tree and could never have succeeded; this repo is a library, so `build` is now the compile check over every package. Clear the 57 findings the canonical linter config reports on `pkg/dcf`. Two were real: `findDCFMountPoints` checked a never-assigned `erro` instead of the error from `findAllMountPoints`, discarding it, and `privatePath` was computed twice so the first computation was dead. Mountpoint selection otherwise behaves exactly as before; the defects that survive are filed as issue #6, not fixed here. Rename `DCFStore` to `Store` and `DCFObject` to `Object` (with its `DCFStoreRoot` field to `StoreRoot`), which revive's stutter rule requires and which the `fs.FS` rework in issue #4 will build on. Replace the placeholder test with tests over the exported surface. The filesystem walk stays uncovered: it is reachable only through `GetDCFStores`, which needs real mounted media. README keeps its content, reorganised into the required sections and gaining Entrypoints. (closes #1)
This commit is contained in:
166
script/assert-context-complete
Executable file
166
script/assert-context-complete
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/bin/sh
|
||||
# script/assert-context-complete: fail unless the build context a stage
|
||||
# was given contained every source file the repository has.
|
||||
#
|
||||
# Usage: script/assert-context-complete LOG STAGE EXPECTED
|
||||
#
|
||||
# LOG must be `docker build --progress=plain` output. EXPECTED is a list
|
||||
# of repo-relative paths, one per line, as script/repo-source-manifest
|
||||
# prints it.
|
||||
#
|
||||
# script/assert-step-ran answers "did the tool execute". This answers
|
||||
# the other half: "was the tool given the tree". They are different
|
||||
# questions, and a green build can satisfy the first while failing the
|
||||
# second — a .dockerignore entry, or a COPY that brings in less than the
|
||||
# whole tree, removes files from the context silently, and golangci-lint
|
||||
# then genuinely runs, genuinely examines what it was handed, and
|
||||
# genuinely reports `0 issues.` over a repo that has a violation in it.
|
||||
# `go test ./...` likewise never runs a package that did not arrive.
|
||||
#
|
||||
# How it knows: each source-consuming stage emits, right after its
|
||||
# `COPY . .`, an inventory of the Go files actually present —
|
||||
#
|
||||
# context-manifest-begin
|
||||
# context-file: cmd/gotemplate/main.go
|
||||
# ...
|
||||
# context-manifest-end
|
||||
#
|
||||
# — and this compares that against EXPECTED, which comes from the git
|
||||
# index rather than from the same build. The two sources are
|
||||
# independent: .dockerignore decides the first and cannot touch the
|
||||
# second. An expectation read from the evidence would prove nothing.
|
||||
#
|
||||
# The manifest lines are only believed under the same discipline
|
||||
# script/assert-step-ran applies to a tool's success line: they must be
|
||||
# attributed to the id of a step whose header is "#ID [STAGE n/m] CMD"
|
||||
# with CMD matching the manifest command, which BuildKit reported DONE,
|
||||
# and both sentinels must be present. So another step's output does not
|
||||
# count, a header does not count, a cached step (which writes nothing)
|
||||
# does not count, and a truncated log fails rather than passing with a
|
||||
# short list.
|
||||
#
|
||||
# Only one direction is checked: everything the repo has must have
|
||||
# arrived. Files in the context that git does not track are not a
|
||||
# failure — untracked local work is normal and hides nothing.
|
||||
#
|
||||
# What this does not cover: a Dockerfile edit to the manifest step
|
||||
# itself, and evidence forged inside a matched command. Both are visible
|
||||
# in the Dockerfile in plain sight, and both are equally outside
|
||||
# script/assert-step-ran. This is not an exhaustive list of ways a green
|
||||
# run can be untrue; it is the ones known.
|
||||
set -eu
|
||||
|
||||
die() {
|
||||
echo "script/assert-context-complete: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ $# -eq 3 ] || die "usage: $0 LOG STAGE EXPECTED"
|
||||
[ -r "$1" ] || die "cannot read $1"
|
||||
[ -r "$3" ] || die "cannot read $3"
|
||||
|
||||
# The stage and the expected-list path travel in the environment, not in
|
||||
# -v: awk expands escape sequences in a -v assignment.
|
||||
ACC_STAGE="$2" ACC_EXPECTED="$3" awk '
|
||||
function fail(msg) {
|
||||
printf "script/assert-context-complete: %s\n", msg | "cat 1>&2"
|
||||
close("cat 1>&2")
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The package a missing file belongs to — its directory, which is what a
|
||||
# reader needs named. A bare "." for a root-level file reads as a typo
|
||||
# next to the sentence punctuation, so it is spelled out.
|
||||
function pkgof(path, i) {
|
||||
i = length(path)
|
||||
while (i > 0 && substr(path, i, 1) != "/") i--
|
||||
return i == 0 ? "the repository root" : substr(path, 1, i - 1)
|
||||
}
|
||||
|
||||
function preview(list, n, limit, i, out) {
|
||||
for (i = 1; i <= n && i <= limit; i++)
|
||||
out = out (i > 1 ? ", " : "") list[i]
|
||||
if (n > limit)
|
||||
out = out sprintf(", and %d more", n - limit)
|
||||
return out
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
stage = ENVIRON["ACC_STAGE"]
|
||||
|
||||
# Fixed by the protocol the Dockerfile emits, so callers cannot
|
||||
# drift from it.
|
||||
step = "^RUN echo context-manifest-begin"
|
||||
beginmark = "context-manifest-begin"
|
||||
endmark = "context-manifest-end"
|
||||
prefix = "context-file: "
|
||||
|
||||
expected_path = ENVIRON["ACC_EXPECTED"]
|
||||
while ((getline line < expected_path) > 0)
|
||||
if (line != "") expected[++nexpected] = line
|
||||
close(expected_path)
|
||||
|
||||
if (nexpected == 0)
|
||||
fail(sprintf("the expected-file list (%s) is empty, so any build context would satisfy this check", expected_path))
|
||||
}
|
||||
|
||||
# Every progress line is "#ID " and then a step header, a status, or one
|
||||
# line the step itself wrote.
|
||||
/^#[0-9]+ / {
|
||||
id = substr($1, 2)
|
||||
rest = substr($0, length($1) + 2)
|
||||
|
||||
if (rest == "CACHED") { cached[id] = 1; next }
|
||||
if (rest ~ /^DONE /) { done[id] = 1; next }
|
||||
|
||||
# Header: "[STAGE n/m] CMD", or "[PLATFORM STAGE n/m] CMD" when the
|
||||
# build names a platform. Bracketed spans with no n/m are BuildKit
|
||||
# internals, not steps.
|
||||
if (substr(rest, 1, 1) == "[") {
|
||||
p = index(rest, "] ")
|
||||
if (p == 0) next
|
||||
n = split(substr(rest, 2, p - 2), part, " ")
|
||||
if (n < 2 || part[n] !~ /^[0-9]+\/[0-9]+$/) next
|
||||
if (part[n - 1] != stage) next
|
||||
if (substr(rest, p + 2) ~ step) matched[id] = 1
|
||||
next
|
||||
}
|
||||
|
||||
# Output: "#ID 0.31 <the line the step wrote>".
|
||||
if (rest ~ /^[0-9]+[.][0-9]+ /) {
|
||||
sub(/^[0-9]+[.][0-9]+ /, "", rest)
|
||||
if (rest == beginmark) begun[id] = 1
|
||||
else if (rest == endmark) ended[id] = 1
|
||||
else if (index(rest, prefix) == 1)
|
||||
arrived[id SUBSEP substr(rest, length(prefix) + 1)] = 1
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
for (id in matched) {
|
||||
nmatched++
|
||||
if (cached[id]) ncached++
|
||||
if (done[id] && begun[id] && ended[id]) chosen = id
|
||||
}
|
||||
|
||||
if (nmatched == 0)
|
||||
fail(sprintf("the build ran no step matching /%s/ in stage \"%s\", so the build context was never inventoried. Either the stage is not in the build graph — renamed, deleted, or nothing the final stage builds depends on it any more — or the manifest step was taken out of it", step, stage))
|
||||
|
||||
if (chosen == "" && ncached == nmatched)
|
||||
fail(sprintf("every step matching /%s/ in stage \"%s\" was served from cache, so its inventory describes an earlier tree rather than this one; the cache bust for that stage is not taking effect", step, stage))
|
||||
|
||||
if (chosen == "")
|
||||
fail(sprintf("a step matching /%s/ ran in stage \"%s\" but wrote no complete inventory between %s and %s. The log is truncated, the build was not run with --progress=plain, or that step no longer emits the manifest", step, stage, beginmark, endmark))
|
||||
|
||||
for (i = 1; i <= nexpected; i++) {
|
||||
if (arrived[chosen SUBSEP expected[i]]) continue
|
||||
missing[++nmissing] = expected[i]
|
||||
pkg = pkgof(expected[i])
|
||||
if (!(pkg in seenpkg)) { seenpkg[pkg] = 1; pkgs[++npkgs] = pkg }
|
||||
}
|
||||
|
||||
if (nmissing == 0) exit 0
|
||||
|
||||
fail(sprintf("%d of the %d source files this repository tracks never reached the build context of stage \"%s\", so nothing examined them. Missing package(s): %s. Missing file(s): %s. A .dockerignore entry, or a COPY that brings in less than the whole tree, drops files silently: the tool still runs, still finds nothing wrong with what it was handed, and still reports success", nmissing, nexpected, stage, preview(pkgs, npkgs, 10), preview(missing, nmissing, 10)))
|
||||
}
|
||||
' "$1"
|
||||
Reference in New Issue
Block a user