Platform configuration for the thaw-app GitHub organization (repositories, teams, branch rulesets, labels, and CODEOWNERS), declared in YAML under config/ and provisioned with Pulumi and the @pulumi/github provider. Runs on Bun.
This repository is the control plane for that org: it lives at thaw-app/platform, is not listed in config/repos.yaml, and is not provisioned by its own Pulumi program.
Scope: only repositories listed in config/repos.yaml are managed here (teams, labels, CODEOWNERS, branch policy, and so on). Other org repos, notably Thaw and AXSwift6, are administered manually and intentionally omitted from config (see the header comment in repos.yaml).
flowchart TB
subgraph config["config/*.yaml"]
org["org.yaml<br/><i>defaults</i>"]
repos["repos.yaml"]
teams["teams.yaml"]
members["members.yaml"]
rulesets["rulesets.yaml"]
labels["labels.yaml"]
codeowners["codeowners.yaml"]
end
subgraph setup["src/setup"]
loader["loader.ts<br/>valibot parse"]
validate["validate.ts<br/>cross-refs"]
membersMerge["members.ts<br/>merge memberships"]
resolve["resolve.ts<br/>defaults + Pulumi inputs"]
end
subgraph program["Pulumi program (index.ts → org.ts)"]
teamsRes["teams.ts<br/>teams + memberships"]
rulesetsRes["rulesets.ts<br/>org rulesets"]
repoRes["OrgRepository<br/>(repo.ts)"]
end
subgraph perRepo["Per-repository resources"]
teamAccess["TeamRepository access"]
branchBp["Branch protection<br/><i>overrides only</i>"]
repoRs["Repository rulesets<br/><i>Free tier</i>"]
envs["Environments"]
repoLabels["Labels"]
co["CODEOWNERS file"]
end
subgraph github["GitHub org (thaw-app)"]
ghTeams["Teams"]
ghRulesets["Rulesets"]
ghRepos["Repositories"]
end
config --> loader
loader --> validate
loader --> membersMerge
membersMerge --> validate
validate --> resolve
resolve --> program
teamsRes --> ghTeams
rulesetsRes --> ghRulesets
repoRes --> perRepo
perRepo --> ghRepos
repoRs --> ghRepos
teamsRes -.-> repoRes
rulesetsRes -.-> repoRs
Source: docs/architecture.mmd. Edit the .mmd file and keep the README block in sync when the flow changes.
| Path | Role |
|---|---|
config/ |
Declarative org config (YAML) and generated JSON Schemas for editor validation |
src/setup/ |
Load, validate, and resolve config into Pulumi inputs |
src/types/ |
Valibot schemas and TypeScript types |
src/resources/ |
Pulumi resources (teams, rulesets, per-repo components) |
src/org.ts |
Program entry: wires setup output to resources |
test/ |
Unit tests (loader, validation, resolve, resources, schema generation) |
.github/workflows/ |
CI/CD, reusable Pulumi workflow, drift detection |
- Load and validate.
src/setup/loader.tsparses each YAML file against a valibot schema insrc/types/, merges member records fromconfig/members.yamlinto team definitions (src/setup/members.ts), then runs cross-reference checks (src/setup/validate.ts): unknown team/repo references, branch patterns claimed by multiple rulesets, and labels defined in multiple groups. - Resolve.
src/setup/resolve.tsfills each repo from org-widedefaultsinconfig/org.yamland translates config into Pulumi inputs. - Provision.
src/org.tscreates teams (whenenableTeamsis on), org rulesets (whenenableRulesetsis on), and oneOrgRepositorycomponent (src/resources/repo.ts) per entry inrepos.yaml. Each component owns that repo's team access, repository rulesets (when org rulesets are off), optional branch-protection overrides, deployment environments, labels, and synced.github/CODEOWNERS.
Schemas use strictObject, so an unknown or misspelled YAML key fails the run instead of being silently ignored.
Branch enforcement:
config/rulesets.yamlis the single policy source. WithenableRulesets: false(GitHub Free, the default forthaw-app), that policy is provisioned as aRepositoryRuleseton each managed repo (src/resources/rulesets.ts). WithenableRulesets: true(Team/Enterprise only), the same file provisions org-level rulesets instead. Each ruleset lists the repos it is on, and there are two: a reviewed one (a pull request, one approval, the repo's CI check) and a basic one (the branch cannot be deleted or force-pushed). A repo on the reviewed one names its CI checks inrepos.yamlunderrequiredStatusChecks, because job names differ from repo to repo.
flowchart LR
subgraph triggers["Triggers"]
pr["pull_request → main"]
push["push → main"]
dispatch["workflow_dispatch"]
end
subgraph ci["CI/CD workflow"]
test["test<br/>typecheck · biome · bun test"]
sonar["sonar<br/>coverage + SonarCloud scan"]
preview["preview<br/>pulumi preview + PR comment"]
deploy["deploy<br/>pulumi up"]
end
pr --> test
pr --> sonar
push --> sonar
dispatch --> test
test --> preview
push --> test
test --> deploy
pr -.-> preview
| Workflow | File | When it runs |
|---|---|---|
| CI/CD | .github/workflows/ci.yml |
PRs and pushes to main (markdown-only changes are ignored), merge queue, or manual dispatch |
| Pulumi Setup | .github/workflows/pulumi.yml |
Reusable: preview on PRs, up on main, or preview --expect-no-changes for drift |
| Drift detection | .github/workflows/drift.yml |
Weekly (Mondays 06:17 UTC) or workflow_dispatch |
| Trigger | Jobs |
|---|---|
| PR opened / updated | test → preview, and sonar alongside |
PR merged → push to main |
deploy only (merge commit detected; tests already ran on the PR) |
Direct push to main |
test → deploy, and sonar alongside |
workflow_dispatch |
test (manual) |
On push to main, merge commits are detected by message (Merge pull request … or squash … (#123)). Those skip test and run deploy only. A direct push (no merge markers) runs test then deploy.
To skip CI on a PR commit, add [skip ci] to the commit message. Required status checks will not run for that commit.
sonar runs beside test and nothing waits on it: a failed scan is a red check, but the preview and the deploy still run.
Every Pulumi run refreshes first, so it compares config against GitHub's current settings and not only against the last recorded state. A setting changed by hand shows up in the preview and in the weekly drift check, and the next deploy puts it back to what config says.
On main, deploy runs pulumi up against stack diazdesandi/dev. CI authenticates to Pulumi Cloud via OIDC; the GitHub provider uses the PULUMI_GITHUB_TOKEN repository secret (Actions' default GITHUB_TOKEN cannot manage org teams, labels, or cross-repo resources).
| Name | Where | Purpose |
|---|---|---|
PULUMI_GITHUB_TOKEN |
GitHub Actions secret | Classic PAT with admin:org and repo; passed to the Pulumi GitHub provider as GITHUB_TOKEN when CI runs up |
PULUMI_GITHUB_READ_TOKEN |
GitHub Actions secret, optional | Read-only token for preview runs (pull requests and the drift check), which execute a branch's code. Without it, previews fall back to PULUMI_GITHUB_TOKEN |
SONAR_TOKEN |
GitHub Actions secret | SonarCloud analysis in the sonar job |
github:token |
Pulumi stack config (pulumi config set --secret) |
Same PAT scope for local pulumi preview / pulumi up; not read from Actions |
Pulumi Cloud access in CI uses OIDC (pulumi/auth-actions), so repository secrets hold no long-lived Pulumi token.
| File | Schema | Purpose |
|---|---|---|
config/org.yaml |
OrgConfigSchema |
Org-wide defaults (visibility, merge strategy, squash commit shaping, feature toggles, security settings). |
config/repos.yaml |
ReposFileSchema |
Repositories and per-repo overrides, including the CI checks a ruleset requires of that repo. |
config/rulesets.yaml |
RulesetsFileSchema |
Branch/tag/push rulesets, and the repos each one is on. |
config/teams.yaml |
TeamsFileSchema |
Teams and per-repo team access. |
config/members.yaml |
MembersFileSchema |
Org members and their team memberships. |
config/labels.yaml |
LabelGroupsSchema |
Issue/PR labels, grouped; applied to every repo. |
config/codeowners.yaml |
CodeownersFileSchema |
Global CODEOWNERS template synced to .github/CODEOWNERS in every managed repo. |
GitHub has no org-level CODEOWNERS file. This repo keeps one canonical template in config/codeowners.yaml and Pulumi writes it into each repository on pulumi up.
Each YAML file carries a # yaml-language-server: $schema=... header pointing at a generated JSON Schema in config/schema/. With the YAML extension for VS Code you get autocomplete and inline validation as you type. Regenerate the schemas after changing a valibot schema:
bun run schemaPrerequisites: Bun 1.4+, Pulumi CLI, access to the diazdesandi Pulumi org, and a GitHub token with admin:org (and repo scope for managed repositories).
bun install
pulumi org set-default diazdesandi
pulumi stack select dev
pulumi config set github:token --secret # first-time only; already set on the shared stack
pulumi preview # dry-run against thaw-appLocal runs need pulumi login. CI uses OIDC instead of a local token.
Add a repository. Append an entry to config/repos.yaml (only name and description are required; everything else inherits from org.yaml defaults). Grant team access in config/teams.yaml under repoAccess. A new repo has no branch rules until a ruleset in config/rulesets.yaml lists it; if that ruleset requires status checks, give the repo's check names under requiredStatusChecks.
Set autoInit: true when Pulumi should create an empty GitHub repo (GitHub seeds the default branch). Set autoInit: false when the repo already exists. For pre-existing repos, add adopt: true so the first pulumi up imports the repo name into state (scoped by github:owner; remove adopt after a successful apply). Synced .github/CODEOWNERS comes from config/codeowners.yaml.
Import an existing repository. Set autoInit: false and adopt: true, then run pulumi up. If a prior apply failed partway (component created, repository not), re-run pulumi up with adopt: true still set.
Rename a managed repo on GitHub. Rename it in the GitHub UI or API, update name in repos.yaml, and set pulumiName to the previous Pulumi resource prefix so state stays aligned (see .github / pulumiName: dot-github). Run scripts/rename_dot_github.sh before applying the dot-github → .github config change.
Add a team. Add it under teams: in config/teams.yaml, then reference its slug in repoAccess and/or config/members.yaml.
Add a member. Append an entry to config/members.yaml with their username and team slugs. Omit role for the default (member); set role: maintainer only when they should manage that team's roster.
Add a ruleset. Append to config/rulesets.yaml and list its repos under conditions.repositoryName.includes. Two rulesets may cover the same branch pattern only when they list different repos, and a listed repo must be one in repos.yaml; validation checks both. Leave pullRequest out for a ruleset that should not require one.
Change a repo's security settings. defaults.security in config/org.yaml sets Dependabot alerts, Dependabot security updates, secret scanning and push protection for every managed repo. A repo overrides any of them under security in config/repos.yaml, for example security: { dependabotAlerts: false }. Secret scanning and push protection are applied to public repos only, since GitHub Free does not offer them on private ones.
Change CODEOWNERS. Edit config/codeowners.yaml, or set codeowners on a repo in config/repos.yaml to give that repo its own content. Pulumi commits the file to each repo's default branch, which the reviewed ruleset tier does not allow. For the repos in that tier, open a pull request in each with the same content first; Pulumi then finds the file already matching and writes nothing.
Add a label. Add it under any group in config/labels.yaml. Label names must be unique across groups.
bun run typecheck # tsc --noEmit
bun run check # biome lint + format check
bun run format # biome check --write
bun test # unit tests (setup, resources, schema)
bun run schema # regenerate config/schema/*.json
pulumi preview # dry-run (stack dev)
pulumi up # apply (stack dev)
./scripts/drop_bootstrap_from_state.sh # one-time: orphan legacy *-bootstrap files in statePre-commit hooks (.husky/pre-commit) run biome lint, biome check, and bun test.
If pulumi up fails deleting *-bootstrap RepositoryFile resources with 409 ("Changes must be made through a pull request"), branch protection was applied in the same run and GitHub no longer allows the API to delete those placeholder READMEs. The files are harmless, so drop them from Pulumi state only:
pulumi stack select dev
./scripts/drop_bootstrap_from_state.sh
pulumi up # or re-run CI deployPer-stack settings use the thaw-config: namespace (Pulumi project name from Pulumi.yaml; npm package name is platform).
| Key | Purpose |
|---|---|
github:owner |
GitHub organization (thaw-app) |
github:token |
Provider credential (secret; set on stack for local runs) |
thaw-config:enableTeams (default true) |
Create teams and memberships |
thaw-config:enableRulesets (default true) |
Create org-level rulesets; when false, provision the same policy as per-repo repository rulesets |
| Stack | Pulumi account | Purpose | Deployed by | github:owner |
enableRulesets |
enableTeams |
Config file |
|---|---|---|---|---|---|---|---|
dev |
diazdesandi (personal) |
Governs the live thaw-app org |
CI/CD deploy job on push to main |
thaw-app |
false¹ |
true |
Pulumi.dev.yaml |
Pulumi project: thaw-config. Stack reference: diazdesandi/dev. github:owner is the GitHub org, not your Pulumi login.
pulumi config --stack dev # inspect thaw-config:* and github:* keys¹ On GitHub Free (enableRulesets: false) org rulesets return 403. Each ruleset in config/rulesets.yaml is applied as a RepositoryRuleset on the repos it lists. A repo's required status checks are the names it gives under requiredStatusChecks in repos.yaml; a repo that gives none is held to the first name in the ruleset's acceptAnyOf.
.github/workflows/drift.yml runs weekly (Mondays 06:17 UTC) and on workflow_dispatch. It runs pulumi preview --expect-no-changes against dev, which is read-only and changes no state.
When it fails, the workflow opens a GitHub issue (deduped by title). Remediate one of two ways:
- Keep the change. Update
config/, open a PR and merge it; CI/CD runspulumi uponmain. - Reject the change. Revert the manual edit in the GitHub UI, then re-run Drift detection via
workflow_dispatch.
When the check passes again, the cleanup job closes any open drift issues automatically.
Some drift is tolerated by design (per-repo ruleset exceptions managed in the GitHub UI; see the comment in src/resources/rulesets.ts). If weekly noise appears, document or codify those exceptions in config.