Publisher Guide
This guide is for maintainers who want their GitHub release assets to be
installable through ghd.
ghd is intentionally narrow. It expects a repository to publish explicit
GitHub release assets, use GitHub's immutable release model, and generate
GitHub Actions provenance for the shipped bytes.
Compatibility Contract
Before ghd can trust a repository, the repository needs all of the following:
- a root
ghd.tomlfile on the default branch soghdcan discover packages; - the same
ghd.tomlfile present on the published release tag, because release-tag metadata is the trust-sensitive source for install, download, check, and update decisions; - explicit GitHub release assets for each supported platform;
- immutable releases enabled in GitHub so published tags and assets cannot be changed after publication;
- GitHub Actions artifact attestations for the shipped assets, with a stable
signer workflow path declared in
ghd.toml.
ghd does not install GitHub's automatically generated source archives. Publish
real release assets instead.
Configure ghd.toml
The repository manifest declares what package names exist, which tags contain a package release, which asset name matches each platform, and which GitHub Actions workflow is trusted to sign provenance.
version = 1
[provenance]
signer_workflow = "owner/repo/.github/workflows/release.yml"
[[packages]]
name = "tool"
description = "Tool CLI"
tag_pattern = "tool-v${version}"
[[packages.assets]]
os = "darwin"
arch = "arm64"
pattern = "tool_${version}_darwin_arm64"
[[packages.assets]]
os = "linux"
arch = "amd64"
pattern = "tool_${version}_linux_amd64"
[[packages.binaries]]
path = "tool"
Important constraints:
provenance.signer_workflowis repository-wide inghd.toml. Per-package signer workflows are not supported.tag_patternand each assetpatternmust contain exactly one${version}token.packages.binaries.pathis the relative path to the exposed binary. For direct-binary assets, this is usually just the filename. For archive assets, it is the relative path inside the extracted archive.
See Reference for the full schema and validation rules.
Enable Immutable Releases
GitHub recommends a draft-first publish flow for immutable releases:
- Create the release as a draft.
- Attach all release assets to the draft.
- Publish the draft release after final inspection.
Enable release immutability in GitHub before relying on ghd compatibility.
For a repository, GitHub documents this under Settings -> Releases ->
Enable release immutability.
If your repository uses tag protection or tag rulesets, the automation identity that creates release tags must be allowed to create them. Otherwise the release pipeline may produce a draft release without a usable tag.
Publish Provenance from GitHub Actions
ghd expects the release assets to have GitHub Actions provenance. The exact
build tool is up to you, but the release workflow needs to:
- build the final release assets;
- generate a checksums file for the shipped assets;
- optionally generate SBOM files;
- upload all assets to the draft release;
- attest the shipped subjects with
actions/attest; - stop while the release is still a draft so a human can inspect it;
- publish the draft release manually after inspection.
This example shows the minimum GitHub Actions shape. It assumes the pushed tag already exists and that the workflow either creates or reuses a draft release for that tag. The workflow prepares the release, but it does not publish it.
name: Release
on:
push:
tags:
- "tool-v*"
permissions: {}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Create draft release if needed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
draft="$(gh release view "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--json isDraft \
--jq .isDraft \
2>/dev/null || true)"
if [[ -z "${draft}" ]]; then
gh release create "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--draft \
--verify-tag \
--title "${GITHUB_REF_NAME}" \
--notes ""
elif [[ "${draft}" != "true" ]]; then
echo "release ${GITHUB_REF_NAME} already exists and is not a draft" >&2
exit 1
fi
- name: Build release assets
run: ./scripts/build-release.sh
- name: Upload assets to the draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${GITHUB_REF_NAME}" \
dist/tool_* \
dist/checksums.txt \
--repo "${GITHUB_REPOSITORY}" \
--clobber
- name: Generate provenance attestation
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-checksums: ./dist/checksums.txt
- name: Summarize draft release for inspection
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
release="$(gh release view "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--json isDraft,url,assets)"
if [[ "$(jq -r .isDraft <<<"${release}")" != "true" ]]; then
echo "release ${GITHUB_REF_NAME} is not a draft" >&2
exit 1
fi
echo "Draft release is ready for inspection:" >>"${GITHUB_STEP_SUMMARY}"
jq -r .url <<<"${release}" >>"${GITHUB_STEP_SUMMARY}"
Notes:
- The attestation step needs
id-token: writeandattestations: write. subject-checksumsis the simplest way to attest many release files at once.- If you also want linked-artifact storage records, GitHub documents an
additional
artifact-metadata: writepermission for that path. - SBOM assets are optional for
ghd, but shipping them alongside binaries is a reasonable default. If you publish them, add those files to thegh release uploadstep too. - GitHub-native artifact attestations are stored in GitHub's attestations API,
not as normal release assets. If a draft is rejected and deleted, stale
attestations for those bytes may remain. That is acceptable for
ghdbecause consumers must also verify the immutable release attestation.
Inspect and Publish the Draft
Inspect the draft release before publishing it. At minimum, confirm that the draft contains the expected assets and that the checksums file matches the downloaded bytes:
tag="tool-v1.2.3"
repo="owner/repo"
tmp="$(mktemp -d)"
gh release view "${tag}" \
-R "${repo}" \
--json isDraft,isImmutable,tagName,targetCommitish,assets
gh release download "${tag}" \
-R "${repo}" \
-D "${tmp}" \
--pattern "*"
(cd "${tmp}" && shasum -a 256 -c checksums.txt)
Verify GitHub Actions provenance for one downloaded asset before publishing:
gh attestation verify "${tmp}/tool_1.2.3_darwin_arm64" \
--repo "${repo}" \
--signer-workflow owner/repo/.github/workflows/release.yml \
--source-ref refs/tags/tool-v1.2.3 \
--deny-self-hosted-runners
If inspection passes, publish the draft. This is the point where immutable release protections start applying:
gh release edit "${tag}" \
-R "${repo}" \
--draft=false \
--latest=false
If inspection fails before publication, delete the draft and tag, then recreate the release from corrected inputs:
gh release delete "${tag}" -R "${repo}" --cleanup-tag
Do not publish and then delete an immutable release as a correction path. Published immutable release tags cannot be reused.
Verify the Published Release
After publishing, validate the release the same way a consumer would.
Verify that the GitHub release exists and is immutable:
gh release verify tool-v1.2.3 -R owner/repo
Verify that a downloaded local asset exactly matches the published release asset:
gh release verify-asset tool-v1.2.3 ./tool_1.2.3_darwin_arm64 -R owner/repo
This checks uploaded release assets only. GitHub's generated source zip and
tarball archives are not valid ghd install targets.
Verify GitHub Actions provenance for one downloaded asset:
gh attestation verify ./tool_1.2.3_darwin_arm64 \
--repo owner/repo \
--signer-workflow owner/repo/.github/workflows/release.yml \
--source-ref refs/tags/tool-v1.2.3 \
--deny-self-hosted-runners
If those checks pass and the repository manifest matches the shipped assets,
ghd has the metadata it needs to discover, verify, and install the package.