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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@
branch policies and reviewer sets, and variable and secret names only. Never
record credential values. Provision them only after the owning workflow is
merged and reviewed, and do not imply that `bin/publish` applies them.
- Record GitHub Pages sites with stable repository identity, exact provider URL,
HTTPS enforcement, owning deployment environment, desired Actions source,
workflow path, and immutable deployment-action marker. Before that marker is
present on the default branch, the verifier permits only the documented
legacy source as pending transition state; once merged, anything except the
Actions source is drift. Switch the live source only after the consuming
workflow is merged and reviewed.
- Treat `config/codeql-advanced-setup.json` as the exhaustive exception
inventory for repositories whose component/path-aware CodeQL workflow cannot
use repository-wide default setup. Keep the advanced security configuration
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ 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
them is merged and reviewed.
GitHub Pages records likewise bind stable repository identity, the provider
URL, HTTPS, the deployment environment, and an immutable marker in the owning
workflow. Keep a pre-merge legacy source only as the verifier's explicit
pending state, then activate the Actions source after that exact workflow lands.

Administrative Actions credentials require a value-free lifecycle entry in
`config/manual-settings.json`. Record stable repository identity, secret scope
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,66 @@ 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.

## Activating the Classic performance Pages site

`atrinik/classic` (repository ID `1327289971`) owns the provider-managed
`https://atrinik.github.io/classic/` site and the `github-pages` deployment
environment. The environment permits only the exact `main` branch and has no
required reviewers, secrets, or variables. The desired Pages build type is
`workflow`; the consuming workflow is
`.github/workflows/daily-client-performance.yml`, bound by the immutable
`actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e` marker.

The live legacy `main`-root source is a bounded pre-activation state only while
that marker is absent from the default-branch workflow.
`bin/verify-manual-settings` reports it as `PENDING`. Once the reviewed Classic
workflow is merged, the same verifier requires the Actions source and fails on
any lingering legacy source. It never changes the source itself.

Before activation, verify the repository, current Pages site, environment, and
the merged workflow without changing live state:

```sh
gh api repos/atrinik/classic --jq '{id,full_name,archived,default_branch}'
gh api repos/atrinik/classic/pages \
--jq '{html_url,build_type,source,public,https_enforced,cname}'
gh api repos/atrinik/classic/environments/github-pages \
--jq '{name,deployment_branch_policy,protection_rule_types:[.protection_rules[].type]}'
gh api --paginate \
'repos/atrinik/classic/environments/github-pages/deployment-branch-policies?per_page=100' \
--jq '[.branch_policies[] | {name,type}]'
gh api repos/atrinik/classic/environments/github-pages/secrets \
--jq '{total_count,names:[.secrets[].name]}'
gh api repos/atrinik/classic/environments/github-pages/variables \
--jq '{total_count,names:[.variables[].name]}'
gh api 'repos/atrinik/classic/contents/.github/workflows/daily-client-performance.yml?ref=main' \
--jq -r .content | base64 --decode | \
grep -F 'actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e'
```

Require the stable repository identity and `main` default branch, exact site
URL, public HTTPS with no custom hostname, the exact custom `main` environment
branch policy, no other protection rule, and empty secret/variable lists. Stop
on any difference. Only after the workflow marker is present on `main`, an
organization owner with repository administration access may switch the exact
site to the Actions source:

```sh
printf '%s\n' '{"build_type":"workflow"}' | gh api --method PUT \
repos/atrinik/classic/pages --input -
```

Immediately run `bin/verify-manual-settings` and require `KEEP atrinik/classic
Pages uses the reviewed Actions workflow source`. Then dispatch `Daily Classic
client performance` from `main` with checkpoint source `final-benchmark-data`,
verify the attempt-qualified evidence/checkpoint artifacts and successful
`github-pages` deployment, and confirm `https://atrinik.github.io/classic/`,
`trend.json`, `v1/state.json`, and `v1/manifest.json`. Subsequent manual and
scheduled runs must use the default `pages` checkpoint. If activation or the
bootstrap run fails, leave or restore the last known-good Pages deployment,
preserve the final `benchmark-data` commit, and do not delete the historical
branch.

