From 6ce2d5f04c81368b1ca565437740261752b309de Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:14:00 +0000 Subject: [PATCH 1/3] feat(release): publish to GHCR with GITHUB_TOKEN when the registry secrets are empty (DF15b) - release-features.sh publish: on OCI_REGISTRY=ghcr.io with neither OCI_REGISTRY_USERNAME nor OCI_REGISTRY_PASSWORD set, authenticate with GITHUB_TOKEN as GITHUB_ACTOR via DEVCONTAINERS_OCI_AUTH; explicit secrets still win - keep the failure modes: one secret only, non-ghcr without secrets, ghcr without secrets or GITHUB_TOKEN (names packages: write) - ADR 0004: dated amendment (Decision text left as accepted, per docs/adrs/README.md) - README: GHCR publishing needs only the OCI_REGISTRY variable once base's release.yml grants packages: write --- README.md | 14 ++++--- docs/adrs/0004-collection-github-releases.md | 35 +++++++++++++++++- scripts/release-features.sh | 39 +++++++++++++++----- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f070f90..a96b585 100644 --- a/README.md +++ b/README.md @@ -286,11 +286,15 @@ largest feature change in it. Each feature's own version still comes from its `devcontainer-feature.json`, and that is what consumers pin, not the collection tag. -Publishing needs, once, in the repository's Actions settings: the -`OCI_REGISTRY` variable set to `ghcr.io`, and the `OCI_REGISTRY_USERNAME` -(the token owner's GitHub login) and `OCI_REGISTRY_PASSWORD` (a classic -personal access token with `write:packages`) secrets; `OCI_IMAGE_NAME` is -optional and defaults to `craftainer/devcontainer-features`. Without +Publishing needs, once, in the repository's Actions settings, only the +`OCI_REGISTRY` variable set to `ghcr.io`; no secrets. `make publish` then +pushes with the release job's `GITHUB_TOKEN` (as `GITHUB_ACTOR`), which +works once template-sync has brought in a base `release.yml` whose `build` +job grants `packages: write`; until then GHCR denies the push and +`make publish` fails. `OCI_IMAGE_NAME` is optional and defaults to +`craftainer/devcontainer-features`. The `OCI_REGISTRY_USERNAME` and +`OCI_REGISTRY_PASSWORD` secrets, when set, take precedence (both are then +required), and are needed for any registry other than `ghcr.io`. Without `OCI_REGISTRY`, a full release publishes nothing and warns. To try the targets locally: `make build release-assets release-check`; diff --git a/docs/adrs/0004-collection-github-releases.md b/docs/adrs/0004-collection-github-releases.md index be63b47..e81dcbd 100644 --- a/docs/adrs/0004-collection-github-releases.md +++ b/docs/adrs/0004-collection-github-releases.md @@ -5,7 +5,9 @@ Accepted. Amends [0001](0001-one-collection-repo-released-with-devcontainers-action.md): features are published by base's `release.yml` through the `Makefile` contract, on a full collection release, not by `devcontainers/action` on -every push to `main`. +every push to `main`. Amended 2026-10-28 (see "Amendment: publish to GHCR +with `GITHUB_TOKEN`" below): on `ghcr.io`, `publish` authenticates with the +job's `GITHUB_TOKEN` when the registry secrets are empty. ## Context @@ -103,3 +105,34 @@ its `Makefile` contract with `scripts/release-features.sh`: public by hand once. - `devcontainers/action` no longer tags `_v` in git; the collection's `v` tags and the registry's own version tags remain. + +## Amendment: publish to GHCR with `GITHUB_TOKEN` (2026-10-28) + +The Decision's `publish` bullet authenticates only with the +`OCI_REGISTRY_USERNAME` / `OCI_REGISTRY_PASSWORD` secrets, on `ghcr.io` a +classic personal access token, because `release.yml`'s `build` job had +`contents: write` only. Base's `release.yml` now grants that job +`packages: write`, and GitHub's guidance ("Publishing and installing a +package with GitHub Actions") recommends the job's `GITHUB_TOKEN` over a +personal access token for pushing to the repository's own GHCR packages. + +So `publish` now picks its credential this way, still passing it to the +CLI only through `DEVCONTAINERS_OCI_AUTH` and still rejecting a `|` or `,` +in any part of it: + +- Either registry secret set: both secrets, as before (one alone fails). + Explicit secrets always win. +- Neither set and `OCI_REGISTRY` is `ghcr.io`: `GITHUB_TOKEN` as + `GITHUB_ACTOR`. Without `GITHUB_TOKEN` it fails, naming the + `packages: write` the `build` job needs. +- Neither set and any other registry: fails, as before. + +`OCI_REGISTRY` stays the opt-in: unset still skips with a warning, a +pre-release still pushes nothing, and only the amd64 leg publishes. On +GHCR, publishing now needs only the `OCI_REGISTRY` variable, once this +repo's synced `release.yml` carries `packages: write`; until template-sync +brings it in, the registry denies a full release's push when the secrets +are empty, and `publish` fails without pushing anything. A features-only +publish job triggered by `workflow_run` was considered instead and +rejected: it would split this repo's release path from every other +craftainer repo's. diff --git a/scripts/release-features.sh b/scripts/release-features.sh index c35f679..e184645 100755 --- a/scripts/release-features.sh +++ b/scripts/release-features.sh @@ -15,6 +15,13 @@ # `devcontainer features publish`, only for a # full release (no pre-release suffix) # +# publish authenticates with the OCI_REGISTRY_USERNAME/OCI_REGISTRY_PASSWORD +# secrets when either is set (both are then required). With neither set and +# OCI_REGISTRY=ghcr.io, it uses the job's GITHUB_TOKEN as GITHUB_ACTOR, which +# needs release.yml's build job to grant `packages: write` (template-sync +# brings that in from the template-base release that adds it). Any other +# registry needs the secrets. +# # The Dev Container CLI is the version pinned in # scripts/develop.d/10-devcontainer-cli.sh (one pin, which Renovate bumps). A # `devcontainer` on PATH (or DEVCONTAINER_CLI) is used when it is that version; @@ -207,7 +214,7 @@ cmd_check() { } cmd_publish() { - local version namespace + local version namespace registry_user registry_token auth_source version="${RELEASE_VERSION:-${RELEASE_TAG:-}}" version="${version#v}" @@ -219,7 +226,7 @@ cmd_publish() { fi if [ -z "${OCI_REGISTRY:-}" ]; then if [ -n "$version" ]; then - echo "::warning::OCI_REGISTRY is not set, so full release ${version} publishes no features; set the OCI_REGISTRY variable and the OCI_REGISTRY_USERNAME/OCI_REGISTRY_PASSWORD secrets (README.md, \"Releasing the collection\"), then re-run this run's build job to publish them." + echo "::warning::OCI_REGISTRY is not set, so full release ${version} publishes no features; set the OCI_REGISTRY variable (ghcr.io needs no secrets; any other registry needs the OCI_REGISTRY_USERNAME/OCI_REGISTRY_PASSWORD secrets; README.md, \"Releasing the collection\"), then re-run this run's build job to publish them." fi echo "release-features: OCI_REGISTRY not set -- skipping publish." return 0 @@ -227,12 +234,24 @@ cmd_publish() { [ -n "$version" ] || die "RELEASE_VERSION (or RELEASE_TAG) is not set" [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "'${version}' is not a full release version (x.y.z)" - [ -n "${OCI_REGISTRY_USERNAME:-}" ] || die "OCI_REGISTRY is set but the OCI_REGISTRY_USERNAME secret is empty" - [ -n "${OCI_REGISTRY_PASSWORD:-}" ] || die "OCI_REGISTRY is set but the OCI_REGISTRY_PASSWORD secret is empty" + # Explicit secrets win; on ghcr.io without them, the job's GITHUB_TOKEN. + if [ -n "${OCI_REGISTRY_USERNAME:-}" ] || [ -n "${OCI_REGISTRY_PASSWORD:-}" ]; then + [ -n "${OCI_REGISTRY_USERNAME:-}" ] || die "OCI_REGISTRY is set but the OCI_REGISTRY_USERNAME secret is empty" + [ -n "${OCI_REGISTRY_PASSWORD:-}" ] || die "OCI_REGISTRY is set but the OCI_REGISTRY_PASSWORD secret is empty" + registry_user="$OCI_REGISTRY_USERNAME" registry_token="$OCI_REGISTRY_PASSWORD" + auth_source="the OCI_REGISTRY_USERNAME/OCI_REGISTRY_PASSWORD secrets" + elif [ "$OCI_REGISTRY" = ghcr.io ]; then + [ -n "${GITHUB_TOKEN:-}" ] || die "OCI_REGISTRY is ghcr.io and the OCI_REGISTRY_USERNAME/OCI_REGISTRY_PASSWORD secrets are empty, but GITHUB_TOKEN is not set either; release.yml's build job needs \`permissions: packages: write\` and GITHUB_TOKEN in its env (template-sync it from the template-base release that adds it), or set both secrets" + [ -n "${GITHUB_ACTOR:-}" ] || die "OCI_REGISTRY is ghcr.io and publish would use GITHUB_TOKEN, but GITHUB_ACTOR is not set" + registry_user="$GITHUB_ACTOR" registry_token="$GITHUB_TOKEN" + auth_source="GITHUB_TOKEN as ${GITHUB_ACTOR} (needs packages: write on release.yml's build job)" + else + die "OCI_REGISTRY is ${OCI_REGISTRY}, not ghcr.io, so the OCI_REGISTRY_USERNAME and OCI_REGISTRY_PASSWORD secrets are required; both are empty" + fi # DEVCONTAINERS_OCI_AUTH is "registry|user|token[,...]"; a '|' or ',' in # a value would split it in the wrong place. - case "${OCI_REGISTRY}${OCI_REGISTRY_USERNAME}${OCI_REGISTRY_PASSWORD}" in - *'|'* | *,*) die "OCI_REGISTRY, OCI_REGISTRY_USERNAME and OCI_REGISTRY_PASSWORD must not contain '|' or ','" ;; + case "${OCI_REGISTRY}${registry_user}${registry_token}" in + *'|'* | *,*) die "OCI_REGISTRY and the registry user and token must not contain '|' or ','" ;; esac namespace="${OCI_IMAGE_NAME:-${GITHUB_REPOSITORY:-}}" @@ -246,11 +265,11 @@ cmd_publish() { load_ids run_checks resolve_cli - echo "release-features: publishing release ${version}'s features to ${OCI_REGISTRY}/${namespace}/ (versions already in the registry are skipped)." + echo "release-features: publishing release ${version}'s features to ${OCI_REGISTRY}/${namespace}/ (versions already in the registry are skipped), authenticating with ${auth_source}." # The credentials reach the CLI through the environment only, never its - # command line. GITHUB_TOKEN is dropped so the CLI can't fall back to the - # job's token (contents: write, no packages: write) for ghcr.io. - DEVCONTAINERS_OCI_AUTH="${OCI_REGISTRY}|${OCI_REGISTRY_USERNAME}|${OCI_REGISTRY_PASSWORD}" \ + # command line. GITHUB_TOKEN is dropped either way, so the only + # credential the CLI sees is the one chosen above, in DEVCONTAINERS_OCI_AUTH. + DEVCONTAINERS_OCI_AUTH="${OCI_REGISTRY}|${registry_user}|${registry_token}" \ env -u GITHUB_TOKEN -u OCI_REGISTRY_PASSWORD \ "$DEVCONTAINER" features publish src --registry "$OCI_REGISTRY" --namespace "$namespace" --log-level info } From 2c9934324feebe3d8aed492adc24e527756e5397 Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:18:50 +0000 Subject: [PATCH 2/3] fix(release): name the GITHUB_TOKEN denial causes on a failed publish, clearer one-secret errors (DF15b) - publish: on a CLI failure, die naming the auth source; in GITHUB_TOKEN mode also packages: write and per-package Manage Actions access - one-secret errors say to set both or, on ghcr.io, delete both - README: migration from the secrets, per-package Actions access --- README.md | 6 +++++- scripts/release-features.sh | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a96b585..7bd8309 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,11 @@ job grants `packages: write`; until then GHCR denies the push and `make publish` fails. `OCI_IMAGE_NAME` is optional and defaults to `craftainer/devcontainer-features`. The `OCI_REGISTRY_USERNAME` and `OCI_REGISTRY_PASSWORD` secrets, when set, take precedence (both are then -required), and are needed for any registry other than `ghcr.io`. Without +required), and are needed for any registry other than `ghcr.io`; to move +`ghcr.io` from them to `GITHUB_TOKEN`, delete both. A feature package that +already exists on GHCR and wasn't first pushed by this repository's +workflow needs, once, this repository given the Write role under the +package's "Manage Actions access" settings, or GHCR denies the push. Without `OCI_REGISTRY`, a full release publishes nothing and warns. To try the targets locally: `make build release-assets release-check`; diff --git a/scripts/release-features.sh b/scripts/release-features.sh index e184645..ba92ef4 100755 --- a/scripts/release-features.sh +++ b/scripts/release-features.sh @@ -214,7 +214,7 @@ cmd_check() { } cmd_publish() { - local version namespace registry_user registry_token auth_source + local version namespace registry_user registry_token auth_source denied_hint="" version="${RELEASE_VERSION:-${RELEASE_TAG:-}}" version="${version#v}" @@ -236,8 +236,8 @@ cmd_publish() { # Explicit secrets win; on ghcr.io without them, the job's GITHUB_TOKEN. if [ -n "${OCI_REGISTRY_USERNAME:-}" ] || [ -n "${OCI_REGISTRY_PASSWORD:-}" ]; then - [ -n "${OCI_REGISTRY_USERNAME:-}" ] || die "OCI_REGISTRY is set but the OCI_REGISTRY_USERNAME secret is empty" - [ -n "${OCI_REGISTRY_PASSWORD:-}" ] || die "OCI_REGISTRY is set but the OCI_REGISTRY_PASSWORD secret is empty" + [ -n "${OCI_REGISTRY_USERNAME:-}" ] || die "the OCI_REGISTRY_PASSWORD secret is set but OCI_REGISTRY_USERNAME is empty; set both, or (on ghcr.io) delete both to publish with GITHUB_TOKEN" + [ -n "${OCI_REGISTRY_PASSWORD:-}" ] || die "the OCI_REGISTRY_USERNAME secret is set but OCI_REGISTRY_PASSWORD is empty; set both, or (on ghcr.io) delete both to publish with GITHUB_TOKEN" registry_user="$OCI_REGISTRY_USERNAME" registry_token="$OCI_REGISTRY_PASSWORD" auth_source="the OCI_REGISTRY_USERNAME/OCI_REGISTRY_PASSWORD secrets" elif [ "$OCI_REGISTRY" = ghcr.io ]; then @@ -245,6 +245,9 @@ cmd_publish() { [ -n "${GITHUB_ACTOR:-}" ] || die "OCI_REGISTRY is ghcr.io and publish would use GITHUB_TOKEN, but GITHUB_ACTOR is not set" registry_user="$GITHUB_ACTOR" registry_token="$GITHUB_TOKEN" auth_source="GITHUB_TOKEN as ${GITHUB_ACTOR} (needs packages: write on release.yml's build job)" + # In CI GITHUB_TOKEN is always set, so a missing `packages: write` + # only shows up as the registry denying the push. + denied_hint="; if the registry denied the push, release.yml's build job needs \`permissions: packages: write\` (template-sync it from the template-base release that adds it), and each feature package that already exists needs this repository given Write in its \"Manage Actions access\" settings" else die "OCI_REGISTRY is ${OCI_REGISTRY}, not ghcr.io, so the OCI_REGISTRY_USERNAME and OCI_REGISTRY_PASSWORD secrets are required; both are empty" fi @@ -271,7 +274,8 @@ cmd_publish() { # credential the CLI sees is the one chosen above, in DEVCONTAINERS_OCI_AUTH. DEVCONTAINERS_OCI_AUTH="${OCI_REGISTRY}|${registry_user}|${registry_token}" \ env -u GITHUB_TOKEN -u OCI_REGISTRY_PASSWORD \ - "$DEVCONTAINER" features publish src --registry "$OCI_REGISTRY" --namespace "$namespace" --log-level info + "$DEVCONTAINER" features publish src --registry "$OCI_REGISTRY" --namespace "$namespace" --log-level info || + die "devcontainer features publish failed, authenticating with ${auth_source}${denied_hint}" } case "${1:-}" in From 13050c334c3b07dcc797f64097add070c162759d Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:20:39 +0000 Subject: [PATCH 3/3] docs(adrs): record GITHUB_TOKEN publishing as ADR 0005, partly superseding 0004 (DF15b) - new ADR 0005; 0004 keeps only a Status pointer (in-place amendment removed) - README links ADR 0005 --- README.md | 4 +- docs/adrs/0004-collection-github-releases.md | 38 ++----------- .../0005-publish-to-ghcr-with-github-token.md | 56 +++++++++++++++++++ 3 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 docs/adrs/0005-publish-to-ghcr-with-github-token.md diff --git a/README.md b/README.md index 7bd8309..cbdc116 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,9 @@ Publishing needs, once, in the repository's Actions settings, only the pushes with the release job's `GITHUB_TOKEN` (as `GITHUB_ACTOR`), which works once template-sync has brought in a base `release.yml` whose `build` job grants `packages: write`; until then GHCR denies the push and -`make publish` fails. `OCI_IMAGE_NAME` is optional and defaults to +`make publish` fails +([ADR 0005](docs/adrs/0005-publish-to-ghcr-with-github-token.md)). +`OCI_IMAGE_NAME` is optional and defaults to `craftainer/devcontainer-features`. The `OCI_REGISTRY_USERNAME` and `OCI_REGISTRY_PASSWORD` secrets, when set, take precedence (both are then required), and are needed for any registry other than `ghcr.io`; to move diff --git a/docs/adrs/0004-collection-github-releases.md b/docs/adrs/0004-collection-github-releases.md index e81dcbd..a17b928 100644 --- a/docs/adrs/0004-collection-github-releases.md +++ b/docs/adrs/0004-collection-github-releases.md @@ -5,9 +5,10 @@ Accepted. Amends [0001](0001-one-collection-repo-released-with-devcontainers-action.md): features are published by base's `release.yml` through the `Makefile` contract, on a full collection release, not by `devcontainers/action` on -every push to `main`. Amended 2026-10-28 (see "Amendment: publish to GHCR -with `GITHUB_TOKEN`" below): on `ghcr.io`, `publish` authenticates with the -job's `GITHUB_TOKEN` when the registry secrets are empty. +every push to `main`. Partly superseded by +[0005](0005-publish-to-ghcr-with-github-token.md): on `ghcr.io`, `publish` +authenticates with the job's `GITHUB_TOKEN` when the registry secrets are +empty, so the secrets are no longer required there. ## Context @@ -105,34 +106,3 @@ its `Makefile` contract with `scripts/release-features.sh`: public by hand once. - `devcontainers/action` no longer tags `_v` in git; the collection's `v` tags and the registry's own version tags remain. - -## Amendment: publish to GHCR with `GITHUB_TOKEN` (2026-10-28) - -The Decision's `publish` bullet authenticates only with the -`OCI_REGISTRY_USERNAME` / `OCI_REGISTRY_PASSWORD` secrets, on `ghcr.io` a -classic personal access token, because `release.yml`'s `build` job had -`contents: write` only. Base's `release.yml` now grants that job -`packages: write`, and GitHub's guidance ("Publishing and installing a -package with GitHub Actions") recommends the job's `GITHUB_TOKEN` over a -personal access token for pushing to the repository's own GHCR packages. - -So `publish` now picks its credential this way, still passing it to the -CLI only through `DEVCONTAINERS_OCI_AUTH` and still rejecting a `|` or `,` -in any part of it: - -- Either registry secret set: both secrets, as before (one alone fails). - Explicit secrets always win. -- Neither set and `OCI_REGISTRY` is `ghcr.io`: `GITHUB_TOKEN` as - `GITHUB_ACTOR`. Without `GITHUB_TOKEN` it fails, naming the - `packages: write` the `build` job needs. -- Neither set and any other registry: fails, as before. - -`OCI_REGISTRY` stays the opt-in: unset still skips with a warning, a -pre-release still pushes nothing, and only the amd64 leg publishes. On -GHCR, publishing now needs only the `OCI_REGISTRY` variable, once this -repo's synced `release.yml` carries `packages: write`; until template-sync -brings it in, the registry denies a full release's push when the secrets -are empty, and `publish` fails without pushing anything. A features-only -publish job triggered by `workflow_run` was considered instead and -rejected: it would split this repo's release path from every other -craftainer repo's. diff --git a/docs/adrs/0005-publish-to-ghcr-with-github-token.md b/docs/adrs/0005-publish-to-ghcr-with-github-token.md new file mode 100644 index 0000000..b566c99 --- /dev/null +++ b/docs/adrs/0005-publish-to-ghcr-with-github-token.md @@ -0,0 +1,56 @@ +# 0005. Publish to GHCR with the release job's `GITHUB_TOKEN` + +## Status + +Accepted. Partly supersedes +[0004](0004-collection-github-releases.md). + +## Context + +[ADR 0004](0004-collection-github-releases.md)'s `publish` authenticates +only with the `OCI_REGISTRY_USERNAME` / `OCI_REGISTRY_PASSWORD` secrets, +which on `ghcr.io` means a classic personal access token with +`write:packages`. It did so because `release.yml`'s `build` job, where +`make publish` runs, had `contents: write` only. A classic token is +long-lived, tied to one person and broader than this repository. + +Base's `release.yml` now grants the `build` job `packages: write`, and +GitHub's guidance ("Publishing and installing a package with GitHub +Actions") recommends the job's `GITHUB_TOKEN` over a personal access token +for pushing to the repository's own GHCR packages. + +## Decision + +We will have `publish` pick its credential this way, still passing it to +the Dev Container CLI only through `DEVCONTAINERS_OCI_AUTH` +(`registry|user|token`) and still rejecting a `|` or `,` in any part of it: + +- Either registry secret set: both secrets, as before (one alone fails). + Explicit secrets always win. +- Neither set and `OCI_REGISTRY` is `ghcr.io`: `GITHUB_TOKEN` as + `GITHUB_ACTOR`. Without either of them it fails. +- Neither set and any other registry: fails, as before. + +`GITHUB_TOKEN` is removed from the CLI's environment either way, so the +only credential it sees is the one chosen here. `OCI_REGISTRY` stays the +opt-in: unset still skips with a warning, a pre-release still pushes +nothing, and only the amd64 leg publishes. + +A features-only publish job triggered by `workflow_run` was considered +instead and rejected: it would split this repo's release path from every +other craftainer repo's. + +## Consequences + +- On GHCR, publishing needs only the `OCI_REGISTRY` variable, once this + repo's synced `release.yml` carries `packages: write`. Until + template-sync brings it in, GHCR denies a full release's push when the + secrets are empty, and `publish` fails naming that cause. +- Moving an existing setup from the secrets to `GITHUB_TOKEN` means + deleting both secrets; leaving one fails, and leaving both keeps using + them. +- A feature package that already exists and wasn't first pushed by this + repository's workflow needs, once, this repository given the Write role + under the package's "Manage Actions access" settings, or GHCR denies the + push. +- Any other registry still needs both secrets.