Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/native-image-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ run-name: Native image parity ${{ inputs.native_image || github.ref }}
# reference, embedded PHP and the native image itself, and compares the three
# records, as the Shared Server fixtures corpus part does for a source build.
#
# Dispatch it with a published native image by digest to qualify that
# artifact. A pull request that changes the comparison itself builds the image
# Dispatch it from a release tag with that tag's published native index by
# digest to qualify that artifact for promotion. A pull request that changes the comparison itself builds the image
# from its own source and compares that build, so the comparison is exercised
# before it is relied on. Nothing is pushed.

Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,12 @@ Repository checks and focused test commands are documented in
[native publication and alias-promotion runbook](docs/native-release-promotion.md)
describes the separate qualification and protected release phases.

Published-image qualification can use a separately reviewed runner without
moving the producer tag. The v2 qualification subject binds the exact published
source/index/run and pinned MySQL and QEMU image subjects. The runner must be on
main and differ from the producer only in the closed qualification-source set.
The existing v1 model still requires runner and producer to be the same commit.
V2 receipts retain both identities, each actual dependency image, the owned
resource scope, body observations and verified cleanup. The four Compose cells
Published-image qualification runs from the release tag itself, so the runner
and the producer are the same commit. The qualification subject is the tag, its
commit and the published index, plus the pinned MySQL and QEMU images committed
in `scripts/ci/native-qualification-subject.json`. V2 receipts retain each actual
dependency image, the owned resource scope, body observations and verified
cleanup. The four Compose cells
retain engine target IDs, platform manifests and config digests distinctly, and
run on amd64 hosts; their arm64 server cells use the pinned QEMU helper. These
receipts do not select alias promotion or a GitHub Release.
Expand Down
87 changes: 82 additions & 5 deletions docs/native-release-promotion.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ moves `X.Y`, `X` or `latest`. A final release follows this order:
tag, then the existing published-Compose four-cell matrix on that same index
from that tag. These are separately admitted invocations, not automatic
promotion workflow dispatches.
3. Review the exact current run attempts, provider job identities and five
canonical receipt artifacts. Dispatch `native-alias-promotion.yml` from the
exact release tag, a matching `tag` input and that data-only selection manifest.
3. Build the data-only selection manifest from the three run IDs with
`scripts/ci/native_alias_manifest.py`, then review its exact current run
attempts, job identities and five canonical receipt artifacts. Dispatch
`native-alias-promotion.yml` from the exact release tag, a matching `tag`
input and that manifest.
4. An unconditional, unprivileged plan first binds ref, tag, manifest source and
protected-main ancestry. The read-only job then verifies the selection. After environment approval, the
mutation job repeats the entire verification while holding
Expand All @@ -36,6 +38,69 @@ aliases. A final Release that already exists before promotion causes an explicit
refusal. Escalate the exact legacy case to General; the promotion phase never
edits a Release or treats it as qualification. Missing historical receipts refuse.

## Release checklist

Every step runs from the tag's own workflow files. No commit, branch or
hand-written evidence file is needed. Run the commands from a checkout of
`main` with `gh` signed in.

1. Create the final tag at its commit on `main` and set the shared values.

```sh
tag=3.6.0
commit=$(git rev-parse "$tag^{commit}")
```

2. Publish the tag and wait for the run to succeed. Note its run ID.

```sh
gh workflow run native-release.yml --ref "$tag" -f tag="$tag" -f release_commit="$commit"
release_run=<native-release run ID>
```

3. Dispatch both qualifiers from the tag with the published index. The run
summary of the release run shows the same index.

```sh
index=$(docker buildx imagetools inspect --format '{{printf "%s" .Manifest.Digest}}' \
"ghcr.io/durable-workflow/server:$tag")
gh workflow run native-image-parity.yml --ref "$tag" \
-f native_image="ghcr.io/durable-workflow/server@$index"
gh workflow run published-compose.yml --ref "$tag" \
-f server_image="ghcr.io/durable-workflow/server@$index"
corpus_run=<native-image-parity run ID>
compose_run=<published-compose run ID>
```

4. After both qualifiers succeed, build the evidence manifest.

```sh
evidence=$(python3 -I scripts/ci/native_alias_manifest.py --tag "$tag" \
--release-run "$release_run" --corpus-run "$corpus_run" --compose-run "$compose_run")
```

5. Promote the aliases from the tag. The `dockerhub-native` Environment asks
for approval.

```sh
gh workflow run native-alias-promotion.yml --ref "$tag" -f tag="$tag" -f evidence="$evidence"
```

6. Create the GitHub Release from `main` with the same manifest.

```sh
gh workflow run native-github-release.yml --ref main -f tag="$tag" \
-f release_run_id="$release_run" \
-f release_run_attempt="$(jq -r .release.run_attempt <<<"$evidence")" \
-f promotion_evidence="$evidence"
```

The manifest builder only reads the provider API. It refuses a run that was
not dispatched from the tag, a qualifier image that is not one canonical GHCR
index, and any receipt artifact name that does not match exactly one artifact.
Its output is a selection. Promotion and Release creation verify everything
again.

## Evidence selection and authority

