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
153 changes: 153 additions & 0 deletions .github/actions/setup-release-cli/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: Set up release-cli
description: >
Acquires release-cli either from the release stamped into this file
(supported path) or from a caller-supplied binary (unsupported escape
hatch), then enforces the version/protocol guard.

inputs:
cli-path:
description: >
Unsupported path to a caller-supplied release-cli binary. You supply
the binary, you own the pairing: a stamp mismatch warns instead of
failing.
required: false
default: ''

outputs:
cli-path:
description: Absolute executable path selected by the action.
value: ${{ steps.resolve.outputs.cli-path }}
reported-version:
description: Version the binary reported.
value: ${{ steps.resolve.outputs.reported-version }}
reported-protocol:
description: Protocol integer the binary reported.
value: ${{ steps.resolve.outputs.reported-protocol }}

runs:
using: composite
steps:
- name: Resolve and verify the CLI
id: resolve
shell: bash
env:
# Release Please keeps this value in step with the released binary.
# x-release-please-start-version
DEFAULT_VERSION: 0.0.0
# x-release-please-end
EXPECTED_PROTOCOL: 1
CLI_PATH_INPUT: ${{ inputs.cli-path }}
SOURCE_REPOSITORY: ${{ github.action_repository }}
GH_TOKEN: ${{ inputs.cli-path == '' && github.token || '' }}
run: |
set -euo pipefail

if [[ -n "${CLI_PATH_INPUT}" ]]; then
# Caller-supplied binary: no download, no attestation, no token.
unset GH_TOKEN
mode=cli-path
binary="$(cd "$(dirname "${CLI_PATH_INPUT}")" && pwd)/$(basename "${CLI_PATH_INPUT}")"
test -f "${binary}"
test -x "${binary}"
else
mode=installed

# Download source and attestation trust root are the same value.
# Never fall back to GITHUB_REPOSITORY: that is the consumer on
# an external call, and a missing action_repository would verify
# an archive against the consumer itself.
if [[ -z "${SOURCE_REPOSITORY:-}" ]]; then
echo '::error::Cannot derive the release-cli distribution repository. Reference this action as $/.github/actions/setup-release-cli from a reusable workflow, or as owner/repo/path@ref. A local ./.github/actions path leaves github.action_repository empty.'
exit 1
fi
repository="${SOURCE_REPOSITORY}"

if ! command -v gh >/dev/null 2>&1; then
echo '::error::gh is required to download and verify release-cli. Install the GitHub CLI and retry.'
exit 1
fi
if ! gh attestation --help >/dev/null 2>&1; then
echo '::error::gh attestation is unavailable. Upgrade the GitHub CLI and retry.'
exit 1
fi

workdir="$(mktemp -d)"
tag="v${DEFAULT_VERSION}"

gh release download "${tag}" \
--repo "${repository}" \
--pattern 'release-cli_*_linux_amd64.tar.gz' \
--pattern 'checksums.txt' \
--dir "${workdir}"

