Pin pipenv by hash and keep one form of the Python lockfile install (closes #133)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Rework of #135 on the review at #135 (comment). A Python test phase now has one form: COPY script/, COPY Pipfile Pipfile.lock, RUN script/bootstrap, then ARG CACHEBUST, and every list of what goes above the line names that step. In that step script/bootstrap installs only system packages, pipenv and its dependencies by hash with pip --require-hashes, and the lockfile's packages, and no Python. The claim about a kept layer covers only what is pinned by hash and says its system packages may be older. The bold rule is conditional, the script/cibuild items expect the step to show CACHED, and the example says why it installs make and git. Model: opus-5-5
This commit is contained in:
+64
-42
@@ -175,26 +175,27 @@ style conventions are in separate documents:
|
||||
`script/docker` are the four, and there is no fifth — `script/check` runs the
|
||||
two gate phases and `script/fmt-check`, and builds no image of its own. In
|
||||
every stage of the `Dockerfile`, `ARG CACHEBUST` comes right after the steps
|
||||
that install system packages, download Go's modules or install a `test`
|
||||
phase's Python packages from `Pipfile.lock`, or right after `FROM` in a stage
|
||||
with none of these, and every step that lints, tests or compiles comes after
|
||||
it. 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. The scripts do not pass `--no-cache`:
|
||||
it reruns every step too, but it also builds the layers above the line again
|
||||
and empties every cache mount, so Go's build cache and golangci-lint's, which
|
||||
the canonical Go `Dockerfile` below mounts so that they survive between check
|
||||
builds, would start empty every time. A bare `docker build .` is not evidence
|
||||
that anything 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 by planting a defect it must catch and
|
||||
watching 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.
|
||||
that install system packages, download Go's modules or run `script/bootstrap`
|
||||
to install a Python `test` phase's packages from `Pipfile.lock`, or right
|
||||
after `FROM` in a stage with none of these, and every step that lints, tests
|
||||
or compiles comes after it. 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. The scripts do
|
||||
not pass `--no-cache`: it reruns every step too, but it also builds the layers
|
||||
above the line again and empties every cache mount, so Go's build cache and
|
||||
golangci-lint's, which the canonical Go `Dockerfile` below mounts so that they
|
||||
survive between check builds, would start empty every time. A bare
|
||||
`docker build .` is not evidence that anything 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 by
|
||||
planting a defect it must catch and watching 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.
|
||||
|
||||
- **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
|
||||
@@ -309,8 +310,9 @@ style conventions are in separate documents:
|
||||
a package's findings only when the package's files and those of every
|
||||
package it imports, `go.mod`, the linters and their settings in
|
||||
`.golangci.yml`, and the golangci-lint version are all unchanged.
|
||||
- In a Python `test` phase, the packages installed from `Pipfile.lock`,
|
||||
as described for that phase below.
|
||||
- In a Python `test` phase, the layer of the step that runs
|
||||
`script/bootstrap` to install the packages from `Pipfile.lock`, as
|
||||
described for that phase below.
|
||||
- If the project uses `//go:embed` directives that reference build artifacts
|
||||
(e.g. a web frontend compiled in a separate stage), the lint phase must
|
||||
create placeholder files so the embed directives resolve. Example:
|
||||
@@ -365,31 +367,51 @@ style conventions are in separate documents:
|
||||
`--build-arg VERSION=...`, as `script/docker` and `script/cibuild` already
|
||||
do.
|
||||
|
||||
- **A Python `test` phase installs its packages from `Pipfile.lock` above
|
||||
`ARG CACHEBUST`, so check builds keep them.** The install has steps of its
|
||||
own: `COPY Pipfile Pipfile.lock ./`, then one `RUN` that runs
|
||||
`pipenv install --deploy`, with `--dev` when the tests need the development
|
||||
packages. Docker keeps what that `RUN` installed until the base image,
|
||||
`Pipfile`, `Pipfile.lock` or a line up to and including the `RUN` changes. If
|
||||
the `RUN` runs `script/bootstrap` instead, as in the example below,
|
||||
`COPY script/ script/` comes before it, so Docker installs again when
|
||||
`script/` changes too. That `script/bootstrap` may install nothing beyond
|
||||
system packages, the pinned pipenv and the packages from the lockfile; one
|
||||
that installs anything else comes after `ARG CACHEBUST`. A kept layer holds
|
||||
the same packages a new install would: `--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`. The packages are kept
|
||||
in a layer rather than a cache mount for the reason Go's modules are: a cache
|
||||
mount is shared by every build on the host, and nothing checks its files
|
||||
again. The tests run after `ARG CACHEBUST`, on every check build. In every
|
||||
other stage, a step that installs Python packages comes after `ARG CACHEBUST`.
|
||||
An example, with a `script/bootstrap` that installs the pinned pipenv and then
|
||||
runs `pipenv install --dev --deploy`:
|
||||
- **A Python `test` phase that installs its packages from `Pipfile.lock` does it
|
||||
above `ARG CACHEBUST`, so check builds keep them.** The steps are
|
||||
`COPY script/ script/`, `COPY Pipfile Pipfile.lock ./` and
|
||||
`RUN script/bootstrap`, with `ARG CACHEBUST` right after them. Docker keeps
|
||||
what that `RUN` installed until the base image, `script/`, `Pipfile`,
|
||||
`Pipfile.lock` or a line up to and including the `RUN` changes. 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`.
|
||||
- It installs pipenv and its dependencies by hash, as the rule that every
|
||||
external reference is pinned by hash requires. 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 the same pipenv, the same dependencies of pipenv and
|
||||
the same lockfile packages that a new install would, because each was
|
||||
checked against its hash. The pip and setuptools inside the virtualenv
|
||||
that pipenv creates are the same too: the `virtualenv` package, one of
|
||||
pipenv's dependencies, copies them from wheels it ships, and its hash
|
||||
covers those wheels. Any system packages the step installed are not pinned
|
||||
by hash: the layer keeps the versions the package manager gave when the
|
||||
layer was built, and a new install may get newer ones.
|
||||
- The packages are kept in a layer rather than a cache mount for the reason
|
||||
Go's modules are: 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`:
|
||||
|
||||
```dockerfile
|
||||
# Test phase. The 90-second timeout covers both pytest runs.
|
||||
# python:3.x-slim, YYYY-MM-DD
|
||||
FROM python@sha256:... AS test
|
||||
# make and git are base tools that script/bootstrap installs when they
|
||||
# are missing. Installed here, they stay in their own layer when
|
||||
# script/ changes.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends make git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
Reference in New Issue
Block a user