Skip to content

Commit 20ee209

Browse files
luisleo526claude
andauthored
feat(ci): no committed base-image pin; supply it as a build-arg (#8)
The base pineforge-release version is no longer hard-coded in docker/Dockerfile. It is recorded in the release TAG MESSAGE (`release=`) by pineforge-release.yml and supplied to the image build as a build-arg by publish.yml. - docker/Dockerfile: bare `ARG PINEFORGE_RELEASE_VERSION` (no default; the FROM fails fast without the build-arg). - pineforge-release.yml: drop the Dockerfile sed; idempotency now compares the dispatch release_version to the latest tag's `release=`; stage VERSION + package.json + server.json only; annotated tag carries `release=`. - publish.yml image job: fetch-depth 0; resolve the base from the annotated tag message (guarded by `git cat-file -t == tag`; fallback resolves latest with a loud warning); wait for the base image to exist; pass `--build-arg PINEFORGE_RELEASE_VERSION=<resolved>`. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 591e6a6 commit 20ee209

3 files changed

Lines changed: 62 additions & 21 deletions

File tree

.github/workflows/pineforge-release.yml

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ name: pineforge-release
33
# Fired by pineforge-release's publish.yml (repository_dispatch
44
# event_type=pineforge-release, client_payload.release_version=X.Y.Z).
55
#
6-
# Bumps the base-image pin (docker/Dockerfile ARG PINEFORGE_RELEASE_VERSION),
7-
# auto patch-bumps THIS repo's own /VERSION (independent lineage), runs
8-
# sync-version, commits the tracked files to main, and pushes v<VERSION> with the
9-
# org GitHub App token. The App-token-pushed tag re-fires the UNCHANGED
10-
# publish.yml (npm + GHCR + MCP registry, OIDC) — the sole publisher; this
11-
# workflow never publishes inline. Idempotent: no-op if the pin already matches.
6+
# Records the base pineforge-release version in the release TAG MESSAGE
7+
# (`release=`) — no committed pin. Auto patch-bumps THIS repo's own /VERSION
8+
# (independent lineage), runs sync-version, commits the tracked files to main, and
9+
# pushes v<VERSION> with the org GitHub App token. The App-token-pushed tag
10+
# re-fires the UNCHANGED publish.yml (npm + GHCR + MCP registry, OIDC) — the sole
11+
# publisher; this workflow never publishes inline. Idempotent: no-op if the base
12+
# already matches the latest release tag.
1213

1314
on:
1415
repository_dispatch:
@@ -50,19 +51,24 @@ jobs:
5051
fi
5152
echo "version=${RAW#v}" >> "$GITHUB_OUTPUT"
5253
53-
- name: Idempotency — stop if pin already matches
54+
- name: Idempotency — stop if the base already matches the last release
5455
id: gate
5556
env:
5657
REL: ${{ steps.rel.outputs.version }}
5758
run: |
5859
set -euo pipefail
59-
cur="$(grep -m1 '^ARG PINEFORGE_RELEASE_VERSION=' docker/Dockerfile | sed -E 's/^ARG PINEFORGE_RELEASE_VERSION=//' | tr -d '[:space:]')"
60+
# The base pineforge-release version is recorded in each release tag's
61+
# message (`release=`), not in a committed pin. Compare against the latest.
62+
git fetch --tags --quiet
63+
LATEST="$(git tag -l 'v*' --sort=-v:refname | head -1)"
64+
cur=""
65+
[ -n "$LATEST" ] && cur="$(git for-each-ref --format='%(contents:body)' "refs/tags/$LATEST" | sed -n 's/^release=//p' | head -1)"
6066
if [ "$cur" = "$REL" ]; then
6167
echo "proceed=false" >> "$GITHUB_OUTPUT"
62-
echo "pin already ${REL} — nothing to do (duplicate fan-out absorbed)."
68+
echo "base already ${REL} at ${LATEST} — nothing to do (duplicate fan-out absorbed)."
6369
else
6470
echo "proceed=true" >> "$GITHUB_OUTPUT"
65-
echo "bumping PINEFORGE_RELEASE_VERSION ${cur} -> ${REL}"
71+
echo "bumping base pineforge-release ${cur:-∅} -> ${REL}"
6672
fi
6773
6874
- name: Configure git identity
@@ -94,16 +100,14 @@ jobs:
94100
node-version: 22
95101
cache: npm
96102

97-
- name: Bump base-image pin + VERSION + sync
103+
- name: Bump VERSION + sync
98104
if: steps.gate.outputs.proceed == 'true'
99105
env:
100-
REL: ${{ steps.rel.outputs.version }}
101106
NEXT: ${{ steps.ver.outputs.next }}
102107
run: |
103108
set -euo pipefail
104-
# Bump ONLY the pre-FROM default ARG line (the bare re-declaration has no '=').
105-
sed -i -E "0,/^ARG PINEFORGE_RELEASE_VERSION=.*/s//ARG PINEFORGE_RELEASE_VERSION=${REL}/" docker/Dockerfile
106-
grep -q "^ARG PINEFORGE_RELEASE_VERSION=${REL}$" docker/Dockerfile || { echo "::error::Dockerfile pin not updated"; exit 1; }
109+
# No committed base pin: the pineforge-release version is supplied to the
110+
# image build as a build-arg (publish.yml) and recorded in the tag message.
107111
printf '%s\n' "$NEXT" > VERSION
108112
npm ci --no-audit --no-fund
109113
npm run sync-version # rewrites package.json + server.json (+ gitignored src/version.ts)
@@ -125,10 +129,10 @@ jobs:
125129
REL: ${{ steps.rel.outputs.version }}
126130
run: |
127131
set -euo pipefail
128-
git add VERSION package.json server.json docker/Dockerfile
132+
git add VERSION package.json server.json
129133
git diff --cached --quiet && { echo "::error::nothing staged — refusing empty release"; exit 1; }
130134
git commit -m "chore(release): ${TAG} — base pineforge-release ${REL}"
131-
git tag -a "${TAG}" -m "Release ${TAG} (pineforge-release ${REL})"
135+
git tag -a "${TAG}" -m "$(printf 'Release %s\n\nrelease=%s\n' "${TAG}" "${REL}")"
132136
git push origin HEAD:main
133137
git push origin "${TAG}"
134-
echo "pushed ${TAG}; publish.yml will run from the tag"
138+
echo "pushed ${TAG} (base ${REL}); publish.yml will run from the tag"

.github/workflows/publish.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ jobs:
7272
packages: write
7373
steps:
7474
- uses: actions/checkout@v4
75+
with:
76+
fetch-depth: 0
7577
# Third-party actions pinned to commit SHA (supply-chain hardening); the
7678
# release pipeline has packages:write, so a hijacked tag could poison
7779
# published images. Tag comments are the resolved version.
@@ -82,13 +84,45 @@ jobs:
8284
registry: ghcr.io
8385
username: ${{ github.actor }}
8486
password: ${{ github.token }}
87+
- name: Resolve base pineforge-release version + wait for its image
88+
id: base
89+
env:
90+
GH_TOKEN: ${{ github.token }}
91+
REF: ${{ github.ref_name }}
92+
run: |
93+
set -euo pipefail
94+
semver() { printf '%s' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][A-Za-z0-9.]+)?$'; }
95+
rel=""
96+
# Trust the tag message ONLY if REF is an ANNOTATED tag — a lightweight
97+
# tag (or workflow_dispatch's branch ref) leaks the commit body, not ours.
98+
if [ "$(git cat-file -t "$REF" 2>/dev/null || true)" = tag ]; then
99+
rel="$(git for-each-ref --format='%(contents:body)' "refs/tags/$REF" | sed -n 's/^release=//p' | head -1)"
100+
fi
101+
if ! semver "$rel"; then
102+
echo "::warning::no valid release= in tag ${REF} — resolving latest pineforge-release (may differ from this commit's intended base)"
103+
for i in 1 2 3 4 5; do rel="$(gh release view -R pineforge-4pass/pineforge-release --json tagName -q .tagName 2>/dev/null || true)"; [ -n "$rel" ] && break; sleep 5; done
104+
rel="${rel#v}"
105+
fi
106+
semver "$rel" || { echo "::error::bad base release version '$rel'"; exit 1; }
107+
# The base image must exist before we build FROM it.
108+
img="ghcr.io/pineforge-4pass/pineforge-release:${rel}"
109+
ok=
110+
for i in $(seq 1 20); do
111+
if docker manifest inspect "$img" >/dev/null 2>&1; then ok=1; break; fi
112+
echo "waiting for base image ${img} ... (${i})"; sleep 15
113+
done
114+
[ -n "$ok" ] || { echo "::error::base image ${img} not available after wait"; exit 1; }
115+
echo "release=$rel" >> "$GITHUB_OUTPUT"
116+
echo "base pineforge-release=$rel"
85117
- name: Build and push
86118
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
87119
with:
88120
context: .
89121
file: docker/Dockerfile
90122
platforms: linux/amd64,linux/arm64
91123
push: true
124+
build-args: |
125+
PINEFORGE_RELEASE_VERSION=${{ steps.base.outputs.release }}
92126
tags: |
93127
ghcr.io/pineforge-4pass/pineforge-codegen-mcp:${{ github.ref_name }}
94128
ghcr.io/pineforge-4pass/pineforge-codegen-mcp:latest

docker/Dockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
#
77
# Build: docker build -f docker/Dockerfile -t pineforge-codegen-mcp .
88

9-
# Single source of truth for the baked pineforge-release version: it tags the
10-
# base image AND is exported as PINEFORGE_VERSION so engine_info can report it.
9+
# The baked pineforge-release version is supplied by CI as a build-arg (resolved
10+
# at release time; recorded in the release tag message `release=`, read back by
11+
# publish.yml). It tags the base image AND is exported as PINEFORGE_VERSION so
12+
# engine_info can report it. No default — building without
13+
# `--build-arg PINEFORGE_RELEASE_VERSION` fails fast at the FROM below.
1114
# Declared before the first FROM so it's usable in the final stage's FROM line.
12-
ARG PINEFORGE_RELEASE_VERSION=0.1.3
15+
ARG PINEFORGE_RELEASE_VERSION
1316

1417
# --- build MCP dist + prod deps (reproducible: committed lockfile + npm ci) ---
1518
FROM node:22-slim AS mcp

0 commit comments

Comments
 (0)