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
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
Apply repeatable GitHub repository defaults and branch rulesets.

`github-sane-defaults` is a small CLI for keeping repository settings
consistent across one GitHub repository or an entire organization. It previews
the changes it would make, then applies the same policy through GitHub's REST
API when you are ready.
consistent across one GitHub repository, an organization, or repositories owned
by a user account. It previews the changes it would make, then applies the same
policy through GitHub's REST API when you are ready.

The default policy is intentionally narrow. It turns on the GitHub merge and
cleanup settings that keep pull request history tidy, and it creates a default
Expand All @@ -14,7 +14,7 @@ history. The CLI manages its own named ruleset, so repeated runs are
idempotent and easy to review.

Use `plan` first to see drift, then switch to `apply` to make the changes.
Both commands accept `owner/repo` targets and organization-wide `--all` runs.
Both commands accept `owner/repo` targets and owner-wide `--all` runs.

## Install

Expand All @@ -38,7 +38,7 @@ Use github-sane-defaults to inspect my existing GitHub repositories.
Attention agent: start here:
https://github.com/dutifuldev/github-sane-defaults#readme

Ask me for the target org or owner/repo list, then run plan only. Show which
Ask me for the target owner or owner/repo list, then run plan only. Show which
repos need branch protection rulesets, deletion/force-push protection, linear
history, or GitHub merge and cleanup settings enabled. Do not apply changes
unless I ask.
Expand All @@ -64,10 +64,10 @@ Preview changes for one repository:
github-sane-defaults plan example-org/example-repo
```

Preview changes for every non-archived repository in an organization:
Preview changes for every non-archived repository owned by an organization or user:

```sh
github-sane-defaults plan example-org --all
github-sane-defaults plan example-owner --all
```

Apply changes to one repository:
Expand All @@ -76,20 +76,25 @@ Apply changes to one repository:
github-sane-defaults apply example-org/example-repo
```

Apply changes to every non-archived repository in an organization:
Apply changes to every non-archived repository owned by an organization or user:

```sh
github-sane-defaults apply example-org --all
github-sane-defaults apply example-owner --all
```

For user account targets, `--all` uses repositories visible through the
authenticated token and filters them to the requested owner. Use a token that has
explicit access to the personal repositories you want to audit or update.

`apply` prints the plan and asks for confirmation before changing repository
settings or rulesets. Use `-y` or `--yes` to skip the prompt in automation:

```sh
github-sane-defaults apply example-org/example-repo --yes
```

The legacy `--org example-org --repo example-repo` form is still accepted.
The `--owner example-owner --repo example-repo` form is also accepted. The legacy
`--org example-org --repo example-repo` form remains supported.

## Defaults

