Blessed GitHub Actions.
Composite actions and reusable workflows shared across my repos.
The podman actions mirror the docker/* action interfaces so a workflow can swap between them with minimal changes.
| Action | Purpose | Docker counterpart |
|---|---|---|
podman-build-push |
Build and optionally push an image with podman, with multi-arch, cache, and SBOM support | docker/build-push-action |
podman-push |
Tag and push an image or manifest list already in podman's store, such as one loaded from a Nix build | docker/build-push-action with push only |
podman-manifest |
Create a manifest list from existing registry images and push it to several tags | docker buildx imagetools create |
podman-login |
Log into a container registry with podman, with ECR auto-detection | docker/login-action |
setup-qemu |
Register QEMU binfmt emulators via tonistiigi/binfmt for cross-platform builds |
docker/setup-qemu-action |
setup-nix |
Install Nix and configure Cachix | none |
setup-podman |
Install or upgrade podman, crun, and skopeo from the Ubuntu archive | none |
| Workflow | Purpose |
|---|---|
docker-build-push.yml |
Build and push with buildx, tagged by docker/metadata-action |
podman-build-push.yml |
Same interface as above, built with podman |
release-please.yml |
Open release PRs and tag releases with release-please |
Each reusable workflow takes a runs-on input (default ubuntu-latest) for callers on self-hosted runners.
Releases are tagged vX.Y.Z and a floating vX tag tracks the latest release of each major version.
The examples use @main for brevity; pin to @v1 for the floating major tag, or to @v1.2.3 or a full commit SHA for an exact version.
setup-qemu registers binfmt_misc handlers with sudo podman run --privileged, so it needs sudo and a rootful podman.
GitHub-hosted Ubuntu runners meet both requirements.
podman-build-push builds from the git repository at the current commit when context is empty, so it needs no checkout; pass context: . to build the checked-out workspace instead.
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: unmango/actions/setup-qemu@main
- uses: unmango/actions/podman-login@main
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- uses: unmango/actions/podman-build-push@main
with:
context: .
platforms: linux/amd64,linux/arm64
push: 'true'
tags: ghcr.io/${{ github.repository }}:lateststeps:
- uses: actions/checkout@v4
- uses: unmango/actions/setup-nix@main
with:
cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }}| Input | Default | Purpose |
|---|---|---|
install_nix |
true |
false skips the install on runners with Nix preinstalled and adds ~/.nix-profile/bin to PATH |
github_access_token |
github.token |
Token for authenticated GitHub fetches by Nix |
cachix_name |
unstoppablemango |
Cachix cache to pull from, and push to when a token is set; '' installs Nix without Cachix |
cachix_auth_token |
Cachix token; without it the cache is pull-only | |
extra_pull_names |
Comma-separated read-only Cachix caches, e.g. mangopkgs,unmango |
|
extra_nix_config |
Extra lines for nix.conf |
|
use_daemon |
true |
Push through the Cachix daemon; false scans the store at job end |
skip_push |
false |
Pull-only, even with cachix_auth_token set |
magic_nix_cache |
false |
Run DeterminateSystems/magic-nix-cache-action between the install and Cachix |
docker/build-push-action's type=gha cache has no podman equivalent.
A registry cache works instead: podman pushes each built layer to a cache repository and pulls matching layers on the next build.
permissions:
contents: read
packages: write
steps:
- uses: unmango/actions/podman-login@main
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- uses: unmango/actions/podman-build-push@main
with:
tags: ghcr.io/${{ github.repository }}:latest
cache-from: ghcr.io/${{ github.repository }}/cache
# Pull requests read the cache but do not write to it.
cache-to: ${{ github.event_name != 'pull_request' && format('ghcr.io/{0}/cache', github.repository) || '' }}Either input makes podman build with --layers.
The cache repository is a separate GHCR package; give it the same visibility as the image.
permissions:
contents: read
packages: write
jobs:
image:
uses: unmango/actions/.github/workflows/podman-build-push.yml@main
with:
image: ghcr.io/${{ github.repository }}
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
sbom: ${{ github.event_name != 'pull_request' }}
secrets:
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}release-please.yml runs release-please in simple mode.
It opens a release PR from Conventional Commits, maintains version.txt and CHANGELOG.md, and on merge creates a vX.Y.Z tag and GitHub release.
The tag push runs the build-push workflow, and docker/metadata-action derives the image tags 1.2.3, 1.2, 1, and latest from it.
The workflow needs credentials with contents, pull-requests, and issues write.
With an app token or PAT, the caller can set permissions: {}; only the GITHUB_TOKEN fallback needs the caller to grant contents and pull-requests write.
issues write covers the autorelease labels release-please puts on its PRs.
Tags created with the default GITHUB_TOKEN do not trigger other workflows, so the image build would never run.
Pass a GitHub App through the app-client-id input and the app-private-key secret, and the workflow mints a token from them.
A personal access token in the token secret is the alternative when app-client-id is empty.
An app token does not act as a person and does not expire, which is the reason to prefer it over a PAT.
release-please writes through the GitHub API, so its commits are signed with either token.
# .github/workflows/release-please.yml
on:
push:
branches: [main]
permissions: {}
jobs:
release:
uses: unmango/actions/.github/workflows/release-please.yml@main
with:
app-client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
secrets:
app-private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}# .github/workflows/image.yml
on:
pull_request:
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
image:
uses: unmango/actions/.github/workflows/podman-build-push.yml@main
with:
image: ghcr.io/${{ github.repository }}
push: ${{ github.event_name != 'pull_request' }}| Event | Image tags |
|---|---|
| Pull request | pr-N |
Push to main |
main, sha-<short> |
Tag v1.2.3 |
1.2.3, 1.2, 1, latest, sha-<short> |
Pass config-file and manifest-file to use a release-please-config.json instead of the simple defaults.
The workflow exposes release_created, tag_name, version, major, minor, patch, and sha as outputs for jobs that need to run only after a release.
prs_created and pr (JSON, e.g. fromJSON(needs.release.outputs.pr).headBranchName) serve jobs that push to the release PR branch, and paths_released lists released package paths in manifest mode.
✅ supported ·
| Input | Podman | Docker | Notes |
|---|---|---|---|
context |
✅ | ✅ | Empty builds from <repo>.git#<sha> like buildx; pass . to build the checkout. #ref:subdir URLs are not supported by podman 5.8 |
file |
✅ | ✅ | |
platforms |
✅ | ✅ | Multiple platforms build a manifest list; run setup-qemu first |
tags |
✅ | ✅ | |
target |
✅ | ✅ | |
labels |
✅ | ✅ | |
annotations |
✅ | ✅ | |
build-args |
✅ | ✅ | |
build-contexts |
✅ | ✅ | |
secrets |
✅ | ✅ | Podman takes the id=id,src=path form only |
no-cache |
✅ | ✅ | |
cache-from |
✅ | ✅ | Podman takes a single registry ref or local directory, not buildx type=... syntax, and forces --layers; see Build cache on GitHub-hosted runners |
cache-to |
✅ | ✅ | Same as cache-from |
pull |
✅ | ✅ | |
network |
✅ | ✅ | |
add-hosts |
✅ | ✅ | |
cgroup-parent |
✅ | ✅ | |
shm-size |
✅ | ✅ | |
ulimit |
✅ | ✅ | |
push |
✅ | ✅ | |
load |
✅ | ✅ | Named docker-load, since podman load imports in the opposite direction; single-platform only |
archive |
✅ | ❌ | Writes a docker-archive tarball of all tags, for kind load image-archive; single-platform only |
sbom |
✅ | ✅ | Podman requires push and needs syft and cosign on the runner |
provenance |
✅ | No native SLSA provenance generation in buildah or podman | |
ssh |
✅ | No buildkit SSH agent forwarding equivalent | |
no-cache-filters |
✅ | buildah cache invalidation is all-or-nothing | |
allow |
✅ | No buildkit entitlement model | |
attests |
❌ | ✅ | |
builder |
❌ | ✅ | |
call |
❌ | ✅ | |
outputs |
❌ | ✅ | archive covers type=docker,dest=<path> |
secret-envs |
❌ | ✅ | |
secret-files |
❌ | ✅ | Use secrets |
github-token |
✅ | ✅ | Authenticates the git context clone |
| Output | Podman | Docker | Notes |
|---|---|---|---|
imageid |
✅ | ✅ | |
digest |
✅ | ✅ | Podman only sets it when push is true |
metadata |
✅ | ✅ | Podman emits containerimage.imageid, containerimage.digest, and image.tags, a subset of the buildx shape |
Podman boolean inputs are strings ('true' and 'false') because composite actions have no typed inputs.
| Input | Podman | Docker | Notes |
|---|---|---|---|
registry |
✅ | ✅ | |
username |
✅ | ✅ | |
password |
✅ | ✅ | |
ecr |
✅ | ✅ | auto, true, or false; needs aws-actions/configure-aws-credentials first |
logout |
✅ | Composite actions have no post-job hook; no warning is logged |
| Input | Podman | Docker | Notes |
|---|---|---|---|
platforms |
✅ | ✅ | |
image |
✅ | ✅ | |
cache-image |
❌ | ✅ | |
cache-binary |
❌ | ✅ |
| Input | Podman | Docker | Notes |
|---|---|---|---|
image |
✅ | ✅ | |
runs-on |
✅ | ✅ | Runner label, default ubuntu-latest |
platforms |
✅ | ✅ | Podman runs setup-qemu when more than one platform is listed |
push |
✅ | ✅ | |
build-args |
✅ | ✅ | |
file |
✅ | ✅ | |
context |
✅ | ✅ | |
dockerhub_token (secret) |
✅ | ✅ | |
secrets-list |
✅ | ❌ | Build secrets in id=id,src=path form |
cache |
✅ | ❌ | none, registry, or local; docker always uses the GHA cache backend |
cache-image |
✅ | ❌ | Registry ref used when cache is registry |
cache-dir |
✅ | ❌ | Directory used when cache is local, persisted with actions/cache |
sbom |
✅ | ❌ | Docker always attaches an SBOM when pushing; podman installs syft and cosign and attaches an SPDX SBOM on request |
provenance |
❌ | Docker always generates provenance when pushing; podman accepts the input and ignores it |
Enter the dev shell with nix develop, or let direnv load it from .envrc.
make check # nix flake check, includes actionlint
make fmt # nix fmt