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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@
never deletes a branch, tag, release, or asset.
- Preserve the selected-actions entry for Codecov and the manual GitHub App
repository-access inventory while coverage uploads use OIDC authentication.
- Preserve external-provider App records as value-free manual state with exact
stable identities, permissions, events, and selected repository IDs. A
shared installation change must retain every unrelated reviewed repository,
must never widen to all repositories, and requires organization-owner UI
readback when the administrative API cannot enumerate the selected set.
- 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
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ repository set in GitHub's installation UI. Keep the App out of bypass lists,
preserve read-only Actions defaults, and prove the consumer's disposable pull
request receives ordinary checks without self-approval or self-merge.

External provider GitHub Apps use a separate value-free inventory entry with
stable App, installation, and selected repository IDs; exact permissions and
events; provider purpose and status producer; accountable review ownership;
and a revocation runbook. Preserve every existing selected repository unless a
separate reviewed retirement authorizes its removal, never select all
repositories, and require organization-owner UI proof of the exact set when
GitHub's administrative API cannot enumerate it. Provider tokens, account
coordinates, and credential values do not belong in this repository.

Organization identity changes must preserve the exact public description and
canonical website in `config/organization.json`, remain delta-aware in plan and
apply modes, and include unchanged-state coverage. Keep the public profile in
Expand Down
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ read-only and are skipped on later runs.
intended repository and consumer, credential metadata names, accountable
owner, verification date, rotation deadline, and runbook. The App private
key and installation tokens never belong in this repository.
- External provider GitHub Apps have a distinct value-free inventory. It binds
the stable App and installation identities, exact permissions and events,
exact selected repository IDs, provider purpose, review owner and deadline,
revocation procedure, and status producer. Provider credentials and account
coordinates remain outside GitHub and this repository.
- GitHub Actions defaults to read-only, cannot approve pull requests, and may
use only Atrinik, GitHub, Codecov coverage, and explicitly allowed Docker
actions.
Expand Down Expand Up @@ -338,6 +343,77 @@ If any ID, permission, event, suspension state, selected repository, or
credential name differs, stop. Disable the consumer and reconcile reviewed
desired state before minting another installation token.

## Cloudflare GitHub App

The existing organization installation for the **Cloudflare Workers and
Pages** GitHub App is App ID `85455`, installation ID `152311798`, and selected-
repository mode. Its exact effective GitHub permissions are administration,
checks, contents, deployments, and pull requests write plus metadata read; its
event subscriptions are exactly `pull_request` and `push`. These broad App
permissions are provider-managed GitHub capabilities, not the Cloudflare build
token or runtime authority. Issue
[`atrinik/metaserver-worker#56`](https://github.com/atrinik/metaserver-worker/issues/56)
separately owns the least-privilege Cloudflare identities and provider
connection.

The selected set must be exactly these two stable repositories:

- `atrinik/website`, ID `1327107093`, preserving its existing Pages
integration; and
- `atrinik/metaserver-worker`, ID `1324297032`, for the serialized Workers
Builds topology reviewed in metaserver-worker issues 53-56.

No other repository and never **All repositories** is authorized. GitHub's
organization-installations API verifies the public installation identity,
mode, effective permissions, events, organization owner, and suspension state.
It does not give this administrative token an authoritative enumeration of the
selected set, so an organization owner must verify that exact two-repository
set in
<https://github.com/organizations/atrinik/settings/installations/152311798>.
`bin/verify-manual-settings` deliberately reports this remaining manual proof;
it also verifies both repository IDs read-only and never reads a credential
value.

After the metaserver-worker production and review contracts are merged and
validated, a separately authorized organization owner may edit only this
installation: preserve `website`, add only `metaserver-worker`, save selected-
repository mode, and immediately re-open the selection to confirm the exact
two entries. Do not change App permissions, events, the website connection,
repository rulesets, or any unrelated installation. Run
`bin/verify-manual-settings` before and after the UI step and retain the owner
confirmation in the owner-controlled
`atrinik/metaserver-worker#56-private-provider-evidence` record. That stable
coordinate is public governance metadata; the evidence contents remain private
and contain no credential values. `bin/publish` remains plan-only for this
record and never changes installation access.

The App may publish its native Cloudflare check/deployment result. For
`metaserver-worker`, `main` remains the sole automatic production branch and a
normal protected-branch pull-request merge is the routine authorization; do
not add another production branch, an Actions environment approval, a release
or tag gate, Deploy Hook, privileged dispatch, bypass actor, or GitHub Actions
deployment credential. The existing PR-only `main` rule remains unchanged.
The provider's post-merge production result is evidence, not a pre-merge merge
gate. Any review-branch result described by issue #55 must be observed as
stable and unambiguous before a later reviewed governance change can make it a
required check.

Migration or separately authorized control-plane recovery may retry only the
provider build for the exact SHA that is still current `main`. It is not a
GitHub branch-policy bypass: never push a recovery ref, create a second
long-lived production branch, invoke a privileged GitHub dispatch, or weaken
the pull-request gate. If `main` advances, abandon that retry and re-evaluate
the new current revision through the provider-owned recovery contract.

To revoke metaserver access, first disconnect the repository in Cloudflare and
stop its Builds triggers under the recovery procedure owned by #56. Then merge
a reviewed desired-state removal and, with separate organization-owner
authorization, remove only `atrinik/metaserver-worker` from installation
`152311798`. Preserve `atrinik/website` and selected-repository mode. Re-run the
verifier, confirm the website still builds through Pages, and confirm the
metaserver repository no longer appears in the installation UI. Suspending or
deleting the shared installation is not an acceptable metaserver rollback.

## Cross-repository planning

Review and apply the planning layers in their dependency order:
Expand Down
6 changes: 6 additions & 0 deletions bin/publish
Original file line number Diff line number Diff line change
Expand Up @@ -1359,4 +1359,10 @@ done < <(jq -r '.archive[]' "${repositories_config}")
if [[ $(jq -r '.members_can_create_teams' <<<"${organization_metadata}") != false ]]; then
echo "MANUAL members can still create teams; disable this in Member privileges."
fi
while IFS= read -r external_app; do
app_slug=$(jq -r '.app_slug' <<<"${external_app}")
installation_id=$(jq -r '.installation_id' <<<"${external_app}")
repositories=$(jq -r '[.repositories[].repository] | join(", ")' <<<"${external_app}")
echo "MANUAL ${app_slug} installation ${installation_id} selected repositories: ${repositories}; publisher does not apply"
done < <(jq -c '.external_provider_apps[]' "${root}/config/manual-settings.json")
echo "Manual confirmation required for config/manual-settings.json."
80 changes: 80 additions & 0 deletions bin/validate
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ jq -e '. == {state: "not-configured"}' \
jq -e '
(keys == [
"codecov_github_app_repositories",
"external_provider_apps",
"github_actions_apps",
"github_actions_credentials",
"github_actions_environments",
Expand All @@ -311,7 +312,86 @@ jq -e \
def valid_day($value):
($value | type == "string" and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$")) and
(try ((day_epoch($value) | strftime("%Y-%m-%d")) == $value) catch false);
.external_provider_apps as $external_apps |
.github_actions_apps as $apps |
($external_apps | type == "array" and length == 1) and
($external_apps[0] | keys == [
"app_id",
"app_slug",
"events",
"evidence_location",
"exceptional_retry",
"installation_id",
"last_verified_on",
"owner",
"permissions",
"provider",
"purpose",
"repositories",
"repository_scope_verification",
"repository_selection",
"review_by",
"review_cadence_days",
"review_owner",
"revocation",
"runbook",
"status_producer"
]) and
($external_apps[0].app_id == 85455) and
($external_apps[0].app_slug == "cloudflare-workers-and-pages") and
($external_apps[0].installation_id == 152311798) and
($external_apps[0].repository_selection == "selected") and
($external_apps[0].repository_scope_verification == "organization_owner_ui") and
($external_apps[0].events == ["pull_request", "push"]) and
($external_apps[0].evidence_location == "atrinik/metaserver-worker#56-private-provider-evidence") and
($external_apps[0].exceptional_retry == {
authorization: "provider-retry-only",
long_lived_production_branches: ["main"],
source: "exact-sha-still-current-main",
stale_policy: "abandon-and-re-evaluate-without-github-bypass"
}) and
($external_apps[0].permissions == {
administration: "write",
checks: "write",
contents: "write",
deployments: "write",
metadata: "read",
pull_requests: "write"
}) and
($external_apps[0].provider == "Cloudflare Workers Builds and Pages") and
($external_apps[0].status_producer == "Cloudflare Workers and Pages GitHub App") and
($external_apps[0].repositories == [
{
purpose: "Preserve the existing Cloudflare Pages website connection.",
repository: "atrinik/website",
repository_id: 1327107093
},
{
purpose: "Authorize the serialized Workers Builds topology governed by atrinik/metaserver-worker issues 53-56.",
repository: "atrinik/metaserver-worker",
repository_id: 1324297032
}
]) and
all(
$external_apps[0].repositories[];
(.repository | split("/")[1]) as $repository |
(.repository | test("^atrinik/[A-Za-z0-9_.-]+$")) and
(.repository_id | type == "number" and floor == . and . > 0) and
(.purpose | type == "string" and length > 0) and
($repositories[0].pull_request_gate | index($repository)) != null and
($repositories[0].archive | index($repository)) == null
) and
($external_apps[0].owner | type == "string" and length > 0) and
($external_apps[0].purpose | type == "string" and length > 0) and
($external_apps[0].review_owner | type == "string" and length > 0) and
($external_apps[0].revocation | type == "string" and length > 0) and
($external_apps[0].runbook == "README.md#cloudflare-github-app") and
($external_apps[0].review_cadence_days == 90) and
valid_day($external_apps[0].last_verified_on) and
valid_day($external_apps[0].review_by) and
(day_epoch($external_apps[0].last_verified_on) <= day_epoch($today)) and
(day_epoch($external_apps[0].review_by) > day_epoch($today)) and
((day_epoch($external_apps[0].review_by) - day_epoch($external_apps[0].last_verified_on)) / 86400 == $external_apps[0].review_cadence_days) and
($apps | type == "array" and length > 0) and
([$apps[].app_id] | length == (unique | length)) and
([$apps[].installation_id] | length == (unique | length)) and
Expand Down
56 changes: 55 additions & 1 deletion bin/verify-manual-settings
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,60 @@ else
exit "${status}"
fi

while IFS= read -r app_record; do
app_id=$(jq -r '.app_id' <<<"${app_record}")
app_slug=$(jq -r '.app_slug' <<<"${app_record}")
installation_id=$(jq -r '.installation_id' <<<"${app_record}")
expected_permissions=$(jq -c '.permissions' <<<"${app_record}")
expected_events=$(jq -c '.events' <<<"${app_record}")
installation=$(jq -sc \
--argjson app_id "${app_id}" \
--argjson installation_id "${installation_id}" '
[.[] | select(.app_id == $app_id and .id == $installation_id)] |
if length == 1 then .[0] else empty end
' "${installations_file}")
if [[ -z ${installation} ]]; then
echo "error: external provider App installation is missing or ambiguous for ${app_slug}" >&2
exit 1
fi
if ! jq -e \
--arg app_slug "${app_slug}" \
--arg organization "${organization}" \
--argjson expected_permissions "${expected_permissions}" \
--argjson expected_events "${expected_events}" '
.app_slug == $app_slug and
.target_type == "Organization" and
.account.login == $organization and
.account.type == "Organization" and
.repository_selection == "selected" and
.permissions == $expected_permissions and
.events == $expected_events and
.suspended_at == null
' <<<"${installation}" >/dev/null; then
echo "error: external provider App installation metadata or permission drift for ${app_slug}" >&2
exit 1
fi

while IFS= read -r repository_record; do
repository=$(jq -r '.repository' <<<"${repository_record}")
repository_id=$(jq -r '.repository_id' <<<"${repository_record}")
repository_owner=${repository%%/*}
if [[ ${repository_owner} != "${organization}" ]]; then
echo "error: external provider App repository owner does not match ${organization}: ${repository}" >&2
exit 1
fi
if verify_repository_identity "${repository}" "${repository_id}"; then
:
else
status=$?
exit "${status}"
fi
done < <(jq -c '.repositories[]' <<<"${app_record}")

echo "KEEP ${app_slug} installation metadata and exact permissions/events"
echo "MANUAL ${app_slug} selected repositories require owner UI proof: atrinik/website and atrinik/metaserver-worker only"
done < <(jq -c '.external_provider_apps[]' "${manual_settings}")

while IFS= read -r app_record; do
app_id=$(jq -r '.app_id' <<<"${app_record}")
app_slug=$(jq -r '.app_slug' <<<"${app_record}")
Expand Down Expand Up @@ -679,4 +733,4 @@ if [[ ${live_pins} != "${desired_pins}" ]]; then
fi
echo "KEEP ${organization} organization pins match the exact governed order"

echo "Manual settings live credential, GitHub App, Pages, environment, and organization pin metadata is present."
echo "Manual settings live credential, GitHub App, external provider App, Pages, environment, and organization pin metadata is present."
52 changes: 52 additions & 0 deletions config/manual-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,58 @@
"renderer",
"server"
],
"external_provider_apps": [
{
"app_id": 85455,
"app_slug": "cloudflare-workers-and-pages",
"events": [
"pull_request",
"push"
],
"evidence_location": "atrinik/metaserver-worker#56-private-provider-evidence",
"exceptional_retry": {
"authorization": "provider-retry-only",
"long_lived_production_branches": [
"main"
],
"source": "exact-sha-still-current-main",
"stale_policy": "abandon-and-re-evaluate-without-github-bypass"
},
"installation_id": 152311798,
"last_verified_on": "2026-08-15",
"owner": "Atrinik organization owners",
"permissions": {
"administration": "write",
"checks": "write",
"contents": "write",
"deployments": "write",
"metadata": "read",
"pull_requests": "write"
},
"provider": "Cloudflare Workers Builds and Pages",
"purpose": "Allow the existing website Pages connection and the reviewed metaserver-worker Workers Builds connection to receive GitHub events, read source, and publish provider-owned checks and deployments without access to any other Atrinik repository.",
"repositories": [
{
"purpose": "Preserve the existing Cloudflare Pages website connection.",
"repository": "atrinik/website",
"repository_id": 1327107093
},
{
"purpose": "Authorize the serialized Workers Builds topology governed by atrinik/metaserver-worker issues 53-56.",
"repository": "atrinik/metaserver-worker",
"repository_id": 1324297032
}
],
"repository_scope_verification": "organization_owner_ui",
"repository_selection": "selected",
"review_by": "2026-11-13",
"review_cadence_days": 90,
"review_owner": "Atrinik organization owners",
"revocation": "Remove only atrinik/metaserver-worker from installation 152311798 after disconnecting its Cloudflare repository connection; preserve atrinik/website unless its separately governed Pages connection is retired.",
"runbook": "README.md#cloudflare-github-app",
"status_producer": "Cloudflare Workers and Pages GitHub App"
}
],
"github_actions_environments": [
{
"deployment_branch_policy": {
Expand Down
6 changes: 3 additions & 3 deletions tests/check-project-health.sh
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ run_health() {
ATRINIK_SETTINGS_TOKEN=settings-token \
ATRINIK_PROJECT_HEALTH_NOW=2026-08-10T05:00:00Z \
ATRINIK_PROJECT_HEALTH_SYNC_COMMAND="${temporary}/bin/sync-plan" \
ATRINIK_VALIDATION_TODAY=2026-08-11 \
ATRINIK_VALIDATION_TODAY=2026-08-15 \
GITHUB_STEP_SUMMARY="${temporary}/step-summary" \
"${root}/bin/check-project-health" "$@"
}
Expand Down Expand Up @@ -396,7 +396,7 @@ if PATH="${temporary}/bin:${PATH}" \
GH_TOKEN=health-token ATRINIK_SETTINGS_TOKEN='' \
ATRINIK_PROJECT_HEALTH_NOW=2026-08-10T05:00:00Z \
ATRINIK_PROJECT_HEALTH_SYNC_COMMAND="${temporary}/bin/sync-plan" \
ATRINIK_VALIDATION_TODAY=2026-08-11 \
ATRINIK_VALIDATION_TODAY=2026-08-15 \
"${root}/bin/check-project-health" --apply \
>"${temporary}/missing.out" 2>"${temporary}/missing.err"; then
echo "error: health check accepted a missing settings credential" >&2
Expand Down Expand Up @@ -471,7 +471,7 @@ if PATH="${temporary}/bin:${PATH}" \
GH_TOKEN=health-token ATRINIK_SETTINGS_TOKEN='' \
ATRINIK_PROJECT_HEALTH_NOW=2026-08-10T05:00:00Z \
ATRINIK_PROJECT_HEALTH_SYNC_COMMAND="${temporary}/bin/sync-plan" \
ATRINIK_VALIDATION_TODAY=2026-08-11 \
ATRINIK_VALIDATION_TODAY=2026-08-15 \
"${root}/bin/check-project-health" --apply \
>"${temporary}/reopen.out" 2>"${temporary}/reopen.err"; then
echo "error: health check accepted a missing credential during recurrence" >&2
Expand Down
3 changes: 3 additions & 0 deletions tests/publish-maintenance-branch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,9 @@ grep -F \
grep -F \
'PLAN VERIFY /repos/atrinik/classic/immutable-releases => enabled=true,enforced_by_owner=true' \
"${plan_output}" >/dev/null
grep -F \
'MANUAL cloudflare-workers-and-pages installation 152311798 selected repositories: atrinik/website, atrinik/metaserver-worker; publisher does not apply' \
"${plan_output}" >/dev/null
jq -e '. == {enforced_repositories: "none"}' \
"${plan_immutable_state}" >/dev/null
jq -s -e '
Expand Down
Loading