Skip to content

Commit ab20e64

Browse files
lex00claude
andauthored
feat(cycle): org-settings reconcile cycle (#5) (#22)
Reconciles org-level settings via GET/PATCH /orgs/{org}: default repo permission, member repo-creation privileges, public metadata, and the surfaced 2FA flag. PATCH is partial, so selective-by-omission holds by sending only declared fields (no full-replacement RMW needed). Registered in the cycle registry and exported from index. Action bundle rebuilt. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c4828ab commit ab20e64

5 files changed

Lines changed: 708 additions & 1 deletion

File tree

action/index.mjs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229338,9 +229338,95 @@ async function fetchLiveForOrg(client, orgLogin, repos, budget) {
229338229338
return { repos: liveRepos };
229339229339
}
229340229340

229341+
// src/cycles/org-settings.ts
229342+
var VALID_DEFAULT_PERMISSIONS = /* @__PURE__ */ new Set(["none", "read", "write", "admin"]);
229343+
function mapOrgToLive(raw) {
229344+
const live = {};
229345+
if (raw.description != null) live.description = raw.description;
229346+
if (raw.email != null) live.email = raw.email;
229347+
if (raw.blog != null) live.websiteUrl = raw.blog;
229348+
if (raw.default_repository_permission != null && VALID_DEFAULT_PERMISSIONS.has(raw.default_repository_permission)) {
229349+
live.defaultRepositoryPermission = raw.default_repository_permission;
229350+
}
229351+
if (typeof raw.members_can_create_public_repositories === "boolean") {
229352+
live.membersCanCreatePublicRepositories = raw.members_can_create_public_repositories;
229353+
}
229354+
if (typeof raw.members_can_create_private_repositories === "boolean") {
229355+
live.membersCanCreatePrivateRepositories = raw.members_can_create_private_repositories;
229356+
}
229357+
if (typeof raw.members_can_create_internal_repositories === "boolean") {
229358+
live.membersCanCreateInternalRepositories = raw.members_can_create_internal_repositories;
229359+
}
229360+
if (typeof raw.two_factor_requirement_enabled === "boolean") {
229361+
live.requireTwoFactorAuthentication = raw.two_factor_requirement_enabled;
229362+
}
229363+
return live;
229364+
}
229365+
function buildOrgPatchBody(desired) {
229366+
const body = {};
229367+
if (desired.description !== void 0) body.description = desired.description;
229368+
if (desired.email !== void 0) body.email = desired.email;
229369+
if (desired.websiteUrl !== void 0) body.blog = desired.websiteUrl;
229370+
if (desired.defaultRepositoryPermission !== void 0) {
229371+
body.default_repository_permission = desired.defaultRepositoryPermission;
229372+
}
229373+
if (desired.membersCanCreatePublicRepositories !== void 0) {
229374+
body.members_can_create_public_repositories = desired.membersCanCreatePublicRepositories;
229375+
}
229376+
if (desired.membersCanCreatePrivateRepositories !== void 0) {
229377+
body.members_can_create_private_repositories = desired.membersCanCreatePrivateRepositories;
229378+
}
229379+
if (desired.membersCanCreateInternalRepositories !== void 0) {
229380+
body.members_can_create_internal_repositories = desired.membersCanCreateInternalRepositories;
229381+
}
229382+
if (desired.requireTwoFactorAuthentication !== void 0) {
229383+
body.two_factor_requirement_enabled = desired.requireTwoFactorAuthentication;
229384+
}
229385+
return body;
229386+
}
229387+
var orgSettingsCycle = {
229388+
name: "org-settings",
229389+
// ── Part 2: fetchLive ──────────────────────────────────────────────────────
229390+
async fetchLive(client, orgLogin, _scope, budget) {
229391+
if (budget.exhausted) {
229392+
const { BudgetExhaustedError: BudgetExhaustedError2 } = await Promise.resolve().then(() => (init_runner(), runner_exports));
229393+
throw new BudgetExhaustedError2();
229394+
}
229395+
budget.use(1);
229396+
let raw;
229397+
try {
229398+
raw = await client.request("GET", `/orgs/${orgLogin}`);
229399+
} catch (err) {
229400+
if (err instanceof Error && err.message.includes("404")) {
229401+
return {};
229402+
}
229403+
throw err;
229404+
}
229405+
return { settings: mapOrgToLive(raw) };
229406+
},
229407+
// ── Part 3: buildDesired ───────────────────────────────────────────────────
229408+
buildDesired(orgConfig, _orgLogin, _scope) {
229409+
if (!orgConfig.settings) return {};
229410+
return { settings: orgConfig.settings };
229411+
},
229412+
// ── Part 4: apply ──────────────────────────────────────────────────────────
229413+
async apply(client, entry, orgLogin, _scope, budget) {
229414+
if (entry.resourceType !== "org-settings") {
229415+
return;
229416+
}
229417+
if (entry.kind === "delete") return;
229418+
const desired = entry.after;
229419+
const body = buildOrgPatchBody(desired);
229420+
if (Object.keys(body).length === 0) return;
229421+
budget.use(1);
229422+
await client.request("PATCH", `/orgs/${orgLogin}`, body);
229423+
}
229424+
};
229425+
229341229426
// src/cli/registry.ts
229342229427
var CYCLE_REGISTRY = {
229343-
[branchProtectionCycle.name]: branchProtectionCycle
229428+
[branchProtectionCycle.name]: branchProtectionCycle,
229429+
[orgSettingsCycle.name]: orgSettingsCycle
229344229430
};
229345229431

229346229432
// node_modules/@intentius/chant/src/audit/fetch.ts

src/cli/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import type { Cycle } from "../reconcile/runner.js";
1212
import { branchProtectionCycle } from "../cycles/branch-protection.js";
13+
import { orgSettingsCycle } from "../cycles/org-settings.js";
1314

1415
/**
1516
* Registry of all available governance cycles, keyed by the name accepted by
@@ -20,4 +21,5 @@ import { branchProtectionCycle } from "../cycles/branch-protection.js";
2021
*/
2122
export const CYCLE_REGISTRY: Record<string, Cycle> = {
2223
[branchProtectionCycle.name]: branchProtectionCycle,
24+
[orgSettingsCycle.name]: orgSettingsCycle,
2325
};

0 commit comments

Comments
 (0)