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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
113 changes: 113 additions & 0 deletions .github/workflows/publish-cli-runtime-image.yml
Original file line number Diff line number Diff line change
@@ -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<<EOF"
printf '%s\n' "${tags[@]}"
echo "EOF"
} >> "$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"
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<repository-owner>/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`.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<repository-owner>/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 <owner>/labview-cdev-cli `
-f promote_v1=true
```

## Operations Runbooks

- Controlled fork/upstream SHA parity recovery:
Expand Down
51 changes: 51 additions & 0 deletions tests/CdevCliRuntimeImagePublishContract.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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'
}
}
14 changes: 14 additions & 0 deletions tools/cli-runtime/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
26 changes: 26 additions & 0 deletions tools/cli-runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# cdev CLI Runtime Image

Base runtime image for `labview-cdev-cli` command execution.

Publish repository:
- `ghcr.io/<repository-owner>/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
```