Compare commits
1 Commits
c4022c0834
...
9b3baec214
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b3baec214 |
@@ -1,6 +1,3 @@
|
|||||||
# .ci-fingerprint is deliberately NOT excluded: it is the CI cache barrier
|
|
||||||
# that keeps the check stages from replaying a cached pass. See the lint
|
|
||||||
# stage of the Dockerfile.
|
|
||||||
.git/
|
.git/
|
||||||
bin/
|
bin/
|
||||||
*.md
|
*.md
|
||||||
|
|||||||
@@ -11,53 +11,5 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 2024-10-23
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 2024-10-23
|
||||||
with:
|
|
||||||
# The fingerprint step below needs history to find the last commit
|
|
||||||
# that touched the Docker build context.
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Neutralize superseded run statuses
|
|
||||||
# Gitea cancels the in-flight run when another commit is pushed to the
|
|
||||||
# same branch and records the cancellation as `failure`, so a commit
|
|
||||||
# that was never tested reads red. The cancellation is unconditional
|
|
||||||
# server-side for push events and cannot be disabled from a workflow
|
|
||||||
# file, so the superseding run rewrites those statuses to `skipped`.
|
|
||||||
# Only the exact cancellation status is touched; a real failure is
|
|
||||||
# left alone.
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
|
|
||||||
ctx='check / check (push)'
|
|
||||||
for sha in $(git rev-list --max-count=20 "${GITHUB_SHA}^" || true); do
|
|
||||||
latest="$(curl -sf "${api}/commits/${sha}/status" | jq -r \
|
|
||||||
--arg c "$ctx" \
|
|
||||||
'[.statuses[] | select(.context == $c)][0] // empty
|
|
||||||
| "\(.status)|\(.description)"')" || continue
|
|
||||||
[ "$latest" = 'failure|Has been cancelled' ] || continue
|
|
||||||
curl -sf -X POST "${api}/statuses/${sha}" \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d "$(jq -nc --arg c "$ctx" '{
|
|
||||||
context: $c,
|
|
||||||
state: "skipped",
|
|
||||||
description: "Superseded by a newer commit; never tested"
|
|
||||||
}')" >/dev/null
|
|
||||||
echo "neutralized superseded status on ${sha}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Fingerprint the build context
|
|
||||||
# `.dockerignore` keeps docs out of the build context, so a docs-only
|
|
||||||
# commit legitimately replays the whole image from cache and stays
|
|
||||||
# cheap. Every other commit writes a new fingerprint into the context,
|
|
||||||
# which invalidates the `COPY . .` layer of both check stages: a
|
|
||||||
# commit that was never linted, formatted-checked, tested and built
|
|
||||||
# cannot report success from cache.
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
fp="$(git log -1 --format=%H -- . ':!*.md' ':!LICENSE' ':!.editorconfig')"
|
|
||||||
printf '%s\n' "${fp:-$GITHUB_SHA}" > .ci-fingerprint
|
|
||||||
|
|
||||||
- name: Build Docker image (runs make check)
|
- name: Build Docker image (runs make check)
|
||||||
run: script/cibuild
|
run: script/cibuild
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -41,7 +41,4 @@ data/
|
|||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
# CI cache barrier, written into the build context by the check workflow
|
|
||||||
.ci-fingerprint
|
|
||||||
@@ -12,11 +12,7 @@ WORKDIR /src
|
|||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy source code. In CI the context also carries .ci-fingerprint, whose
|
# Copy source code
|
||||||
# value changes with every commit that touches the build context (see
|
|
||||||
# .gitea/workflows/check.yml). That invalidates this layer, so the checks
|
|
||||||
# below cannot report success by replaying a cached pass. Do not add it to
|
|
||||||
# .dockerignore.
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Run formatting check and linter
|
# Run formatting check and linter
|
||||||
@@ -40,8 +36,7 @@ WORKDIR /build
|
|||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy source code, including the .ci-fingerprint cache barrier described in
|
# Copy source code
|
||||||
# the lint stage above.
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Run tests and build
|
# Run tests and build
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -1112,34 +1112,6 @@ binary is statically linked and runs on Alpine.
|
|||||||
`docker build .` is the CI gate — if it passes, the code is formatted,
|
`docker build .` is the CI gate — if it passes, the code is formatted,
|
||||||
linted, tested, and compiled.
|
linted, tested, and compiled.
|
||||||
|
|
||||||
#### CI gate honesty
|
|
||||||
|
|
||||||
A layer cache lets `docker build .` exit 0 in seconds with the lint and
|
|
||||||
test stages replayed rather than executed, which would make a green
|
|
||||||
check meaningless. The `check` workflow therefore writes
|
|
||||||
`.ci-fingerprint` into the build context before building. Its value is
|
|
||||||
the hash of the last commit that touched the build context, so:
|
|
||||||
|
|
||||||
- Any commit that changes code (including a squash merge whose tree
|
|
||||||
matches an already-built branch) gets a new fingerprint, invalidates
|
|
||||||
the `COPY . .` layer of both check stages, and really runs
|
|
||||||
`make fmt-check`, `make lint`, `make test`, and `make build`. A run
|
|
||||||
that reports success ran them.
|
|
||||||
- A docs-only commit leaves the fingerprint unchanged — `.dockerignore`
|
|
||||||
excludes `*.md` and `LICENSE` from the context anyway — so the image
|
|
||||||
replays from cache and costs seconds.
|
|
||||||
|
|
||||||
The module download layer sits above `COPY . .` and stays cached either
|
|
||||||
way.
|
|
||||||
|
|
||||||
The workflow's first step covers a second way the gate lied: Gitea
|
|
||||||
cancels an in-flight run when a newer commit lands on the same branch
|
|
||||||
and records that cancellation as a `failure` status, marking a commit
|
|
||||||
red that was never tested. Cancellation is unconditional server-side for
|
|
||||||
push events, so the superseding run rewrites the exact
|
|
||||||
`Has been cancelled` status to `skipped`. Genuine failures are never
|
|
||||||
touched.
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
See [TODO.md](TODO.md).
|
See [TODO.md](TODO.md).
|
||||||
|
|||||||
Reference in New Issue
Block a user