Skip to main content

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.toml file on the default branch so ghd can discover packages;
  • the same ghd.toml file 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_workflow is repository-wide in ghd.toml. Per-package signer workflows are not supported.
  • tag_pattern and each asset pattern must contain exactly one ${version} token.
  • packages.binaries.path is 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:

  1. Create the release as a draft.
  2. Attach all release assets to the draft.
  3. 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:

  1. build the final release assets;
  2. generate a checksums file for the shipped assets;
  3. optionally generate SBOM files;
  4. upload all assets to the draft release;
  5. attest the shipped subjects with actions/attest;
  6. stop while the release is still a draft so a human can inspect it;
  7. 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: write and attestations: write.
  • subject-checksums is the simplest way to attest many release files at once.
  • If you also want linked-artifact storage records, GitHub documents an additional artifact-metadata: write permission 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 the gh release upload step 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 ghd because 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.