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
5 changes: 1 addition & 4 deletions .github/workflows/go-oci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,12 @@ jobs:
output="$(jq -er '.result.output' <<<"${envelope}")"
version="$(jq -er '.result.version' <<<"${envelope}")"
result_build_date="$(jq -er '.result.build_date' <<<"${envelope}")"
binary_name="$(jq -er '.result.binary' <<<"${envelope}")"

{
printf 'work=%s\n' "${work}"
printf 'output=%s\n' "${output}"
printf 'version=%s\n' "${version}"
printf 'build-date=%s\n' "${result_build_date}"
printf 'binary-name=%s\n' "${binary_name}"
} >>"${GITHUB_OUTPUT}"

- name: Verify authoritative OCI image
Expand All @@ -187,13 +185,12 @@ jobs:
RELEASE_CLI: ${{ steps.setup-cli.outputs.cli-path }}
RELEASE_OUTPUT: ${{ steps.build.outputs.output }}
RELEASE_WORK: ${{ steps.build.outputs.work }}
RELEASE_BINARY: ${{ steps.build.outputs.binary-name }}
RELEASE_VERSION: ${{ steps.build.outputs.version }}
shell: bash
run: |
set -euo pipefail
# The CLI reads the layout byte for byte: the index, every platform
# manifest and config, the staged binary inside the layer blob, and
# manifest and config, every staged binary inside the layer blob, and
# the SPDX documents. Revision and source provenance come from the
# Actions environment, so they are not passed as flags. The CLI also
# writes image-digest.txt into the output root for the publisher.
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# Release workflows

This repository publishes `release-cli` and reusable GitHub Actions workflows
for releasing one static Go application from one repository. A release can
produce GitHub Release assets, a multi-architecture image in GHCR, Homebrew and
Scoop update pull requests, and signed DEB, RPM, and APK repositories in
Cloudflare R2.
for releasing static Go binaries from one repository. A release can produce
GitHub Release assets, a multi-architecture image in GHCR, Homebrew and Scoop
update pull requests, and signed DEB, RPM, and APK repositories in Cloudflare
R2.

## Supported release

The supported application contract is intentionally narrow:

- one Go application and binary per repository;
- one Go repository, one unscoped tag stream, and one GHCR image;
- Linux `amd64` and `arm64` must publish the same nonempty set of static
binary names;
- stable, unscoped `vMAJOR.MINOR.PATCH` tags;
- static Darwin, Linux, and Windows binaries for `amd64` and `arm64`;
- Linux `amd64` and `arm64` images at `ghcr.io/<owner>/<repository>`;
Expand Down
17 changes: 12 additions & 5 deletions docs/explanation/architecture-and-trust.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,25 @@ onboarding is a reviewed policy and public-key change rather than a grant of
shared production credentials. A replay can converge from current object state,
and an immutable conflict fails instead of silently replacing history.

## One application per repository is a deliberate limit
## One repository, one image, one unscoped tag stream

The repository name determines the GHCR image name, the caller has one stable
unscoped tag stream, the OCI layout has one entrypoint, and Release Please owns
one root manifest version. Supporting multiple applications would require
component-aware tags, separate asset namespaces, multiple image names, and more
complex ownership and recovery rules across every publisher.

Keeping one application per repository avoids that cross-product. The tradeoff
is more repositories and repeated organization setup. The repeated unit is
operationally visible: each application has one App installation entry, one
release-unit SHA, one draft, one image, and one set of optional destinations.
One repository may stage several GoReleaser binaries into that single image
when Linux `amd64` and `arm64` publish the same nonempty name set. The image
entrypoint remains `/usr/bin/<name>` for one of those names. Multiple
applications, image names, or unscoped tag streams still require separate
repositories.

Keeping one application identity per repository avoids that cross-product. The
tradeoff is more repositories and repeated organization setup. The repeated
unit is operationally visible: each application has one App installation
entry, one release-unit SHA, one draft, one image, and one set of optional
destinations.

## Unsupported cases preserve these boundaries

Expand Down
44 changes: 34 additions & 10 deletions docs/how-to/adopt-the-release-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ Use this guide to add the reusable release unit to an existing Go application
repository. Complete [Prepare your GitHub organization](prepare-your-github-organization.md)
first.

The supported unit releases one application and one binary from one repository.
Use a separate repository and caller for each additional application.
The unit stages every Linux `amd64` and `arm64` GoReleaser binary from one
repository into one GitHub Release and one multi-architecture image. The binary
name set must be identical and nonempty on both architectures. A name present
on only one architecture is an error. Duplicate `(arch, name)` pairs are
rejected. Each platform config Entrypoint must be exactly `/usr/bin/<name>`
for one of those staged names.