The input is at most 8,192 UTF-8 bytes of JSON with the closed schema
Expand All @@ -56,7 +121,16 @@ hex digits. The three run IDs and five artifact IDs are distinct. Compose cells
are exactly `linux/amd64` and `linux/arm64`, each with `local` and `production`.
Both qualifier runner commits equal the release source commit, with workflow
blobs read at that exact commit; being another ancestor of main is insufficient.
No alternative runner allowlist exists in this version.
No alternative runner allowlist exists in this version. The earlier v2 manifest
and its separate runner branch are gone.

The qualification subject is derived, not edited per release. Its release part
is the tag the qualifier was dispatched from, that tag's commit and the
requested index. Admission refuses the run unless GHCR's exact version tag
names that index. The pinned MySQL and QEMU helper images are the only
committed part, in `scripts/ci/native-qualification-subject.json`. The verifier
reads that file at the release commit and checks its Git blob identity against
every receipt.

Selection is not proof. Fixed `durable-workflow/server` provider reads establish
repository/head-repository IDs, numeric workflow identity/path, dispatch event,
Expand Down Expand Up @@ -88,10 +162,13 @@ The existing corpus job retains its broad comparison artifact and adds
contains only the canonical JSON receipt. PR/default/local image contexts emit a
separate diagnostic schema with `eligible=false`, never positive evidence.

A positive `durable-workflow.native-qualification-receipt.v1` binds repository,
A positive `durable-workflow.native-qualification-receipt.v2` binds repository,
run/attempt/job, runner/workflow blob, original requested index, platform/profile,
resolved child manifest and image config, actual container config ID,
architecture/OS, source/release labels, whitelisted observations and cleanup.
It also binds the runner ref, which must be the release tag, the subject blob,
the owned resource scope, the local image identity and each pinned dependency
image the job actually ran.
The index, child manifest and config are separate identities. Registry membership
and both registries' source labels are checked independently by the consumer.

Expand Down
6 changes: 3 additions & 3 deletions scripts/ci/check-docker-workflow-isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
# checks/CLI transport is bound to reviewed bytes. A source change requires an
# explicit model update and countercontrol review before gaining coverage.
QUALIFICATION_CLEANUP_SOURCES = {
"scripts/ci/native_qualification.py": "65916e73b7f0c6431c612f411e27f9429a2c37dbab287d1f17b840dee0f8d185",
"scripts/ci/native_alias_evidence.py": "dc213ce6d43e5ad050de2acb730140aa33e4c21825f8dc5bf86e66f69106b95d",
"scripts/ci/native_alias_transport.py": "16e495ed6874f7d62f29f0f4dc11aaf9e1715bd7046823f48d3443b3ed370c74",
"scripts/ci/native_qualification.py": "775655e8d60ecd9a468970cdc28ea40860e0b1549b75343786837f2a35f37eac",
"scripts/ci/native_alias_evidence.py": "7097eb55cf32b698539860b4a0a1bd010e251baf2def9f9e77f145aab96f60db",
"scripts/ci/native_alias_transport.py": "5a925299f3fc5c58fb937e85de579d3bed70942b19b464aca78a77e6d0d63273",
}


Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/native-qualification-subject.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"mysql":{"child_manifest_digest":"sha256:62fb722c78b24245ddff1796a0fcee4a49cc5b87e0aaaf20c92d1da9e0a2497b","image_config_digest":"sha256:6cd09145362dfe6831b14545de3d5fd6cc75c37cfd6ef8561429c1fc73518b39","requested_image":"mysql:8.0@sha256:62fb722c78b24245ddff1796a0fcee4a49cc5b87e0aaaf20c92d1da9e0a2497b"},"qemu":{"child_manifest_digest":"sha256:465d3fdd28d0f2b871ba4b4ec98bd183292e96167f00d9fd40bd249f8632d705","image_config_digest":"sha256:15935d0512bf0a8e5afa1df990d971cf97a619ede08fa60f2d5966a62de6d6b7","requested_image":"docker.io/tonistiigi/binfmt@sha256:465d3fdd28d0f2b871ba4b4ec98bd183292e96167f00d9fd40bd249f8632d705"},"release":{"index_digest":"sha256:b5776fea2a4627ebc477b3b350caaf600dc220e7ab75aa29d32b70e404fd7a68","run_attempt":1,"run_id":43109713180,"source_commit":"1b8e9ee4bc6402261cb465cb6996056bacedd97c","tag":"3.5.0"},"schema":"durable-workflow.native-qualification-subject.v1"}
{"mysql":{"child_manifest_digest":"sha256:62fb722c78b24245ddff1796a0fcee4a49cc5b87e0aaaf20c92d1da9e0a2497b","image_config_digest":"sha256:6cd09145362dfe6831b14545de3d5fd6cc75c37cfd6ef8561429c1fc73518b39","requested_image":"mysql:8.0@sha256:62fb722c78b24245ddff1796a0fcee4a49cc5b87e0aaaf20c92d1da9e0a2497b"},"qemu":{"child_manifest_digest":"sha256:465d3fdd28d0f2b871ba4b4ec98bd183292e96167f00d9fd40bd249f8632d705","image_config_digest":"sha256:15935d0512bf0a8e5afa1df990d971cf97a619ede08fa60f2d5966a62de6d6b7","requested_image":"docker.io/tonistiigi/binfmt@sha256:465d3fdd28d0f2b871ba4b4ec98bd183292e96167f00d9fd40bd249f8632d705"},"schema":"durable-workflow.native-qualification-subject.v2"}
Loading
Loading