Skip to content
Draft
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 @@ -116,6 +116,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.
- Planned crates.io Trusted Publishing belongs in the value-free manual
inventory with exact repository, workflow, environment and immutable action
identities. Keep its desired maintainer review policy explicit; pending source
releases do not invent release pins or imply live environment/registration.
Verify repository/reviewer identities and maintainer permission read-only.
Activation, environment provisioning, external registration and dispatch each
require their owning authorization; publishers never perform these steps.
- 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
Expand Down
71 changes: 69 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ read-only and are skipped on later runs.
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.
use only Atrinik, GitHub, Codecov coverage, explicitly allowed Docker
actions, and the exact reviewed crates.io authentication action commit.
- Historical repositories listed in `config/repositories.json` are archived.
The five former standalone classic component repositories are already
archived read-only after their history, active work, issues, and release
Expand Down Expand Up @@ -1075,3 +1075,70 @@ 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.

## Protocol crate Trusted Publishing

`config/manual-settings.json` records a **pending**, value-free crates.io
Trusted Publishing contract for `atrinik-protocol`. It binds
`atrinik/protocol` (repository ID `1327106950`), the workflow filename
`publish-crate.yml`, and the `crates-io-release` environment. The workflow is
manual (`workflow_dispatch`) on `refs/heads/main` only. The desired environment
allows only the selected `main` branch, requires reviewer `zoeyrose` (User ID
`3865595`), permits that maintainer to review their own manually initiated run,
and disallows administrator bypass. It has no secret or variable slots.
The owner identity and current repository admin permission were verified on
2026-10-04; the verifier checks both identity and maintainer permission again.
This record does not claim that the environment or Trusted Publisher exists.
The existing `crates-io-bootstrap` environment and published 0.1 crate are
separate historical state and are not changed by this contract.

