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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Source: [`docs/architecture.mmd`](docs/architecture.mmd). Edit the `.mmd` file a

1. **Load and validate.** [`src/setup/loader.ts`](src/setup/loader.ts) parses each YAML file against a [valibot](https://valibot.dev/) schema in [`src/types/`](src/types/), merges member records from [`config/members.yaml`](config/members.yaml) into team definitions ([`src/setup/members.ts`](src/setup/members.ts)), then runs cross-reference checks ([`src/setup/validate.ts`](src/setup/validate.ts)): unknown team/repo references, branch patterns claimed by multiple rulesets, and labels defined in multiple groups.
2. **Resolve.** [`src/setup/resolve.ts`](src/setup/resolve.ts) fills each repo from org-wide `defaults` in [`config/org.yaml`](config/org.yaml) and translates config into Pulumi inputs.
3. **Provision.** [`src/org.ts`](src/org.ts) creates teams (when `enableTeams` is on), org rulesets (when `enableRulesets` is on), and one `OrgRepository` component ([`src/resources/repo.ts`](src/resources/repo.ts)) per entry in `repos.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`.
3. **Provision.** [`src/org.ts`](src/org.ts) creates teams (when `enableTeams` is on), org rulesets (when `enableRulesets` is on), and one `OrgRepository` component ([`src/resources/repo.ts`](src/resources/repo.ts)) per entry in `repos.yaml`. Each component owns that repo's team access, repository rulesets (when org rulesets are off), optional branch-protection overrides, deployment environments, labels, synced `.github/CODEOWNERS`, and a synced `.github/dependabot.yml` for repos that ask for one.

Schemas use `strictObject`, so an unknown or misspelled YAML key fails the run instead of being silently ignored.

Expand Down Expand Up @@ -211,6 +211,8 @@ Set `autoInit: true` when Pulumi should create an empty GitHub repo (GitHub seed

**Add a label.** Add it under any group in `config/labels.yaml`. Label names must be unique across groups.

**Give a repo a Dependabot config.** List its ecosystems under `dependabot` in `config/repos.yaml`. The schedule, labels and assignees come from `defaults.dependabot` in `config/org.yaml`, so every synced file behaves the same way. Like CODEOWNERS, the file is committed to the default branch, so validation rejects it on a repo in the reviewed ruleset tier. Add the file in those repos by pull request.

## Scripts

```sh
Expand Down
3 changes: 3 additions & 0 deletions config/labels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ development:
test:
color: "9ad857"
description: "Test additions or updates"
dependencies:
color: "0366d6"
description: "Updates a dependency"
chore:
color: "BDC3C7"
description: "Housekeeping, non-user-facing maintenance"
Expand Down
12 changes: 11 additions & 1 deletion config/org.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ defaults:
# does not offer them on private ones.
security:
dependabotAlerts: true
dependabotSecurityUpdates: false
dependabotSecurityUpdates: true
secretScanning: true
pushProtection: true
# How a synced .github/dependabot.yml behaves, matching Thaw's. A repo opts
# in by listing its ecosystems under `dependabot` in repos.yaml.
dependabot:
interval: weekly
day: monday
openPullRequestsLimit: 10
labels:
- dependencies
assignees:
- diazdesandi
13 changes: 6 additions & 7 deletions config/repos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# Out of scope (manual admin, not listed here): Thaw, prk-bin, shieldcn,
# AXSwift6, and platform itself.
#
# `adopt: true` imports an existing repo on its first apply and is removed
# afterwards; every repo below is already imported.
#
# Branch enforcement is defined in config/rulesets.yaml, which lists the repos
# each ruleset is on. A repo on a ruleset that requires status checks names its
# own CI checks here (requiredStatusChecks).
Expand All @@ -22,6 +25,9 @@ repos:
- squash
deleteBranchOnMerge: true
autoInit: true
dependabot:
- bun
- github-actions

- name: updates
description: >-
Expand Down Expand Up @@ -61,7 +67,6 @@ repos:
- name: brand-assets
description: "Shared Thaw brand assets: headers, icons, packaging art, marketing."
visibility: public
adopt: true
bare: true
hasIssues: false
hasWiki: false
Expand All @@ -80,7 +85,6 @@ repos:
- squash
deleteBranchOnMerge: true
autoInit: false
adopt: true

- name: raycast-extension
description: Control Thaw directly from Raycast. Toggle visibility sections, search menu bar items, and open settings without touching your mouse.
Expand All @@ -94,7 +98,6 @@ repos:
- squash
deleteBranchOnMerge: true
autoInit: false
adopt: true
requiredStatusChecks:
- ci

Expand All @@ -104,7 +107,6 @@ repos:
- name: PlatformRuntimeKit
description: Proprietary Swift package providing current-platform menu bar runtime support for Thaw.
visibility: private
adopt: true
bare: true
autoInit: false

Expand Down Expand Up @@ -149,13 +151,10 @@ repos:
- squash
deleteBranchOnMerge: false
autoInit: false
adopt: true
bare: true
hasWiki: true
hasProjects: true
hasDiscussions: true
security:
dependabotSecurityUpdates: true
codeowners: |
* @diazdesandi
branchProtection:
Expand Down
8 changes: 4 additions & 4 deletions config/rulesets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ rulesets:
# branch, so a change to config/codeowners.yaml reaches these repos by pull
# request.
bypassActors:
# No actorId: GitHub stores none for organization admins, so naming one
# shows as a change on every run.
- actorType: OrganizationAdmin
actorId: 1
bypassMode: pull_request
- actorType: RepositoryRole
actorId: 5 # repository admin
Expand All @@ -55,9 +56,8 @@ rulesets:
requiredLinearHistory: true
# Merges are squashed by GitHub, which signs them.
requiredSignatures: true
copilotCodeReview:
reviewDraftPullRequests: false
reviewOnPush: false
# No copilotCodeReview: the rule's presence is what requests a Copilot
# review on every pull request.
pullRequest:
requiredApprovingReviewCount: 1
dismissStaleReviewsOnPush: true
Expand Down
50 changes: 50 additions & 0 deletions config/schema/org.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,56 @@
"secretScanning": true,
"pushProtection": true
}
},
"dependabot": {
"type": "object",
"properties": {
"interval": {
"enum": ["daily", "weekly", "monthly"],
"type": "string"
},
"day": {
"enum": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday"
],
"type": "string"
},
"openPullRequestsLimit": {
"type": "integer",
"minimum": 0
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"assignees": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"interval",
"openPullRequestsLimit",
"labels",
"assignees"
],
"additionalProperties": false,
"default": {
"interval": "weekly",
"openPullRequestsLimit": 5,
"labels": [],
"assignees": []
}
}
},
"required": [
Expand Down
15 changes: 15 additions & 0 deletions config/schema/repos.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@
"codeowners": {
"type": "string"
},
"dependabot": {
"type": "array",
"items": {
"enum": [
"bun",
"npm",
"github-actions",
"swift",
"bundler",
"pip",
"docker"
],
"type": "string"
}
},
"requiredStatusChecks": {
"type": "array",
"items": {
Expand Down
2 changes: 2 additions & 0 deletions src/org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OrgRepository,
} from "@/resources";
import {
assertOrgRulesetsSupported,
assertReviewerTeamsCreatable,
buildRepoConfig,
initConfig,
Expand All @@ -22,6 +23,7 @@ export default async function setupOrg() {
const enableRulesets = pulumiConfig.getBoolean("enableRulesets") ?? true;

assertReviewerTeamsCreatable(repos, enableTeams);
if (enableRulesets) assertOrgRulesetsSupported(repos, rulesets);

const teamResources = enableTeams ? createTeams(teams) : {};
if (enableTeams) createTeamMemberships(teams, teamResources);
Expand Down
26 changes: 0 additions & 26 deletions src/resources/codeowners.ts

This file was deleted.

82 changes: 82 additions & 0 deletions src/resources/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import github from "@pulumi/github";
import { mergeOptions, type ResourceOptions } from "@pulumi/pulumi";

interface SyncedFile {
resourceName: string;
repo: github.Repository;
defaultBranch: string;
file: string;
content: string;
commitMessage: string;
}

// Commits a file to the repo's default branch. Empty content means the repo
// does not have the file synced.
function createSyncedFile(
{
resourceName,
repo,
defaultBranch,
file,
content,
commitMessage,
}: SyncedFile,
opts?: ResourceOptions,
): github.RepositoryFile | undefined {
const trimmed = content.trim();
if (!trimmed) return undefined;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return new github.RepositoryFile(
resourceName,
{
repository: repo.name,
branch: defaultBranch,
file,
content: trimmed,
commitMessage,
overwriteOnCreate: true,
},
mergeOptions(opts, { dependsOn: [repo] }),
);
}

export function createCodeowners(
resourcePrefix: string,
repo: github.Repository,
defaultBranch: string,
content: string,
opts?: ResourceOptions,
): github.RepositoryFile | undefined {
return createSyncedFile(
{
resourceName: `${resourcePrefix}-codeowners`,
repo,
defaultBranch,
file: ".github/CODEOWNERS",
content,
commitMessage: "chore: sync CODEOWNERS from org config",
},
opts,
);
}

export function createDependabot(
resourcePrefix: string,
repo: github.Repository,
defaultBranch: string,
content: string,
opts?: ResourceOptions,
): github.RepositoryFile | undefined {
return createSyncedFile(
{
resourceName: `${resourcePrefix}-dependabot`,
repo,
defaultBranch,
file: ".github/dependabot.yml",
content,
commitMessage: "chore: sync dependabot.yml from org config",
},
// A repo that stops listing ecosystems keeps the file it has.
mergeOptions(opts, { retainOnDelete: true }),
);
}
16 changes: 15 additions & 1 deletion src/resources/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
} from "@pulumi/pulumi";
import type { ResolvedRepoConfig, TeamAccess, TeamResourceMap } from "@/types";
import { createBranchProtection } from "./branch";
import { createCodeowners } from "./codeowners";
import { createEnvironments } from "./environments";
import { createCodeowners, createDependabot } from "./files";
import { createLabels } from "./labels";
import { createRepositoryRulesets } from "./rulesets";

Expand All @@ -22,6 +22,7 @@ type ActiveRepoContext = {
security: ResolvedRepoConfig["security"];
defaultBranch: string;
codeownersContent: string;
dependabotContent: string;
teamResources: TeamResourceMap;
};

Expand Down Expand Up @@ -159,6 +160,17 @@ function provisionActiveRepoResources(
dependsOn: [ctx.repo],
},
);

createDependabot(
ctx.resourcePrefix,
ctx.repo,
ctx.defaultBranch,
ctx.dependabotContent,
{
parent,
dependsOn: [ctx.repo],
},
);
}

export default class OrgRepository extends ComponentResource {
Expand Down Expand Up @@ -195,6 +207,7 @@ export default class OrgRepository extends ComponentResource {
squashMergeCommitMessage,
defaultBranch,
codeownersContent,
dependabotContent,
} = config;

const allowSquashMerge = mergeStrategies.includes("squash");
Expand Down Expand Up @@ -249,6 +262,7 @@ export default class OrgRepository extends ComponentResource {
security,
defaultBranch,
codeownersContent,
dependabotContent,
teamResources,
});
}
Expand Down
Loading