diff --git a/README.md b/README.md index f070f90..cbdc116 100644 --- a/README.md +++ b/README.md @@ -286,11 +286,21 @@ 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 +([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 +`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/docs/adrs/0004-collection-github-releases.md b/docs/adrs/0004-collection-github-releases.md index be63b47..a17b928 100644 --- a/docs/adrs/0004-collection-github-releases.md +++ b/docs/adrs/0004-collection-github-releases.md @@ -5,7 +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`. +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 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. diff --git a/scripts/release-features.sh b/scripts/release-features.sh index c35f679..ba92ef4 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 denied_hint="" 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,27 @@ 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 "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 + [ -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)" + # 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 # 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,13 +268,14 @@ 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 + "$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