Reference for ConfigButler/settings-image@v0. The Action runs the CLI (cli.md) in a
workflow: a commit that changes a settings folder becomes a new settings image. §1–§2 are for
users; §3–§6 are for maintaining the Action.
on:
push:
branches: [main]
paths: [configs/card-reveal/**]
permissions:
contents: read
packages: write
# Publish in commit order: a later run must not finish before an earlier one.
concurrency:
group: settings-image-card-reveal
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha> # pinned
with:
fetch-depth: 0 # the CLI refuses a shallow clone (cli.md §3)
- id: settings
uses: ConfigButler/settings-image@v0
with:
path: configs/card-reveal
repository: ghcr.io/configbutler/card-reveal-config
- run: echo "published ${{ steps.settings.outputs.reference }}"On ghcr.io, nothing else is needed: the workflow's own GITHUB_TOKEN can push to a package in its
organisation.
Another registry. For a registry that accepts the workflow's GitHub OIDC token as a bearer
token (zot, with a rule on the token's repository_id and ref claims), the job asks for an ID
token and names the audience the registry expects:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # the OIDC token
steps:
- uses: actions/checkout@<sha> # pinned
with:
fetch-depth: 0
- uses: ConfigButler/settings-image@v0
with:
path: coffee # coffee/resources and coffee/raw
repository: zot.example.com/acme/shop-settings
oidc-audience: zot.example.com # the registry host: the conventionA registry with its own login action (ECR, ACR, Artifact Registry) runs that action first; the
Action then uses the Docker config it leaves. A registry with a username and a token takes
username and password. Only one credential is used per run (cli.md §4).
The root is where the Marketplace requires it, and it lets a workflow say
uses: ConfigButler/settings-image@v0.
Inputs:
| Input | Default | Passed as |
|---|---|---|
path |
required | the folder |
repository |
required | --repository |
branch-tag |
${{ github.ref_name }} |
--branch-tag. Set it to '' for none |
tags |
'' |
one --tag per line |
sensitive-kinds |
'' |
one --sensitive-kind per line |
provenance |
'true' |
--no-provenance when 'false' |
provenance-author |
none |
--provenance-author: none, name or full |
timestamp-tag |
'' |
--timestamp-tag, e.g. ${{ github.ref_name }}: a sortable tag for Flux (flux.md) |
username |
${{ github.actor }} |
SETTINGS_IMAGE_USERNAME, only together with a password |
password |
'' |
SETTINGS_IMAGE_PASSWORD. When no credential input is set and repository is on ghcr.io, the Action uses ${{ github.token }}. Otherwise, with no credential set, the Docker config applies, as docker/login-action left it |
registry-token |
'' |
SETTINGS_IMAGE_REGISTRY_TOKEN: a bearer token sent unchanged |
oidc-audience |
'' |
the Action requests the workflow's GitHub OIDC token for this audience, masks it, and passes it as the registry token. Use the registry host (zot.example.com), as Flux's RFC-0014 defaults it. The token is requested in the push step, after the Go build, right before the push: it lives minutes. Needs permissions: id-token: write; without it the step fails and says so |
plain-http |
'false' |
--plain-http when 'true' (a service-container registry) |
At most one of password, registry-token and oidc-audience may be set; two is an error.
github.token goes to ghcr.io only. The password default used to be ${{ github.token }} for
every registry, so a workflow pushing elsewhere handed the repository's token to that registry. A
token is a secret of the place that issued it. With the new default nothing changes on ghcr.io, and
password: '' (the old way to choose the Docker config) still means the same.
Outputs: digest, reference, tags (comma-separated), changed, commit (empty without
provenance) and skipped (comma-separated), read from the CLI's --json with jq, which is on
every hosted runner.
runs: using: composite. Every input reaches the script through env:, never through
${{ }} inside run:. A branch name or path is then data, not shell. (That is the usual injection
hole in composite actions.)
Today: built from the Action's own source.
actions/setup-gowithgo-version-file: ${{ github.action_path }}/go.mod, caching on the Action'sgo.sum.- Then
go build -o "$RUNNER_TEMP/settings-image" ./cmd/settings-image, ingithub.action_path.
The binary is then always exactly the ref the workflow pinned, whether a tag, a branch or a SHA, and it needs no release to exist. The cost is a Go setup, about 10–20 s with a warm cache.
Later, if the build time matters: a released binary. Releases carry binaries
(settings-image_<os>_<arch> and settings-image.sha256sums). The Action would download the one
for its own version and check it against that file.
action.ymlhas an inputversion, whose default is the released version. release-please keeps that default current through itsextra-filesgeneric updater: the line carries a# x-release-please-versionmarker.version: sourcekeeps today's behaviour, for testing an unreleased ref.
- Releases are
vX.Y.Z, cut by release-please. - After each release, the release workflow moves a floating
v0tag (laterv1) to the release commit. That is the Actions convention, and it is what@v0resolves to. - Moving a tag is a force-push of that tag only, never of a branch.
- Users who pin a SHA (recommended in security-minded repositories) are unaffected by the move.
.github/workflows/action-selftest.yml runs the Action from the checkout (uses: ./), against
testdata/selftest/ (one ConfigMap, one plain file):
-
On every pull request: a
registry:2service container onlocalhost:5000, andplain-http: 'true'. There are no tokens, so it is fork-safe. Afterwardscranereads the image back and checks:- the digest equals the Action's
digestoutput; - the layer lists the expected files with mode
0644; - the provenance referrer is listed under the fallback tag.
It reads the registry with
curlandjq, which every hosted runner has, instead of installingcrane. - the digest equals the Action's
-
A second run of the Action in the same job must report
changed: falsewith the same digest. -
On
mainonly: the same, pushed toghcr.io/configbutler/settings-image-selftestwithGITHUB_TOKEN, so the real ghcr.io path stays green.
The requirements: a public repository, action.yml at the root, one action per repository, and an
action name that is unique on the Marketplace.
action.yml field |
Value |
|---|---|
name |
Push settings image (unique as of 2026-11-02) |
description |
Turn a folder of settings into a mountable OCI image and push it |
branding |
a Feather icon (package) and a colour |
Listing it is a manual step in the GitHub UI, and it has to happen while the release is still a draft. The checkbox ("Publish this Action to the GitHub Marketplace") has no API, and immutable releases are on, so a published release can no longer be listed. The release workflow publishes its draft on its own, so it is paused for the listing:
- In Settings → Environments →
release, add yourself as a required reviewer. - Merge the release PR. The release workflow builds and attaches every binary to the draft, then waits at Publish GitHub Release for a review.
- Open the draft under Releases and Edit it. Its tag is already filled in. Tick Publish this Action to the GitHub Marketplace, choose a primary category, and Publish release.
- In the waiting run, Review deployments → Approve. The job sees the release is already published and leaves it alone. The major tag moves and the next release PR opens as usual.
- Remove the required reviewer, so later releases publish without waiting.
Without a reviewer the release environment does nothing. A release that skips the pause stays
off the Marketplace page but is still used: @v0 moves to it. Once the Action is listed, a later
release only needs the pause to show its version on the Marketplace page.