What the release process produces, how it is produced, and what a consumer runs to verify it before installing into an air-gapped environment.
| File | Contents |
|---|---|
piace-<version>-<os>-<arch> |
One statically linked binary per supported platform |
SHA256SUMS |
One SHA-256 line per binary, sorted by filename |
SHA256SUMS.sigstore.json |
Keyless cosign signature bundle over SHA256SUMS |
SHA256SUMS.asc |
Optional detached OpenPGP signature over the same manifest |
Published alongside them, from the same artifacts:
| Image | Contents |
|---|---|
example42/piace:<version> |
A linux/amd64 + linux/arm64 manifest list; :latest moves with every non-prerelease |
ghcr.io/example42/piace:<version> |
The same manifest, mirrored |
A signature covers the manifest, not each binary individually. One signature then transitively covers every artifact, and a consumer verifies one thing rather than one signature per platform.
The binaries also carry a GitHub build provenance attestation, and both image manifests are signed by digest and attested. Provenance answers a different question from a signature: which workflow, at which commit, produced these bytes.
The supported OS and architecture matrix is release metadata: it is published
with the release and edited deliberately in scripts/build-release.sh, never
discovered or downloaded at run time.
Pushing a v* tag runs the whole thing:
git tag v1.0.0
git push origin v1.0.0A tagged run uses the workflow as it exists at the tagged commit, so
tag a commit that already carries .github/workflows/ci.yml. Tagging a
branch the workflow has not reached yet does nothing at all: no run, no
error, nothing in the Actions log, which is a confusing way to spend a
version number.
.github/workflows/ci.yml then runs the test matrix, builds every
platform, verifies the manifest, confirms the Linux binaries are
statically linked and that each reports the version it was stamped with,
and publishes a GitHub Release with the binaries and SHA256SUMS
attached. The release job publishes the artifacts the build job produced
rather than rebuilding, so what a consumer downloads is what CI checked.
A tag that is not vMAJOR.MINOR.PATCH[-prerelease] fails before anything
is built; a tag whose version carries a -suffix is published as a
prerelease.
The signature is part of that. The release job signs SHA256SUMS with
keyless cosign: the certificate is issued against the workflow's own OIDC
identity and lives for minutes, so there is no signing key to store, rotate or
lose, and the signature is attached from the moment the release exists. The
release notes carry the exact cosign verify-blob command, including the
certificate identity for the tag being published.
An OpenPGP signature over the same manifest remains available for sites whose policy requires one. It is an extra, not the verification path, and CI holds no key for it:
gh release download v1.0.0 --pattern SHA256SUMS
gpg --armor --detach-sign --local-user <signing-key-id> SHA256SUMS
gh release upload v1.0.0 SHA256SUMS.ascOnce the release exists, a fourth job packages those same binaries and pushes
one manifest to both Docker Hub and GHCR. It waits on the release rather than
running beside it, so the GitHub Release stays the primary artifact: if the
push fails, the release is already out and re-running the publish container image job on its own finishes the work. A prerelease publishes its version tag
but does not move latest.
Docker Hub is the name the documentation uses. GHCR exists because an anonymous pull from a shared CI runner IP is exactly what Docker Hub rate-limits, and a pipeline failing for that reason is failing for a reason that has nothing to do with this project. GHCR needs no stored credential: the job pushes with the workflow's own token.
Docker Hub needs two repository secrets, and the job fails visibly on the first tag pushed without them:
| Secret | Value |
|---|---|
DOCKERHUB_USERNAME |
A Docker Hub account with push access to example42/piace |
DOCKERHUB_TOKEN |
A Docker Hub access token for that account with Read & Write scope, not the account password |
Use an access token: it is scoped, revocable on its own, and does not carry the account's Hub session.
CI runs exactly this, and it stays usable directly for an air-gapped or out-of-band build:
scripts/build-release.sh 1.0.0The script builds each platform with:
CGO_ENABLED=0 GOOS=<os> GOARCH=<arch> \
go build -trimpath -ldflags "-s -w -X main.toolVersion=<version>" \
-o dist/piace-<version>-<os>-<arch> ./cmd/piaceCGO_ENABLED=0keeps the artifact a static binary.cmd/piace'sTestRelease_BuildsWithCGODisabledasserts the build succeeds without cgo, andTestRelease_NoNonStandardDependenciesBeyondYAMLasserts the transitive dependency set is the standard library plusgopkg.in/yaml.v3and nothing else, which is how the no-runtime-dependency "no Ruby, Puppet agent, Facter, package manager, or runtime dependency resolution" is kept true as the code changes.-trimpathremoves local filesystem paths from the binary, so the same commit built on two machines produces the same bytes and a mismatched checksum means a real difference rather than a different build directory.-X main.toolVersion=<version>stamps the version the tool reports and records in every result document's invocation metadata.
The Dockerfile copies release artifacts; it does not compile. Build
them first, then hand the same version in as a build argument:
scripts/build-release.sh 1.0.0
docker build --build-arg VERSION=1.0.0 -t piace:1.0.0 .The image is gcr.io/distroless/static-debian12:nonroot plus the one
binary: no shell, no package manager, and a CA bundle only because
piace explain verifies an inference service against the system roots
(the compiler and PuppetDB transports carry their own CA bundle from the
services file, and trust nothing else). Assembling it from the built
artifacts rather than from a golang builder stage is what makes the
binary inside the image the same bytes SHA256SUMS certifies.
Running piace:1.0.0 is the same as running the published image: mount your
workspace at /work and pass your own uid, since the image runs as uid 65532
and cannot otherwise write a report into the mount. See the
README.
The image deliberately carries the binary and nothing else, which decides
where it fits. piace change-context runs there like any other subcommand but
execs git, which the image does not carry, so change context is produced on the
runner. For the same reason the image cannot serve as a GitLab or GitHub CI job
image, which must provide a shell: in CI, install the verified binary instead.
See ci.md.
CI signs with keyless cosign, so nothing here is needed for an ordinary release. For an out-of-band build, or a site that requires OpenPGP:
gpg --armor --detach-sign --local-user <signing-key-id> dist/SHA256SUMSThis writes dist/SHA256SUMS.asc. Publish SHA256SUMS, SHA256SUMS.asc and
the binaries together, and publish the signing key's fingerprint through a
channel independent of the artifact repository: a signature verified against a
key fetched from the same place as the artifact proves nothing about the
artifact's origin.
Verify in this order. Checking the checksum first would confirm only that the binary matches a manifest that may itself have been substituted.
1. Verify the manifest signature.
cosign verify-blob SHA256SUMS \
--bundle SHA256SUMS.sigstore.json \
--certificate-identity "https://github.com/example42/piace/.github/workflows/ci.yml@refs/tags/v<version>" \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe certificate identity is the workflow that published the tag, so it names both the repository and the release. Substitute the tag you downloaded.
Where policy requires OpenPGP instead, and the release carries SHA256SUMS.asc:
gpg --verify SHA256SUMS.asc SHA256SUMSConfirm the reported key fingerprint matches the published one. gpg
reports a valid signature from any key in the local keyring, so an
unchecked "Good signature" line is not on its own evidence of origin.
2. Verify the binary against the now-trusted manifest.
sha256sum --check --ignore-missing SHA256SUMSOn macOS, where sha256sum is absent:
shasum -a 256 --check --ignore-missing SHA256SUMS--ignore-missing lets a consumer verify only the platform they
downloaded without the check failing over absent files. Both commands
exit non-zero on any mismatch, so they are usable directly as a CI gate.
3. Confirm the binary is the one you verified.
./piace-<version>-<os>-<arch> versionThe reported version comes from the -X main.toolVersion stamp above, so
it ties the running binary back to the manifest entry.
All three files transfer as ordinary artifacts; verification is entirely
local and needs no network beyond the trusted key already being present.
The binary itself opens network connections only to the compiler and
PuppetDB endpoints named in its own --services file, asserted by
TestAcceptance_EndpointsRestrictedToConfiguredServices, so
an installed PIACE reaches nothing a release process introduced.