Derive the image version from git; send .git to the build (closes #69)
check / check (push) Successful in 28s
check / check (push) Successful in 28s
The canonical documents told every repo to exclude .git from the build context, default ARG VERSION to dev and never run git describe in a build stage, so an image built from a clone with no build argument reported dev. .dockerignore now sends .git. The Dockerfile example in REPO_POLICIES.md installs git, takes the VERSION build argument when one is given and otherwise git describe --tags --always, and fails when .git is present but the version is empty, dev or unknown. The checklists, the Go docs' Makefile comments, the README, and this repo's Dockerfile and scripts now say the same. Model: opus-5-5
This commit is contained in:
+2
-3
@@ -13,9 +13,8 @@
|
|||||||
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
||||||
# deletes the package directory from the context.
|
# deletes the package directory from the context.
|
||||||
|
|
||||||
# Excluding .git means `git describe` cannot run in any build stage and
|
# .git is sent: without a VERSION build argument, the stage that compiles
|
||||||
# fails quietly there; pass the version in with --build-arg VERSION.
|
# takes the version from `git describe --tags --always` on it.
|
||||||
.git
|
|
||||||
|
|
||||||
# Agent scratch: one full checkout of the repo per in-flight agent.
|
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||||
# Anchored because it occurs once where agents run at the repo root.
|
# Anchored because it occurs once where agents run at the repo root.
|
||||||
|
|||||||
+4
-3
@@ -52,7 +52,8 @@ RUN script/bootstrap
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# The version is computed on the host and passed in, because
|
# Nothing here is compiled and a LABEL cannot run git, so the version is
|
||||||
# .dockerignore excludes .git.
|
# the VERSION build argument that script/docker and script/cibuild pass;
|
||||||
ARG VERSION=dev
|
# a plain `docker build .` leaves it empty.
|
||||||
|
ARG VERSION
|
||||||
LABEL org.opencontainers.image.version="${VERSION}"
|
LABEL org.opencontainers.image.version="${VERSION}"
|
||||||
|
|||||||
@@ -132,8 +132,7 @@ alpine. We provide:
|
|||||||
`script/check`, compute `version` from `git describe`, then
|
`script/check`, compute `version` from `git describe`, then
|
||||||
`docker build --no-cache --build-arg VERSION="$version" -t prompts .` (what CI
|
`docker build --no-cache --build-arg VERSION="$version" -t prompts .` (what CI
|
||||||
runs; it bootstraps because CI checks out and runs this alone while
|
runs; it bootstraps because CI checks out and runs this alone while
|
||||||
`script/fmt-check` is native, and the version is computed on the host because
|
`script/fmt-check` is native)
|
||||||
`.dockerignore` excludes `.git`)
|
|
||||||
- `script/precommit` — run by the git pre-commit hook (our own extension); calls
|
- `script/precommit` — run by the git pre-commit hook (our own extension); calls
|
||||||
`script/check`
|
`script/check`
|
||||||
- `script/install-precommit` — installs the git pre-commit hook (our own
|
- `script/install-precommit` — installs the git pre-commit hook (our own
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Code Styleguide — Go
|
title: Code Styleguide — Go
|
||||||
last_modified: 2026-09-08
|
last_modified: 2026-10-02
|
||||||
---
|
---
|
||||||
|
|
||||||
1. Try to hard wrap long lines at 77 characters or less.
|
1. Try to hard wrap long lines at 77 characters or less.
|
||||||
@@ -48,13 +48,10 @@ last_modified: 2026-09-08
|
|||||||
```
|
```
|
||||||
|
|
||||||
```make
|
```make
|
||||||
# ?= rather than := because this `$(shell git describe ...)` is only
|
# ?= rather than := so that a `VERSION` build argument takes precedence:
|
||||||
# correct on the host: `.dockerignore` excludes `.git`, so evaluated
|
# where a build stage invokes make, `ARG VERSION` puts it in the
|
||||||
# inside a build stage it expands to the empty string without failing
|
# environment and `?=` defers to it. Otherwise `git describe` runs, in a
|
||||||
# and the binary reports no version. The version is computed on the
|
# build stage on the `.git` the build context carries.
|
||||||
# host by `script/docker` / `script/cibuild` and passed with
|
|
||||||
# `--build-arg VERSION=...`; where a build stage invokes make,
|
|
||||||
# `ARG VERSION` puts it in the environment and `?=` defers to it.
|
|
||||||
VERSION ?= $(shell git describe --always --dirty)
|
VERSION ?= $(shell git describe --always --dirty)
|
||||||
|
|
||||||
GOLDFLAGS += -X main.Version=$(VERSION)
|
GOLDFLAGS += -X main.Version=$(VERSION)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Existing Repo Checklist
|
title: Existing Repo Checklist
|
||||||
last_modified: 2026-09-08
|
last_modified: 2026-10-02
|
||||||
---
|
---
|
||||||
|
|
||||||
Use this checklist when beginning work in a repo that may not yet conform to our
|
Use this checklist when beginning work in a repo that may not yet conform to our
|
||||||
@@ -44,7 +44,7 @@ with your task.
|
|||||||
`script/test` — those are themselves a `docker build` and would recurse
|
`script/test` — those are themselves a `docker build` and would recurse
|
||||||
inside a build step
|
inside a build step
|
||||||
- [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix,
|
- [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix,
|
||||||
only genuinely root-anchored entries such as `.git` are unprefixed, and
|
only genuinely root-anchored entries such as `.claude` are unprefixed, and
|
||||||
`.gitignore`'s patterns have not been transplanted unmodified — the
|
`.gitignore`'s patterns have not been transplanted unmodified — the
|
||||||
transplanted form leaves `config/.env` and `certs/server.key` in the build
|
transplanted form leaves `config/.env` and `certs/server.key` in the build
|
||||||
context while reading as solved
|
context while reading as solved
|
||||||
@@ -58,9 +58,15 @@ with your task.
|
|||||||
can copy another session's unreviewed work into an image layer. If agents
|
can copy another session's unreviewed work into an image layer. If agents
|
||||||
here run anywhere other than the repo root, the anchored entry misses
|
here run anywhere other than the repo root, the anchored entry misses
|
||||||
`services/api/.claude/`: add anchored entries for those directories.
|
`services/api/.claude/`: add anchored entries for those directories.
|
||||||
- [ ] If the repo embeds a version in a binary, that version is computed on the
|
- [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into
|
||||||
host and passed with `--build-arg VERSION=...` by `script/docker` and
|
the build context. The stage that compiles has `git` (the Debian Go image
|
||||||
`script/cibuild`, and no stage calls `git describe`. A tag-derived version
|
has it; an alpine one needs `apk add --no-cache git`) and takes the
|
||||||
|
version from the `VERSION` build argument when one is given, otherwise
|
||||||
|
from `git describe --tags --always` (a tag when the commit has one,
|
||||||
|
otherwise the short commit). `ARG VERSION` has no default, and the build
|
||||||
|
fails if the context carries `.git` and the version still comes out empty,
|
||||||
|
`dev` or `unknown`. `script/docker` and `script/cibuild` pass the version
|
||||||
|
they compute on the host; it takes precedence. A tag-derived version
|
||||||
additionally needs `fetch-depth: 0` on the CI checkout step, which clones
|
additionally needs `fetch-depth: 0` on the CI checkout step, which clones
|
||||||
shallow and fetches no tags by default.
|
shallow and fetches no tags by default.
|
||||||
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
|
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Go HTTP Server Conventions
|
title: Go HTTP Server Conventions
|
||||||
last_modified: 2026-09-08
|
last_modified: 2026-10-02
|
||||||
---
|
---
|
||||||
|
|
||||||
This document defines the architectural patterns, design decisions, and
|
This document defines the architectural patterns, design decisions, and
|
||||||
@@ -984,13 +984,10 @@ func main() {
|
|||||||
Use ldflags to inject version information at build time:
|
Use ldflags to inject version information at build time:
|
||||||
|
|
||||||
```makefile
|
```makefile
|
||||||
# ?= rather than := because this `$(shell git describe ...)` is only correct
|
# ?= rather than := so that a `VERSION` build argument takes precedence:
|
||||||
# on the host: `.dockerignore` excludes `.git`, so evaluated inside a build
|
# where a build stage invokes make, `ARG VERSION` puts it in the
|
||||||
# stage it expands to the empty string without failing and the binary reports
|
# environment and `?=` defers to it. Otherwise `git describe` runs, in a
|
||||||
# no version. The version is computed on the host by `script/docker` /
|
# build stage on the `.git` the build context carries.
|
||||||
# `script/cibuild` and passed with `--build-arg VERSION=...`; where the build
|
|
||||||
# stage invokes make, `ARG VERSION` puts it in the environment and `?=` defers
|
|
||||||
# to it.
|
|
||||||
VERSION ?= $(shell git describe --tags --always)
|
VERSION ?= $(shell git describe --tags --always)
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: New Repo Checklist
|
title: New Repo Checklist
|
||||||
last_modified: 2026-09-08
|
last_modified: 2026-10-02
|
||||||
---
|
---
|
||||||
|
|
||||||
Use this checklist when creating a new repository from scratch. Follow the steps
|
Use this checklist when creating a new repository from scratch. Follow the steps
|
||||||
@@ -67,11 +67,14 @@ Template files can be fetched from:
|
|||||||
note that it only covers agents running at the repo root — if this repo
|
note that it only covers agents running at the repo root — if this repo
|
||||||
will run them in subdirectories, `services/api/.claude/` needs its own
|
will run them in subdirectories, `services/api/.claude/` needs its own
|
||||||
anchored entry.
|
anchored entry.
|
||||||
- If the image embeds a version in a binary, the version is computed on the
|
- If the image embeds a version in a binary: `.dockerignore` lets `.git`
|
||||||
host and passed with `--build-arg VERSION=...`, and `ARG VERSION=dev` is
|
into the build context. The stage that compiles has `git` (the Debian Go
|
||||||
declared in the stage that compiles. **No stage calls `git describe`** —
|
image has it; an alpine one needs `apk add --no-cache git`) and takes the
|
||||||
`.dockerignore` excludes `.git`, so it yields an empty version without
|
version from the `VERSION` build argument when one is given, otherwise
|
||||||
failing the build.
|
from `git describe --tags --always` (a tag when the commit has one,
|
||||||
|
otherwise the short commit). `ARG VERSION` has no default, and the build
|
||||||
|
fails if the context carries `.git` and the version still comes out empty,
|
||||||
|
`dev` or `unknown`.
|
||||||
- The Dockerfile carries a `lint` phase and a `test` phase, each invoking
|
- The Dockerfile carries a `lint` phase and a `test` phase, each invoking
|
||||||
its tool directly rather than through `make` or `script/`, and the final
|
its tool directly rather than through `make` or `script/`, and the final
|
||||||
stage carries a `COPY --from=` of a harmless file from each so the image
|
stage carries a `COPY --from=` of a harmless file from each so the image
|
||||||
|
|||||||
+32
-15
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Repository Policies
|
title: Repository Policies
|
||||||
last_modified: 2026-09-08
|
last_modified: 2026-10-02
|
||||||
---
|
---
|
||||||
|
|
||||||
This document covers repository structure, tooling, and workflow standards. Code
|
This document covers repository structure, tooling, and workflow standards. Code
|
||||||
@@ -191,15 +191,27 @@ style conventions are in separate documents:
|
|||||||
FROM golang@sha256:... AS builder
|
FROM golang@sha256:... AS builder
|
||||||
COPY --from=lint /src/go.sum /dev/null
|
COPY --from=lint /src/go.sum /dev/null
|
||||||
COPY --from=test /src/go.sum /dev/null
|
COPY --from=test /src/go.sum /dev/null
|
||||||
|
RUN apk add --no-cache git
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ARG VERSION=dev
|
# The VERSION build arg when one is given, otherwise the tag or short
|
||||||
RUN CGO_ENABLED=0 go build -trimpath \
|
# commit from the .git in the build context. With .git present, a
|
||||||
-ldflags="-s -w -X main.Version=${VERSION}" \
|
# version that is still empty, dev or unknown fails the build: git is
|
||||||
-o /app ./cmd/app/
|
# missing or could not read the checkout.
|
||||||
|
ARG VERSION
|
||||||
|
RUN VERSION="${VERSION:-$(git describe --tags --always)}"; \
|
||||||
|
if [ -d .git ]; then \
|
||||||
|
case "$VERSION" in ""|dev|unknown) \
|
||||||
|
echo "version is '$VERSION' although .git is present" >&2; \
|
||||||
|
exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
fi; \
|
||||||
|
CGO_ENABLED=0 go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.Version=${VERSION}" \
|
||||||
|
-o /app ./cmd/app/
|
||||||
|
|
||||||
# Runtime stage, and the last one
|
# Runtime stage, and the last one
|
||||||
FROM alpine@sha256:...
|
FROM alpine@sha256:...
|
||||||
@@ -223,8 +235,13 @@ style conventions are in separate documents:
|
|||||||
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
|
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
|
||||||
- If the project requires CGO or system libraries for linting (e.g.
|
- If the project requires CGO or system libraries for linting (e.g.
|
||||||
`vips-dev`), install them in the lint phase with `apk add`.
|
`vips-dev`), install them in the lint phase with `apk add`.
|
||||||
- `ARG VERSION=dev` is declared in the stage that compiles and supplied by
|
- `.dockerignore` lets `.git` into the build context. The stage that
|
||||||
`script/docker` and `script/cibuild`; no stage may call `git describe`.
|
compiles has `git` (the Debian Go image has it; an alpine one needs
|
||||||
|
`apk add --no-cache git`) and takes the version from the `VERSION` build
|
||||||
|
argument when one is given, otherwise from `git describe --tags --always`
|
||||||
|
(a tag when the commit has one, otherwise the short commit). `ARG VERSION`
|
||||||
|
has no default, and the build fails if the context carries `.git` and the
|
||||||
|
version still comes out empty, `dev` or `unknown`.
|
||||||
|
|
||||||
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
||||||
runs `script/cibuild` on push, and checks out the repo as its only other step.
|
runs `script/cibuild` on push, and checks out the repo as its only other step.
|
||||||
@@ -340,7 +357,7 @@ style conventions are in separate documents:
|
|||||||
— which is more dangerous than a short file with no secret patterns at all,
|
— which is more dangerous than a short file with no secret patterns at all,
|
||||||
because it reads as solved and stops anyone looking. Give every
|
because it reads as solved and stops anyone looking. Give every
|
||||||
depth-independent pattern the `**/` prefix and leave only genuinely
|
depth-independent pattern the `**/` prefix and leave only genuinely
|
||||||
root-anchored entries unprefixed: `.git`, and the repo's own host-built
|
root-anchored entries unprefixed: `.claude`, and the repo's own host-built
|
||||||
binary, written `/myapp` and never `**/myapp`, which would also match
|
binary, written `/myapp` and never `**/myapp`, which would also match
|
||||||
`cmd/myapp/` and delete the package directory from the context. Matching is
|
`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
|
case-sensitive, and an ALL-CAPS twin per pattern still misses `Server.Key`, so
|
||||||
@@ -365,12 +382,12 @@ style conventions are in separate documents:
|
|||||||
directory, so a repo running agents in subdirectories still ships
|
directory, so a repo running agents in subdirectories still ships
|
||||||
`services/api/.claude/` and must add its own anchored entry there.
|
`services/api/.claude/` and must add its own anchored entry there.
|
||||||
|
|
||||||
- **Excluding `.git` means `git describe` cannot run inside any build stage, and
|
- **A plain `docker build .` of a clone stamps the tag or short commit**,
|
||||||
it fails quietly there.** In a build stage there is no repository, so
|
derived from the `.git` in the build context as the canonical `Dockerfile`
|
||||||
`git describe` writes nothing to stdout, `-X main.Version=` comes out empty,
|
above shows. Without its failure check, a missing `git` or an unreadable
|
||||||
the binary reports no version at all, and the build still exits 0. Compute the
|
checkout would leave `-X main.Version=` empty and the build would still
|
||||||
version on the host and thread it in as a build arg. `script/docker` and
|
exit 0. `script/docker` and `script/cibuild` pass the version they compute on
|
||||||
`script/cibuild` do this, byte-identically across repos:
|
the host; it takes precedence. They do this byte-identically across repos:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Own line: a failing command substitution inside an argument does not
|
# Own line: a failing command substitution inside an argument does not
|
||||||
@@ -387,7 +404,7 @@ style conventions are in separate documents:
|
|||||||
fallback is applied — a live check that fires on a build from an export with
|
fallback is applied — 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
|
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
|
substitution as `|| echo unknown`, which makes the guard unreachable. The
|
||||||
Dockerfile's side is `ARG VERSION=dev` in the stage that compiles, declared
|
Dockerfile's side is `ARG VERSION` in the stage that compiles, declared
|
||||||
there because `ARG` is stage-scoped; passing `VERSION` to a repo whose
|
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
|
Dockerfile declares no such `ARG` is ignored and costs nothing, which is why
|
||||||
the scripts stay byte-identical. One consequence for CI: the standard
|
the scripts stay byte-identical. One consequence for CI: the standard
|
||||||
|
|||||||
+2
-3
@@ -16,9 +16,8 @@ main() {
|
|||||||
"$SCRIPT_DIR/check"
|
"$SCRIPT_DIR/check"
|
||||||
# Own line: a failing command substitution inside an argument does
|
# Own line: a failing command substitution inside an argument does
|
||||||
# not trip `set -e`, so the inline form degrades silently to an
|
# not trip `set -e`, so the inline form degrades silently to an
|
||||||
# empty constant. VERSION is computed here because .dockerignore
|
# empty constant. The VERSION build argument takes precedence over
|
||||||
# excludes .git, so `git describe` in a build stage yields an empty
|
# the version a build stage derives from the .git in the context.
|
||||||
# version without failing.
|
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -n "$version" ] || version="unknown"
|
||||||
docker build --no-cache \
|
docker build --no-cache \
|
||||||
|
|||||||
+2
-3
@@ -12,9 +12,8 @@ main() {
|
|||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# Own line: a failing command substitution inside an argument does
|
# Own line: a failing command substitution inside an argument does
|
||||||
# not trip `set -e`, so the inline form degrades silently to an
|
# not trip `set -e`, so the inline form degrades silently to an
|
||||||
# empty constant. VERSION is computed here because .dockerignore
|
# empty constant. The VERSION build argument takes precedence over
|
||||||
# excludes .git, so `git describe` in a build stage yields an empty
|
# the version a build stage derives from the .git in the context.
|
||||||
# version without failing.
|
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -n "$version" ] || version="unknown"
|
||||||
docker build --no-cache \
|
docker build --no-cache \
|
||||||
|
|||||||
Reference in New Issue
Block a user