Deploying to your own infrastructure only needs an artifact. Publishing asks for more: a dated and versioned release, an image a third party can address, a visible link between the package and the source code. This page covers the two channels GitHub offers natively, Releases and the GHCR registry, and what ties them to the promotion chain.
What you will learn
- Trigger a workflow when a release is published rather than on every push
- Attach artifacts to a release from a workflow
- Push an image to GHCR with the right token and the right permissions
- Tie the package back to the repository so it stops being an orphan object
- Understand package visibility and anonymous access
The release as a trigger, not as a consequence
The common reflex is to publish on every push to main. The result is a
continuous stream of meaningless versions, and a signature applied to
intermediate states nobody decided to ship.
The release: [published] trigger inverts the logic: publishing becomes a
deliberate act, and the workflow only runs for explicitly tagged versions.
name: Release
on: release: types: [published]
permissions: {}The benefit goes beyond a tidy stream. A release carries a tag, therefore a readable version, therefore a stable reference for a ticket, a changelog or a rollback. It is also the granularity at which posture controls expect a signature: the Signed-Releases check of OpenSSF Scorecard examines the latest releases, not the commits.
Attaching artifacts to the release
There are two ways to attach files to a release. The most direct uses the
gh CLI, already present on the runners, which avoids an extra dependency:
permissions: contents: write # required to write to the release steps: - name: Attach the artifacts to the release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ github.event.release.tag_name }} run: gh release upload "$TAG" dist/my-app.tar.gz provenance.intoto.jsonl --clobberThe second goes through a dedicated action, useful when you create the release from the workflow rather than from the interface:
- name: Create the release and attach the files uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: files: | dist/my-app.tar.gz sbom.cdx.jsonIn both cases, contents: write is required at job level only. It is exactly
the kind of permission that must never move up to workflow level: it authorises
writing to the repository.
Pushing an image to GHCR
GHCR (ghcr.io) is GitHub's container registry. From a workflow of the
repository, authentication needs no secret to create: the run's
GITHUB_TOKEN is enough, provided you open the packages: write permission on
the job.
publish: runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: packages: write # push to GHCR id-token: write # keyless OIDC for the attestation attestations: write env: IMAGE: ghcr.io/my-org/my-app steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Login GHCR uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: true tags: ${{ env.IMAGE }}:${{ github.event.release.tag_name }} labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }}Outside a workflow, on a machine or a server, authentication goes through a
personal access token carrying the write:packages scope to push.
Tying the package back to its repository
A package published with no visible link to its source code is an orphan object: the consumer sees an image, not the repository producing it, nor its sources, nor its licence. Two mechanisms solve that.
The first is the OCI label org.opencontainers.image.source, carrying the
URL of the repository associated with the package. It is what feeds the link
shown on the package page.
The second is simpler still: publishing from a workflow of the repository with
the GITHUB_TOKEN. That is the path GitHub's documentation presents as the
most direct way to connect a repository to a container package. The two stack
without conflict, and the label keeps the advantage of being readable outside
GitHub.
Visibility and anonymous access
One point regularly surprises people: at its first publication, a package is private by default. Package visibility is configured independently, on its own page. Publishing from a public repository is therefore not enough to make the image pullable.
Once the package is made public, images are pulled anonymously, with no authentication. That is what lets a reader of your documentation pull the image to verify it, and it is also what makes provenance verification useful: anyone can check what you publish.
What publishing adds to the chain
| Published element | What it brings the consumer |
|---|---|
| A tagged release | A named, dated, referenceable version |
| An attached artifact | The binary or archive, with no build needed |
| SBOM | The inventory of components, analysable by a scanner |
| Provenance bundle | The proof of origin, verifiable outside the registry |
| A GHCR image by digest | An immutable execution target |
The image.source label | The link to the source code and its licence |
Key points
- Triggering on
release: [published]makes publishing a deliberate act, and gives the granularity posture controls expect. - The
ghCLI attaches files to a release with no extra dependency;contents: writestays at job level. - On GHCR, the workflow's
GITHUB_TOKENis enough withpackages: write; outside a workflow you need a token carryingwrite:packages. - The
org.opencontainers.image.sourcelabel and publishing from the repository's workflow tie the package to its source code. - A package is private at its first publication: visibility is set separately, even on a public repository.
- A public package is pulled anonymously, which makes your attestations verifiable by anyone.
Next steps
- Lab: promoting an approved deployment: publishing wired into the promotion chain, attested image and approval included.
- Sharing between jobs (artifacts): the artifact mechanics feeding the files attached to a release.
- Runners: an introduction: the machines that build and publish these images, and what their choice changes for the chain.