Use a separate repository and caller for another application, image name, or
unscoped tag stream.

## Select one immutable release unit

Expand Down Expand Up @@ -71,7 +78,9 @@ mixed revision is not a supported migration state.

Edit `.goreleaser.yaml` for the producer's command:

- set `project_name`, build ID, archive ID, binary name, and `main` package;
- set `project_name`, each build ID, archive ID, GoReleaser `binary` name, and
`main` package. Linux `amd64` and `arm64` must publish the same set of
binary names;
- keep `CGO_ENABLED=0` only if the command is genuinely static on all supported
targets;
- keep Darwin, Linux, and Windows on `amd64` and `arm64`;
Expand Down Expand Up @@ -145,23 +154,37 @@ an undeclared replacement when the lock is incomplete.

## Adapt Melange and apko

Staging writes each canonical Linux binary to
`work/sources/<apkarch>/<binary-name>`, where `<binary-name>` is the
GoReleaser `builds[].binary` value. The previous staged filename was
`application`. Melange must install each file by its real name.

In `melange.yaml`:

- use the application binary as the package name;
- set the Melange package name;
- keep `version: ${{vars.version}}`;
- keep `x86_64` and `aarch64`;
- replace the organization metadata and SPDX license expression; and
- install the staged `application` file at `/usr/bin/<binary>` with mode `0755`
and ownership `0:0`.
- install each staged file by its GoReleaser binary name at
`/usr/bin/<name>` with mode `0755` and ownership `0:0`.

A single-binary repository is the same contract with one name. Change any
pipeline that still copies `application` to the real binary name before the
next release-unit pin.

In `apko.yaml`:

- consume the same Melange package;
- set the entrypoint to `/usr/bin/<binary>`;
- set the entrypoint to `/usr/bin/<name>` for one staged binary name;
- keep `amd64` and `arm64`;
- keep numeric runtime user and group `65532`; and
- set title, description, source, and SPDX license annotations.

Each platform config Entrypoint must be exactly `["/usr/bin/<name>"]` for
some expected staged name. The same name is required on every platform. The
image has one entrypoint; additional staged binaries are present at
`/usr/bin/<name>` and are not extra entrypoints.

The current example includes CA certificates. Keep them for a command that
makes TLS connections. Add other runtime files through apko packages rather
than copying the build environment into the image.
Expand Down Expand Up @@ -247,6 +270,7 @@ After the first image publication, confirm GHCR visibility as described in the
organization guide. Consumers that require repeatability must use the
`ghcr.io/<owner>/<repository>@sha256:<digest>` output, not a moving channel tag.

