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
69 changes: 37 additions & 32 deletions .github/workflows/go-oci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ on:
required: false
default: apko.yaml
type: string
cli-path:
description: >
Unsupported path to a caller-supplied release-cli binary. You
supply the binary, you own the pairing.
required: false
type: string
default: ''

outputs:
artifact-id:
description: ID of the authoritative OCI image artifact.
Expand Down Expand Up @@ -49,6 +57,7 @@ jobs:
image-digest: ${{ steps.verify.outputs.image-digest }}
permissions:
actions: read
attestations: read
contents: read
env:
MISE_EXEC_AUTO_INSTALL: 'false'
Expand Down Expand Up @@ -103,42 +112,38 @@ jobs:
apko version
'

- name: Download dogfood release-cli
if: inputs.cli-path != ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-cli-dogfood
path: ${{ runner.temp }}/release-cli-dogfood

- name: Place dogfood release-cli
if: inputs.cli-path != ''
env:
CLI_PATH: ${{ inputs.cli-path }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$(dirname "${CLI_PATH}")"
install -m755 "${RUNNER_TEMP}/release-cli-dogfood/release-cli" "${CLI_PATH}"

- name: Set up release-cli
id: setup-cli
uses: $/.github/actions/setup-release-cli
with:
cli-path: ${{ inputs.cli-path }}

- name: Verify canonical binary handoff
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
RELEASE_CLI: ${{ steps.setup-cli.outputs.cli-path }}
ARTIFACT_ID: ${{ inputs.artifact-id }}
EXPECTED_DIGEST: ${{ inputs.artifact-digest }}
with:
retries: 3
script: |
const artifactId = Number(process.env.ARTIFACT_ID)
if (!Number.isSafeInteger(artifactId) || artifactId <= 0) {
throw new Error(`Invalid artifact ID: ${process.env.ARTIFACT_ID}`)
}

const {data: artifact} = await github.rest.actions.getArtifact({
...context.repo,
artifact_id: artifactId,
})
if (artifact.expired) {
throw new Error(`Artifact ${artifactId} has expired.`)
}
if (artifact.workflow_run?.id !== Number(context.runId)) {
throw new Error(
`Artifact ${artifactId} belongs to workflow run ` +
`${artifact.workflow_run?.id}, expected ${context.runId}.`,
)
}
const normalize = (digest) => String(digest ?? '')
.replace(/^sha256:/, '')
.toLowerCase()
const expected = normalize(process.env.EXPECTED_DIGEST)
const actual = normalize(artifact.digest)
if (!expected || actual !== expected) {
throw new Error(
`Artifact digest mismatch: expected ${process.env.EXPECTED_DIGEST}, got ${artifact.digest}.`,
)
}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
"${RELEASE_CLI}" verify handoff --artifact-id "${ARTIFACT_ID}" --digest "${EXPECTED_DIGEST}"

- name: Download canonical Linux binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand Down
68 changes: 36 additions & 32 deletions .github/workflows/publish-github-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ on:
required: false
default: ''
type: string
cli-path:
description: >
Unsupported path to a caller-supplied release-cli binary. You
supply the binary, you own the pairing.
required: false
type: string
default: ''

secrets:
release-app-private-key:
description: Private key of the GitHub App that publishes releases.
Expand Down Expand Up @@ -126,42 +134,38 @@ jobs:
private-key: ${{ secrets.release-app-private-key }}
permission-contents: write

- name: Download dogfood release-cli
if: inputs.cli-path != ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-cli-dogfood
path: ${{ runner.temp }}/release-cli-dogfood

- name: Place dogfood release-cli
if: inputs.cli-path != ''
env:
CLI_PATH: ${{ inputs.cli-path }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$(dirname "${CLI_PATH}")"
install -m755 "${RUNNER_TEMP}/release-cli-dogfood/release-cli" "${CLI_PATH}"

- name: Set up release-cli
id: setup-cli
uses: $/.github/actions/setup-release-cli
with:
cli-path: ${{ inputs.cli-path }}

- name: Verify artifact handoff
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
RELEASE_CLI: ${{ steps.setup-cli.outputs.cli-path }}
ARTIFACT_ID: ${{ inputs.artifact-id }}
EXPECTED_DIGEST: ${{ inputs.artifact-digest }}
with:
retries: 3
script: |
const artifactId = Number(process.env.ARTIFACT_ID)
if (!Number.isSafeInteger(artifactId) || artifactId <= 0) {
throw new Error(`Invalid artifact ID: ${process.env.ARTIFACT_ID}`)
}

const {data: artifact} = await github.rest.actions.getArtifact({
...context.repo,
artifact_id: artifactId,
})
if (artifact.expired) {
throw new Error(`Artifact ${artifactId} has expired.`)
}
if (artifact.workflow_run?.id !== Number(context.runId)) {
throw new Error(
`Artifact ${artifactId} belongs to workflow run ` +
`${artifact.workflow_run?.id}, expected ${context.runId}.`,
)
}
const normalize = (digest) => String(digest ?? '')
.replace(/^sha256:/, '')
.toLowerCase()
const expected = normalize(process.env.EXPECTED_DIGEST)
const actual = normalize(artifact.digest)
if (!expected || actual !== expected) {
throw new Error(
`Artifact digest mismatch: expected ${process.env.EXPECTED_DIGEST}, got ${artifact.digest}.`,
)
}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
"${RELEASE_CLI}" verify handoff --artifact-id "${ARTIFACT_ID}" --digest "${EXPECTED_DIGEST}"

- name: Wait for matching draft release
id: draft
Expand Down
68 changes: 36 additions & 32 deletions .github/workflows/publish-oci-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ on:
required: false
default: false
type: boolean
cli-path:
description: >
Unsupported path to a caller-supplied release-cli binary. You
supply the binary, you own the pairing.
required: false
type: string
default: ''

outputs:
image-name:
description: Canonical GHCR image name.
Expand Down Expand Up @@ -85,42 +93,38 @@ jobs:
install_args: aqua:oras-project/oras aqua:sigstore/cosign
cache: true

- name: Download dogfood release-cli
if: inputs.cli-path != ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-cli-dogfood
path: ${{ runner.temp }}/release-cli-dogfood

- name: Place dogfood release-cli
if: inputs.cli-path != ''
env:
CLI_PATH: ${{ inputs.cli-path }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$(dirname "${CLI_PATH}")"
install -m755 "${RUNNER_TEMP}/release-cli-dogfood/release-cli" "${CLI_PATH}"

- name: Set up release-cli
id: setup-cli
uses: $/.github/actions/setup-release-cli
with:
cli-path: ${{ inputs.cli-path }}

- name: Verify authoritative OCI artifact
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
RELEASE_CLI: ${{ steps.setup-cli.outputs.cli-path }}
ARTIFACT_ID: ${{ inputs.artifact-id }}
EXPECTED_DIGEST: ${{ inputs.artifact-digest }}
with:
retries: 3
script: |
const artifactId = Number(process.env.ARTIFACT_ID)
if (!Number.isSafeInteger(artifactId) || artifactId <= 0) {
throw new Error(`Invalid artifact ID: ${process.env.ARTIFACT_ID}`)
}

const {data: artifact} = await github.rest.actions.getArtifact({
...context.repo,
artifact_id: artifactId,
})
if (artifact.expired) {
throw new Error(`Artifact ${artifactId} has expired.`)
}
if (artifact.workflow_run?.id !== Number(context.runId)) {
throw new Error(
`Artifact ${artifactId} belongs to workflow run ` +
`${artifact.workflow_run?.id}, expected ${context.runId}.`,
)
}
const normalize = (digest) => String(digest ?? '')
.replace(/^sha256:/, '')
.toLowerCase()
const expected = normalize(process.env.EXPECTED_DIGEST)
const actual = normalize(artifact.digest)
if (!expected || actual !== expected) {
throw new Error(
`Artifact digest mismatch: expected ${process.env.EXPECTED_DIGEST}, got ${artifact.digest}.`,
)
}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
"${RELEASE_CLI}" verify handoff --artifact-id "${ARTIFACT_ID}" --digest "${EXPECTED_DIGEST}"

- name: Download authoritative OCI image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ jobs:
with:
artifact-id: ${{ needs.release-assets.outputs.oci-input-artifact-id }}
artifact-digest: ${{ needs.release-assets.outputs.oci-input-artifact-digest }}
cli-path: .release-cli/release-cli
oci-publish:
name: Publish OCI image
needs: oci-image
Expand All @@ -96,6 +97,7 @@ jobs:
artifact-digest: ${{ needs.oci-image.outputs.artifact-digest }}
image-digest: ${{ needs.oci-image.outputs.image-digest }}
publish-image: true
cli-path: .release-cli/release-cli



Expand All @@ -120,5 +122,6 @@ jobs:
oci-image-reference: ${{ needs.oci-publish.outputs.image-reference }}
release-app-client-id: ${{ vars.MEIGMA_RELEASE_APP_CLIENT_ID }}
publish-release: true
cli-path: .release-cli/release-cli
secrets:
release-app-private-key: ${{ secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY }}
77 changes: 77 additions & 0 deletions .github/workflows/verify-setup-installed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Exercises the composite's installed acquisition path end to end:
# archive download, checksum verification, gh attestation verify with
# the derived --signer-workflow and --deny-self-hosted-runners, then
# stamp equality.
#
# This cannot pass until a release exists (release-please PR #6,
# chore(main): release 0.1.0, is still open). It is workflow_dispatch
# only and is intentionally not a merge gate.
name: Verify setup-release-cli installed path

on:
workflow_dispatch:
inputs:
version:
description: Semantic version the installed CLI must report (without a v prefix).
required: true
type: string

permissions: {}

jobs:
verify-installed:
name: Verify installed release-cli
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
actions: read
attestations: read
contents: read
env:
MISE_EXEC_AUTO_INSTALL: 'false'
steps:
- name: Setup mise
uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
with:
version: 2026.8.8
mise_toml: |
[tools]
"aqua:cli/cli" = "2.97.0"
install_args: aqua:cli/cli
cache: true
add_shims_to_path: true
export_path: true

- name: Set up release-cli
id: setup-cli
uses: $/.github/actions/setup-release-cli

- name: Assert installed version and protocol
env:
RELEASE_CLI: ${{ steps.setup-cli.outputs.cli-path }}
EXPECTED_VERSION: ${{ inputs.version }}
COMPOSITE_VERSION: ${{ steps.setup-cli.outputs.reported-version }}
COMPOSITE_PROTOCOL: ${{ steps.setup-cli.outputs.reported-protocol }}
shell: bash
run: |
set -euo pipefail
report="$("${RELEASE_CLI}" version --json)"
printf '%s\n' "${report}"
reported_version="$(printf '%s' "${report}" | jq -r .result.version)"
reported_protocol="$(printf '%s' "${report}" | jq -r .result.protocol)"
if [[ "${reported_version}" != "${EXPECTED_VERSION}" ]]; then
echo "::error::Installed CLI reported version ${reported_version}, expected ${EXPECTED_VERSION}."
exit 1
fi
if [[ "${reported_protocol}" != "1" ]]; then
echo "::error::Installed CLI reported protocol ${reported_protocol}, expected 1."
exit 1
fi
if [[ "${reported_version}" != "${COMPOSITE_VERSION}" ]]; then
echo "::error::version --json does not match the composite reported-version output."
exit 1
fi
if [[ "${reported_protocol}" != "${COMPOSITE_PROTOCOL}" ]]; then
echo "::error::version --json does not match the composite reported-protocol output."
exit 1
fi
25 changes: 25 additions & 0 deletions .mockery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Interfaces live in the consumer package (I2). Generated mocks live under
# the implementing adapter's mocks/ package (T3), not next to the interface.
# Add an interface here only when its slice lands. The port budget is closed
# at 13; do not invent a fourteenth.
all: false
dir: '{{.InterfaceDir}}'
filename: '{{ .InterfaceName | snakecase }}.go'
force-file-write: true
formatter: goimports
formatter-options:
goimports:
local-prefix: github.com/meigma/release
log-level: info
structname: '{{.Mock}}{{.InterfaceName}}'
pkgname: mocks
recursive: false
template: testify
packages:
github.com/meigma/release/internal/stage/pubgh:
config:
dir: internal/adapter/ghact/mocks
interfaces:
ArtifactMeta:
config:
filename: artifact_meta.go
Loading