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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
deployment target. Never hand-edit that repository, add independent release
automation to it, or add its issue forms' `projects:` key: public
contributors may not have the Project write permission that key requires.
- Keep the game-first organization description and canonical website in
`config/organization.json`, and publish them only on real drift. Keep the
generated public profile in `community-health/profile/README.md`. Record the
six ordered public pins with stable repository IDs in
`config/manual-settings.json`, verify them read-only through GraphQL, and use
the documented organization-owner UI step instead of an undocumented writer.
- Keep required workflow job names synchronized with rulesets. Workflow or
permissions changes also require actionlint, least-privilege review, and
immutable action references according to policy.
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ material. Changes require credential-schema negative tests, the read-only
`bin/verify-manual-settings` plan, and the documented manual and scheduled
post-rotation checks.

Organization identity changes must preserve the exact public description and
canonical website in `config/organization.json`, remain delta-aware in plan and
apply modes, and include unchanged-state coverage. Keep the public profile in
`community-health/profile/README.md`; never edit `atrinik/.github` directly.
Public pins are stable-ID manual state in `config/manual-settings.json`. Verify
their exact count, public/active identity, and order read-only through GraphQL,
and document the organization-owner profile-settings step rather than using an
undocumented mutation interface.

Synchronization authentication tests must distinguish readable from writable
credentials, preserve exact CLI failure statuses, reject missing advertised
classic-PAT scopes and write capabilities before mutation, and fail closed on
Expand Down
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ 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
Expand Down Expand Up @@ -100,9 +105,18 @@ read-only and are skipped on later runs.
release policy.
- `community-health/` is the source for organization-wide issue forms,
pull-request guidance, contribution guidance, conduct policy, and security
reporting. `bin/publish-community-health` generates `atrinik/.github`
directly from this released source; the generated repository is not edited
or released independently.
reporting, including the public organization profile.
`bin/publish-community-health` generates `atrinik/.github` directly from this
released source; the generated repository is not edited or released
independently.
- `config/organization.json` owns the game-first organization description and
preserves `https://atrinik.org` as the canonical website. The publisher
compares every owned field and patches only when live state differs.
- `config/manual-settings.json` owns the exact public repository pin order:
`classic`, `atrinik`, `website`, `content`, `protocol`, and `playtester`, with
stable repository IDs. `bin/verify-manual-settings` reads 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
Expand Down Expand Up @@ -287,6 +301,46 @@ 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.

### Organization identity and public pins

Review and deploy organization identity only after the governing pull request
is merged. First inspect both complete plans:

```sh
bin/publish
bin/publish-community-health
```

With 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**:

1. `classic`
2. `atrinik`
3. `website`
4. `content`
5. `protocol`
6. `playtester`

After saving, verify the exact live order and stable identities without
mutation:

```sh
bin/verify-manual-settings
```

Do 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
Expand Down
37 changes: 34 additions & 3 deletions bin/publish
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ for command in gh git jq; do
done

root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
organization_config=${root}/config/organization.json
repositories_config=${root}/config/repositories.json
advisory_merge_windows_config=${root}/config/advisory-merge-windows.json
codeql_advanced_config=${root}/config/codeql-advanced-setup.json
Expand Down Expand Up @@ -63,6 +64,30 @@ run_api() {
fi
}

converge_organization_settings() {
local current=$1
local desired

desired=$(jq -c . "${organization_config}")
if jq -e --argjson desired "${desired}" '
. as $current |
all($desired | to_entries[]; $current[.key] == .value)
' <<<"${current}" >/dev/null; then
echo "KEEP /orgs/${organization} metadata and repository defaults"
return
fi

jq -r \
--arg endpoint "/orgs/${organization}" \
--argjson desired "${desired}" '
. as $current |
$desired | to_entries[] |
select($current[.key] != .value) |
"DRIFT \($endpoint) \(.key) live=\($current[.key] | tojson) desired=\(.value | tojson)"
' <<<"${current}"
run_api PATCH "orgs/${organization}" "${organization_config}"
}