To release another application, repeat this guide in another repository. Do
not add a second command, component-prefixed tag, or second image name to the
same caller.
To release another application or image, repeat this guide in another
repository. Do not add a component-prefixed tag or second image name to the
same caller. Additional GoReleaser binaries in the same repository are staged
into the same image when both architectures publish the same name set.
55 changes: 30 additions & 25 deletions docs/reference/release-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,12 @@ goreleaser release --clean --skip=publish
```

The command then verifies every `checksums.txt` entry, requires a regular
`checksums.txt.sigstore.json`, reads `artifacts.json`, requires one static
executable Linux binary for `amd64` and one for `arm64`, confines both paths,
and writes `oci-build-inputs.json`.
`checksums.txt.sigstore.json`, reads `artifacts.json`, and selects every
`linux/{amd64,arm64}` Binary record. Duplicate `(arch, name)` pairs are
rejected. The name set must be identical and nonempty on both architectures; a
name present on only one architecture is an error that names it. Selected
paths are confined, and the command writes `oci-build-inputs.json` as
`release.dev/oci-build-inputs/v2`.

Native package signing is controlled by environment only:

Expand All @@ -116,8 +119,7 @@ JSON result:
| Field | Contract |
| --- | --- |
| `assets` | Number of checksum-verified payloads. |
| `binaries.amd64.path`, `binaries.arm64.path` | Original dist-prefixed paths from `artifacts.json`. |
| `binaries.<arch>.mode` | Observed permission bits in octal. |
| `binaries` | Selected Linux binaries in platform-major order (`amd64` then `arm64`), then name ascending. Each entry has `arch`, `name`, original dist-prefixed `path`, and observed permission `mode` in octal. |

`--clean` deletes and rebuilds the distribution directory. This command is not
read-only.
Expand Down Expand Up @@ -146,52 +148,55 @@ Required Actions context is `GITHUB_REPOSITORY`, `GITHUB_REPOSITORY_OWNER`,
`GITHUB_SERVER_URL`, and `GITHUB_SHA`. Work and output roots must be disjoint,
absent or empty, and neither may contain the other.

The command verifies the projected binary digests and ELF contract, stages each
as `application`, creates an ephemeral Melange signing key, builds `x86_64` and
`aarch64` APK repositories, writes the public key, locks apko, and composes the
OCI layout and architecture SBOMs. The private Melange key remains under the
scratch root.
The command verifies the projected binary digests and ELF contract, stages
each file at `work/sources/<apkarch>/<binary-name>`, creates an ephemeral
Melange signing key, builds `x86_64` and `aarch64` APK repositories, writes
the public key, locks apko, and composes the OCI layout and architecture
SBOMs. The private Melange key remains under the scratch root.

`oci-build-inputs.json` is limited to 4 MiB.

The `release.dev/image-build/v1` JSON result contains `version`, `binary`,
`work`, `output`, `build_date`, and two `packages` entries. Each package entry
contains `platform`, APK `arch`, output-relative `package`, and
`binary_digest`.
The `release.dev/image-build/v2` JSON result contains `version`, `binaries`
(sorted name-ascending), `work`, `output`, `build_date`, and two `packages`
entries. Each package entry contains `platform`, APK `arch`, output-relative
`package`, and `binary_digests`. Each `binary_digests` entry is
`{name, digest}` sorted by name.

## `image verify`

```text
release-cli image verify --output DIR --work DIR --binary NAME \
release-cli image verify --output DIR --work DIR \
[--version VERSION] [--json]
```

| Value | Flag | Environment | Default |
| --- | --- | --- | --- |
| Output root | `--output` | `RELEASE_OUTPUT` | Required. |
| Scratch root | `--work` | `RELEASE_WORK` | Required. |
| Binary | `--binary` | `RELEASE_BINARY` | Required. |
| Version | `--version` | `RELEASE_VERSION` | Tag name without one leading `v`. |

The command also requires `GITHUB_SHA`, `GITHUB_SERVER_URL`, and
`GITHUB_REPOSITORY`. It verifies:
`GITHUB_REPOSITORY`. Expected binary names and canonical digests come from
the staged `work/sources/<apkarch>/<binary-name>` trees and the v2
projection. It verifies:

- one OCI index with Linux `amd64` and `arm64` manifests;
- required source, version, revision, title, description, and license
annotations on index, manifests, and config labels;
- one layer, entrypoint `/usr/bin/<binary>`, and runtime user `65532`;
- one regular binary with ownership `0:0`, mode `0755`, no special bits, and
bytes equal to the staged canonical binary; and
- one layer, runtime user `65532`, and an Entrypoint of exactly
`/usr/bin/<name>` for one expected staged name on every platform;
- every expected name present exactly once in that layer as a regular `0755`
uid/gid `0` file within 64 MiB, with bytes equal to its canonical digest;
and
- one SPDX `APPLICATION` package at `<version>-r0` per architecture.

Index, manifest, config, and SPDX JSON documents are limited to 4 MiB. The
binary entry in each layer is limited to 64 MiB.
Index, manifest, config, and SPDX JSON documents are limited to 4 MiB.

The index digest is SHA-256 of the exact `layout/index.json` bytes. On success,
the command writes it to `image-digest.txt` and returns a
`release.dev/image-verify/v1` result containing `version`, `binary`,
`index_digest`, and two platform records with manifest, config, layer, and
binary digests.
`release.dev/image-verify/v2` result containing `version`, `binaries` (sorted
name-ascending), `index_digest`, and two platform records with manifest,
config, layer, and `binary_digests` (`{name, digest}` sorted by name).

## `plan tags`

Expand Down
24 changes: 15 additions & 9 deletions docs/reference/release-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ failure handling, see [Operate and recover releases](../how-to/operate-and-recov

| Domain | Supported contract |
| --- | --- |
| Application layout | One Go application and binary per repository. |
| Application layout | One Go repository, one unscoped tag stream, and one GHCR image. Linux `amd64` and `arm64` must publish the same nonempty set of static binary names. |
| Source tags | Stable, unscoped `vMAJOR.MINOR.PATCH`. |
| Binary operating systems | Darwin, Linux, and Windows. |
| Binary architectures | `amd64` and `arm64`. |
Expand Down Expand Up @@ -328,7 +328,7 @@ one convergent repository publication.

The producer supplies:

- one Go module and command;
- one Go module and one or more commands that share that module;
- `.goreleaser.yaml` schema version 2;
- `mise.toml` and `mise.lock` with Go, GoReleaser, Syft, Cosign, GitHub CLI,
Melange, and apko;
Expand All @@ -353,14 +353,20 @@ A compatible GoReleaser configuration:
- uses `skip_upload: true` for Homebrew and Scoop controls; and
- keeps nFPM ID `release` when optional native signing is enabled.

Staging requires exactly one executable static Linux binary for `amd64` and one
for `arm64`, both with the same filename. It writes the digest-bound
`oci-build-inputs.json` projection.
Staging selects every `linux/{amd64,arm64}` GoReleaser Binary record. It
rejects a duplicate `(arch, name)` pair and requires the name set to be
identical and nonempty on both architectures. A name present on only one
architecture is an error that names it. The `release.dev/oci-build-inputs/v2`
projection lists those binaries in platform-major order (`linux/amd64` before
`linux/arm64`), then name ascending within a platform.

Melange packages the projected files for `x86_64` and `aarch64` without
compiling them. apko composes one index for `amd64` and `arm64`, runs as numeric
user and group `65532`, installs one `/usr/bin/<binary>` entrypoint, and carries
source, version, revision, title, description, and license annotations.
compiling them. Each staged file is named for its GoReleaser binary, not
`application`. apko composes one index for `amd64` and `arm64`, runs as numeric
user and group `65532`, and requires each platform config Entrypoint to be
exactly `/usr/bin/<name>` for one staged binary name. The same name is required
on every platform. Source, version, revision, title, description, and license
annotations remain required.

## Actions artifacts and public assets

Expand Down Expand Up @@ -617,7 +623,7 @@ immutable R2 conflict. A public release has no automated rollback.
The current release system does not support:

- languages other than the Go producer profile;
- more than one application, binary, or image entrypoint per repository;
- more than one application, GHCR image, or image entrypoint per repository;
- monorepo component tags or scoped versions;
- prereleases or build metadata;
- CGO-dependent or dynamically linked commands;
Expand Down
7 changes: 4 additions & 3 deletions docs/tutorials/release-your-first-go-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ Edit the copied files as follows:
and release URL owner and repository to `acme/widget`; and replace the
example organization metadata.
4. In `release-please-config.json`, set `package-name` to `widget`.
5. In `melange.yaml`, set the package and installed binary to `widget`, and
replace the example organization metadata.
6. In `apko.yaml`, set the package and entrypoint to `widget` and use
5. In `melange.yaml`, set the package name to `widget`, install the staged
`widget` file at `/usr/bin/widget`, and replace the example organization
metadata.
6. In `apko.yaml`, set the package and entrypoint to `/usr/bin/widget` and use
`https://github.com/acme/widget` as the source annotation.

