Merge branch 'ci/cloud-image-full-sha' into ci/probe-cloud-combined
This commit is contained in:
commit
e8e9177335
|
|
@ -520,6 +520,7 @@ jobs:
|
|||
io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }}
|
||||
|
||||
- name: Build and push (cloud)
|
||||
id: build-cloud
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
|
|
@ -557,18 +558,16 @@ jobs:
|
|||
|
||||
- name: Verify the pushed image resolves the declared Sentry version
|
||||
env:
|
||||
IMAGE_TAGS: ${{ steps.meta-cloud.outputs.tags }}
|
||||
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build-cloud.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="$(printf '%s\n' "$IMAGE_TAGS" | head -n 1)"
|
||||
test -n "$image"
|
||||
|
||||
expected="$(node -e "process.stdout.write(require('./server/package.json').peerDependencies['@sentry/node'])")"
|
||||
test -n "$expected"
|
||||
|
||||
installed="$(docker run --rm --pull always \
|
||||
-v "$PWD/scripts/assert-cloud-image-sentry.mjs:/app/server/.ci-sentry-probe.mjs:ro" \
|
||||
--entrypoint node "$image" /app/server/.ci-sentry-probe.mjs)"
|
||||
--entrypoint node "$IMAGE" /app/server/.ci-sentry-probe.mjs)"
|
||||
|
||||
echo "Declared optional peer version: $expected"
|
||||
echo "Installed in the pushed image: $installed"
|
||||
|
|
@ -578,6 +577,21 @@ jobs:
|
|||
fi
|
||||
echo "The pushed image resolves the declared @sentry/node version."
|
||||
|
||||
# Cloud's commit resolver and preview-artifact planner use the full SHA.
|
||||
# Publish that address only after checking this build's exact digest.
|
||||
# Retagging reuses the registry manifest and does not rebuild the image.
|
||||
- name: Publish verified full-SHA cloud tag
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build-cloud.outputs.digest }}
|
||||
FULL_SHA_TAG: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}-cloud
|
||||
run: |
|
||||
set -euo pipefail
|
||||
revision="$(docker image inspect "$IMAGE" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')"
|
||||
platform="$(docker image inspect "$IMAGE" --format '{{ .Os }}/{{ .Architecture }}')"
|
||||
test "$revision" = "$GITHUB_SHA"
|
||||
test "$platform" = linux/amd64
|
||||
docker buildx imagetools create --prefer-index=false --tag "$FULL_SHA_TAG" "$IMAGE"
|
||||
|
||||
# Moves the mutable `:canary` / `:canary-cloud` channel tags. Kept OUT
|
||||
# of the build jobs and serialized in its own lane, and — the load-
|
||||
# bearing property — CONVERGENT rather than self-interested: a
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ docker build -t paperclip-local \
|
|||
--build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) .
|
||||
```
|
||||
|
||||
## Cloud image addresses
|
||||
|
||||
The Docker workflow publishes the managed deployment image for Linux AMD64.
|
||||
After the pushed image passes its Sentry check, the workflow verifies its
|
||||
commit label and platform and adds `ghcr.io/paperclipai/paperclip:sha-<full-commit-sha>-cloud`.
|
||||
This address lets commit-based deployment tooling reuse the normal build.
|
||||
Existing short-SHA and release tags remain available.
|
||||
|
||||
The full-SHA tag identifies the source commit. It does not certify that source
|
||||
tests passed or that a compatible database migrator is available. Deployment
|
||||
tooling must still check those prerequisites and pin the resolved image digest;
|
||||
a rebuild of the same source can update the tag's digest.
|
||||
|
||||
## One-liner (build + run)
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { readFileSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { previewManifest, assertMetadata, validateRequest, versionFor, tarManifest, packageExists, imageExists, publishPreview, publishImage } from "./preview-artifacts.mjs";
|
||||
|
||||
const sha = "a".repeat(40);
|
||||
|
|
@ -126,3 +127,49 @@ test("commits sharing a short prefix use separate full-SHA image addresses", asy
|
|||
await imageExists(other, fetchImpl);
|
||||
assert.deepEqual(urls.filter((url) => url.includes("/manifests/")), [sha, other].map((commit) => `https://ghcr.io/v2/paperclipai/paperclip/manifests/sha-${commit}-cloud`));
|
||||
});
|
||||
|
||||
test("normal cloud builds publish the checked digest only when source and platform match", () => {
|
||||
const workflow = readFileSync(new URL("../.github/workflows/docker.yml", import.meta.url), "utf8");
|
||||
const cloud = workflow.split(" build-and-push-cloud:")[1].split(" promote_canary_channel:")[0];
|
||||
const verify = cloud.indexOf(" - name: Verify the pushed image resolves the declared Sentry version");
|
||||
const publish = cloud.indexOf(" - name: Publish verified full-SHA cloud tag");
|
||||
assert.ok(verify >= 0 && publish > verify);
|
||||
const verification = cloud.slice(verify, publish);
|
||||
assert.match(verification, /IMAGE: ghcr.io\/\$\{\{ github.repository \}\}@\$\{\{ steps.build-cloud.outputs.digest \}\}/);
|
||||
assert.doesNotMatch(verification, /continue-on-error:|if: always\(/);
|
||||
const step = cloud.slice(publish).split(/\n(?: #| - name:)/)[0];
|
||||
assert.doesNotMatch(step, /continue-on-error:|if:/);
|
||||
assert.match(step, /FULL_SHA_TAG: ghcr.io\/\$\{\{ github.repository \}\}:sha-\$\{\{ github.sha \}\}-cloud/);
|
||||
const script = step.split(" run: |\n")[1].split("\n").map((line) => line.replace(/^ {10}/, "")).join("\n");
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "cloud-tag-test-"));
|
||||
const image = `ghcr.io/paperclipai/paperclip@sha256:${"b".repeat(64)}`;
|
||||
const tag = `ghcr.io/paperclipai/paperclip:sha-${sha}-cloud`;
|
||||
try {
|
||||
writeFileSync(path.join(dir, "docker"), `#!/bin/sh
|
||||
case "$1 $2" in
|
||||
'image inspect')
|
||||
case "$5" in
|
||||
*revision*) printf '%s\\n' "$TEST_REVISION" ;;
|
||||
*) printf '%s\\n' "$TEST_PLATFORM" ;;
|
||||
esac ;;
|
||||
'buildx imagetools') printf '%s\\n' "$@" > "$TEST_CALLS" ;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
`, { mode: 0o755 });
|
||||
for (const [revision, platform, succeeds] of [[sha, "linux/amd64", true], ["c".repeat(40), "linux/amd64", false], [sha, "linux/arm64", false]]) {
|
||||
const calls = path.join(dir, "calls");
|
||||
rmSync(calls, { force: true });
|
||||
const result = spawnSync("bash", ["-c", script], { encoding: "utf8", env: {
|
||||
...process.env, PATH: `${dir}${path.delimiter}${process.env.PATH}`, GITHUB_SHA: sha,
|
||||
IMAGE: image, FULL_SHA_TAG: tag, TEST_REVISION: revision, TEST_PLATFORM: platform, TEST_CALLS: calls,
|
||||
} });
|
||||
if (succeeds) {
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(readFileSync(calls, "utf8").trim().split("\n"), ["buildx", "imagetools", "create", "--prefer-index=false", "--tag", tag, image]);
|
||||
} else {
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.throws(() => readFileSync(calls), { code: "ENOENT" });
|
||||
}
|
||||
}
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue