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
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,13 @@
- 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.
stable identities, permissions, events, and selected repository IDs. Planned
integrations may retain null live App/installation IDs only with an explicit
pending owner-UI state, proposed name-only secret/variable slots, and a
documented follow-up. 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
79 changes: 74 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,13 @@ 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.
- External provider GitHub Apps have a distinct value-free inventory. Active
installations bind stable App and installation identities, exact permissions
and events, exact selected repository IDs, provider purpose, review owner and
deadline, revocation procedure, and status producer. Planned integrations
explicitly retain null live IDs, the proposed selected set, and a pending
owner-UI verification state. 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 @@ -418,6 +420,73 @@ 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.

## Deploy-control repository and GitHub App

`atrinik/deploy-control` is a public MIT seed repository with stable repository
ID `1352850539`, default branch `main`, and no release-tag or semantic-release
contract. Its desired repository properties are `service`, `replacement`,
`seed`, and `none`. It remains pull-request governed and requires the two
contexts emitted by its current workflows:

- `Deploy-control validation`, from the Check workflow's `npm run check` job;
and
- `Conventional PR title`, from the Pull Request Policy workflow.

The repository's Dependabot and CodeQL workflows remain enabled, but they are
not merge gates until a stable aggregate check contract exists. The existing
Actions allowlist, read-only default permissions, merge settings, security
baseline, community-health defaults, and Team-compatible rulesets apply. No
Cloudflare resource, package grant, environment, or deployment is part of this
repository registration.

The future control-plane integration is recorded in
`config/manual-settings.json` as the planned GitHub App
`atrinik-deploy-control`. The App and installation do not exist yet: both live
numeric IDs are deliberately `null`, while the control-plane repository is
recorded as `atrinik/deploy-control`, ID `1352850539`. After owner provisioning,
the installation must select exactly one repository, `atrinik/classic`, ID
`1327289971`, and subscribe only to `workflow_run` with these exact read-only
permissions:

- `actions: read`;
- `metadata: read`.

The App is intended to receive successful Classic workflow-run completion for
the reviewed staging-image handoff. It has no authorization for metaserver,
packages, environments, organization-wide selection, source writes, or any
other repository. Its value-free name-only slots are:

- secrets: `DEPLOY_CONTROL_AGENT_ENROLLMENT_SECRET`,
`DEPLOY_CONTROL_GITHUB_APP_PRIVATE_KEY`, and
`DEPLOY_CONTROL_GITHUB_WEBHOOK_SECRET`;
- variables: `CLOUDFLARE_ACCOUNT_ID`,
`CLOUDFLARE_DURABLE_OBJECT_BINDING`, `CLOUDFLARE_WORKER_NAME`,
`DEPLOY_CONTROL_GITHUB_APP_ID`, and
`DEPLOY_CONTROL_GITHUB_INSTALLATION_ID`.

These are future secret and variable names only; no values, private key,
installation token, webhook payload, Cloudflare binding, or account credential
is recorded. Atrinik organization owners own the record and review it every 90
days. `atrinik/deploy-control#3` owns the control-plane follow-up and the
provisioning runbook. `repository_scope_verification` remains
`pending_provisioning` until an owner creates the App, installs it in selected
mode, and reads back the exact Classic-only selection in the GitHub UI.

GitHub App creation and installation remain owner-only manual UI operations on
Team. `bin/verify-manual-settings` verifies the stable repository identities and
reports this planned integration as `PENDING`; it does not pretend that an
absent App is live. `bin/publish` remains plan-only for this record and never
creates the App or changes an installation. The existing Cloudflare Workers and
Pages installation and its `atrinik/metaserver-worker` boundary remain
unchanged.

To revoke the future integration, first stop workflow-run delivery and disable
the control-plane consumer, revoke the App keys and webhook secret, remove only
the named control-plane secret and variable slots, uninstall the App from Classic,
and verify the selected set is empty. Preserve the existing Cloudflare
installation and metaserver access record; do not use this runbook to alter
unrelated Apps or repositories.

## Cross-repository planning

