From 1309e3d5a6c7cfb535e5309d3c032c2758978bde Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Mon, 10 Aug 2026 23:17:01 +0000 Subject: [PATCH] fix(packages): retire private build-image grants --- AGENTS.md | 9 +- CONTRIBUTING.md | 6 +- README.md | 154 ++++++++---------------- config/manual-settings.json | 23 +--- tests/validate-manual-package-access.sh | 38 +++--- 5 files changed, 76 insertions(+), 154 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 181884d..29c121a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,9 +80,12 @@ - Record cross-repository private-package consumption in `config/manual-settings.json` with stable package and repository IDs. Grant consumer workflows only the `read` role through the package's **Manage - Actions access** UI. Do not change package visibility, permission - inheritance, or source-repository association, and do not automate the grant - through an undocumented API. + Actions access** UI, preserve visibility and source association, and do not + automate the grant through an undocumented API. A transition from private to + public is a separate, effectively irreversible organization-owner action: + merge reviewed desired-state and consumer-permission changes first, preserve + the source association, verify anonymous digest access, and remove obsolete + repository grants without altering foreign entries. - Record manually provisioned GitHub Actions environments in `config/manual-settings.json` with stable repository IDs, exact deployment branch policies and reviewer sets, and variable and secret names only. Never diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b085cee..b54e194 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,11 @@ administrative token required by the manual publishing workflow. Package Actions-access grants that lack a documented public API belong in `config/manual-settings.json`; record stable package and repository IDs, grant only read access to consumer workflows, and preserve the package's existing -visibility and source-repository association. Manually provisioned GitHub +visibility and source-repository association. Making a reusable package public +requires a reviewed desired-state removal, explicit organization-owner +authorization, anonymous immutable-digest verification, and removal of only +the obsolete consumer grant; treat the visibility change as effectively +irreversible. Manually provisioned GitHub Actions environments belong in the same inventory with a stable repository ID, exact branch policy and reviewer set, and variable and secret names only. Never commit their values, and provision them only after the workflow that uses diff --git a/README.md b/README.md index 2b8ef59..d9b052e 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,11 @@ read-only and are skipped on later runs. request GitHub Apps for owner review. These UI-only settings are recorded in `config/manual-settings.json`. - Private GitHub Packages consumed across repositories have explicit, - read-only workflow grants recorded in `config/manual-settings.json`. The - package's visibility, permission inheritance, and source-repository link are - not changed by these grants. + read-only workflow grants recorded in `config/manual-settings.json`. Public + reusable build images instead require anonymous immutable-digest access and + no consumer grant or package permission. A private-to-public transition is a + separately reviewed, effectively irreversible owner action that preserves + the source-repository link. - GitHub Actions environments that depend on externally issued credentials have name-only contracts in `config/manual-settings.json`. The inventory records the stable repository identity, exact deployment branch policy and @@ -122,9 +124,11 @@ The GitHub REST API does not expose every organization control. The desired values are recorded in `config/manual-settings.json` and must be confirmed in the organization UI under **Member privileges**, **Authentication security**, **GitHub Apps**, each listed repository's **Environments** settings, and each -listed package's **Manage Actions access** section. Codecov must be installed -for the listed repositories so their OIDC-authenticated coverage uploads and -badges remain available. +listed private package's **Manage Actions access** section. Public package +identity, visibility, source association, and anonymous digest access use the +separate checked procedure below. Codecov must be installed for the listed +repositories so their OIDC-authenticated coverage uploads and badges remain +available. ## Usage @@ -649,120 +653,56 @@ an external provider. Complete every retirement in this order: Record the exact environment and external-token names in the retirement pull request handoff without recording their values. -## Granting Classic access to private build images +## Public reusable build images -### Windows cross-build image +`ghcr.io/atrinik/classic-build` (package ID `14345002`) and +`ghcr.io/atrinik/windows-build` (package ID `14204802`) are public reusable +toolchain images linked to `atrinik/devcontainer`. Their public visibility lets +fork-controlled pull-request workflows consume immutable digests without a +package permission, registry login, repository secret, or cross-repository +**Manage Actions access** grant. Accordingly, +`github_packages_actions_access` contains no entry for either package. -The private `ghcr.io/atrinik/windows-build` image is owned by the -`atrinik/windows-build` container package (package ID `14204802`) and remains -linked to `atrinik/devcontainer`. Classic release workflows run from the public -`atrinik/classic` repository (repository ID `1327289971`). They need an -explicit **Read** grant under the package's **Manage Actions access** section; -connecting the package to Classic, changing inherited permissions, making the -package public, or adding a personal access token are not substitutes for this -grant. +Changing a GitHub Container Registry package from private to public is an +effectively irreversible organization-owner action. Before the change, merge a +reviewed desired-state removal, verify both stable package identities and the +`atrinik/devcontainer` source association, and confirm that every intended +consumer change removes package permissions and registry login. Never change +permission inheritance, detach the source repository, or alter a foreign +Actions-access entry. -Before changing the manual setting, an organization owner with package -administration access must verify the stable identities and the pinned build -image version: +Verify the live contract read-only: ```sh +gh api /orgs/atrinik/packages/container/classic-build \ + --jq '{id, name, package_type, visibility, source: .repository.full_name}' gh api /orgs/atrinik/packages/container/windows-build \ --jq '{id, name, package_type, visibility, source: .repository.full_name}' gh api /repos/atrinik/classic \ --jq '{id, full_name, visibility, archived, default_branch}' -gh api --paginate \ - '/orgs/atrinik/packages/container/windows-build/versions?per_page=100' \ - --jq '.[] | select(.id == 1107232303) | {id, name, tags: .metadata.container.tags}' -``` - -The expected values are: - -- package `windows-build`, ID `14204802`, type `container`, visibility - `private`, source `atrinik/devcontainer`; -- repository `atrinik/classic`, ID `1327289971`, visibility `public`, not - archived, default branch `main`; -- package version ID `1107232303`, digest - `sha256:9cc373f620a577328fc0a7a7fa823bddaca6d7dc75ac73bcf21be421c49676f7`, - with tag `1.0.5`. - -Open the -[Windows build package settings](https://github.com/orgs/atrinik/packages/container/windows-build/settings), -find **Manage Actions access**, select **Add repository**, add -`atrinik/classic`, and leave its role at **Read**. Do not modify the package's -**Private** visibility, repository source, or permission-inheritance setting. -The public Packages REST API does not expose this workflow-grant list, so the -package settings UI is the authoritative verification surface: it must list -`atrinik/classic` exactly once with **Read** access. - -After confirming the UI state, rerun the failed read-only release rehearsal and -inspect both Windows jobs: - -```sh -gh run rerun 31239475233 --repo atrinik/classic --failed -gh run watch 31239475233 --repo atrinik/classic --exit-status -gh run view 31239475233 --repo atrinik/classic \ - --json jobs \ - --jq '.jobs[] | select(.name | test("Build Windows (client|server) package$")) | [.name, .conclusion] | @tsv' ``` -Both Windows package jobs must succeed, including the digest-pinned -`docker pull`. If they still report `manifest unknown`, recheck the **Manage -Actions access** entry; do not weaken package visibility or add a repository -secret. - -To roll back the grant, first merge a reviewed change removing its entry from -`github_packages_actions_access`. Then return to the same package settings, -remove only `atrinik/classic` from **Manage Actions access**, and confirm the -repository is absent while the package remains private and linked to -`atrinik/devcontainer`. No `bin/publish --apply` run is involved in either -direction because this setting has no supported publisher API. - -### Classic Linux build image - -The private `ghcr.io/atrinik/classic-build` image is owned by the -`atrinik/classic-build` container package (package ID `14345002`) and remains -linked to `atrinik/devcontainer`. The same Classic repository identity above -needs an explicit **Read** grant for its Linux Check jobs. Before changing the -manual setting, verify the exact package, consumer, and candidate version: - -```sh -gh api /orgs/atrinik/packages/container/classic-build \ - --jq '{id, name, package_type, visibility, source: .repository.full_name}' -gh api /repos/atrinik/classic \ - --jq '{id, full_name, visibility, archived, default_branch}' -gh api --paginate \ - '/orgs/atrinik/packages/container/classic-build/versions?per_page=100' \ - --jq '.[] | select(.id == 1118365045) | {id, name, tags: .metadata.container.tags}' -``` +Require the exact package IDs above, type `container`, visibility `public`, and +source `atrinik/devcontainer`. Require Classic repository ID `1327289971`, +visibility `public`, `archived: false`, and default branch `main`. An API +failure, missing field, identity mismatch, private package, detached source, or +unexpected state is a stop condition. -Require package ID `14345002`, type `container`, visibility `private`, and -source `atrinik/devcontainer`; require the Classic repository values documented -above; and require version ID `1118365045`, digest -`sha256:e117b858d5aecdb8eb39dc56451378b6e6bd72dd5e042ab96fee5b6154000043`, -with tag -`candidate-sha-b9a86c4f52205c927373caa7583d3a43989cfca7`. -An API failure, an empty version query, any identity mismatch, or any -unexpected live state is a stop condition. - -Open the -[Classic build package settings](https://github.com/orgs/atrinik/packages/container/classic-build/settings), -find **Manage Actions access**, select **Add repository**, add -`atrinik/classic`, and leave its role at **Read**. Do not modify private -visibility, repository source, or permission inheritance. Require the UI to -list `atrinik/classic` exactly once with the **Read** role, then rerun the -failed Classic Check jobs: +Then use a new empty Docker configuration to prove anonymous access to every +consumer-pinned digest: ```sh -gh run rerun 31429664785 --repo atrinik/classic --failed -gh run watch 31429664785 --repo atrinik/classic --exit-status +anonymous_docker_config=$(mktemp -d) +DOCKER_CONFIG="${anonymous_docker_config}" \ + docker buildx imagetools inspect \ + 'ghcr.io/atrinik/classic-build:1.2.3@sha256:d0ec0a31f97fa1d699f62b81bbe697d95b335f44f1c99fde8704dfc528e2102f' +DOCKER_CONFIG="${anonymous_docker_config}" \ + docker buildx imagetools inspect \ + 'ghcr.io/atrinik/windows-build:1.2.1@sha256:d1f082eb28891600a9cf018a1d4310b9f3e1f985f82139fa48fbd4ac77b623bb' ``` -Core, server, and client validation must all pull the exact candidate digest -and succeed. If any pull still reports `manifest unknown`, recheck the manual -grant; do not make the package public or add a repository secret. To roll back, -first merge a reviewed desired-state removal, then remove only -`atrinik/classic` from this package's **Manage Actions access** list and verify -that the repository entry is absent while private visibility and the -`atrinik/devcontainer` association remain unchanged. Any unexpected UI state -is a stop condition; do not remove or alter another entry. +Both inspections must succeed without `docker login`. The package settings UI +must not list an obsolete `atrinik/classic` Actions-access grant; remove only +that exact entry if it remains after the visibility transition. Finally, run a +real fork pull request and require its digest pulls and complete required +checks to succeed without package permissions or registry authentication. diff --git a/config/manual-settings.json b/config/manual-settings.json index ca39635..876e579 100644 --- a/config/manual-settings.json +++ b/config/manual-settings.json @@ -97,28 +97,7 @@ "secret_scope": "repository" } ], - "github_packages_actions_access": [ - { - "package_owner": "atrinik", - "package_type": "container", - "package_name": "classic-build", - "package_id": 14345002, - "preserve_visibility": "private", - "actions_repository": "atrinik/classic", - "actions_repository_id": 1327289971, - "role": "read" - }, - { - "package_owner": "atrinik", - "package_type": "container", - "package_name": "windows-build", - "package_id": 14204802, - "preserve_visibility": "private", - "actions_repository": "atrinik/classic", - "actions_repository_id": 1327289971, - "role": "read" - } - ], + "github_packages_actions_access": [], "require_two_factor_authentication": true, "require_secure_two_factor_methods": true, "reason": "The public GitHub API does not expose supported update parameters for every setting in this file, including organization pins, and external credential values must never be stored here. Confirm these settings, the exact ordered pin set, credential lifecycle metadata and live secret-name presence, Codecov GitHub App repository access, any listed GitHub Actions environments, and GitHub Packages Actions access in the corresponding organization, repository, and package settings UI." diff --git a/tests/validate-manual-package-access.sh b/tests/validate-manual-package-access.sh index 6921973..11549e4 100755 --- a/tests/validate-manual-package-access.sh +++ b/tests/validate-manual-package-access.sh @@ -8,6 +8,7 @@ trap 'rm -rf "${temporary}"' EXIT mkdir -p "${temporary}/bin" cp "${root}/bin/validate" "${temporary}/bin/validate" +cp -R "${root}/.github" "${temporary}/.github" cp -R "${root}/config" "${temporary}/config" cp -R "${root}/community-health" "${temporary}/community-health" @@ -31,7 +32,14 @@ rewrite_manual_settings() { } jq -e ' - .github_packages_actions_access == [ + .github_packages_actions_access == [] +' "${root}/config/manual-settings.json" >/dev/null + +reset_valid_access() { + cp "${root}/config/manual-settings.json" \ + "${temporary}/config/manual-settings.json" + rewrite_manual_settings ' + .github_packages_actions_access = [ { package_owner: "atrinik", package_type: "container", @@ -41,44 +49,32 @@ jq -e ' actions_repository: "atrinik/classic", actions_repository_id: 1327289971, role: "read" - }, - { - package_owner: "atrinik", - package_type: "container", - package_name: "windows-build", - package_id: 14204802, - preserve_visibility: "private", - actions_repository: "atrinik/classic", - actions_repository_id: 1327289971, - role: "read" } - ] -' "${root}/config/manual-settings.json" >/dev/null + ]' + "${temporary}/bin/validate" >/dev/null +} +reset_valid_access rewrite_manual_settings \ '.github_packages_actions_access += [.github_packages_actions_access[0]]' assert_invalid 'a duplicate package Actions-access grant' -cp "${root}/config/manual-settings.json" \ - "${temporary}/config/manual-settings.json" +reset_valid_access rewrite_manual_settings \ '.github_packages_actions_access[0].role = "write"' assert_invalid 'a write-capable package Actions-access grant' -cp "${root}/config/manual-settings.json" \ - "${temporary}/config/manual-settings.json" +reset_valid_access rewrite_manual_settings \ 'del(.github_packages_actions_access[0].actions_repository_id)' assert_invalid 'a package grant without a stable repository ID' -cp "${root}/config/manual-settings.json" \ - "${temporary}/config/manual-settings.json" +reset_valid_access rewrite_manual_settings \ '.github_packages_actions_access[0].package_id = 0' assert_invalid 'a package grant without a positive package ID' -cp "${root}/config/manual-settings.json" \ - "${temporary}/config/manual-settings.json" +reset_valid_access rewrite_manual_settings \ '.github_packages_actions_access[0].actions_repository = "classic"' assert_invalid 'a package grant without a full repository name'