repository_uses_advanced_codeql() {
local repository=$1

Expand Down Expand Up @@ -1015,7 +1040,13 @@ converge_immutable_release_policy() {
return 1
}

organization_plan=$(github_api "orgs/${organization}" --jq '.plan.name')
organization_metadata=$(github_api "orgs/${organization}")
if ! jq -e 'type == "object" and (.plan.name | type == "string")' \
<<<"${organization_metadata}" >/dev/null; then
echo "error: GitHub returned invalid organization metadata" >&2
exit 1
fi
organization_plan=$(jq -r '.plan.name' <<<"${organization_metadata}")
if [[ ${policy_scope} == auto ]]; then
case ${organization_plan} in
team | enterprise) policy_scope=organization ;;
Expand Down Expand Up @@ -1089,7 +1120,7 @@ done < <(

converge_immutable_release_policy

run_api PATCH "orgs/${organization}" "${root}/config/organization.json"
converge_organization_settings "${organization_metadata}"
run_api PUT "orgs/${organization}/actions/permissions" \
"${root}/config/actions-permissions.json"
run_api PUT "orgs/${organization}/actions/permissions/selected-actions" \
Expand Down Expand Up @@ -1129,7 +1160,7 @@ while IFS= read -r repository; do
run_api PATCH "repos/${organization}/${repository}" "${archive_payload}"
done < <(jq -r '.archive[]' "${repositories_config}")

if [[ $(github_api "orgs/${organization}" --jq '.members_can_create_teams') != false ]]; then
if [[ $(jq -r '.members_can_create_teams' <<<"${organization_metadata}") != false ]]; then
echo "MANUAL members can still create teams; disable this in Member privileges."
fi
echo "Manual confirmation required for config/manual-settings.json."
58 changes: 58 additions & 0 deletions bin/validate
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
set -euo pipefail

root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
organization_config=${root}/config/organization.json
repositories_config=${root}/config/repositories.json
advisory_merge_windows_config=${root}/config/advisory-merge-windows.json
advanced_codeql_config=${root}/config/codeql-advanced-setup.json
Expand Down Expand Up @@ -38,6 +39,17 @@ while IFS= read -r -d '' config; do
jq empty "${config}" >/dev/null
done < <(find "${root}/config" -name '*.json' -print0 | sort -z)

jq -e '
. == {
description: "A cooperative fantasy world of islands, ruins, and crystal-lit mysteries—open source, playable in Classic, and rebuilding for the future.",
blog: "https://atrinik.org",
default_repository_permission: "none",
members_can_create_repositories: false,
members_can_create_public_repositories: false,
members_can_create_private_repositories: false
}
' "${organization_config}" >/dev/null

jq -e '
.bypass_actors == [] and
[.rules[].type] == ["deletion", "non_fast_forward"]
Expand Down Expand Up @@ -221,13 +233,37 @@ jq -e '
"members_can_create_teams",
"members_can_delete_or_transfer_repositories",
"members_can_request_github_apps",
"organization_pins",
"reason",
"repository_admins_can_install_github_apps",
"require_secure_two_factor_methods",
"require_two_factor_authentication"
])
' "${manual_settings_config}" >/dev/null

jq -e '
.organization_pins as $pins |
($pins | keys == ["apply_path", "repositories", "verification"]) and
$pins.apply_path == "https://github.com/atrinik" and
$pins.verification == "read_only_graphql" and
($pins.repositories | type == "array" and length == 6) and
[$pins.repositories[].name] == [
"classic",
"atrinik",
"website",
"content",
"protocol",
"playtester"
] and
([ $pins.repositories[].repository_id ] | length == (unique | length)) and
all(
$pins.repositories[];
(keys == ["name", "repository_id"]) and
(.name | type == "string" and length > 0) and
(.repository_id | type == "number" and floor == . and . > 0)
)
' "${manual_settings_config}" >/dev/null