The Actions allowlist adds only
`rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18`,
verified against upstream `v1.0.5`. The
[pinned official action](https://github.com/rust-lang/crates-io-auth-action/blob/c6f97d42243bad5fab37ca0427f495c86d5b1a18/README.md)
exchanges GitHub OIDC for a temporary crates.io token and revokes it in its
post step. Only the owning workflow's upload job may request `id-token: write`,
with `contents: read`; only the Cargo upload step receives the returned token.
Do not store a crates.io token in GitHub secrets or credential files.
The [GitHub environment controls](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments)
allow this public repository's manual reviewer, branch restriction, and bypass
policy on GitHub Team. The [crates.io documentation](https://crates.io/docs/trusted-publishing)
owns the external registration procedure.

Activation is a later, separately authorized operation:

1. Merge and review the protocol workflow and governance source. Keep the
protocol release policy at `pending-source-release` with null release pins
until the actual source release exists; the upload path must fail before
requesting an OIDC token while those prerequisites are missing.
2. Use credential-free preparation to bind the actual published tag and source
revision to a reproducible crate digest and its release asset/manifest proof.
Review and commit those exact coordinates through the protocol repository.
No future release SHA or crate digest belongs in this planning record.
3. Review `bin/publish` and apply only the merged, authorized desired-state
policy. The organization allowlist applies to all governed repositories, but
the one added authentication-action pattern matches only that exact commit.
Check the complete plan for unrelated drift before requesting apply.
4. With separate owner authorization, provision `crates-io-release` with the
exact desired reviewer/branch/bypass settings and register a crates.io
Trusted Publisher for `atrinik/protocol`, `publish-crate.yml`, and that
environment. Verify live identity and protection settings, then record the
observed activation through a reviewed governance update. This pending-only
schema deliberately cannot represent activation by changing a status string.
5. Obtain separate workflow-dispatch authorization and use the reviewed manual
publication workflow. Environment approval remains the maintainer's action.

`bin/publish` prints the manual pending requirement; it does not create an
environment or register a publisher. `bin/verify-manual-settings` verifies the
repository and reviewer identities/permission, then reports `PENDING`. It does
not inspect token values or claim the future source release, environment,
external registration, or publication is ready. Missing reviewer access or
identity drift fails closed. Existing environment contracts retain their exact
reviewer restrictions.

Rollback of this unactivated plan is a reviewed removal of this manual record
and the single action pattern, followed by a plan review. Once activated, stop
new manual publication, revoke the exact crates.io Trusted Publisher through
its supported owner UI, and separately review removal of the matching release
environment/action allowance. Never mutate existing crate versions, release
assets, tags, unrelated environments, or other action allowlist entries.
5 changes: 5 additions & 0 deletions bin/publish
Original file line number Diff line number Diff line change
Expand Up @@ -1379,4 +1379,9 @@ while IFS= read -r external_app; do
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")
while IFS= read -r crate_record; do
repository=$(jq -r '.repository' <<<"${crate_record}")
crate=$(jq -r '.crate' <<<"${crate_record}")
echo "MANUAL PENDING ${repository} crates.io ${crate}: reviewed source release, maintainer-reviewed crates-io-release environment and Trusted Publisher owner setup; publisher does not apply"
done < <(jq -c '.crates_io_trusted_publishing[]' "${root}/config/manual-settings.json")
echo "Manual confirmation required for config/manual-settings.json."
42 changes: 42 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",
"crates_io_trusted_publishing",
"external_provider_apps",
"github_actions_apps",
"github_actions_credentials",
Expand Down Expand Up @@ -766,6 +767,47 @@ jq -e '
)
' "${manual_settings_config}" >/dev/null

# Planned crate publishing is a distinct manual contract: its desired review
# policy is explicit, but no live registration or release pin is invented.
jq -e \
--slurpfile repositories "${repositories_config}" \
--slurpfile actions "${root}/config/actions-selected.json" '
.github_actions_environments as $environments |
.crates_io_trusted_publishing as $publishers |
($publishers | type == "array" and length <= 1) and
all($publishers[];
(keys == ["activation_state", "authentication_action", "crate", "environment",
"provider", "repository", "repository_id", "runbook", "workflow_filename",
"workflow_ref", "workflow_trigger"]) and
.activation_state == "pending-source-release" and
.authentication_action == "rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18" and
(.authentication_action as $action |
($actions[0].patterns_allowed | map(select(startswith("rust-lang/")))) == [$action]) and
.crate == "atrinik-protocol" and .provider == "github" and
.repository == "atrinik/protocol" and .repository_id == 1327106950 and
($repositories[0].pull_request_gate | index("protocol")) != null and
($repositories[0].archive | index("protocol")) == null and
.workflow_filename == "publish-crate.yml" and
.workflow_ref == "refs/heads/main" and .workflow_trigger == "workflow_dispatch" and
.runbook == "README.md#protocol-crate-trusted-publishing" and
.environment == {
can_admins_bypass: false,
deployment_branch_policy: {
custom_branch_policies: true,
patterns: [{name: "main", type: "branch"}],
protected_branches: false
},
name: "crates-io-release", prevent_self_review: false,
required_reviewers: [{type: "User", id: 3865595, login: "zoeyrose"}],
reviewer_requirement: "repository-maintainer",
secret_names: [], variable_names: []
}
) and
all($publishers[]; . as $publisher |
all($environments[];
.repository != $publisher.repository or .environment != $publisher.environment.name))
' "${manual_settings_config}" >/dev/null

jq -e \
--slurpfile repositories "${repositories_config}" '
.github_actions_environments as $environments |
Expand Down
30 changes: 30 additions & 0 deletions bin/verify-manual-settings
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,36 @@ while IFS= read -r pages_record; do
fi
done < <(jq -c '.github_pages_sites[]' "${manual_settings}")

# Planned registration has no live publisher/environment identity yet. Verify
# only its repository identity and report the unresolved activation explicitly.
while IFS= read -r crate_record; do
repository=$(jq -r '.repository' <<<"${crate_record}")
repository_id=$(jq -r '.repository_id' <<<"${crate_record}")
if [[ ${repository%%/*} != "${organization}" ]]; then
echo "error: crate publisher repository owner does not match ${organization}" >&2
exit 1
fi
if verify_repository_identity "${repository}" "${repository_id}"; then
:
else
status=$?
exit "${status}"
fi
reviewer_identity=$(github_api "read crate publishing reviewer identity" "users/zoeyrose")
if ! jq -e '.login == "zoeyrose" and .id == 3865595' <<<"${reviewer_identity}" >/dev/null; then
echo "error: crate publishing reviewer identity drift" >&2
exit 1
fi
reviewer_permission=$(github_api "read crate publishing reviewer permission" \
"repos/${repository}/collaborators/zoeyrose/permission")
if ! jq -e '.permission == "admin" or .permission == "maintain"' \
<<<"${reviewer_permission}" >/dev/null; then
echo "error: crate publishing reviewer is not a repository maintainer" >&2
exit 1
fi
echo "PENDING ${repository} crates.io atrinik-protocol via publish-crate.yml / crates-io-release: source release and owner setup remain unverified; no registration or environment applied"
done < <(jq -c '.crates_io_trusted_publishing[]' "${manual_settings}")

while IFS= read -r environment_record; do
repository=$(jq -r '.repository' <<<"${environment_record}")
repository_owner=${repository%%/*}
Expand Down
3 changes: 2 additions & 1 deletion config/actions-selected.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"codecov/codecov-action@*",
"docker/login-action@*",
"docker/setup-buildx-action@*",
"docker/build-push-action@*"
"docker/build-push-action@*",
"rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18"
]
}
39 changes: 39 additions & 0 deletions config/manual-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,45 @@
"renderer",
"server"
],
"crates_io_trusted_publishing": [
{
"activation_state": "pending-source-release",
"authentication_action": "rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18",
"crate": "atrinik-protocol",
"environment": {
"can_admins_bypass": false,
"deployment_branch_policy": {
"custom_branch_policies": true,
"patterns": [
{
"name": "main",
"type": "branch"
}
],
"protected_branches": false
},
"name": "crates-io-release",
"prevent_self_review": false,
"required_reviewers": [
{
"type": "User",
"id": 3865595,
"login": "zoeyrose"
}
],
"reviewer_requirement": "repository-maintainer",
"secret_names": [],
"variable_names": []
},
"provider": "github",
"repository": "atrinik/protocol",
"repository_id": 1327106950,
"runbook": "README.md#protocol-crate-trusted-publishing",
"workflow_filename": "publish-crate.yml",
"workflow_ref": "refs/heads/main",
"workflow_trigger": "workflow_dispatch"
}
],
"external_provider_apps": [
{
"app_id": 85455,
Expand Down
83 changes: 83 additions & 0 deletions tests/validate-crate-trusted-publishing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash

set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
temporary=$(mktemp -d)
trap 'rm -rf "${temporary}"' EXIT
mkdir -p "${temporary}/bin"
cp "${root}/bin/validate" "${temporary}/bin/validate"
cp -R "${root}/config" "${root}/community-health" "${root}/.github" "${temporary}/"

reset_policy() {
cp "${root}/config/manual-settings.json" "${temporary}/config/manual-settings.json"
cp "${root}/config/actions-selected.json" "${temporary}/config/actions-selected.json"
}

assert_rejected() {
local filter=$1
jq "${filter}" "${root}/config/manual-settings.json" >"${temporary}/config/manual-settings.json"
if "${temporary}/bin/validate" >/dev/null 2>&1; then
echo "error: accepted invalid crate publishing policy: ${filter}" >&2
exit 1
fi
reset_policy
}

"${temporary}/bin/validate" >/dev/null
for filter in \
'.crates_io_trusted_publishing[0].activation_state = "active"' \
'.crates_io_trusted_publishing[0].crate = "other-crate"' \
'.crates_io_trusted_publishing[0].provider = "other-provider"' \
'.crates_io_trusted_publishing[0].repository = "atrinik/classic"' \
'.crates_io_trusted_publishing[0].repository_id = 1' \
'.crates_io_trusted_publishing[0].workflow_filename = "../publish-crate.yml"' \
'.crates_io_trusted_publishing[0].workflow_ref = "refs/tags/v1.0.0"' \
'.crates_io_trusted_publishing[0].workflow_trigger = "push"' \
'.crates_io_trusted_publishing[0].authentication_action = "rust-lang/crates-io-auth-action@v1"' \
'.crates_io_trusted_publishing[0].environment.name = "crates-io-bootstrap"' \
'.crates_io_trusted_publishing[0].environment.required_reviewers = []' \
'.crates_io_trusted_publishing[0].environment.required_reviewers = null' \
'.crates_io_trusted_publishing[0].environment.required_reviewers[0].id = 1' \
'.crates_io_trusted_publishing[0].environment.required_reviewers[0].login = "unknown"' \
'.crates_io_trusted_publishing[0].environment.required_reviewers[0].type = "Team"' \
'.crates_io_trusted_publishing[0].environment.prevent_self_review = true' \
'.crates_io_trusted_publishing[0].environment.can_admins_bypass = true' \
'.crates_io_trusted_publishing[0].environment.deployment_branch_policy.patterns[0].name = "*"' \
'.crates_io_trusted_publishing[0].environment.deployment_branch_policy.patterns[0].type = "tag"' \
'.crates_io_trusted_publishing[0].environment.deployment_branch_policy.patterns += [{name:"v*",type:"tag"}]' \
'.crates_io_trusted_publishing[0].environment.secret_names = ["CARGO_REGISTRY_TOKEN"]' \
'.crates_io_trusted_publishing[0].environment.variable_names = ["UNREVIEWED"]' \
'.crates_io_trusted_publishing[0].token = "forbidden-value"' \
'.crates_io_trusted_publishing[0].release_sha = "invented"' \
'.crates_io_trusted_publishing[0].crate_sha256 = "invented"' \
'.crates_io_trusted_publishing += [.crates_io_trusted_publishing[0]]'; do
assert_rejected "${filter}"
done

jq '.patterns_allowed |= map(select(startswith("rust-lang/crates-io-auth-action@") | not))' \
"${root}/config/actions-selected.json" >"${temporary}/config/actions-selected.json"
if "${temporary}/bin/validate" >/dev/null 2>&1; then
echo "error: accepted planned publisher without exact action allowance" >&2
exit 1
fi
reset_policy
jq '.patterns_allowed |= map(if startswith("rust-lang/crates-io-auth-action@") then "rust-lang/*" else . end)' \
"${root}/config/actions-selected.json" >"${temporary}/config/actions-selected.json"
if "${temporary}/bin/validate" >/dev/null 2>&1; then
echo "error: accepted wildcard action allowance instead of exact pin" >&2
exit 1
fi
reset_policy
jq '.patterns_allowed += ["rust-lang/*"]' "${root}/config/actions-selected.json" \
>"${temporary}/config/actions-selected.json"
if "${temporary}/bin/validate" >/dev/null 2>&1; then
echo "error: accepted broader Rust action access beside the exact pin" >&2
exit 1
fi
reset_policy
# Removing unactivated manual intent is a valid reviewed rollback.
jq '.crates_io_trusted_publishing = []' "${root}/config/manual-settings.json" \
>"${temporary}/config/manual-settings.json"
"${temporary}/bin/validate" >/dev/null

echo "Planned crates.io Trusted Publishing validation tests passed."
34 changes: 34 additions & 0 deletions tests/verify-manual-settings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,28 @@ graphql)
}}}'
fi
;;
repos/atrinik/protocol)
repository_id=1327106950
if [[ ${FAKE_GH_SCENARIO} == crate-repository-drift ]]; then
repository_id=1
fi
jq -n --argjson id "${repository_id}" \
'{id: $id, full_name: "atrinik/protocol", archived: false, default_branch: "main"}'
;;
users/zoeyrose)
reviewer_id=3865595
if [[ ${FAKE_GH_SCENARIO} == crate-reviewer-drift ]]; then
reviewer_id=1
fi
jq -n --argjson id "${reviewer_id}" '{id: $id, login: "zoeyrose"}'
;;
repos/atrinik/protocol/collaborators/zoeyrose/permission)
permission=admin
if [[ ${FAKE_GH_SCENARIO} == crate-reviewer-permission ]]; then
permission=read
fi
jq -n --arg permission "${permission}" '{permission: $permission}'
;;
repos/atrinik/github-settings)
if [[ ${FAKE_GH_SCENARIO} == identity-drift ]]; then
jq -n '{
Expand Down Expand Up @@ -631,6 +653,18 @@ run_verify() {

: >"${temporary}/gh.log"
output=$(run_verify present)
grep -Fq 'PENDING atrinik/protocol crates.io atrinik-protocol via publish-crate.yml / crates-io-release' <<<"${output}"
if grep -Eq 'protocol/environments|crates.io|/trusted-publishers' "${temporary}/gh.log"; then
echo "error: planned crate contract inspected or mutated unprovisioned state" >&2
exit 1
fi
for scenario in crate-repository-drift crate-reviewer-drift crate-reviewer-permission; do
if run_verify "${scenario}" >"${temporary}/${scenario}.out" 2>"${temporary}/${scenario}.err"; then
echo "error: verifier accepted ${scenario}" >&2
exit 1
fi
done

grep -Fq 'KEEP atrinik/github-settings repository Actions secret ATRINIK_SETTINGS_TOKEN' \
<<<"${output}"
grep -Fq 'KEEP atrinik/classic environment discord-release metadata' <<<"${output}"
Expand Down
Loading