diff --git a/.github/workflows/internal-image.yml b/.github/workflows/internal-image.yml new file mode 100644 index 0000000000..5b3d507cdd --- /dev/null +++ b/.github/workflows/internal-image.yml @@ -0,0 +1,73 @@ +name: Internal Image + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: false + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Build and publish CHEK image + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c + + - name: Log in to GHCR + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Refuse to overwrite immutable tag + env: + IMAGE_REF: ghcr.io/${{ github.repository }}:sha-${{ github.sha }} + run: | + if docker buildx imagetools inspect "${IMAGE_REF}" >/dev/null 2>inspect-error.log; then + echo "::error::Immutable image tag already exists: ${IMAGE_REF}" + exit 1 + fi + if ! grep -Eiq 'manifest unknown|name unknown|not found' inspect-error.log; then + cat inspect-error.log >&2 + echo "::error::Unable to prove immutable image tag is absent" + exit 1 + fi + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha,prefix=sha-,format=long + + - name: Build and push image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a + with: + context: . + file: Dockerfile + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=codex-lb-internal + cache-to: type=gha,mode=max,scope=codex-lb-internal diff --git a/openspec/changes/publish-chek-internal-images/.openspec.yaml b/openspec/changes/publish-chek-internal-images/.openspec.yaml new file mode 100644 index 0000000000..5081c98763 --- /dev/null +++ b/openspec/changes/publish-chek-internal-images/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-12 diff --git a/openspec/changes/publish-chek-internal-images/proposal.md b/openspec/changes/publish-chek-internal-images/proposal.md new file mode 100644 index 0000000000..aa4729c0b3 --- /dev/null +++ b/openspec/changes/publish-chek-internal-images/proposal.md @@ -0,0 +1,27 @@ +# Change: publish-chek-internal-images + +## Why + +CHEK deploys its maintained `chekdata/codex-lb` fork through ArgoCD. Production +must consume an image built from the reviewed fork commit rather than an +upstream image or a mutable local build, but the fork currently has no +repository-owned image publisher. + +## What Changes + +- Add a focused GitHub Actions workflow that publishes the fork to + `ghcr.io/chekdata/codex-lb` after a push to `main` or an explicit manual run. +- Publish both supported Linux architectures in one manifest. +- Publish an immutable full-SHA tag for GitOps while granting the workflow only + read-content and write-package access; do not publish a mutable `main` tag. +- Fail closed before building when the full-SHA tag already exists or the + registry cannot prove that it is absent. +- Serialize all runs for the same commit across branch and tag aliases so the + absence check and first publication cannot race. + +## Impact + +- Affected capability: `github-automation` +- Affected automation: `.github/workflows/internal-image.yml` +- Production manifests can pin a reviewed `sha-*` image without depending on + upstream publishing permissions or abbreviated-tag collisions. diff --git a/openspec/changes/publish-chek-internal-images/specs/github-automation/spec.md b/openspec/changes/publish-chek-internal-images/specs/github-automation/spec.md new file mode 100644 index 0000000000..2a9d0d3541 --- /dev/null +++ b/openspec/changes/publish-chek-internal-images/specs/github-automation/spec.md @@ -0,0 +1,62 @@ +## ADDED Requirements + +### Requirement: CHEK fork publishes internally owned container images + +The `chekdata/codex-lb` repository SHALL build and publish a container image to +`ghcr.io/chekdata/codex-lb` after changes land on `main` and when an operator +explicitly dispatches the workflow. The published manifest MUST support +`linux/amd64` and `linux/arm64`, MUST include an immutable `sha-` +tag, and MUST NOT publish a mutable `main` tag. Production GitOps consumers +MUST be able to select the immutable tag. + +Before publishing, the workflow MUST prove that the full-SHA tag does not +already exist. It MUST fail without building when the tag exists and MUST fail +closed when a registry or network error prevents that absence check. It MUST +NOT overwrite a previously published full-SHA tag. Runs selecting the same +commit through different branch or tag refs MUST be serialized by the full +commit SHA and MUST NOT cancel the run that currently owns publication. + +The workflow MUST use pinned action revisions and MUST limit its repository +permissions to reading contents and writing packages. Pull request events MUST +NOT publish images. + +#### Scenario: Main commit produces an immutable multi-architecture image + +- **WHEN** a reviewed commit lands on `main` +- **THEN** the workflow publishes `ghcr.io/chekdata/codex-lb:sha-` +- **AND** the image manifest supports `linux/amd64` and `linux/arm64` +- **AND** the run does not publish a mutable `main` tag + +#### Scenario: Pull request validation cannot publish a package + +- **WHEN** a pull request is opened or updated +- **THEN** the internal image workflow does not run from that event +- **AND** no image tag is published by the pull request + +#### Scenario: Operator can rebuild the current selected revision + +- **WHEN** an operator manually dispatches the workflow on an allowed ref +- **AND** the selected commit's full-SHA tag does not yet exist +- **THEN** the workflow builds and publishes the selected commit with its + immutable full-SHA tag + +#### Scenario: Repeated publication cannot change an immutable tag + +- **GIVEN** the selected commit's full-SHA tag already exists in GHCR +- **WHEN** the workflow is rerun or manually dispatched for that commit +- **THEN** it fails before the image build +- **AND** the existing tag is not overwritten + +#### Scenario: Registry uncertainty fails closed + +- **GIVEN** the workflow cannot determine whether the full-SHA tag exists +- **WHEN** the pre-publish check encounters a registry or network error +- **THEN** the workflow fails without publishing + +#### Scenario: Branch and tag aliases cannot race publication + +- **GIVEN** a branch and a tag both select the same commit +- **WHEN** push and manual-dispatch runs overlap for those refs +- **THEN** the runs execute the absence check and publication serially +- **AND** the later run observes the tag created by the first run and fails + before building diff --git a/openspec/changes/publish-chek-internal-images/tasks.md b/openspec/changes/publish-chek-internal-images/tasks.md new file mode 100644 index 0000000000..2cdbc3c5b0 --- /dev/null +++ b/openspec/changes/publish-chek-internal-images/tasks.md @@ -0,0 +1,16 @@ +# Tasks: publish-chek-internal-images + +## 1. Internal image publisher + +- [x] 1.1 Add a main-branch and manual-dispatch GHCR workflow with pinned actions +- [x] 1.2 Build and publish a multi-architecture image for amd64 and arm64 +- [x] 1.3 Emit only an immutable full-SHA tag with minimal permissions +- [x] 1.4 Refuse to overwrite an existing full-SHA tag and fail closed when its + registry state cannot be confirmed +- [x] 1.5 Serialize publication by full commit SHA across branch and tag refs + +## 2. Verification + +- [x] 2.1 Validate the workflow YAML and repository action policy +- [x] 2.2 Run strict OpenSpec validation +- [ ] 2.3 Merge the focused PR and verify the first main-branch image manifest