Skip to content

Commit 2a48ff6

Browse files
committed
chore: lock the 1.2.0 version
1 parent e8f18bb commit 2a48ff6

6 files changed

Lines changed: 181 additions & 3 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ jobs:
3030
python-version: ${{ matrix.python }}
3131
enable-cache: true
3232

33+
# uv.lock records the project's own version as well as its
34+
# dependencies, so bumping `version` in pyproject.toml needs a `uv lock`
35+
# in the same commit. This has failed CI twice; as a named step it says
36+
# which file is stale instead of reporting a resolver error from Install.
37+
- name: The lockfile is current
38+
run: |
39+
uv lock --check || {
40+
echo "::error::uv.lock does not match pyproject.toml. Run 'uv lock' and commit it."
41+
exit 1
42+
}
43+
3344
# Installs the versions pinned in uv.lock into a project venv.
3445
# The examples extra brings the ladybug driver, so the LadybugDB sink tests run.
3546
- name: Install

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,6 @@ docs/get-started/python.md
123123
docs/domains/index.md
124124
docs/reference/cli.md
125125
docs/guides/entity-resolution.md
126+
127+
# scripts/publish-image.sh mounts this for its smoke test
128+
.publish-smoke/

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The fraud pack, the coordination pack, the social domain, schemas and every sink
3131
Or without a Python environment, as a container ([docs](https://graphfaker.readthedocs.io/en/latest/get-started/docker.html)):
3232

3333
```sh
34-
docker run --rm -v "$PWD/bank:/data" ghcr.io/graphgeeks-lab/graphfaker:main fraud --scale 0.01 --seed 42 --out /data
34+
docker run --rm -v "$PWD/bank:/data" ghcr.io/graphgeeks-lab/graphfaker fraud --scale 0.01 --seed 42 --out /data
3535
```
3636

3737
For development:

‎docs/get-started/docker.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ docker pull ghcr.io/graphgeeks-lab/graphfaker:main
77
docker run --rm ghcr.io/graphgeeks-lab/graphfaker:main info
88
```
99

10-
Tags: `main` is the current development build, pushed after every change to `main` once it has passed the smoke test. Releases publish `1.0.0`, `1.0`, `1` and `latest`; pin one of those to reproduce a dataset, since `main` moves. Until the first release with an image is tagged, `latest` does not exist and `docker pull` of it says `manifest unknown`. `info` prints the version inside the image and which extras it carries.
10+
Tags: `latest` is the newest release and is what you get without a tag. A release also publishes its exact version and the prefixes of it, so 1.2.0 publishes `1.2.0`, `1.2`, `1` and `latest`; pin the exact version to reproduce a dataset. `main` is the development build, pushed after every change to `main` once it has passed the smoke test, and it moves. Every tag is built for `linux/amd64` and `linux/arm64`. `info` prints the version inside the image and which extras it carries.
1111

1212
## Run
1313

@@ -100,3 +100,20 @@ docker run --rm graphfaker info
100100
```
101101

102102
The workflow (`.github/workflows/docker.yml`) builds the same file on every push and pull request and runs a generate, a load and a verify against the built image. A push to `main` publishes the `main` tag; a `v*` tag publishes the version tags and `latest`.
103+
104+
So publishing a release is tagging it:
105+
106+
```bash
107+
git tag -a v1.2.0 -m "graphfaker 1.2.0" && git push origin v1.2.0
108+
```
109+
110+
That builds both architectures, smoke-tests the exact image it is about to push, and publishes `1.2.0`, `1.2`, `1`, `latest` and a short sha. The version inside the image is the checkout's, and the PyPI workflow refuses a tag whose version does not match `pyproject.toml`, so the image and the release cannot drift.
111+
112+
When that path is not available, `scripts/publish-image.sh` does the same thing from a laptop: same checks in the same order, both architectures, and the same five tags. It refuses a version that does not match `pyproject.toml`, and `DRY_RUN=1` runs every check and pushes nothing.
113+
114+
```bash
115+
DRY_RUN=1 scripts/publish-image.sh # build and smoke-test only
116+
scripts/publish-image.sh # publish, including latest
117+
```
118+
119+
It needs a token with `write:packages`, either in `GITHUB_TOKEN` or from `gh auth token`.

‎scripts/publish-image.sh‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env bash
2+
# Publish the image to GHCR by hand, including `latest`.
3+
#
4+
# The tag push is the normal path: tagging `v1.2.0` makes
5+
# .github/workflows/docker.yml build both architectures, smoke-test the exact
6+
# image it is about to publish, and push `1.2.0`, `1.2`, `1`, `latest` and a
7+
# short sha. Use this script when that path is not available: the workflow is
8+
# failing, the runner cannot reach the registry, or an image has to go out
9+
# from a laptop.
10+
#
11+
# scripts/publish-image.sh # version from pyproject.toml
12+
# scripts/publish-image.sh 1.2.0 # or say it, and it is checked
13+
# DRY_RUN=1 scripts/publish-image.sh # build and smoke-test, push nothing
14+
#
15+
# It does what the workflow does, in the same order, because the point of the
16+
# workflow is that nothing reaches `latest` without passing a generate, a load
17+
# and a verify first. It refuses to publish a version that does not match
18+
# pyproject.toml, for the same reason python-publish.yml does: the image and
19+
# the PyPI release must not drift.
20+
set -euo pipefail
21+
22+
# MSYS and git-bash rewrite anything that looks like a unix path when they
23+
# spawn a native binary, which mangles the *container* side of a mount:
24+
# `--out /data` arrives as `--out C:/Program Files/Git/data`, the container
25+
# writes a directory literally called `C:`, and the host sees no dataset. The
26+
# host side is converted deliberately, by host_path below.
27+
export MSYS_NO_PATHCONV=1
28+
export MSYS2_ARG_CONV_EXCL='*'
29+
30+
IMAGE="${IMAGE:-ghcr.io/graphgeeks-lab/graphfaker}"
31+
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
32+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
33+
cd "$ROOT"
34+
35+
say() { printf '\n== %s\n' "$*"; }
36+
die() { printf '\nerror: %s\n' "$*" >&2; exit 1; }
37+
38+
# A bind mount needs a path the host's docker understands. Under MSYS or
39+
# git-bash on Windows, $PWD is a virtual path: docker accepts it, mounts
40+
# something inside its own VM, and the container writes a bank the host cannot
41+
# see. The smoke test would then pass having proved nothing about the one thing
42+
# a user cares about, which is the output landing in their directory.
43+
host_path() {
44+
if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else printf '%s' "$1"; fi
45+
}
46+
47+
PROJECT_VERSION="$(sed -nE 's/^version = "(.*)"/\1/p' pyproject.toml | head -1)"
48+
[ -n "$PROJECT_VERSION" ] || die "no version in pyproject.toml"
49+
VERSION="${1:-$PROJECT_VERSION}"
50+
51+
if [ "$VERSION" != "$PROJECT_VERSION" ]; then
52+
die "asked to publish $VERSION but pyproject.toml says $PROJECT_VERSION.
53+
Bump the version and HISTORY.rst first; an image whose version does not match
54+
the release it claims to be is worse than no image."
55+
fi
56+
57+
MAJOR="${VERSION%%.*}"
58+
MINOR="${VERSION%.*}"
59+
case "$VERSION" in
60+
*[!0-9.]*) die "'$VERSION' is not a release version; this script does not publish pre-releases to latest" ;;
61+
esac
62+
63+
say "Publishing $IMAGE $VERSION as $VERSION, $MINOR, $MAJOR and latest"
64+
command -v docker >/dev/null || die "docker is not on PATH"
65+
docker buildx version >/dev/null 2>&1 || die "docker buildx is not available"
66+
67+
# arm64 is emulated when the host is amd64, so QEMU has to be registered
68+
# before buildx can target it. Harmless when it is already there.
69+
say "Registering QEMU for the other architecture"
70+
docker run --privileged --rm tonistiigi/binfmt --install all >/dev/null 2>&1 \
71+
|| echo " (could not register QEMU; a cross build may fail)"
72+
73+
# Built to the local daemon first, so the smoke test runs against the exact
74+
# image that gets pushed rather than a rebuild of it.
75+
say "Building for this machine, to smoke-test"
76+
docker buildx build --load -t "graphfaker:publish" .
77+
78+
WORK="$(mktemp -d)"
79+
# The bind mount lives under the checkout rather than in a temp directory,
80+
# because that is a path both the host and docker agree about on every
81+
# platform. Removed on the way out, including on failure.
82+
SMOKE="$ROOT/.publish-smoke"
83+
rm -rf "$SMOKE" || die "could not clear $SMOKE; remove it and try again"
84+
trap 'rm -rf "$WORK" "$SMOKE" 2>/dev/null || true' EXIT
85+
86+
# Output is captured to a file before it is searched. Piping `docker run` into
87+
# `grep -q` makes grep exit on its first match, docker take SIGPIPE, and
88+
# `pipefail` fail the whole thing: "write /dev/stdout: The pipe is being
89+
# closed", reported as a missing fraud domain. The workflow has the same note
90+
# over the same two commands.
91+
say "The CLI is there and reports the version in pyproject.toml"
92+
docker run --rm graphfaker:publish --version > "$WORK/version.txt"
93+
grep -qx "graphfaker $VERSION" "$WORK/version.txt" \
94+
|| { cat "$WORK/version.txt"; die "the image reports a different version"; }
95+
docker run --rm graphfaker:publish domains > "$WORK/domains.txt"
96+
grep -q fraud "$WORK/domains.txt" \
97+
|| die "the fraud domain did not ship in the image"
98+
99+
say "It runs as a non-root user"
100+
[ "$(docker run --rm --entrypoint id graphfaker:publish -u)" != "0" ] \
101+
|| die "the image runs as root"
102+
103+
say "It generates a bank into a volume, loads it and verifies it"
104+
mkdir -p "$SMOKE/bank" && chmod 777 "$SMOKE/bank"
105+
MOUNT="$(host_path "$SMOKE/bank")"
106+
docker run --rm -v "$MOUNT:/data" graphfaker:publish \
107+
fraud --scale 0.001 --seed 42 --out /data --no-report
108+
[ -f "$SMOKE/bank/manifest.json" ] \
109+
|| die "the container wrote no manifest.json the host can see.
110+
Mounted $MOUNT; if that is not a path your docker resolves to this directory,
111+
the bind mount went somewhere else."
112+
docker run --rm -v "$MOUNT:/data" graphfaker:publish load duckdb /data --no-verify
113+
docker run --rm -v "$MOUNT:/data" graphfaker:publish verify duckdb /data
114+
115+
if [ "${DRY_RUN:-}" = "1" ]; then
116+
say "DRY_RUN=1, so nothing was pushed. The image passed every check."
117+
exit 0
118+
fi
119+
120+
say "Logging in to ghcr.io"
121+
if [ -n "${GITHUB_TOKEN:-}" ]; then
122+
echo "$GITHUB_TOKEN" | docker login ghcr.io -u "${GITHUB_ACTOR:-$USER}" --password-stdin
123+
elif command -v gh >/dev/null; then
124+
gh auth token | docker login ghcr.io -u "$(gh api user --jq .login)" --password-stdin
125+
else
126+
die "set GITHUB_TOKEN (a classic token with write:packages) or install gh"
127+
fi
128+
129+
say "Building both architectures and pushing"
130+
docker buildx build \
131+
--platform "$PLATFORMS" \
132+
--tag "$IMAGE:$VERSION" \
133+
--tag "$IMAGE:$MINOR" \
134+
--tag "$IMAGE:$MAJOR" \
135+
--tag "$IMAGE:latest" \
136+
--push .
137+
138+
say "What is on the registry now"
139+
docker manifest inspect "$IMAGE:latest" > "$WORK/manifest.json" \
140+
|| die "latest was pushed but its manifest cannot be read back"
141+
grep -E '"architecture"' "$WORK/manifest.json" \
142+
|| die "latest was pushed but its manifest lists no architecture"
143+
printf '\nPublished:\n'
144+
for tag in "$VERSION" "$MINOR" "$MAJOR" latest; do
145+
printf ' %s:%s\n' "$IMAGE" "$tag"
146+
done
147+
printf '\nPull it somewhere else to be sure:\n docker run --rm %s:latest info\n' "$IMAGE"

‎uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)