This repository is the source of truth for Atrinik organization settings and
repository rulesets. bin/publish uses the GitHub REST API to apply the policy
to every targeted repository in one run.
The same repository owns Atrinik's cross-repository planning system: organization issue types and fields, the public Atrinik work Project and shared views, scheduled item synchronization, repository custom properties, and the generated organization community-health repository.
The organization description and canonical website are also governed here.
The public profile README is generated from community-health/profile/README.md,
while the six public organization pins remain explicit, read-only-verified
manual state because GitHub exposes no supported pin mutation API.
Atrinik uses GitHub Team. The publisher detects the organization plan and uses organization-level rulesets on Team or Enterprise. On GitHub Free it installs equivalent repository-level rulesets instead. Archived repositories are read-only and are skipped on later runs.
- Every active repository protects its default branch from deletion and non-fast-forward pushes without a bypass. A separate linear-history rule permits organization owners to bypass only through a pull request, preserving an audit trail for GitHub's security-advisory merge lifecycle.
- Every active repository uses
mainas its default branch. - Maintained repositories require changes through pull requests. Organization owners may bypass this gate only through a pull request, including the temporary private pull request GitHub associates with a security advisory.
- Repositories with reliable CI require checks from the GitHub Actions app.
Replacement repositories require their proven stable aggregate validation
job plus
Conventional PR title; component/platform jobs remain diagnostic and are not independent merge gates. Theclassicmonorepo requires its stable aggregateClassic validationandCodeQL validationjobs plusConventional PR title. - Declared maintenance branches reject deletion and non-fast-forward updates, require linear history and pull requests, and require only checks already emitted for that branch.
- Release tags in release-producing repositories cannot be moved or deleted.
- Published releases in the exhaustive
config/immutable-releases.jsoninventory are owner-enforced immutable releases. The inventory initially selects onlyatrinik/classicby its stable repository ID. This organization policy is independent of the release-tag ruleset and applies in both publisher policy scopes. - Active repositories allow squash merges only. The squash commit uses the pull-request title and description, and merged head branches are deleted.
- Every repository uses an enforced security baseline: dependency graph,
Dependabot alerts and security updates, secret scanning, push protection,
validity checks, and private vulnerability reporting remain enabled in both
security configurations. Ordinary repositories use CodeQL default setup from
config/code-security.json. Repositories inconfig/codeql-advanced-setup.jsoninstead receive the otherwise-identicalconfig/code-security-advanced.jsonconfiguration, whose supportedcode_scanning_options.allow_advancedsetting permits their governed component-aware workflow. The publisher explicitly disables default setup for those repositories and configures it everywhere else. The ordinary baseline remains the default for new repositories. - Organization members cannot create repositories. Deletion, transfer, and visibility changes are recorded as required UI settings because GitHub does not expose them through the public API.
- Only organization owners may create teams or install GitHub Apps. Members may
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. 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 reviewer set, and variable and secret names; credential values remain manual and are never committed. - Administrative Actions credentials have value-free lifecycle contracts in
config/manual-settings.json: stable repository identity, secret placement, PAT type and scopes, consumers, accountable ownership, verification date, rotate-by deadline, cadence, and runbook. Validation rejects expired records and any value-like field.bin/verify-manual-settingschecks credential secret-name presence plus inventoried environment identity, deployment branches, reviewer rules, and exact secret and variable name sets without reading or proving any value. - Repository-scoped GitHub Apps used by Actions have separate value-free
lifecycle contracts in
config/manual-settings.json. The inventory binds the stable App and installation IDs, exact permissions and event set, 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. - GitHub Actions defaults to read-only, cannot approve pull requests, and may use only Atrinik, GitHub, Codecov coverage, and explicitly allowed Docker actions.
- Historical repositories listed in
config/repositories.jsonare archived. The five former standalone classic component repositories are already archived read-only after their history, active work, issues, and release metadata were preserved for theatrinik/classictransition. config/planning.jsondefines one public organization Project, its workflow statuses and shared views, enabled issue types, and public organization issue fields. New issues enter Inbox, pull requests enter Review, and the pre-existing portfolio enters Backlog. Human status changes on open work are preserved; closed work converges to Done.- Organization-wide issue forms omit empty optional metadata and the top-level
typekey so they pass GitHub's live validator. The Bug and Feature forms applybugandenhancement; the synchronizer maps those labels to Bug and Feature, while the unlabeled Task form uses the Task fallback. Parent issues still become Initiative,new featureremains a recognized Feature alias, and existing issue types are never overwritten. config/repository-properties.jsondeclares a complete taxonomy for active and archived repositories: component role, provider set, lifecycle, and release policy.community-health/is the source for organization-wide issue forms, pull-request guidance, contribution guidance, conduct policy, and security reporting, including the public organization profile.bin/publish-community-healthgeneratesatrinik/.githubdirectly from this released source; the generated repository is not edited or released independently.config/organization.jsonowns the game-first organization description and preserveshttps://atrinik.orgas the canonical website. The publisher compares every owned field and patches only when live state differs.config/manual-settings.jsonowns the exact public repository pin order:classic,atrinik,website,content,protocol, andplaytester, with stable repository IDs.bin/verify-manual-settingsreads the ordered pins through GraphQL and fails closed on count, identity, visibility, archival, or order drift; it never mutates them.
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 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.
Requirements: git, gh, jq, and an authenticated organization-owner
account.
gh auth refresh -h github.com -s admin:org,project,repo
bin/publish
bin/publish --applyThe default invocation prints the operations without changing GitHub. The
--apply form is idempotent: existing Atrinik rulesets, immutable-release
selection, and the security configuration are converged to the declared
policy. On Team, the publisher creates organization rulesets before removing
their repository-level equivalents so protection is never absent during
migration. Before changing immutable releases, it snapshots the exact current
organization mode and selected repository IDs. A failed update or verification
automatically restores and verifies that snapshot.
The organization security phase compares GitHub's normalized configuration, default, and repository-association state before writing. It updates only drifted configurations and attaches only repositories that are not already attached or enforced on the intended configuration. This is a correctness boundary as well as an optimization: GitHub handles attachment as an asynchronous enablement event, including when an already-associated repository is submitted again, so a no-op publish must not start a replacement event.
To force the per-repository implementation before or after a downgrade to GitHub Free, use the retained fallback publisher:
bin/publish-repositories
bin/publish-repositories --applyThis applies merge settings, security features, the inventory-specific CodeQL
setup state, and all rulesets repository by repository. It removes the managed
organization rulesets only after their repository equivalents exist. The
fallback preserves every non-CodeQL security feature for advanced-setup
repositories while converging their default setup to not-configured.
The manual Publish settings workflow provides the same operation in GitHub
Actions. It requires an ATRINIK_SETTINGS_TOKEN repository secret with
organization administration and repository administration access. Never store
that token in this repository.
ATRINIK_SETTINGS_TOKEN is a repository-scoped Actions secret on
atrinik/github-settings. Its value-free lifecycle record lives in
config/manual-settings.json; the current record's rotate_by date is an
enforced maximum, not a suggestion. The credential value belongs only in an
approved private credential manager and GitHub Actions.
Provision or rotate it as an Atrinik organization owner:
-
Before the current
rotate_bydate, prepare a pull request that updateslast_verified_onand a newrotate_byno later than the declared rotation cadence. Keep ownership, consumers, and required scopes accurate. -
Create a classic PAT with exactly
admin:org,project, andrepo, an expiration no later than the recorded deadline, and organization access if GitHub requires separate authorization. -
Store it without echoing the value:
gh secret set ATRINIK_SETTINGS_TOKEN --repo atrinik/github-settings -
Verify repository identity and secret-name presence, then dispatch only the synchronization workflow:
bin/verify-manual-settings gh workflow run sync-project.yml --repo atrinik/github-settings gh run list --repo atrinik/github-settings --workflow sync-project.yml --limit 2 bin/sync-project
-
Require the manual run to succeed, the read-only plan to report
Items to add: 0andTotal mutations: 0, and a later scheduled run plus Project health check to succeed before revoking the previous credential.
If verification fails, do not run a publisher or expose the token while
debugging. Restore the prior unexpired credential from the approved private
credential manager, or issue a replacement; then repeat the complete check.
For suspected disclosure, revoke the affected PAT immediately, provision a new
one, review Actions logs and organization audit events, and verify every
consumer workflow. bin/verify-manual-settings proves only that GitHub exposes
the declared secret name at the stable repository—it cannot inspect the value,
expiry, or effective scopes. The synchronization preflight is the scope and
functionality proof: apply mode verifies the classic PAT's advertised scopes,
Project update capability, and write access for every repository needing an
issue-type change before the first mutation.
atrinik-classic-dependency-updater is an organization-owned GitHub App used
only by the planned atrinik/classic consumer workflow
.github/workflows/update-content.yml. App ID 4564008 and installation ID
153045686 are public stable identifiers. The installation selects only
atrinik/classic, repository ID 1327289971, and has exactly these repository
permissions:
- metadata: read;
- contents: write;
- pull requests: write.
The App subscribes to no webhook events and has no Actions, checks,
deployments, environments, issues, packages, secrets, organization
administration, or ruleset-bypass permission. Organization Actions defaults
remain read-only and Actions cannot approve pull-request reviews. The App ID
is stored as the repository Actions variable DEPENDENCY_UPDATE_APP_ID; its
private key is stored as the repository Actions secret
DEPENDENCY_UPDATE_APP_PRIVATE_KEY. Never commit, print, cache, artifact, or
place either the private key or an installation token in a pull-request body,
step output, command line, fixture, or log.
The public REST response available to the settings administration credential
proves the organization owner, stable App and installation IDs, selected-mode
installation, exact effective permissions, empty event set, and unsuspended
state. GitHub does not expose the installation's exact selected-repository list
to that credential. An organization owner must therefore also inspect
https://github.com/organizations/atrinik/settings/installations/153045686
and confirm that the only selected repository is classic whenever the record
is created, rotated, or reviewed. repository_scope_verification records this
manual boundary; bin/verify-manual-settings additionally verifies Classic's
stable repository identity and both credential metadata names without reading
or logging their values.
GitHub App installation access tokens expire after one hour. The consumer must
mint a token only in the branch/PR mutation job, scope it to classic, avoid
exporting it beyond the necessary steps, and allow the pinned token action to
revoke it when the job finishes. The automation boundary is the single stable
branch automation/content-update and one open pull request from that branch.
Use one non-cancelling concurrency group, classic-content-update; a queued
run re-evaluates current state after the prior run completes. A run updates the
existing App-owned branch and PR only when their base, head owner, author, and
changed-path contract are intact. Zero matching PRs permits creation, one
permits refresh, and multiple matches or unexpected commits fail closed.
GitHub has no branch-only App permission: contents: write, which is required
to update the automation branch, also authorizes Git-reference and release API
operations. GitHub likewise has no separate create-or-update-PR permission that
excludes review APIs: pull requests: write covers both. The App credential
therefore cannot by itself prove that tag, release, or review calls are
technically impossible. Because the App owns its generated pull request, it
cannot provide the distinct human approval required by the ordinary gate, and
it has no bypass. The consumer must never review, approve, merge, tag, publish,
dispatch a release, write the default branch, or change repository settings.
Its reviewed code, exact branch and changed-path checks, ordinary pull-request
gate, protected release-tag rules, workflow contract tests, and audit trail
jointly enforce that operational boundary. Do not describe the credential as
release- or review-incapable; treat any use outside the exact automation branch
and pull-request operations as an incident.
Provision or rotate the key as an Atrinik organization owner:
- Prepare a reviewed change that advances
last_verified_onandrotate_byby no more than the recorded 90-day cadence. Reconfirm the App owner, installation ID, Classic-only selection, empty event set, and exact three permissions in the GitHub UI before changing credentials. - Generate a new App private key while the prior key remains valid. Store the
complete new PEM in the approved private credential manager and replace the
DEPENDENCY_UPDATE_APP_PRIVATE_KEYrepository secret without echoing it. KeepDEPENDENCY_UPDATE_APP_IDequal to the public numeric App ID. - Run
bin/verify-manual-settings. This proves metadata and name presence, not the key value. After the Classic consumer exists, dispatch onlyupdate-content.ymland require one App-authored disposable pull request to receive ordinaryClassic validation,CodeQL validation, andConventional PR titlechecks. - Confirm the App-authored pull request cannot satisfy its own human approval or merge gate, close the disposable pull request, remove its branch, and only then delete the previous private key. A later scheduled run must also succeed before rotation is considered complete.
For suspected disclosure or misuse, disable the Classic updater workflow,
suspend installation 153045686, delete the repository secret, and revoke the
affected private key immediately. Review Actions logs, App and organization
audit events, open pull requests, branches, tags, and releases; remove only
verified App-owned disposable state. Rotate to a new key and repeat the full
proof before re-enabling. To revoke permanently, uninstall the App from
Classic, delete its keys plus the repository secret and variable, and reconcile
config/manual-settings.json through a reviewed rollback. Do not leave a
credential inventory entry claiming a revoked installation is active.
Read non-secret live metadata and run the complete verifier with:
gh api orgs/atrinik/installations \
--jq '[.installations[] | {id, app_id, app_slug, repository_selection, permissions, events, suspended_at}]'
gh api repos/atrinik/classic/actions/secrets \
--jq '{total_count, names: [.secrets[].name]}'
gh api repos/atrinik/classic/actions/variables \
--jq '{total_count, names: [.variables[].name]}'
bin/verify-manual-settingsIf 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.
Review and apply the planning layers in their dependency order:
bin/publish-planning
bin/publish-planning --apply
bin/sync-project
bin/sync-project --apply
bin/publish-community-health
bin/publish-community-health --apply
bin/publish-repository-properties
bin/publish-repository-properties --applybin/publish-planning creates or updates organization issue metadata, the
single public Project, its Status options, linked organization fields, and the
six shared views. It reuses the new Project's default View 1 as Triage
and preserves any additional manually created shared views.
bin/sync-project is the Team-compatible alternative to Project auto-add
workflows, which are limited to five repository-specific workflows and do not
backfill existing work. It discovers every non-archived repository, adds all
open issues and pull requests, sets intake status only when missing or when
work is reopened, infers only missing issue types, and moves tracked closed
items to Done. The scheduled Synchronize project workflow requests runs
twice per hour, but GitHub schedules are best-effort and can be delayed or
dropped. The first apply intentionally performs a large one-time backfill;
later runs are incremental and idempotent.
GitHub caps each GraphQL search at 1,000 results. The synchronizer reads the reported count, requires it to remain stable across pages, and reconciles it with the exact number of unique returned node IDs. It fails before mutation or a success summary if either search is inconsistent or exceeds that window; it must be changed to enumerate or partition the inventory before a larger organization can be safely synchronized.
config/planning-health.json sets a 90-minute maximum freshness age and alerts
after two consecutive failed synchronization runs. The separate
Project synchronization health workflow runs on its own schedule, after each
completed synchronization, and on manual dispatch. It uses the ordinary
least-privilege GITHUB_TOKEN to read Actions and maintain one deduplicated
health incident, so a missing ATRINIK_SETTINGS_TOKEN cannot suppress the
alert. It recognizes only marker-bearing incidents authored by
github-actions[bot], ignores foreign copies of the public marker, and records
a hidden timestamp for each new or reopened outage episode. The administration
PAT is used only for the optional read-only convergence plan.
Inspect health without changing GitHub:
bin/check-project-health
gh run list --repo atrinik/github-settings \
--workflow check-project-health.yml --limit 5The health summary reports the last run, last successful run and revision, age, consecutive failures, and pending Project mutations. A managed incident opens or updates after the threshold is crossed. It closes only after a newer successful synchronization in the current outage episode and a zero-mutation plan. Do not remove its hidden marker or episode timestamp, create one incident per missed window, or use the administration PAT for alert publication. The monitoring owner is the Atrinik organization-owner role. If organization-wide GitHub scheduling itself becomes an unacceptable single point of failure, provision an external dispatcher/monitor under a separately reviewed owner and lifecycle contract; the repository workflow does not pretend to provide that external guarantee.
Retire or replace this monitor only through a reviewed governance change. Establish and prove the replacement signal first, remove the health workflow's triggers, then remove its badge, configuration, validator rules, and tests in the same change. Close any marker-owned incident with a final note pointing to the replacement; preserve the issue and Actions history rather than deleting the operational record.
bin/publish-community-health creates the public atrinik/.github deployment
repository if needed and converges every file listed in
config/community-health.json. Local community-health files in a component
repository continue to take precedence over these defaults.
Review and deploy organization identity only after the governing pull request is merged. First inspect both complete plans:
bin/publish
bin/publish-community-healthWith separate live-mutation authorization, bin/publish --apply converges the
description while preserving the canonical website and the other owned
organization defaults. bin/publish-community-health --apply publishes
community-health/profile/README.md as .github/profile/README.md; never edit
the generated repository directly.
GitHub does not provide a supported public API for organization pins. An organization owner must open https://github.com/atrinik, choose View as: Public, select Customize pins in the Pinned section (or pin repositories when the section is empty), select exactly six repositories, arrange them in this order, and select Save pins:
classicatrinikwebsitecontentprotocolplaytester
After saving, verify the exact live order and stable identities without mutation:
bin/verify-manual-settingsDo not use browser automation or an undocumented endpoint to apply pins. A change is complete only after the public organization view renders the profile README and links correctly and the verifier reports the governed pin order.
bin/publish-repository-properties creates the organization property schema
and assigns the complete desired value set to every repository. It runs after
the generated .github repository exists so the inventory and live repository
set agree.
The manual Publish planning workflow performs those four apply steps in the
same order. Both planning workflows use ATRINIK_SETTINGS_TOKEN; in addition
to the existing organization and repository administration access, that token
must have the classic PAT project scope. The current GitHub Actions
GITHUB_TOKEN cannot administer an organization Project.
GitHub exposes shared Project views through GraphQL but does not expose a
public API for a person's issue-dashboard saved views. The desired personal
views are therefore recorded in personal_saved_views in
config/planning.json. Each user creates them once at
https://github.com/issues using these queries:
- My issues:
org:atrinik is:open is:issue assignee:@me - Review requests:
org:atrinik is:open is:pr review-requested:@me - Mentions:
org:atrinik is:open mentions:@me - Unassigned issues:
org:atrinik is:open is:issue no:assignee
Create the repository's main branch before applying policy. Default-branch
integrity, merge settings, and security policy are discovered dynamically and
apply to every non-archived organization repository. The Team
security configuration is also the default for newly created repositories.
GitHub's temporary private security-advisory workspaces are the sole discovery
exception: the publisher recognizes their strict <governed-repository>-ghsa-
name and disabled collaboration-feature signature and leaves them under
GitHub's advisory lifecycle. A private repository that merely resembles that
name, or whose base repository is not governed, remains in policy scope.
Because integrations and status checks cannot access these workspaces, the
base repository's linear-history, pull-request, and required-CI rules allow
organization-owner bypass in pull-request-only mode. Deletion and
non-fast-forward protections remain non-bypassable.
GitHub does not classify the final advisory-level merge as a pull-request
action. During a specifically authorized Classic advisory merge window,
Classic is split from the shared integrity, linear-history, and pull-request
rules and organization owners receive always bypass for those Classic rules
and Classic required CI. GitHub requires every rule reported for the target
branch to be bypassable before enabling the advisory-level merge, including
deletion and non-fast-forward. The window is recorded exhaustively in
config/advisory-merge-windows.json, and must be rolled back to an empty
inventory and pull-request-only bypass immediately after the advisory merge.
Add repositories to the appropriate arrays in config/repositories.json when
they also need a pull-request gate, required CI, immutable release tags, or
archival.
Immutable releases require a stricter release contract than an immutable tag
alone. Add a repository to config/immutable-releases.json only after its
release workflow creates a draft, uploads and verifies every intended asset,
and publishes the draft as its final step. Record both the repository name and
the ID reported by GitHub. Validation requires every selected repository to be
active, pull-request governed, and protected by the release-tag ruleset; the
publisher fails closed if a live repository ID differs from the inventory.
Do not add a repository to config/codeql-advanced-setup.json merely because
default setup needs tuning. The inventory is reserved for a reviewed advanced
CodeQL workflow that needs repository-specific build, component, or path
coverage. Both security configurations must remain identical except for their
name, description, and code_scanning_options.allow_advanced value.
Moving a repository to archive is an executable policy change: the next
bin/publish --apply makes it read-only. Before applying an archival change,
confirm that remaining pull requests and branches have been preserved at their
new location. For atrinik/classic, also complete any intentional tag rebuild
before applying immutable release-tag policy.
Add a unique repository/branch entry to maintenance_branches in
config/repositories.json. Branch names are repository-relative, so 1.x
becomes the exact ruleset ref refs/heads/1.x. Every entry receives deletion,
non-fast-forward, linear-history, and pull-request rules. Its required_ci
array may contain only stable checks already declared for that repository and
emitted by workflows on the maintenance branch.
Retired maintenance lines move out of that active array and into
config/retired-maintenance-branches.json. The immutable record binds the
stable repository and ruleset IDs, exact final branch commit, final rollback
tag/commit and asset names, and every default-branch/tag ruleset that must stay
active. The targeted publisher refuses missing, ambiguous, or drifted live
state and emits only the exact maintenance-ruleset deletion:
bin/validate
bin/publish --retire-maintenance content/1.xFor the completed content cutover, the final rollback anchor is
v1.8.19@566bd25f78b80b08d5f75f4b02017ab2429204db; the preserved retirement
branch tip is 080a9ea41741e4e67adc7b09b3ccb51475d93d3a. The source archive,
Classic runtime archive, and SHA256SUMS must remain accessible and checksum
clean. content@main is the sole authored and released source, and the Classic
updater remains justified only for proposing locks to verified main-built
Classic artifacts.
After the desired-state pull request merges, an organization owner may explicitly authorize this one policy mutation:
bin/publish --apply --retire-maintenance content/1.xThe targeted apply verifies that only ruleset 20571870 is absent and that
the recorded main and immutable-tag protections remain active. It does not
delete the branch. Before deleting refs/heads/1.x, repeat the live repository,
ruleset, open-PR, consumer, tag, release, asset, checksum, and reachability
preflight and obtain a second explicit organization-owner authorization for
that exact Git-reference deletion. After deletion, verify the branch is absent
and every preserved tag/release asset and commit remains reachable. Recreating
1.x is a new organization-owner recovery decision, not automatic rollback.
On GitHub Team the publisher creates one organization ruleset per maintenance branch. The repository-policy fallback creates the equivalent repository ruleset. Both paths remove stale managed maintenance rulesets, and migration creates the destination protection before deleting the previous scope.
Validate semantic configuration and both publisher scopes when adding or changing active maintenance protection. For a recorded retirement, additionally review the targeted plan above before any live authorization:
bin/validate
for test in tests/*.sh; do "$test"; done
bin/publishThe inventory currently contains only classic. GitHub's
security configuration API
represents its exception as default setup enabled with advanced setup
allowed. The publisher attaches that configuration only to the inventory,
then uses the
repository default-setup API
to set it explicitly to not-configured. All other active repositories are
attached to the enforced ordinary baseline and explicitly set to configured.
Use this order for the first Classic transition so default and advanced scans do not compete and a failed handoff cannot become a silent permanent gap:
- Open the Classic advanced-workflow pull request and let its non-CodeQL validation complete. Keep the pull request open; do not merge it while repository-wide default setup still controls CodeQL uploads.
- Merge this governance policy and, while that Classic pull request is ready,
review
bin/publishin plan mode and run the explicitly authorizedbin/publish --apply. The plan must attach Classic to the advanced-allowed configuration before setting its default setup tonot-configured, while attaching every other active repository to the ordinary baseline and setting it toconfigured. - Immediately rerun the Classic pull request's advanced CodeQL workflow. Confirm every intended component/path partition completes and GitHub accepts each CodeQL upload. The short interval after default setup is disabled is an observed transition window, not a state that may be left unattended.
- Merge the Classic workflow only after its advanced analysis is green, then
confirm a successful advanced analysis on
mainand that no repository-wide default CodeQL workflow is scheduled. - Only after the Classic workflow is merged and its
CodeQL validationaggregate exists onmain, merge and apply the governance change that adds that context to Classic's required CI. For the initial release-pipeline rollout, activate this gate after Classic pull request #20 and before the separate phase-two change that enables automatic releases.
If the advanced workflow cannot upload and pass promptly, do not leave Classic without scheduled analysis. Remove Classic from the inventory in a reviewed governance change (or revert the exception), run and review the publisher, and confirm a successful default-setup analysis before rescheduling the migration. Never delete the advanced workflow first and assume a later policy run will repair coverage.
The initial desired state is the organization-level selected mode with only
repository ID 1327289971 (atrinik/classic). Do not apply it until the
Classic draft-first release workflow is merged and its build, packaging,
retry/recovery, and publication paths have passed on main. A published
release must be complete because its tag and assets can no longer be replaced.
Use this rollout order:
-
Merge and verify the Classic release workflow. Confirm it creates or recovers a draft, attaches the complete release payload, and publishes only after all validation succeeds.
-
Merge this governance policy. Run
bin/publishand inspect the exactPUT /orgs/atrinik/settings/immutable-releasespayload. It must select only[1327289971]. -
With explicit deployment authorization, run
bin/publish --apply. The publisher verifies the organization selection and the repository'senabled=true,enforced_by_owner=truestate before continuing. -
Confirm the live state independently:
gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ orgs/atrinik/settings/immutable-releases gh api --paginate -H 'X-GitHub-Api-Version: 2026-03-10' \ 'orgs/atrinik/settings/immutable-releases/repositories?per_page=100' \ --jq '.repositories[] | [.name, .id]' gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ repos/atrinik/classic/immutable-releases
If the apply or its verification fails, the publisher automatically sends the pre-change mode and complete selected-ID set back to GitHub and verifies the rollback. Preserve its full error output and stop; do not retry with an ad hoc payload. For a later intentional rollback, review and merge a desired-state change first, then run the normal plan and explicitly authorized apply flow so the previous selection is not guessed or partially overwritten.
The discord-release environment is manual desired state for
atrinik/classic (repository ID 1327289971). It permits deployments only
from the selected main branch for checked recovery and immutable v* tags
for normal releases, has no required reviewers or environment variables, and
contains one value-free secret-name contract:
DISCORD_APPLICATION_ID. The Application ID is public package configuration,
but its value remains outside Git so release packaging can be enabled only
through the reviewed environment boundary. bin/publish reports this manual
state but does not create, update, or delete it.
Provision it only after the consuming Classic workflow has been merged and reviewed. From an organization-owner account with repository administration access, first verify the stable repository identity and default branch:
gh api repos/atrinik/classic \
--jq '{id, full_name, archived, default_branch}'Require exactly id: 1327289971, full_name: atrinik/classic,
archived: false, and default_branch: main. Before any mutation, check
whether the environment already exists:
gh api --include -H 'X-GitHub-Api-Version: 2026-03-10' \
repos/atrinik/classic/environments/discord-releaseProceed with the creation commands below only when GitHub returns an explicit
404 Not Found. Any authentication, authorization, transport, or other error
is a stop condition. If GitHub returns environment metadata, do not run the
PUT, POST, or gh secret set commands: inspect the complete live state
with the read-only verification commands below and reconcile any difference
through a separately reviewed change. The secret command is an upsert, so an
existing same-name secret must not be replaced without separately authorized
rotation.
For a confirmed absent environment, create it with no reviewers, no wait timer, and an exact custom-branch policy, then provide the initial secret:
printf '%s\n' '{
"wait_timer": 0,
"prevent_self_review": false,
"reviewers": [],
"deployment_branch_policy": {
"protected_branches": false,
"custom_branch_policies": true
}
}' | gh api --method PUT \
-H 'X-GitHub-Api-Version: 2026-03-10' \
repos/atrinik/classic/environments/discord-release \
--input - &&
printf '%s\n' '{"name":"main","type":"branch"}' | \
gh api --method POST \
-H 'X-GitHub-Api-Version: 2026-03-10' \
repos/atrinik/classic/environments/discord-release/deployment-branch-policies \
--input - &&
printf '%s\n' '{"name":"v*","type":"tag"}' | \
gh api --method POST \
-H 'X-GitHub-Api-Version: 2026-03-10' \
repos/atrinik/classic/environments/discord-release/deployment-branch-policies \
--input - &&
gh secret set DISCORD_APPLICATION_ID \
--env discord-release --repo atrinik/classicEnter the Application ID only at the final prompt; never place it in a shell argument, environment variable, file in this repository, log, fixture, or pull request. If either policy creation reports that its exact policy already exists, stop and verify the complete live state instead of adding a duplicate.
Verify the exact environment contract without reading the secret value:
gh api -H 'X-GitHub-Api-Version: 2026-03-10' \
repos/atrinik/classic/environments/discord-release \
--jq '{name, deployment_branch_policy, protection_rule_types: [.protection_rules[].type]}'
gh api --paginate -H 'X-GitHub-Api-Version: 2026-03-10' \
'repos/atrinik/classic/environments/discord-release/deployment-branch-policies?per_page=100' \
--jq '[.branch_policies[] | {name, type}]'
gh api -H 'X-GitHub-Api-Version: 2026-03-10' \
repos/atrinik/classic/environments/discord-release/secrets \
--jq '{total_count, names: [.secrets[].name]}'
gh api -H 'X-GitHub-Api-Version: 2026-03-10' \
repos/atrinik/classic/environments/discord-release/variables \
--jq '{total_count, names: [.variables[].name]}'
bin/verify-manual-settingsRequire the environment name to be discord-release; the deployment policy
to have custom branch policies enabled and protected-branch mode disabled;
protection_rule_types to equal ["branch_policy"]; the deployment
branch-policy list to equal
[{"name":"main","type":"branch"},{"name":"v*","type":"tag"}] (in
either API order); the secret result to equal
{"total_count":1,"names":["DISCORD_APPLICATION_ID"]}; and the variable
result to equal {"total_count":0,"names":[]}. Finally, run the Classic
release rehearsal and confirm its Discord configuration job is skipped and no
production Application ID appears in rehearsal artifacts. Then run an
explicitly authorized production package workflow for a reviewed release tag
and require its environment-bound configuration job and Windows package
verification to succeed. If any identity, policy, reviewer, name, or count
differs, stop: do not delete or overwrite unknown live settings. Reconcile the
reviewed desired-state contract first, then repeat the complete verification.
Removing an entry from github_actions_environments records the desired
absence but does not delete the live environment or revoke credentials held by
an external provider. Complete every retirement in this order:
- Merge the reviewed desired-state removal only after the consuming workflow has been removed from its default branch.
- Record the repository's other environment names, delete only the exact
retired environment through Settings → Environments, confirm its API
lookup returns
404, and confirm the other environments are unchanged. - Remove any exact external DNS, domain, or deployment resource owned solely by that environment, preserving unrelated production resources.
- Revoke the dedicated external-provider token separately and clear retained local copies. Deleting a GitHub environment removes its stored secret but does not revoke the underlying credential.
bin/publish --apply does not provision or delete these manual environments.
Record the exact environment and external-token names in the retirement pull
request handoff without recording their values.
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.
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.
Verify the live contract read-only:
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}'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.
Then use a new empty Docker configuration to prove anonymous access to every consumer-pinned digest:
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'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.