Expand Down
2 changes: 1 addition & 1 deletion src/app/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function applyDefaults(
): Promise<ApplySummary> {
const planned = await buildPlan(client, selection);

await applyPlannedDefaults(client, selection.org, planned);
await applyPlannedDefaults(client, selection.owner, planned);

return {
planned,
Expand Down
6 changes: 3 additions & 3 deletions src/app/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function buildPlan(
const plans: RepoPlan[] = [];

for (const repo of activeRepos) {
plans.push(await buildRepoPlan(client, selection.org, repo));
plans.push(await buildRepoPlan(client, selection.owner, repo));
}

return plans;
Expand All @@ -25,10 +25,10 @@ async function selectRepos(
selection: TargetSelection
): Promise<GitHubRepo[]> {
if (selection.all) {
return client.listOrgRepos(selection.org);
return client.listOwnerRepos(selection.owner);
}

return Promise.all(selection.repos.map((repo) => client.getRepo(selection.org, repo)));
return Promise.all(selection.repos.map((repo) => client.getRepo(selection.owner, repo)));
}

async function buildRepoPlan(
Expand Down
2 changes: 1 addition & 1 deletion src/app/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { RepoSettingChange } from "../policy/types.js";

export type TargetSelection = {
org: string;
owner: string;
repos: string[];
all: boolean;
};
Expand Down
58 changes: 29 additions & 29 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function parseArgs(args: string[]): CliOptions {

const options: CliOptions = {
command,
org: parsed.org,
owner: parsed.owner,
repos: parsed.repos,
all: parsed.all,
yes: parsed.yes
Expand All @@ -34,11 +34,11 @@ export function parseArgs(args: string[]): CliOptions {
return options;
}

function validateFlags(parsed: ParsedFlags): asserts parsed is ParsedFlags & { org: string } {
function validateFlags(parsed: ParsedFlags): asserts parsed is ParsedFlags & { owner: string } {
applyPositionalTargets(parsed);

if (parsed.org === undefined) {
throw new Error("Missing required --org option.\n\n" + usage());
if (parsed.owner === undefined) {
throw new Error("Missing required owner target.\n\n" + usage());
}

if (!parsed.all && parsed.repos.length === 0) {
Expand All @@ -51,7 +51,7 @@ function validateFlags(parsed: ParsedFlags): asserts parsed is ParsedFlags & { o
}

type ParsedFlags = {
org?: string;
owner?: string;
token?: string;
repos: string[];
targets: string[];
Expand Down Expand Up @@ -101,7 +101,7 @@ function parseFlag(args: string[], index: number, flags: ParsedFlags): number {
}

function parseValueFlag(args: string[], index: number, flags: ParsedFlags, arg: string): number {
if (arg !== "--org" && arg !== "--repo" && arg !== "--token") {
if (arg !== "--org" && arg !== "--owner" && arg !== "--repo" && arg !== "--token") {
throw new Error(`Unknown option: ${arg}`);
}

Expand All @@ -116,8 +116,8 @@ function parseValueFlag(args: string[], index: number, flags: ParsedFlags, arg:
}

function setFlag(flags: ParsedFlags, arg: string, value: string): void {
if (arg === "--org") {
flags.org = value;
if (arg === "--org" || arg === "--owner") {
flags.owner = value;
return;
}

Expand All @@ -135,7 +135,7 @@ function applyPositionalTargets(flags: ParsedFlags): void {
}

if (flags.all) {
applyOrgTarget(flags);
applyOwnerTarget(flags);
return;
}

Expand All @@ -144,52 +144,52 @@ function applyPositionalTargets(flags: ParsedFlags): void {
}
}

function applyOrgTarget(flags: ParsedFlags): void {
function applyOwnerTarget(flags: ParsedFlags): void {
if (flags.targets.length !== 1 || flags.targets[0]?.includes("/") === true) {
throw new Error("Use an organization name with --all, for example: example-org --all.");
throw new Error("Use an owner name with --all, for example: example-owner --all.");
}

setTargetOrg(flags, flags.targets[0]);
setTargetOwner(flags, flags.targets[0]);
}

function applyRepoTarget(flags: ParsedFlags, target: string): void {
const [org, repo, extra] = target.split("/");
const [owner, repo, extra] = target.split("/");

if (
org === undefined ||
owner === undefined ||
repo === undefined ||
org.length === 0 ||
owner.length === 0 ||
repo.length === 0 ||
extra !== undefined
) {
throw new Error(`Repository targets must look like <org>/<repo>: ${target}`);
throw new Error(`Repository targets must look like <owner>/<repo>: ${target}`);
}

setTargetOrg(flags, org);
setTargetOwner(flags, owner);
flags.repos.push(repo);
}

function setTargetOrg(flags: ParsedFlags, org: string | undefined): void {
if (org === undefined) {
throw new Error("Missing organization target.");
function setTargetOwner(flags: ParsedFlags, owner: string | undefined): void {
if (owner === undefined) {
throw new Error("Missing owner target.");
}

if (flags.org !== undefined && flags.org !== org) {
throw new Error(`Conflicting organization targets: ${flags.org} and ${org}.`);
if (flags.owner !== undefined && flags.owner !== owner) {
throw new Error(`Conflicting owner targets: ${flags.owner} and ${owner}.`);
}

flags.org = org;
flags.owner = owner;
}

export function usage(): string {
return [
"Usage:",
" github-sane-defaults plan <org>/<repo>",
" github-sane-defaults apply <org>/<repo>",
" github-sane-defaults plan <org> --all",
" github-sane-defaults apply <org> --all",
" github-sane-defaults plan --org <org> --repo <repo>",
" github-sane-defaults apply --org <org> --repo <repo>",
" github-sane-defaults plan <owner>/<repo>",
" github-sane-defaults apply <owner>/<repo>",
" github-sane-defaults plan <owner> --all",
" github-sane-defaults apply <owner> --all",
" github-sane-defaults plan --owner <owner> --repo <repo>",
" github-sane-defaults apply --owner <owner> --repo <repo>",
"",
"Options:",
" -y, --yes Skip apply confirmation"
Expand Down
2 changes: 1 addition & 1 deletion src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async function main(): Promise<void> {
return;
}

await applyPlannedDefaults(client, options.org, planned);
await applyPlannedDefaults(client, options.owner, planned);

const summary: ApplySummary = { planned, applied: countChangedRepos(planned) };
console.log("");
Expand Down
Loading
Loading