## Retiring a manually inventoried Actions environment

Removing an entry from `github_actions_environments` records the desired
Expand Down
64 changes: 63 additions & 1 deletion bin/validate
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ jq -e '
"github_actions_credentials",
"github_actions_environments",
"github_packages_actions_access",
"github_pages_sites",
"members_can_change_repository_visibility",
"members_can_create_teams",
"members_can_delete_or_transfer_repositories",
Expand Down Expand Up @@ -664,10 +665,10 @@ jq -e \
(
.secret_names |
type == "array" and
length > 0 and
length == (unique | length) and
all(.[]; type == "string" and test("^[A-Z][A-Z0-9_]*$"))
) and
((.secret_names | length) > 0 or .environment == "github-pages") and
(
.variable_names as $variables |
all(.secret_names[]; . as $secret | ($variables | index($secret)) == null)
Expand All @@ -684,6 +685,67 @@ jq -e \
)
' "${manual_settings_config}" >/dev/null

jq -e \
--slurpfile repositories "${repositories_config}" '
.github_actions_environments as $environments |
.github_pages_sites as $sites |
($sites | type == "array") and
([$sites[].repository] | length == (unique | length)) and
all(
$sites[];
(keys == [
"activation_marker",
"build_type",
"environment",
"https_enforced",
"repository",
"repository_id",
"site_url",
"workflow_path"
]) and
(.build_type == "workflow") and
(.https_enforced == true) and
(.environment == "github-pages") and
(
.repository |
type == "string" and
test("^atrinik/[A-Za-z0-9_.-]+$")
) and
(
.repository as $full_name |
($full_name | split("/")[1]) as $repository |
($repositories[0].pull_request_gate | index($repository)) != null and
($repositories[0].archive | index($repository)) == null
) and
(.repository_id | type == "number" and . > 0 and floor == .) and
(
.repository as $repository |
($repository | split("/")) as $parts |
.site_url == ("https://" + $parts[0] + ".github.io/" + $parts[1] + "/")
) and
(
.workflow_path |
type == "string" and
test("^\\.github/workflows/[A-Za-z0-9_.-]+\\.ya?ml$")
) and
(
.activation_marker |
type == "string" and
test("^actions/deploy-pages@[0-9a-f]{40}$")
)
) and
all(
$sites[];
. as $site |
any(
$environments[];
.repository == $site.repository and
.repository_id == $site.repository_id and
.environment == $site.environment
)
)
' "${manual_settings_config}" >/dev/null