shopt -s nullglob
archives=("${workdir}"/release-cli_*_linux_amd64.tar.gz)
if [[ ${#archives[@]} -ne 1 ]]; then
echo "::error::Expected exactly one release-cli_*_linux_amd64.tar.gz archive, found ${#archives[@]}."
exit 1
fi
archive="${archives[0]}"
archive_name="$(basename "${archive}")"

mapfile -t checksum_lines < <(
awk -v name="${archive_name}" '
$1 ~ /^[0-9A-Fa-f]{64}$/ {
n = $0
sub(/^[0-9A-Fa-f]{64} [ *]/, "", n)
if (n == name) print
}
' "${workdir}/checksums.txt"
)
if [[ ${#checksum_lines[@]} -ne 1 ]]; then
echo "::error::checksums.txt must contain exactly one SHA-256 entry for ${archive_name}."
exit 1
fi
(
cd "${workdir}"
printf '%s\n' "${checksum_lines[0]}" > .archive.sha256
sha256sum -c .archive.sha256
)

gh attestation verify "${archive}" \
--repo "${repository}" \
--signer-workflow "${repository}/.github/workflows/publish-github-release.yml" \
--deny-self-hosted-runners

tar -xzf "${archive}" -C "${workdir}"
binary="${workdir}/release-cli"
test -f "${binary}"
chmod +x "${binary}"
fi

report="$("${binary}" version --json)"
reported_version="$(printf '%s' "${report}" | jq -r .result.version)"
reported_protocol="$(printf '%s' "${report}" | jq -r .result.protocol)"

{
echo "cli-path=${binary}"
echo "reported-version=${reported_version}"
echo "reported-protocol=${reported_protocol}"
} >>"${GITHUB_OUTPUT}"

echo "mode=${mode} version=${reported_version} protocol=${reported_protocol}"

if [[ "${mode}" == "installed" ]]; then
# Supported path: fail closed before any command runs.
if [[ "${reported_version}" != "${DEFAULT_VERSION}" ]]; then
echo "::error::Installed CLI reported version ${reported_version}, expected ${DEFAULT_VERSION}."
exit 1
fi
if [[ "${reported_protocol}" != "${EXPECTED_PROTOCOL}" ]]; then
echo "::error::Installed CLI reported protocol ${reported_protocol}, expected ${EXPECTED_PROTOCOL}."
exit 1
fi
echo "installed path verified: version and protocol match the release unit"
else
# Unsupported path: report, warn, and continue.
if [[ "${reported_protocol}" != "${EXPECTED_PROTOCOL}" ]]; then
echo "::warning::Off-contract binary: protocol ${reported_protocol} != expected ${EXPECTED_PROTOCOL}. You supplied the binary, you own the pairing."
fi
if [[ "${reported_version}" != "${DEFAULT_VERSION}" ]]; then
echo "::warning::Off-contract binary: version ${reported_version} != release-unit version ${DEFAULT_VERSION}."
fi
fi
60 changes: 34 additions & 26 deletions .github/workflows/go-pre-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ name: Reusable Go Pre-publish

on:
workflow_call:
inputs:
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 release-assets artifact.
Expand Down Expand Up @@ -37,6 +45,8 @@ jobs:
oci-input-artifact-url: ${{ steps.upload-oci-input.outputs.artifact-url }}
oci-input-artifact-digest: ${{ steps.upload-oci-input.outputs.artifact-digest }}
permissions:
actions: read
attestations: read
contents: read
id-token: write
env:
Expand Down Expand Up @@ -85,37 +95,35 @@ jobs:
goreleaser release --clean --skip=publish
'

- name: Verify release asset checksums
working-directory: dist
- 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: |
sha256sum --check checksums.txt
test -s checksums.txt.sigstore.json
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 Linux binaries
- name: Stage Go release artifacts
env:
RELEASE_CLI: ${{ steps.setup-cli.outputs.cli-path }}
shell: bash
run: |
mapfile -t binaries < <(
jq -r '
.[]
| select(.type == "Binary" and .goos == "linux")
| [.goarch, .path]
| @tsv
' dist/artifacts.json
)
if [[ ${#binaries[@]} -ne 2 ]]; then
echo "::error::Expected two canonical Linux binaries; found ${#binaries[@]}."
exit 1
fi
printf '%s\n' "${binaries[@]}" |
cut -f1 |
sort |
diff -u <(printf 'amd64\narm64\n') -

for record in "${binaries[@]}"; do
path=${record#*$'\t'}
test -x "${path}"
done
"${RELEASE_CLI}" stage --profile go --dist dist

- name: Upload canonical Linux binaries
id: upload-oci-input
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,64 @@ concurrency:
cancel-in-progress: false

jobs:
build-release-cli:
name: Build dogfood release-cli
if: github.event.deleted == false
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
env:
GOTOOLCHAIN: local
MISE_EXEC_AUTO_INSTALL: 'false'
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Setup mise
uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
with:
version: 2026.8.8
install_args: go
cache: true
add_shims_to_path: false
export_path: false

- name: Build release-cli
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
mkdir -p "${RUNNER_TEMP}/release-cli-dogfood"
mise exec -- go build \
-trimpath \
-ldflags "-s -w -X main.version=${version} -X main.commit=${GITHUB_SHA}" \
-o "${RUNNER_TEMP}/release-cli-dogfood/release-cli" \
./cmd/release-cli

- name: Upload dogfood release-cli
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-cli-dogfood
path: ${{ runner.temp }}/release-cli-dogfood/release-cli
if-no-files-found: error
retention-days: 1
compression-level: 0

release-assets:
name: Build release assets
if: github.event.deleted == false
needs: build-release-cli
permissions:
actions: read
attestations: read
contents: read
id-token: write
uses: ./.github/workflows/go-pre-publish.yml
with:
cli-path: .release-cli/release-cli
oci-image:
name: Build OCI image
needs: release-assets
Expand Down
12 changes: 6 additions & 6 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2

project_name: release-mvp
project_name: release-cli

gomod:
proxy: true
Expand All @@ -11,9 +11,9 @@ gomod:
- GOSUMDB=sum.golang.org

builds:
- id: release-mvp
main: ./cmd/release-mvp
binary: release-mvp
- id: release-cli
main: ./cmd/release-cli
binary: release-cli
env:
- CGO_ENABLED=0
goos:
Expand All @@ -32,9 +32,9 @@ builds:
mod_timestamp: "{{ .CommitTimestamp }}"

archives:
- id: release-mvp
- id: release-cli
ids:
- release-mvp
- release-cli
formats:
- tar.gz
format_overrides:
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# Meigma release workflows

This repository defines the reusable workflows and repository contract that Meigma uses to build, sign, attest, and publish Go binaries through GitHub Releases and multi-architecture OCI images through GHCR.
This repository defines the reusable workflows and repository contract that Meigma uses to build, sign, attest, and publish Go binaries through GitHub Releases and multi-architecture OCI images through GHCR. It also builds and publishes `release-cli`, which the Go producer uses to validate staged release artifacts. The repository's tagged release builds the CLI from its own source and supplies that binary to the producer workflow.

## Documentation

- [Configure GitHub releases](docs/how-to/configure-github-releases.md)
- [Configure OCI image publication](docs/how-to/configure-oci-images.md)
- [Rehearse and recover GitHub releases](docs/how-to/rehearse-and-recover-github-releases.md)
- [Upgrade GitHub release workflows](docs/how-to/upgrade-github-release-workflows.md)
- [`release-cli` contract reference](docs/reference/release-cli-contract.md)
- [GitHub release contract reference](docs/reference/github-release-contract.md)
- [OCI image contract reference](docs/reference/oci-image-contract.md)
- [Copyable Go release example](examples/go-release/)

Consumer repositories call the reusable workflows at a full commit SHA. The documented revision is `fb8c8098ff27968fb3070e928c00e925f38c698e`.
Consumer repositories call the reusable workflows at one full commit SHA. `FULL_SHA` is the placeholder for the released commit and will be replaced when this program's final pull request lands; the copyable example remains pinned to the last released revision until then.
6 changes: 3 additions & 3 deletions apko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ contents:
packages:
- alpine-release
- ca-certificates-bundle
- release-mvp
- release-cli

entrypoint:
command: /usr/bin/release-mvp
command: /usr/bin/release-cli

accounts:
groups:
Expand All @@ -27,7 +27,7 @@ archs:
- arm64

annotations:
org.opencontainers.image.title: release-mvp
org.opencontainers.image.title: release-cli
org.opencontainers.image.description: Exercise the Meigma release pipeline.
org.opencontainers.image.source: https://github.com/meigma/release
org.opencontainers.image.licenses: LicenseRef-Proprietary
5 changes: 5 additions & 0 deletions cmd/release-cli/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Package main is the release-cli process entrypoint.
//
// It installs a signal-aware [context.Context], injects real process streams
// and linker-stamped version metadata, and exits with [cli.ExitCode].
package main
Loading