Compare commits
1 Commits
20c0d13a25
...
ae74852ea2
| Author | SHA1 | Date | |
|---|---|---|---|
| ae74852ea2 |
@@ -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
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,6 +42,3 @@ 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
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -867,19 +867,14 @@ limit leaves room for one address to drive several entrypoints at their
|
|||||||
full rate, and it is not configurable separately.
|
full rate, and it is not configurable separately.
|
||||||
|
|
||||||
What that aggregate limit bounds is the database work an invented path
|
What that aggregate limit bounds is the database work an invented path
|
||||||
costs; log volume it caps rather than eliminates. A path that names no
|
costs, not the number of log lines it produces. The path is
|
||||||
entrypoint is recorded by the handler at `DEBUG`, and the aggregate
|
attacker-controlled, so nothing on this route writes it to the log
|
||||||
limiter logs its own rejections at `DEBUG` and without the path, so
|
above `DEBUG`: a path that names no entrypoint is recorded by the
|
||||||
neither appears at all under the default level. The per-entrypoint
|
handler at `DEBUG`, and the aggregate limiter logs its rejections at
|
||||||
limiter is the loud one: it still logs every rejection at `WARN` with
|
`DEBUG` and without the path. Every request is still recorded once by
|
||||||
the request path, which on this route is attacker-controlled text. A
|
the access log, at `INFO`, with its full URL, whether it was served or
|
||||||
client hammering a single invented path is served `RECEIVER_RATE_LIMIT`
|
rejected — so a flood of invented paths still writes one `INFO` line
|
||||||
requests and has the rest of its aggregate budget rejected there, so
|
per request.
|
||||||
the aggregate limit is what bounds those `WARN` lines — to under ten
|
|
||||||
times `RECEIVER_RATE_LIMIT` per minute per client IP, 1080 at the
|
|
||||||
defaults, where before it there was no bound at all. The access log is
|
|
||||||
bounded by neither limit: every request is recorded once at `INFO` with
|
|
||||||
its full URL, served or rejected alike.
|
|
||||||
|
|
||||||
Every limiter here — receiver, login, and password change — identifies
|
Every limiter here — receiver, login, and password change — identifies
|
||||||
the client the same way, through one shared key function: the
|
the client the same way, through one shared key function: the
|
||||||
@@ -1141,34 +1136,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).
|
||||||
|
|||||||
@@ -171,11 +171,9 @@ func (m *Middleware) clientKey(r *http.Request) string {
|
|||||||
return peer.String()
|
return peer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// tooManyRequests returns the 429 handler used by the login,
|
// tooManyRequests returns the 429 handler shared by every limiter:
|
||||||
// password-change and per-entrypoint receiver limiters: it logs the
|
// it logs the rejection with logMessage and answers with
|
||||||
// rejection with logMessage and answers with responseMessage.
|
// responseMessage. httprate adds the Retry-After header (RFC 6585).
|
||||||
// httprate adds the Retry-After header (RFC 6585). The aggregate
|
|
||||||
// receiver limiter uses floodTooManyRequests instead.
|
|
||||||
func (m *Middleware) tooManyRequests(
|
func (m *Middleware) tooManyRequests(
|
||||||
logMessage, responseMessage string,
|
logMessage, responseMessage string,
|
||||||
) http.HandlerFunc {
|
) http.HandlerFunc {
|
||||||
|
|||||||
Reference in New Issue
Block a user