Keep these release controls unchanged:
Expand Down
8 changes: 5 additions & 3 deletions examples/go-release/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Go release example

This directory is the maintained template for one static Go application in one
This directory is the maintained template for one static Go command in one
repository. It includes the release workflows, a minimal command, GitHub
Release and OCI configuration, Homebrew and Scoop control generation, and a
native package-repository request.
native package-repository request. Melange installs the staged `example`
binary by that GoReleaser name; the image entrypoint is `/usr/bin/example`.

It is not a complete repository policy. Add the adopter's CI, review, rulesets,
and ownership controls.
Expand Down Expand Up @@ -39,7 +40,8 @@ Before the workflows run:
`SCOOP-BUCKET`, and the `PACKAGE-REPOSITORY-*` values only after those
adopter-owned destinations exist.
4. Replace the `example` project, package, binary, cask, manifest, command path,
module path, and Release Please package name.
module path, and Release Please package name. `melange.yaml` must install
the staged file by that GoReleaser binary name, not `application`.
5. Replace the organization metadata, maintainer, description, homepage, and
SPDX license expression.
6. Change the Release Please branch and manifest version when the repository
Expand Down
2 changes: 1 addition & 1 deletion examples/go-release/melange.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ environment:

pipeline:
- runs: |
install -Dm755 -o 0 -g 0 application "${{targets.destdir}}/usr/bin/example"
install -Dm755 -o 0 -g 0 example "${{targets.destdir}}/usr/bin/example"
Loading