|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
---
|
|
|
|
|
title: Repository Policies
|
|
|
|
|
last_modified: 2026-11-03
|
|
|
|
|
last_modified: 2026-11-04
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
This document covers repository structure, tooling, and workflow standards. Code
|
|
|
|
@@ -16,9 +16,9 @@ style conventions are in separate documents:
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- Cross-project documentation, such as this file, must include
|
|
|
|
|
`last_modified: YYYY-MM-DD` in its YAML front matter. The date is how a copy
|
|
|
|
|
is kept in sync with the authoritative source as policies evolve.
|
|
|
|
|
- Cross-project documentation (such as this file) must include
|
|
|
|
|
`last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync
|
|
|
|
|
with the authoritative source as policies evolve.
|
|
|
|
|
|
|
|
|
|
- **External references must be pinned by cryptographic hash.** This is the
|
|
|
|
|
single most important rule in this document. It covers everything fetched from
|
|
|
|
@@ -88,10 +88,10 @@ style conventions are in separate documents:
|
|
|
|
|
- `script/check`
|
|
|
|
|
- `script/docker`
|
|
|
|
|
|
|
|
|
|
The scripts must be POSIX sh: `#!/bin/sh`, `set -eu`, no bashisms. That is
|
|
|
|
|
so they run in minimal containers; alpine images, for example, have no bash.
|
|
|
|
|
Locate the repo root with `$(cd "$(dirname "$0")/.." && pwd -P)`, and `cd`
|
|
|
|
|
there before acting.
|
|
|
|
|
The scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they
|
|
|
|
|
run in minimal containers; alpine images, for example, have no bash. Locate
|
|
|
|
|
the repo root with `$(cd "$(dirname "$0")/.." && pwd -P)`, and `cd` there
|
|
|
|
|
before acting.
|
|
|
|
|
|
|
|
|
|
From the standard's canonical set we use `bootstrap`, `setup`, `test`, and
|
|
|
|
|
`cibuild`. `setup` makes the repo ready for development after a fresh clone:
|
|
|
|
@@ -103,31 +103,30 @@ style conventions are in separate documents:
|
|
|
|
|
that order; apt runs noninteractive. For node it uses the installed node
|
|
|
|
|
only when its major version is the pinned one. When node is missing or has
|
|
|
|
|
another major, it installs the PINNED node version via nvm, and installs
|
|
|
|
|
yarn and the packages under it. The node 24 the CI runner image ships is one
|
|
|
|
|
such other major. nvm itself is installed first if missing, from a
|
|
|
|
|
hash-verified GitHub release archive, never `curl | sh`. bash is installed
|
|
|
|
|
as an explicit prerequisite, since nvm requires bash. Only the major version
|
|
|
|
|
is compared, for two reasons. The `Dockerfile` stages start from a node
|
|
|
|
|
image whose exact version is not the pin, and nvm cannot install a prebuilt
|
|
|
|
|
node on alpine. yarn is pinned via
|
|
|
|
|
`corepack prepare yarn@<version> --activate`. Never install "latest" or
|
|
|
|
|
"lts"; always exact versions.
|
|
|
|
|
yarn and the packages under it. The CI runner image ships node 24, for
|
|
|
|
|
example. nvm itself is installed first if missing, from a hash-verified
|
|
|
|
|
GitHub release archive, never `curl | sh`. bash is installed as an explicit
|
|
|
|
|
prerequisite, since nvm requires bash. Only the major version is compared
|
|
|
|
|
because the `Dockerfile` stages start from a node image whose exact version
|
|
|
|
|
is not the pin, and nvm cannot install a prebuilt node on alpine. yarn is
|
|
|
|
|
pinned via `corepack prepare yarn@<version> --activate`. Never install
|
|
|
|
|
"latest" or "lts"; always exact versions.
|
|
|
|
|
|
|
|
|
|
`script/cibuild` runs the CI build, and the Gitea workflow calls it. It
|
|
|
|
|
changes to the repo root, runs `script/bootstrap`, runs `script/check`, and
|
|
|
|
|
builds the image with the version. **`script/cibuild` runs
|
|
|
|
|
`script/bootstrap` first.** The workflow checks out the repo and runs
|
|
|
|
|
nothing else, while `script/fmt-check` runs the formatter on the host.
|
|
|
|
|
Without the bootstrap, on a pristine checkout with nothing installed, the
|
|
|
|
|
run dies there, after the containerised gates have passed. **The bootstrap
|
|
|
|
|
alone is not enough.** When `script/bootstrap` installs node and yarn under
|
|
|
|
|
nvm, it leaves neither on the `PATH` of the shell that called it. A bare
|
|
|
|
|
`yarn` then either exits 127 or runs under another node. The host
|
|
|
|
|
entrypoints that need yarn, `script/fmt` and `script/fmt-check`, therefore
|
|
|
|
|
use the same test as `script/bootstrap`. When the node on `PATH` has the
|
|
|
|
|
pinned major version, they run the `yarn` on `PATH`. Otherwise they load nvm
|
|
|
|
|
through `$HOME/.nvm/nvm.sh` and run yarn under the pinned node. A runner
|
|
|
|
|
carrying nothing but docker and git then gets through `script/check`.
|
|
|
|
|
`script/cibuild` runs the CI build, and the Gitea workflow calls it. The
|
|
|
|
|
script changes to the repo root, runs `script/bootstrap`, runs
|
|
|
|
|
`script/check`, and builds the image with the version. **`script/cibuild`
|
|
|
|
|
runs `script/bootstrap` first**, because the workflow checks out the repo
|
|
|
|
|
and runs nothing else, while `script/fmt-check` runs the formatter on the
|
|
|
|
|
host. On a pristine checkout with nothing installed, the run dies there,
|
|
|
|
|
after the containerised gates have passed. **The bootstrap alone is not
|
|
|
|
|
enough.** When `script/bootstrap` installs node and yarn under nvm, it
|
|
|
|
|
leaves neither on the `PATH` of the shell that called it. A bare `yarn` then
|
|
|
|
|
either exits 127 or runs under another node. The host entrypoints that need
|
|
|
|
|
yarn, `script/fmt` and `script/fmt-check`, therefore use the same test as
|
|
|
|
|
`script/bootstrap`. When the node on `PATH` has the pinned major version,
|
|
|
|
|
they run the `yarn` on `PATH`. Otherwise they load nvm through
|
|
|
|
|
`$HOME/.nvm/nvm.sh` and run yarn under the pinned node. A runner carrying
|
|
|
|
|
nothing but docker and git then gets through `script/check`.
|
|
|
|
|
|
|
|
|
|
Four further scripts are our own extensions to the standard:
|
|
|
|
|
- `script/check` runs `script/test`, `script/lint` and `script/fmt-check`.
|
|
|
|
@@ -170,7 +169,7 @@ style conventions are in separate documents:
|
|
|
|
|
cannot be built unless they pass. For non-server repos the final stage brings
|
|
|
|
|
up a development environment; for server repos it is the runtime image. The
|
|
|
|
|
gate phases and the build stage start from their pinned base images and
|
|
|
|
|
install what those images lack. They do that in one of two ways:
|
|
|
|
|
install what those images lack in either of these ways:
|
|
|
|
|
- inline, as the canonical Go `Dockerfile` below does for `git`
|
|
|
|
|
- by running `script/bootstrap`, as the `prompts` repo's own `Dockerfile`
|
|
|
|
|
does for its yarn packages
|
|
|
|
@@ -194,9 +193,9 @@ style conventions are in separate documents:
|
|
|
|
|
|
|
|
|
|
**A stage that is not the last one in the file is built only when the final
|
|
|
|
|
stage's chain depends on it, or when `--target` names it.** That is why the
|
|
|
|
|
two gates are always invoked by name here. It is also why the final stage
|
|
|
|
|
carries a `COPY --from=` of a harmless file from each of them. Without that
|
|
|
|
|
edge, a plain `docker build .` builds the last stage alone and exits 0
|
|
|
|
|
two gates are always invoked by name here, and why the final stage carries a
|
|
|
|
|
`COPY --from=` of a harmless file from each of them. Without that
|
|
|
|
|
dependency, a plain `docker build .` builds the last stage alone and exits 0
|
|
|
|
|
having linted and tested nothing.
|
|
|
|
|
|
|
|
|
|
**The gate builds write no image.** With `--output type=cacheonly` the phase
|
|
|
|
@@ -205,12 +204,12 @@ style conventions are in separate documents:
|
|
|
|
|
image holds the toolchain. A build given neither `--output` nor `-t` writes
|
|
|
|
|
an untagged image and leaves it dangling, on every developer host and every
|
|
|
|
|
CI runner. `script/cibuild` and `script/docker` build the image that ships
|
|
|
|
|
and tag it, so each build replaces the previous image. Each assigns the tag
|
|
|
|
|
on its own line before the build, so `set -e` stops it where
|
|
|
|
|
and tag it, so each build replaces the previous image. Each script assigns
|
|
|
|
|
the tag on its own line before the build, so `set -e` stops it where
|
|
|
|
|
`script/projectname` fails.
|
|
|
|
|
|
|
|
|
|
Inside a phase the tool is invoked directly: `golangci-lint`, `go test`,
|
|
|
|
|
`eslint`, `prettier`. It is never invoked through `make lint` or
|
|
|
|
|
`eslint`, `prettier`. The tool is never invoked through `make lint` or
|
|
|
|
|
`script/test`. Those are themselves a `docker build`, and would recurse into
|
|
|
|
|
a daemon that does not exist in a build step. Formatting is the exception
|
|
|
|
|
and stays on the host: `script/fmt` writes the working tree, and
|
|
|
|
@@ -224,9 +223,11 @@ style conventions are in separate documents:
|
|
|
|
|
caller cannot tell from real findings. Both have produced wrong verdicts in
|
|
|
|
|
this org, in both directions. A container has its own `TMPDIR`, so the lock
|
|
|
|
|
is out of reach. The lint phase does keep golangci-lint's cache between
|
|
|
|
|
check builds (below). But it lints every checkout at `/src`, and a finding
|
|
|
|
|
is taken from that cache only for the same files, settings and golangci-lint
|
|
|
|
|
version.
|
|
|
|
|
check builds (below), but it lints every checkout at `/src`. A finding is
|
|
|
|
|
taken from that cache only when all of these are the same:
|
|
|
|
|
- the files
|
|
|
|
|
- the settings
|
|
|
|
|
- the golangci-lint version
|
|
|
|
|
|
|
|
|
|
- **Any build that runs checks is given a new `CACHEBUST` value.** Docker
|
|
|
|
|
invalidates a `COPY` layer only when the copied content changes. On an
|
|
|
|
@@ -234,8 +235,7 @@ style conventions are in separate documents:
|
|
|
|
|
executes, and the build still exits 0. Every `docker build` in `script/`
|
|
|
|
|
therefore passes `--build-arg CACHEBUST="$(date +%s)-$$"`, the time and the
|
|
|
|
|
script's process id. So no two builds share a value, not even the three in one
|
|
|
|
|
`script/cibuild`. The scripts that run `docker build` are these four, and
|
|
|
|
|
there is no fifth:
|
|
|
|
|
`script/cibuild`. These are the four, and there is no fifth:
|
|
|
|
|
- `script/lint`
|
|
|
|
|
- `script/test`
|
|
|
|
|
- `script/cibuild`
|
|
|
|
@@ -251,13 +251,17 @@ style conventions are in separate documents:
|
|
|
|
|
packages from `Pipfile.lock`
|
|
|
|
|
|
|
|
|
|
A stage with none of these steps declares `ARG CACHEBUST` right after
|
|
|
|
|
`FROM`. A step that installs anything else as well, such as a
|
|
|
|
|
`script/bootstrap` that also installs node packages, comes after it too. A
|
|
|
|
|
new value reruns every `RUN` step after that line; `COPY` steps are compared
|
|
|
|
|
by content and need nothing. Docker keeps the layers above the line until
|
|
|
|
|
the base image, their own lines or the files they copy change. A stage
|
|
|
|
|
without the `ARG` line is served from cache on an unchanged tree, so a
|
|
|
|
|
repository re-vendors the scripts and adds the `ARG` lines in one commit.
|
|
|
|
|
`FROM`. A step that also installs anything else comes after `ARG CACHEBUST`
|
|
|
|
|
too. A `script/bootstrap` that also installs node packages is one. A new
|
|
|
|
|
value reruns every `RUN` step after that line; `COPY` steps are compared by
|
|
|
|
|
content and need nothing. Docker keeps the layers above the line until one
|
|
|
|
|
of these changes:
|
|
|
|
|
- the base image
|
|
|
|
|
- their own lines
|
|
|
|
|
- the files they copy
|
|
|
|
|
|
|
|
|
|
A stage without the `ARG` line is served from cache on an unchanged tree, so
|
|
|
|
|
a repository re-vendors the scripts and adds the `ARG` lines in one commit.
|
|
|
|
|
The scripts do not pass `--no-cache`. That flag reruns every step too, but
|
|
|
|
|
it also builds the layers above the line again and empties every cache
|
|
|
|
|
mount. Go's build cache and golangci-lint's would then start empty every
|
|
|
|
@@ -266,14 +270,19 @@ style conventions are in separate documents:
|
|
|
|
|
ran: a sub-second build reporting success is a cache hit, not a result.
|
|
|
|
|
Never invalidate by pruning: `docker builder prune` and friends destroy a
|
|
|
|
|
build cache shared with every other build on the host. When a check is added
|
|
|
|
|
or changed, prove it works: plant a defect it must catch, watch the run fail
|
|
|
|
|
on it, then revert the defect. A green run alone shows neither that the
|
|
|
|
|
check ran nor that it covers what it should.
|
|
|
|
|
or changed, prove it works:
|
|
|
|
|
- plant a defect it must catch
|
|
|
|
|
- watch the run fail on it
|
|
|
|
|
- revert the defect
|
|
|
|
|
|
|
|
|
|
A green run alone shows neither that the check ran nor that it covers what
|
|
|
|
|
it should.
|
|
|
|
|
|
|
|
|
|
- **The gate phases are separate stages, and the build stage depends on both.**
|
|
|
|
|
The lint phase is based on the `golangci/golangci-lint` image, pinned by hash,
|
|
|
|
|
so lint failures surface in seconds rather than after a full compile. The test
|
|
|
|
|
phase is based on the Debian Go image. The canonical Go repo `Dockerfile`:
|
|
|
|
|
The lint phase is based on the `golangci/golangci-lint` image (pinned by
|
|
|
|
|
hash), so lint failures surface in seconds rather than after a full compile.
|
|
|
|
|
The test phase is based on the Debian Go image. The canonical Go repo
|
|
|
|
|
`Dockerfile`:
|
|
|
|
|
|
|
|
|
|
```dockerfile
|
|
|
|
|
# In every stage ARG CACHEBUST comes right after the steps that install
|
|
|
|
@@ -367,22 +376,22 @@ style conventions are in separate documents:
|
|
|
|
|
- The lint phase uses the `golangci/golangci-lint` image directly, so
|
|
|
|
|
nothing needs installing: it has both Go and the linter.
|
|
|
|
|
- `COPY --from=<phase> /src/go.sum /dev/null` is a no-op copy whose only
|
|
|
|
|
purpose is the ordering edge. BuildKit runs stages in parallel by default,
|
|
|
|
|
and a stage nothing depends on is not built at all. Without these two
|
|
|
|
|
lines a red gate would not fail the build.
|
|
|
|
|
purpose is the dependency on that phase. BuildKit runs stages in parallel
|
|
|
|
|
by default, and a stage nothing depends on is not built at all. Without
|
|
|
|
|
these two lines a red gate would not fail the build.
|
|
|
|
|
- Keep the runtime stage last. If you add a stage after it, give it the same
|
|
|
|
|
two copies: a plain `docker build .` builds the last stage's chain and
|
|
|
|
|
two copies. A plain `docker build .` builds the last stage's chain and
|
|
|
|
|
nothing else.
|
|
|
|
|
- Check builds keep these, and none of them can hand back a stale result:
|
|
|
|
|
- The layers above `ARG CACHEBUST`: the system packages and Go's
|
|
|
|
|
modules. Docker installs the packages again when the base image or the
|
|
|
|
|
install line changes. It downloads the modules again when `go.mod` or
|
|
|
|
|
`go.sum` changes. Go checks every module against `go.sum` as it
|
|
|
|
|
install line changes. Docker downloads the modules again when `go.mod`
|
|
|
|
|
or `go.sum` changes. Go checks every module against `go.sum` as it
|
|
|
|
|
downloads it, and no later build can change a kept layer. The modules
|
|
|
|
|
are kept in a layer rather than a cache mount, for two reasons. A
|
|
|
|
|
cache mount is shared by every build on the host. And Go builds from
|
|
|
|
|
whatever files it holds without checking them again (only
|
|
|
|
|
`go mod verify` does), so a file changed there would go unnoticed.
|
|
|
|
|
are kept in a layer rather than a cache mount. A cache mount is shared
|
|
|
|
|
by every build on the host, and Go builds from whatever files the
|
|
|
|
|
mount holds without checking them again (only `go mod verify` does). A
|
|
|
|
|
file changed there would go unnoticed.
|
|
|
|
|
- Go's build cache (`/root/.cache/go-build`, where Go keeps it for
|
|
|
|
|
root), mounted on the steps that compile: `golangci-lint run`, both
|
|
|
|
|
`go test` runs and `go build`. Go reuses a compiled package only when
|
|
|
|
@@ -431,12 +440,13 @@ style conventions are in separate documents:
|
|
|
|
|
|
|
|
|
|
`git describe` does not need them, and each can hold a credential: a
|
|
|
|
|
password in a remote URL, or the token the CI checkout step stores
|
|
|
|
|
there. A submodule whose name has a `config` segment (`config`,
|
|
|
|
|
`deploy/config`, `config/lib`) loses its whole git directory to
|
|
|
|
|
`**/.git/modules/**/config`, and Go's version stamping then fails the
|
|
|
|
|
build. Give it a name without that segment (`git submodule add --name`).
|
|
|
|
|
The stage that compiles has `git`: the Debian Go image has it, and an
|
|
|
|
|
alpine one needs `apk add --no-cache git`. It takes the version from the
|
|
|
|
|
there. A submodule name may have a `config` segment, as in `config`,
|
|
|
|
|
`deploy/config` or `config/lib`. Such a submodule loses its whole git
|
|
|
|
|
directory to `**/.git/modules/**/config`, and Go's version stamping then
|
|
|
|
|
fails the build. Give it a name without that segment
|
|
|
|
|
(`git submodule add --name`). The stage that compiles has `git`: the
|
|
|
|
|
Debian Go image has it, and an alpine one needs
|
|
|
|
|
`apk add --no-cache git`. That stage takes the version from the
|
|
|
|
|
`VERSION` build argument when one is given, otherwise from
|
|
|
|
|
`git describe --tags --always`. That gives:
|
|
|
|
|
- the tag, on a tagged commit
|
|
|
|
@@ -476,24 +486,30 @@ style conventions are in separate documents:
|
|
|
|
|
The tests run after `ARG CACHEBUST`, on every check build. In every other
|
|
|
|
|
stage, a step that installs Python packages comes after `ARG CACHEBUST`.
|
|
|
|
|
- When `script/bootstrap` runs in that step, it installs nothing beyond
|
|
|
|
|
system packages, pipenv and its dependencies, and the packages from
|
|
|
|
|
`Pipfile.lock`. It installs no Python there: the tests use the base
|
|
|
|
|
image's Python, which the image's hash pins. If it installs anything else
|
|
|
|
|
there, the step goes after `ARG CACHEBUST`.
|
|
|
|
|
these:
|
|
|
|
|
- system packages
|
|
|
|
|
- pipenv and its dependencies
|
|
|
|
|
- the packages from `Pipfile.lock`
|
|
|
|
|
|
|
|
|
|
`script/bootstrap` installs no Python there: the tests use the base
|
|
|
|
|
image's Python, which the image's hash pins. If it installs anything
|
|
|
|
|
else there, the step goes after `ARG CACHEBUST`.
|
|
|
|
|
|
|
|
|
|
- PyPI is not trusted, so `script/bootstrap` installs pipenv and its
|
|
|
|
|
dependencies by hash. It does so with `pip install --require-hashes -r`
|
|
|
|
|
and a requirements file in `script/` that lists each of them with `==` and
|
|
|
|
|
its `--hash`. pip refuses to install a dependency the file does not list.
|
|
|
|
|
- It then runs `pipenv install --deploy`, with `--dev` when the tests need
|
|
|
|
|
the development packages. `--deploy` installs only what `Pipfile.lock`
|
|
|
|
|
names, checks every package against the lockfile's hashes, and fails when
|
|
|
|
|
the lockfile is out of date with `Pipfile`.
|
|
|
|
|
- A kept layer holds nothing a new install would refuse: pip checked pipenv
|
|
|
|
|
and its dependencies, and pipenv checked the lockfile's packages, against
|
|
|
|
|
their hashes. pipenv creates its virtual environment with the `virtualenv`
|
|
|
|
|
package. The packages that `virtualenv` adds come from wheels inside that
|
|
|
|
|
package, so the hash pip checked for it covers them. Two things in the
|
|
|
|
|
layer are not fixed by a hash:
|
|
|
|
|
the development packages. `--deploy` does all of these:
|
|
|
|
|
- installs only what `Pipfile.lock` names
|
|
|
|
|
- checks every package against the lockfile's hashes
|
|
|
|
|
- fails when the lockfile is out of date with `Pipfile`
|
|
|
|
|
- A kept layer holds nothing a new install would refuse: pip checked the
|
|
|
|
|
hashes of pipenv and its dependencies, and pipenv checked the hashes of
|
|
|
|
|
the lockfile's packages. pipenv creates its virtual environment with the
|
|
|
|
|
`virtualenv` package. The packages that `virtualenv` adds come from wheels
|
|
|
|
|
inside that package, so the hash pip checked for it covers them. Two
|
|
|
|
|
things in the layer are not fixed by a hash:
|
|
|
|
|
- A package with no ready-built wheel for the image is built from
|
|
|
|
|
source. pip downloads the build tools without a hash check, so a kept
|
|
|
|
|
layer's build of such a package may differ from a new one.
|
|
|
|
@@ -503,9 +519,10 @@ style conventions are in separate documents:
|
|
|
|
|
A cache mount is shared by every build on the host, and nothing checks its
|
|
|
|
|
files again.
|
|
|
|
|
|
|
|
|
|
In this example, `script/bootstrap` finds `make` and `git` already
|
|
|
|
|
installed, installs pipenv and its dependencies by hash, and runs
|
|
|
|
|
`pipenv install --dev --deploy`:
|
|
|
|
|
In the example below, `script/bootstrap` does all of these:
|
|
|
|
|
- finds `make` and `git` already installed
|
|
|
|
|
- installs pipenv and its dependencies by hash
|
|
|
|
|
- runs `pipenv install --dev --deploy`
|
|
|
|
|
|
|
|
|
|
```dockerfile
|
|
|
|
|
# Test phase. Both pytest runs share one 90-second timeout, so a hang
|
|
|
|
@@ -530,12 +547,12 @@ style conventions are in separate documents:
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
|
|
|
|
runs `script/cibuild` on push. Its only other step checks out the repo, with
|
|
|
|
|
`persist-credentials: false`. `script/cibuild` needs no token, and without
|
|
|
|
|
that setting the checkout leaves the job's token in `.git/config` for every
|
|
|
|
|
later step. The checkout step also sets `fetch-depth: 0`, which fetches the
|
|
|
|
|
tags `git describe` needs. By default it clones shallow with no tags, and a
|
|
|
|
|
tagged repository's CI build would stamp a bare short commit id. The
|
|
|
|
|
runs `script/cibuild` on push. The workflow's only other step checks out the
|
|
|
|
|
repo, with `persist-credentials: false`. `script/cibuild` needs no token, and
|
|
|
|
|
without that setting the checkout leaves the job's token in `.git/config` for
|
|
|
|
|
every later step. The checkout step also sets `fetch-depth: 0`, which fetches
|
|
|
|
|
the tags `git describe` needs. By default it clones shallow with no tags, and
|
|
|
|
|
a tagged repository's CI build would stamp a bare short commit id. The
|
|
|
|
|
workflow's `concurrency` block groups runs by workflow and branch
|
|
|
|
|
(`${{ github.workflow }}-${{ github.ref }}`) with `cancel-in-progress: true`.
|
|
|
|
|
A new push then cancels the older run on the same branch, queued or running,
|
|
|
|
@@ -549,7 +566,7 @@ style conventions are in separate documents:
|
|
|
|
|
entry. The `check` job sets `timeout-minutes: 20`, so a hung build frees the
|
|
|
|
|
shared runner after 20 minutes. That allows for the three Docker builds
|
|
|
|
|
described above, each held to the 5-minute Docker build limit below, plus the
|
|
|
|
|
bootstrap. The three are the test phase, the lint phase, then the image. A
|
|
|
|
|
bootstrap. Those builds are the test phase, the lint phase, then the image. A
|
|
|
|
|
separate workflow limited to `main` by a `branches` list under `on: push`
|
|
|
|
|
cannot be checked by review. To try a change to it, add the feature branch to
|
|
|
|
|
that list and push, then remove the branch from the list again before merging.
|
|
|
|
@@ -643,9 +660,9 @@ style conventions are in separate documents:
|
|
|
|
|
passing result in its cache directory (`GOCACHE`). When the same tests run
|
|
|
|
|
again on unchanged code, it prints that result, marked `(cached)`, without
|
|
|
|
|
running them. The directory lasts from one run to the next on a developer's
|
|
|
|
|
machine, where this target runs. It also lasts in the `test` phase of the
|
|
|
|
|
`Dockerfile`, which mounts it to keep compiled packages between check
|
|
|
|
|
builds, so the phase's `go test` runs carry `-count=1` too. A new
|
|
|
|
|
machine, where this target runs. The directory also lasts in the `test`
|
|
|
|
|
phase of the `Dockerfile`, which mounts it to keep compiled packages between
|
|
|
|
|
check builds, so the phase's `go test` runs carry `-count=1` too. A new
|
|
|
|
|
`CACHEBUST` value (above) is what makes that step run on an unchanged tree.
|
|
|
|
|
|
|
|
|
|
Python example:
|
|
|
|
@@ -703,14 +720,14 @@ style conventions are in separate documents:
|
|
|
|
|
the build-context root. A `.dockerignore` listing `.env`, `*.pem` and `*.key`
|
|
|
|
|
therefore excludes only the copies at the repository root. `config/.env` and
|
|
|
|
|
`certs/server.key` still reach the context and can land in an image layer.
|
|
|
|
|
That is more dangerous than a short file with no secret patterns at all,
|
|
|
|
|
because it reads as solved and stops anyone looking. Give every
|
|
|
|
|
depth-independent pattern the `**/` prefix, and leave only genuinely
|
|
|
|
|
Such a `.dockerignore` is more dangerous than a short file with no secret
|
|
|
|
|
patterns at all, because it reads as solved and stops anyone looking. Give
|
|
|
|
|
every depth-independent pattern the `**/` prefix, and leave only genuinely
|
|
|
|
|
root-anchored entries unprefixed. Those are `.claude`, and the repo's own
|
|
|
|
|
host-built binary. The binary is written `/myapp` and never `**/myapp`, which
|
|
|
|
|
would also match `cmd/myapp/` and delete the package directory from the
|
|
|
|
|
context. Matching is case-sensitive, and an ALL-CAPS twin per pattern still
|
|
|
|
|
misses `Server.Key`. So secret names use character ranges:
|
|
|
|
|
context. Matching is case-sensitive, and an all-caps copy of each pattern
|
|
|
|
|
still misses `Server.Key`. So secret names use character ranges:
|
|
|
|
|
`**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`, and likewise for `.envrc` and the
|
|
|
|
|
extensionless SSH keys. Where such a pattern also catches something the build
|
|
|
|
|
needs, re-include it with a negation (`!docs/example.env`). Deleting the
|
|
|
|
@@ -726,11 +743,12 @@ style conventions are in separate documents:
|
|
|
|
|
be copied into an image layer. In `.gitignore` the entry is `.claude/`,
|
|
|
|
|
unanchored. In `.dockerignore` it is `.claude`, anchored and with **no** `**/`
|
|
|
|
|
prefix, because the prefixed form would also delete any nested directory of
|
|
|
|
|
that name from the build. Anchoring carries a known gap, which the canonical
|
|
|
|
|
`.dockerignore` states in its own comment, since consuming repos receive the
|
|
|
|
|
file and not the tracker. The directory is created in the agent's working
|
|
|
|
|
directory, so a repo running agents in subdirectories still ships
|
|
|
|
|
`services/api/.claude/` and must add its own anchored entry there.
|
|
|
|
|
that name from the build. Anchoring carries a known gap. The canonical
|
|
|
|
|
`.dockerignore` states the gap in its own comment, since consuming repos
|
|
|
|
|
receive the file and not the tracker. The gap is that the directory is created
|
|
|
|
|
in the agent's working directory. A repo running agents in subdirectories
|
|
|
|
|
therefore still ships `services/api/.claude/` and must add its own anchored
|
|
|
|
|
entry there.
|
|
|
|
|
|
|
|
|
|
- **A plain `docker build .` of a clone stamps the version that
|
|
|
|
|
`git describe --tags --always` gives.** The version is derived from the `.git`
|
|
|
|
@@ -754,10 +772,10 @@ style conventions are in separate documents:
|
|
|
|
|
|
|
|
|
|
`--always` makes an untagged repo yield an abbreviated commit hash rather
|
|
|
|
|
than failing. The `[ -n "$version" ]` line is the single place the fallback
|
|
|
|
|
is applied. It is a live check that fires on a build from an export with no
|
|
|
|
|
`.git`, and on a repository with no commits yet. Do not fold it into the
|
|
|
|
|
substitution as `|| echo unknown`, which makes the guard unreachable. The
|
|
|
|
|
Dockerfile's side is `ARG VERSION` in the stage that compiles, declared
|
|
|
|
|
is applied. That line is a live check that fires on a build from an export
|
|
|
|
|
with no `.git`, and on a repository with no commits yet. Do not fold it into
|
|
|
|
|
the substitution as `|| echo unknown`, which makes the guard unreachable.
|
|
|
|
|
The Dockerfile's side is `ARG VERSION` in the stage that compiles, declared
|
|
|
|
|
there because `ARG` is stage-scoped. Passing `VERSION` to a repo whose
|
|
|
|
|
Dockerfile declares no such `ARG` is ignored and costs nothing, which is why
|
|
|
|
|
the scripts stay byte-identical. One consequence for CI: the standard
|
|
|
|
@@ -772,11 +790,15 @@ style conventions are in separate documents:
|
|
|
|
|
transfers only the delta from the previous build.
|
|
|
|
|
|
|
|
|
|
- **No build artifacts in version control.** Code-derived data must never be
|
|
|
|
|
committed to the repository if it can be avoided: compiled bundles, minified
|
|
|
|
|
output, generated assets. The build process (e.g. Dockerfile, Makefile) should
|
|
|
|
|
generate these at build time. Notable exception: Go protobuf generated files
|
|
|
|
|
(`.pb.go`) ARE committed. Repos need to work with `go get`, which downloads
|
|
|
|
|
code but does not execute code generation.
|
|
|
|
|
committed to the repository if it can be avoided. Code-derived data includes:
|
|
|
|
|
- compiled bundles
|
|
|
|
|
- minified output
|
|
|
|
|
- generated assets
|
|
|
|
|
|
|
|
|
|
The build process (e.g. Dockerfile, Makefile) should generate these at build
|
|
|
|
|
time. Notable exception: Go protobuf generated files (`.pb.go`) ARE
|
|
|
|
|
committed because repos need to work with `go get`, which downloads code but
|
|
|
|
|
does not execute code generation.
|
|
|
|
|
|
|
|
|
|
- Never use `git add -A` or `git add .`. Always stage files explicitly by name.
|
|
|
|
|
|
|
|
|
@@ -796,8 +818,8 @@ style conventions are in separate documents:
|
|
|
|
|
`test-support` depguard rule. There a repo names its own test-support packages
|
|
|
|
|
by full import path. A repo adds entries there and changes nothing else, and a
|
|
|
|
|
re-vendor carries its entries forward. The canonical golangci-lint version is
|
|
|
|
|
v2.15.0 (released 2026-10-16). It is pinned as the digest of the lint phase's
|
|
|
|
|
base image,
|
|
|
|
|
v2.15.0 (released 2026-10-16). The version is pinned as the digest of the lint
|
|
|
|
|
phase's base image,
|
|
|
|
|
`golangci/golangci-lint@sha256:fbfcfd12a6f8d7f1b97b3362ca80fc2a244d46863148c73cb88e7793af96c481`,
|
|
|
|
|
which reports `2.15.0 built with go1.27.0 from 25c5369a`. A module's `go`
|
|
|
|
|
directive must not name a newer Go minor version than the one golangci-lint
|
|
|
|
@@ -817,22 +839,27 @@ style conventions are in separate documents:
|
|
|
|
|
disagree about what the tool even is. The canonical form:
|
|
|
|
|
- compares the installed version against the pin over the **whole** version
|
|
|
|
|
token. A parser that stops at the first `-` reports `2.12.2` for a host
|
|
|
|
|
running `2.12.2-rc1` and skips the install;
|
|
|
|
|
- treats absent, non-zero, empty or unrecognised `--version` output as a
|
|
|
|
|
mismatch, so the failure direction is a redundant install and never a
|
|
|
|
|
skipped one;
|
|
|
|
|
running `2.12.2-rc1` and skips the install.
|
|
|
|
|
- treats `--version` output as a mismatch when it is any of these:
|
|
|
|
|
- absent
|
|
|
|
|
- non-zero
|
|
|
|
|
- empty
|
|
|
|
|
- unrecognised
|
|
|
|
|
|
|
|
|
|
So a failure causes a redundant install, never a skipped one.
|
|
|
|
|
|
|
|
|
|
- after installing, re-resolves the binary the way callers do, with
|
|
|
|
|
`hash -r` and then through `PATH`, not through the directory the installer
|
|
|
|
|
wrote to. It fails naming the resolved path, since an install that a
|
|
|
|
|
shadowing binary hides succeeds while changing nothing any caller sees;
|
|
|
|
|
wrote to. It fails and names the resolved path. A shadowing binary can
|
|
|
|
|
hide an install, which then succeeds while no caller sees any change.
|
|
|
|
|
- is actually called, and prints the version on both success paths. A
|
|
|
|
|
function defined and never invoked has the same exit status and the same
|
|
|
|
|
empty output as one that worked.
|
|
|
|
|
|
|
|
|
|
Node is the exception to the whole-token comparison. The canonical
|
|
|
|
|
`script/bootstrap` compares only its major version, for two reasons. The
|
|
|
|
|
node 22 alpine image the `Dockerfile` stages start from is not the exact
|
|
|
|
|
pin, and nvm cannot install a prebuilt node on alpine.
|
|
|
|
|
`script/bootstrap` compares only node's major version, because the
|
|
|
|
|
`Dockerfile` stages start from a node 22 alpine image, which is not the
|
|
|
|
|
exact pin, and nvm cannot install a prebuilt node on alpine.
|
|
|
|
|
|
|
|
|
|
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
|
|
|
|
|
|
|
|
|
@@ -900,9 +927,15 @@ style conventions are in separate documents:
|
|
|
|
|
an explicit allowlist of known origins. Wildcard (`*`) is acceptable
|
|
|
|
|
only for public, unauthenticated read-only APIs.
|
|
|
|
|
- **Error handling:**
|
|
|
|
|
- Internal errors must never leak stack traces, SQL queries, file paths,
|
|
|
|
|
or other implementation details to the client. Return generic error
|
|
|
|
|
messages in production; detailed errors only when `DEBUG` is enabled.
|
|
|
|
|
- Internal errors must never leak implementation details to the client,
|
|
|
|
|
such as:
|
|
|
|
|
- stack traces
|
|
|
|
|
- SQL queries
|
|
|
|
|
- file paths
|
|
|
|
|
|
|
|
|
|
Return generic error messages in production; detailed errors only
|
|
|
|
|
when `DEBUG` is enabled.
|
|
|
|
|
|
|
|
|
|
- **TLS:**
|
|
|
|
|
- Services never terminate TLS directly. They are always deployed behind
|
|
|
|
|
a TLS-terminating reverse proxy. The service itself listens on plain
|
|
|
|
|