jq -e \
--arg today "${validation_today}" \
--slurpfile repositories "${repositories_config}" '
Expand Down Expand Up @@ -720,6 +756,10 @@ jq -e '
has_wiki: false
}) and
(.files | type == "array" and length > 0) and
any(
.files[];
. == {source: "profile/README.md", target: "profile/README.md"}
) and
([.files[].source] | length == (unique | length)) and
([.files[].target] | length == (unique | length)) and
all(
Expand All @@ -739,6 +779,24 @@ while IFS= read -r community_file; do
fi
done < <(jq -r '.files[].source' "${community_health_config}")

profile_readme=${root}/community-health/profile/README.md
for required_text in \
'# Atrinik' \
'Atrinik is an open-source cooperative fantasy role-playing world' \
'GPL-licensed [Atrinik Classic](https://github.com/atrinik/classic)' \
'MIT-licensed Go, Rust,' \
'They do not yet form a complete game release.' \
'Game content and' \
'media remain separately licensed' \
'[Play Atrinik Classic](https://atrinik.org/downloads/)' \
'[Follow the next-generation roadmap](https://github.com/atrinik/atrinik/issues/168)' \
'Direct human contributions are welcome.'; do
if ! grep -Fq "${required_text}" "${profile_readme}"; then
echo "error: organization profile is missing required text: ${required_text}" >&2
exit 1
fi
done

validate_issue_form() {
local form_name=$1
local inference_key=$2
Expand Down
44 changes: 43 additions & 1 deletion bin/verify-manual-settings
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,46 @@ while IFS= read -r environment_record; do
echo "KEEP ${repository} environment ${environment} metadata"
done < <(jq -c '.github_actions_environments[]' "${manual_settings}")

echo "Manual settings live credential and environment metadata is present."
# GraphQL variables must remain literal for GitHub to substitute.
# shellcheck disable=SC2016
pins_response=$(github_api \
"read ${organization} organization pins" \
graphql \
-f query='query($login:String!){organization(login:$login){login pinnedItems(first:10,types:REPOSITORY){totalCount nodes{... on Repository{name databaseId nameWithOwner isArchived visibility}}}}}' \
-F login="${organization}")
if ! jq -e \
--arg organization "${organization}" '
.data.organization as $organization_data |
($organization_data | type == "object") and
$organization_data.login == $organization and
($organization_data.pinnedItems.totalCount | type == "number") and
($organization_data.pinnedItems.nodes | type == "array") and
$organization_data.pinnedItems.totalCount ==
($organization_data.pinnedItems.nodes | length) and
all(
$organization_data.pinnedItems.nodes[];
(.name | type == "string" and length > 0) and
(.databaseId | type == "number" and floor == . and . > 0) and
.nameWithOwner == "\($organization)/\(.name)" and
.isArchived == false and
.visibility == "PUBLIC"
)
' <<<"${pins_response}" >/dev/null; then
echo "error: GitHub returned invalid organization pin metadata for ${organization}" >&2
exit 1
fi

desired_pins=$(jq -c '.organization_pins.repositories' "${manual_settings}")
live_pins=$(jq -c '
[.data.organization.pinnedItems.nodes[] |
{name, repository_id: .databaseId}]
' <<<"${pins_response}")
if [[ ${live_pins} != "${desired_pins}" ]]; then
echo "error: organization pin order or identity drift for ${organization}" >&2
echo "desired: ${desired_pins}" >&2
echo "live: ${live_pins}" >&2
exit 1
fi
echo "KEEP ${organization} organization pins match the exact governed order"

echo "Manual settings live credential, environment, and organization pin metadata is present."
7 changes: 4 additions & 3 deletions community-health/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Atrinik community health defaults

This repository provides Atrinik's organization-wide issue forms, pull-request
template, contribution guidance, and security policy. A repository's local
file takes precedence when it needs more specific instructions.
This repository provides Atrinik's public organization profile, organization-wide
issue forms, pull-request template, contribution guidance, and security policy.
A repository's local file takes precedence when it needs more specific
instructions.

The source of truth is
[`atrinik/github-settings`](https://github.com/atrinik/github-settings). Do not
Expand Down
34 changes: 34 additions & 0 deletions community-health/profile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Atrinik

> A world of crystal, memory, and unfinished paths.

Atrinik is an open-source cooperative fantasy role-playing world of islands,
ruins, and crystal-lit mysteries.

The maintained GPL-licensed [Atrinik Classic](https://github.com/atrinik/classic)
line keeps the world playable today. In parallel, fresh MIT-licensed Go, Rust,
and Protobuf foundations are being developed as a separate clean-room
implementation. They do not yet form a complete game release. Game content and
media remain separately licensed under the terms in their source repositories.

## Start here

- [Play Atrinik Classic](https://atrinik.org/downloads/)
- [Explore the development workspace](https://github.com/atrinik/atrinik)
- [Follow the next-generation roadmap](https://github.com/atrinik/atrinik/issues/168)
- [Discover the world](https://atrinik.org/)

## The project

| Repository | Purpose |
| --- | --- |
| [classic](https://github.com/atrinik/classic) | The playable, maintained C17 client, server, editor, libraries, and protocol |
| [content](https://github.com/atrinik/content) | Atrinik's authored maps, quests, archetypes, and world data |
| [client](https://github.com/atrinik/client) | Clean-room Rust and SDL3 next-generation client foundation |
| [server](https://github.com/atrinik/server) | Clean-room Go authoritative server foundation |
| [protocol](https://github.com/atrinik/protocol) | Game Protocol 1 schemas, QUIC specifications, and Go/Rust bindings |
| [renderer](https://github.com/atrinik/renderer) | Shared Rust GPU renderer for clients, editors, and tools |

Human creators direct the game world. Software is developed publicly—primarily
through Codex-driven workflows under maintainer direction, review, provenance
controls, and repository validation. Direct human contributions are welcome.
4 changes: 4 additions & 0 deletions config/community-health.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"has_wiki": false
},
"files": [
{
"source": "profile/README.md",
"target": "profile/README.md"
},
{
"source": "README.md",
"target": "README.md"
Expand Down
Loading