Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
5 changes: 4 additions & 1 deletion docs/adrs/0004-collection-github-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
56 changes: 56 additions & 0 deletions docs/adrs/0005-publish-to-ghcr-with-github-token.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 34 additions & 11 deletions scripts/release-features.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}"

Expand All @@ -219,20 +226,35 @@ 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
fi
[ -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:-}}"
Expand All @@ -246,13 +268,14 @@ cmd_publish() {
load_ids
run_checks
resolve_cli
echo "release-features: publishing release ${version}'s features to ${OCI_REGISTRY}/${namespace}/<id> (versions already in the registry are skipped)."
echo "release-features: publishing release ${version}'s features to ${OCI_REGISTRY}/${namespace}/<id> (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
Expand Down