Review and apply the planning layers in their dependency order:
Expand Down
96 changes: 95 additions & 1 deletion bin/validate
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ jq -e \
(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 | type == "array" and length == 2) and
([$external_apps[].status] == ["active", "planned"]) and
($external_apps[0] | keys == [
"app_id",
"app_slug",
Expand All @@ -335,8 +336,10 @@ jq -e \
"review_owner",
"revocation",
"runbook",
"status",
"status_producer"
]) and
($external_apps[0].status == "active") 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
Expand Down Expand Up @@ -392,6 +395,97 @@ jq -e \
(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
($external_apps[1] | keys == [
"app_id",
"app_slug",
"control_plane_repository",
"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",
"secret_names",
"status",
"status_producer",
"variable_names"
]) and
($external_apps[1].status == "planned") and
($external_apps[1].app_id == null) and
($external_apps[1].app_slug == "atrinik-deploy-control") and
($external_apps[1].control_plane_repository == {
default_branch: "main",
license: "MIT",
repository: "atrinik/deploy-control",
repository_id: 1352850539,
visibility: "public"
}) and
($external_apps[1].installation_id == null) and
($external_apps[1].repository_selection == "selected") and
($external_apps[1].repository_scope_verification == "pending_provisioning") and
($external_apps[1].events == ["workflow_run"]) and
($external_apps[1].exceptional_retry == null) and
($external_apps[1].permissions == {
actions: "read",
metadata: "read"
}) and
($external_apps[1].provider == "Cloudflare Workers deploy-control control plane") and
($external_apps[1].status_producer == "Atrinik deploy-control control-plane owner") and
($external_apps[1].repositories == [{
purpose: "Authorize only the Classic workflow-run source for the deploy-control control plane.",
repository: "atrinik/classic",
repository_id: 1327289971
}]) and
($external_apps[1].control_plane_repository.repository | test("^atrinik/[A-Za-z0-9_.-]+$")) and
($external_apps[1].control_plane_repository.repository_id | type == "number" and floor == . and . > 0) and
all(
$external_apps[1].repositories[];
. as $repository_record |
($repository_record.repository | split("/")[1]) as $repository |
($repository_record.repository | test("^atrinik/[A-Za-z0-9_.-]+$")) and
($repository_record.repository_id | type == "number" and floor == . and . > 0) and
($repository_record.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[1].owner | type == "string" and length > 0) and
($external_apps[1].purpose | type == "string" and length > 0) and
($external_apps[1].evidence_location | type == "string" and length > 0) and
($external_apps[1].review_owner | type == "string" and length > 0) and
($external_apps[1].revocation | type == "string" and length > 0) and
($external_apps[1].runbook == "README.md#deploy-control-github-app") and
($external_apps[1].review_cadence_days == 90) and
valid_day($external_apps[1].last_verified_on) and
valid_day($external_apps[1].review_by) and
(day_epoch($external_apps[1].last_verified_on) <= day_epoch($today)) and
(day_epoch($external_apps[1].review_by) > day_epoch($today)) and
((day_epoch($external_apps[1].review_by) - day_epoch($external_apps[1].last_verified_on)) / 86400 == $external_apps[1].review_cadence_days) and
($external_apps[1].secret_names | type == "array" and length > 0 and . == (sort | unique) and all(.[]; type == "string" and test("^[A-Z][A-Z0-9_]*$"))) and
($external_apps[1].variable_names | type == "array" and length > 0 and . == (sort | unique) and all(.[]; type == "string" and test("^[A-Z][A-Z0-9_]*$"))) and
($external_apps[1].secret_names == [
"DEPLOY_CONTROL_AGENT_ENROLLMENT_SECRET",
"DEPLOY_CONTROL_GITHUB_APP_PRIVATE_KEY",
"DEPLOY_CONTROL_GITHUB_WEBHOOK_SECRET"
]) and
($external_apps[1].variable_names == [
"CLOUDFLARE_ACCOUNT_ID",
"CLOUDFLARE_DURABLE_OBJECT_BINDING",
"CLOUDFLARE_WORKER_NAME",
"DEPLOY_CONTROL_GITHUB_APP_ID",
"DEPLOY_CONTROL_GITHUB_INSTALLATION_ID"
]) and
all($external_apps[1].secret_names[]; . as $secret | ($external_apps[1].variable_names | index($secret)) == null) and
($apps | type == "array" and length > 0) and
([$apps[].app_id] | length == (unique | length)) and
([$apps[].installation_id] | length == (unique | length)) and
Expand Down
50 changes: 49 additions & 1 deletion bin/verify-manual-settings
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,59 @@ else
fi

while IFS= read -r app_record; do
app_status=$(jq -r '.status' <<<"${app_record}")
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}")

if [[ ${app_status} == planned ]]; then
if jq -s -e --arg app_slug "${app_slug}" \
'any(.[]; .app_slug == $app_slug)' "${installations_file}" \
>/dev/null; then
echo "error: planned external provider App has a live installation for ${app_slug}" >&2
exit 1
fi
control_plane_repository=$(jq -r '.control_plane_repository.repository' <<<"${app_record}")
control_plane_repository_id=$(jq -r '.control_plane_repository.repository_id' <<<"${app_record}")
if verify_repository_identity "${control_plane_repository}" "${control_plane_repository_id}"; then
:
else
status=$?
exit "${status}"
fi
control_plane_repository_visibility=$(jq -r '.control_plane_repository.visibility' <<<"${app_record}")
control_plane_repository_license=$(jq -r '.control_plane_repository.license' <<<"${app_record}")
if ! jq -e \
--arg visibility "${control_plane_repository_visibility}" \
--arg license "${control_plane_repository_license}" \
'.visibility == $visibility and .license.spdx_id == $license' \
"${repository_data_files["${control_plane_repository}"]}" >/dev/null; then
echo "error: repository visibility or license drift for ${control_plane_repository}" >&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: planned 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 "PENDING ${app_slug} GitHub App installation and credentials; owner UI proof is required for ${control_plane_repository} and selected repository atrinik/classic only"
continue
fi

installation=$(jq -sc \
--argjson app_id "${app_id}" \
--argjson installation_id "${installation_id}" '
Expand Down Expand Up @@ -733,4 +781,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, external provider 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; planned integrations are recorded without live provisioning."
54 changes: 54 additions & 0 deletions config/manual-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,61 @@
"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": "active",
"status_producer": "Cloudflare Workers and Pages GitHub App"
},
{
"app_id": null,
"app_slug": "atrinik-deploy-control",
"control_plane_repository": {
"default_branch": "main",
"license": "MIT",
"repository": "atrinik/deploy-control",
"repository_id": 1352850539,
"visibility": "public"
},
"events": [
"workflow_run"
],
"evidence_location": "atrinik/deploy-control#3",
"exceptional_retry": null,
"installation_id": null,
"last_verified_on": "2026-08-31",
"owner": "Atrinik organization owners",
"permissions": {
"actions": "read",
"metadata": "read"
},
"provider": "Cloudflare Workers deploy-control control plane",
"purpose": "Receive only successful workflow_run events from atrinik/classic for the reviewed deploy-control staging-image handoff; do not authorize source, package, environment, metaserver, or organization-wide access.",
"repositories": [
{
"purpose": "Authorize only the Classic workflow-run source for the deploy-control control plane.",
"repository": "atrinik/classic",
"repository_id": 1327289971
}
],
"repository_scope_verification": "pending_provisioning",
"repository_selection": "selected",
"review_by": "2026-11-29",
"review_cadence_days": 90,
"review_owner": "Atrinik organization owners",
"revocation": "Before retiring the control plane, disable its workflow-run delivery, revoke the App keys and webhook secret, remove only the named control-plane secret/variable slots, and verify the App installation no longer selects atrinik/classic; leave the Cloudflare Workers and Pages installation and metaserver boundary unchanged.",
"runbook": "README.md#deploy-control-github-app",
"secret_names": [
"DEPLOY_CONTROL_AGENT_ENROLLMENT_SECRET",
"DEPLOY_CONTROL_GITHUB_APP_PRIVATE_KEY",
"DEPLOY_CONTROL_GITHUB_WEBHOOK_SECRET"
],
"status": "planned",
"status_producer": "Atrinik deploy-control control-plane owner",
"variable_names": [
"CLOUDFLARE_ACCOUNT_ID",
"CLOUDFLARE_DURABLE_OBJECT_BINDING",
"CLOUDFLARE_WORKER_NAME",
"DEPLOY_CONTROL_GITHUB_APP_ID",
"DEPLOY_CONTROL_GITHUB_INSTALLATION_ID"
]
}
],
"github_actions_environments": [
Expand Down
5 changes: 5 additions & 0 deletions config/repositories.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"client",
"content",
"content-toolkit",
"deploy-control",
"devcontainer",
"editor",
"github-settings",
Expand Down Expand Up @@ -65,6 +66,10 @@
"Content validation",
"Conventional PR title"
],
"deploy-control": [
"Deploy-control validation",
"Conventional PR title"
],
"resources": [
"Asset validation",
"Conventional PR title"
Expand Down
10 changes: 10 additions & 0 deletions config/repository-properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@
"lifecycle": "active",
"release_policy": "semantic-release"
},
"deploy-control": {
"component_role": [
"service"
],
"provider_set": [
"replacement"
],
"lifecycle": "seed",
"release_policy": "none"
},
"devcontainer": {
"component_role": [
"infrastructure"
Expand Down
Loading