jq -e '
(keys == [
"issue_fields",
Expand Down
78 changes: 76 additions & 2 deletions bin/verify-manual-settings
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ if (($#)); then
exit 2
fi

for command in gh jq; do
for command in base64 gh jq; do
if ! command -v "${command}" >/dev/null 2>&1; then
echo "error: ${command} is required" >&2
exit 1
Expand Down Expand Up @@ -332,6 +332,80 @@ if ((missing > 0)); then
exit 1
fi

while IFS= read -r pages_record; do
repository=$(jq -r '.repository' <<<"${pages_record}")
repository_owner=${repository%%/*}
repository_id=$(jq -r '.repository_id' <<<"${pages_record}")
site_url=$(jq -r '.site_url' <<<"${pages_record}")
workflow_path=$(jq -r '.workflow_path' <<<"${pages_record}")
activation_marker=$(jq -r '.activation_marker' <<<"${pages_record}")
pages_key=${repository//\//-}

if [[ ${repository_owner} != "${organization}" ]]; then
echo "error: Pages repository owner does not match ${organization}: ${repository}" >&2
exit 1
fi
if verify_repository_identity "${repository}" "${repository_id}"; then
:
else
status=$?
exit "${status}"
fi

pages_file=${temporary}/${pages_key}-pages.json
workflow_file=${temporary}/${pages_key}-pages-workflow.json
if github_api "read ${repository} Pages metadata" \
"repos/${repository}/pages" >"${pages_file}" &&
github_api "read ${repository} Pages workflow from the default branch" \
"repos/${repository}/contents/${workflow_path}?ref=main" >"${workflow_file}"; then
:
else
status=$?
exit "${status}"
fi
if ! jq -e --arg site_url "${site_url}" '
.html_url == $site_url and
.public == true and
.https_enforced == true and
.cname == null and
(.build_type | IN("legacy", "workflow"))
' "${pages_file}" >/dev/null; then
echo "error: Pages identity or HTTPS drift for ${repository}" >&2
exit 1
fi
if ! jq -e '
.type == "file" and
.encoding == "base64" and
(.content | type == "string")
' "${workflow_file}" >/dev/null; then
echo "error: Pages workflow metadata is invalid for ${repository}" >&2
exit 1
fi
if ! jq -r '.content' "${workflow_file}" | base64 --decode \
>"${temporary}/${pages_key}-pages-workflow.yml"; then
echo "error: Pages workflow content is not valid base64 for ${repository}" >&2
exit 1
fi

if grep -Fq -- "${activation_marker}" \
"${temporary}/${pages_key}-pages-workflow.yml"; then
if [[ $(jq -r '.build_type' "${pages_file}") != workflow ]]; then
echo "error: Pages Actions source is not active for merged ${repository} workflow" >&2
exit 1
fi
echo "KEEP ${repository} Pages uses the reviewed Actions workflow source"
else
if ! jq -e '
.build_type == "legacy" and
.source == {branch: "main", path: "/"}
' "${pages_file}" >/dev/null; then
echo "error: pending Pages activation has unexpected live source for ${repository}" >&2
exit 1
fi
echo "PENDING ${repository} Pages remains on the exact legacy main-root source until the reviewed workflow merges"
fi
done < <(jq -c '.github_pages_sites[]' "${manual_settings}")

while IFS= read -r environment_record; do
repository=$(jq -r '.repository' <<<"${environment_record}")
repository_owner=${repository%%/*}
Expand Down Expand Up @@ -605,4 +679,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, environment, and organization pin metadata is present."
echo "Manual settings live credential, GitHub App, Pages, environment, and organization pin metadata is present."
32 changes: 31 additions & 1 deletion config/manual-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,36 @@
"DISCORD_APPLICATION_ID"
],
"variable_names": []
},
{
"deployment_branch_policy": {
"custom_branch_policies": true,
"patterns": [
{
"name": "main",
"type": "branch"
}
],
"protected_branches": false
},
"environment": "github-pages",
"repository": "atrinik/classic",
"repository_id": 1327289971,
"required_reviewers": [],
"secret_names": [],
"variable_names": []
}
],
"github_pages_sites": [
{
"activation_marker": "actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e",
"build_type": "workflow",
"environment": "github-pages",
"https_enforced": true,
"repository": "atrinik/classic",
"repository_id": 1327289971,
"site_url": "https://atrinik.github.io/classic/",
"workflow_path": ".github/workflows/daily-client-performance.yml"
}
],
"github_actions_apps": [
Expand Down Expand Up @@ -131,5 +161,5 @@
"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 the exact selected-repository set for an organization GitHub App installation to an administrative token, 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, GitHub App repository selection, Codecov GitHub App repository access, any listed GitHub Actions environments, and GitHub Packages Actions access in the corresponding organization, repository, and package settings UI."
"reason": "The public GitHub API does not expose supported update parameters for every setting in this file, including organization pins and the exact selected-repository set for an organization GitHub App installation to an administrative token, 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, GitHub App repository selection, Codecov GitHub App repository access, any listed GitHub Pages sites and GitHub Actions environments, and GitHub Packages Actions access in the corresponding organization, repository, and package settings UI."
}
Loading