diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c6c7f3..6a8b53c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,8 @@ jobs: './tests/CdevCliLinuxContract.Tests.ps1', './tests/CdevCliCiContract.Tests.ps1', './tests/CdevCliSyncGuardContract.Tests.ps1', - './tests/CdevCliForceAlignOpsContract.Tests.ps1' + './tests/CdevCliForceAlignOpsContract.Tests.ps1', + './tests/CdevCliRuntimeImagePublishContract.Tests.ps1' ) -CI -Output Detailed cli-contract: diff --git a/.github/workflows/publish-cli-runtime-image.yml b/.github/workflows/publish-cli-runtime-image.yml new file mode 100644 index 0000000..153663f --- /dev/null +++ b/.github/workflows/publish-cli-runtime-image.yml @@ -0,0 +1,113 @@ +name: publish-cli-runtime-image + +on: + workflow_dispatch: + inputs: + promote_v1: + description: Also refresh the v1 tag. + required: false + default: true + type: boolean + additional_tag: + description: Optional extra tag (for example canary or rc1). + required: false + default: '' + type: string + push: + branches: + - main + paths: + - tools/cli-runtime/Dockerfile + - scripts/Invoke-CdevCli.ps1 + - scripts/lib/** + - cli-contract.json + +permissions: + contents: read + packages: write + +concurrency: + group: publish-cli-runtime-image-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Publish CLI Runtime Image + runs-on: ubuntu-latest + env: + IMAGE_REPO: ghcr.io/${{ github.repository_owner }}/labview-cdev-cli-runtime + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve deterministic tags + id: resolve + shell: bash + run: | + set -euo pipefail + + date_utc="$(date -u +%Y%m%d)" + short_sha="${GITHUB_SHA:0:12}" + promote_v1="${{ github.event.inputs.promote_v1 }}" + additional_tag="${{ github.event.inputs.additional_tag }}" + + if [[ -z "$promote_v1" ]]; then + promote_v1="true" + fi + + if [[ -n "$additional_tag" ]] && [[ ! "$additional_tag" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "additional_tag must match ^[A-Za-z0-9._-]+$" >&2 + exit 1 + fi + + tags=() + tags+=("${IMAGE_REPO}:sha-${short_sha}") + tags+=("${IMAGE_REPO}:v1-${date_utc}") + if [[ "$promote_v1" == "true" ]]; then + tags+=("${IMAGE_REPO}:v1") + fi + if [[ -n "$additional_tag" ]]; then + tags+=("${IMAGE_REPO}:${additional_tag}") + fi + + { + echo "date_utc=$date_utc" + echo "short_sha=$short_sha" + echo "tags<> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push image + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ./tools/cli-runtime/Dockerfile + push: true + tags: ${{ steps.resolve.outputs.tags }} + + - name: Publish summary + shell: bash + run: | + { + echo "## cdev CLI Runtime Image Published" + echo "" + echo "- Image: \`${IMAGE_REPO}\`" + echo "- Digest: \`${{ steps.build.outputs.digest }}\`" + echo "- Commit: \`${GITHUB_SHA}\`" + echo "- Tags:" + while IFS= read -r tag; do + echo " - \`$tag\`" + done <<< "${{ steps.resolve.outputs.tags }}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/AGENTS.md b/AGENTS.md index 9577b07..1200fd0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,3 +81,5 @@ This repository is the control-plane CLI for deterministic `C:\dev` workspace or - `.sha256` - `cdev-cli.spdx.json` - `cdev-cli.slsa.json` +- `publish-cli-runtime-image.yml` publishes base runtime image `ghcr.io//labview-cdev-cli-runtime` with immutable tags (`sha-*`, `v1-YYYYMMDD`) and optional mutable `v1`. +- Canonical consumer image path is `ghcr.io/labview-community-ci-cd/labview-cdev-cli-runtime`. diff --git a/README.md b/README.md index 1418f75..3745e37 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,27 @@ Release artifacts: - `cdev-cli.spdx.json` - `cdev-cli.slsa.json` +## Runtime Image (Base Layer) + +`publish-cli-runtime-image.yml` publishes the base CLI runtime image to: +- `ghcr.io//labview-cdev-cli-runtime` + +Canonical consumer reference remains: +- `ghcr.io/labview-community-ci-cd/labview-cdev-cli-runtime` + +Deterministic tags: +- `sha-<12-char-commit>` +- `v1-YYYYMMDD` +- `v1` (when promoted) + +Dispatch manually: + +```powershell +gh workflow run publish-cli-runtime-image.yml ` + -R /labview-cdev-cli ` + -f promote_v1=true +``` + ## Operations Runbooks - Controlled fork/upstream SHA parity recovery: diff --git a/tests/CdevCliRuntimeImagePublishContract.Tests.ps1 b/tests/CdevCliRuntimeImagePublishContract.Tests.ps1 new file mode 100644 index 0000000..f33cbc6 --- /dev/null +++ b/tests/CdevCliRuntimeImagePublishContract.Tests.ps1 @@ -0,0 +1,51 @@ +#Requires -Version 7.0 +#Requires -Modules Pester + +$ErrorActionPreference = 'Stop' + +Describe 'cdev CLI runtime image publish contract' { + BeforeAll { + $script:repoRoot = (Resolve-Path -Path (Join-Path $PSScriptRoot '..')).Path + $script:dockerfilePath = Join-Path $script:repoRoot 'tools/cli-runtime/Dockerfile' + $script:workflowPath = Join-Path $script:repoRoot '.github/workflows/publish-cli-runtime-image.yml' + $script:agentsPath = Join-Path $script:repoRoot 'AGENTS.md' + + foreach ($path in @($script:dockerfilePath, $script:workflowPath, $script:agentsPath)) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "Missing runtime-image contract file: $path" + } + } + + $script:dockerfile = Get-Content -LiteralPath $script:dockerfilePath -Raw + $script:workflow = Get-Content -LiteralPath $script:workflowPath -Raw + $script:agents = Get-Content -LiteralPath $script:agentsPath -Raw + } + + It 'builds a PowerShell-based CLI runtime image with required tooling and entrypoint' { + $script:dockerfile | Should -Match 'mcr\.microsoft\.com/powershell' + $script:dockerfile | Should -Match 'git jq gh' + $script:dockerfile | Should -Match 'ENTRYPOINT \["pwsh", "-NoProfile", "-File", "/opt/cdev-cli/scripts/Invoke-CdevCli\.ps1"\]' + $script:dockerfile | Should -Match 'COPY scripts' + } + + It 'defines deterministic GHCR publish flow with package write permission' { + $script:workflow | Should -Match 'workflow_dispatch:' + $script:workflow | Should -Match 'push:' + $script:workflow | Should -Match 'packages:\s*write' + $script:workflow | Should -Match 'ghcr\.io/\$\{\{\s*github\.repository_owner\s*\}\}/labview-cdev-cli-runtime' + $script:workflow | Should -Match 'docker/login-action@v3' + $script:workflow | Should -Match 'docker/build-push-action@v6' + } + + It 'publishes immutable tags and summary digest evidence' { + $script:workflow | Should -Match 'sha-\$\{short_sha\}' + $script:workflow | Should -Match 'v1-\$\{date_utc\}' + $script:workflow | Should -Match 'steps\.build\.outputs\.digest' + } + + It 'documents fork-safe mutation target for runtime publish operations' { + $script:agents | Should -Match 'Allowed mutation target' + $script:agents | Should -Match 'svelderrainruiz/labview-cdev-cli' + $script:agents | Should -Match 'ghcr\.io/labview-community-ci-cd/labview-cdev-cli-runtime' + } +} diff --git a/tools/cli-runtime/Dockerfile b/tools/cli-runtime/Dockerfile new file mode 100644 index 0000000..5877d1e --- /dev/null +++ b/tools/cli-runtime/Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/powershell:7.4-ubuntu-22.04 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git jq gh ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/cdev-cli + +COPY scripts ./scripts +COPY cli-contract.json ./cli-contract.json +COPY README.md ./README.md + +ENTRYPOINT ["pwsh", "-NoProfile", "-File", "/opt/cdev-cli/scripts/Invoke-CdevCli.ps1"] +CMD ["help"] diff --git a/tools/cli-runtime/README.md b/tools/cli-runtime/README.md new file mode 100644 index 0000000..ac1058b --- /dev/null +++ b/tools/cli-runtime/README.md @@ -0,0 +1,26 @@ +# cdev CLI Runtime Image + +Base runtime image for `labview-cdev-cli` command execution. + +Publish repository: +- `ghcr.io//labview-cdev-cli-runtime` + +Canonical consumer repository: +- `ghcr.io/labview-community-ci-cd/labview-cdev-cli-runtime` + +Deterministic tags: +- `sha-<12-char-commit>` +- `v1-YYYYMMDD` +- `v1` (when promoted) + +Local build: + +```powershell +docker build -f .\tools\cli-runtime\Dockerfile -t cdev-cli-runtime:local . +``` + +Local run: + +```powershell +docker run --rm cdev-cli-runtime:local help +```