Skip to content

feat(cli): github-settings-as-code and gsac commands over the library - #152

Merged
Vivswan merged 6 commits into
mainfrom
feat/cli
Sep 12, 2026
Merged

Vivswan merged 6 commits into
mainfrom
feat/cli

Conversation

@Vivswan

@Vivswan Vivswan commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner
bunx @vivswan/github-settings-as-code@next check --repository o/r --settings-file .github/settings.yml
  • The npm package is also a command: github-settings-as-code and gsac, built by tsdown from src/cli.ts to lib/pkg/cli.js, plain Node 22+.
  • check, apply, and merge are the action's modes: the subcommand is the mode input, every other input of that mode is a --flag generated from INPUT_DECLS, and the library's parseConfig derives the config. A test pins argv-to-config equal to the action's env-to-config over every RunConfig arm.
  • validate <file> and permissions <file> read a settings file alone, no token.
  • Globals: --token (or GITHUB_TOKEN), --json (one object of outputs on stdout), --summary <file>, --verbose. Exit codes are the action's.
  • The CLI is a second consumer of src/index.ts: the new cli layer in architecture.yml has the action's one edge.
  • Guide: docs/start/cli.md.
Technical details
Command Flags Exit
check every apply/check input as --<input> <value> (--repository, --settings-file, --on-missing-permission, --required-sections, --sections, --api-version, --repos, --repos-dir, --defaults-file, --private-repos, --private-report, --visibility, --archived, --forks, --exclude, --topics, --affiliation) 0 clean or partial, 1 drift or failure
apply the same flags 0 applied or partial, 1 failure
merge --settings-file (repeatable or comma list), --merged-file, --layering 0 merged, 1 failure
validate <file> none 0 valid, 1 invalid or unreadable
permissions <file> none 0, one grant line per declared section
global --token, --json, --summary <file>, --verbose
  • Which inputs are lists is one declaration: InputDecl.list in src/flows/inputs.ts, read by parseConfig's list() port (a name without it cannot be split) and by the CLI (a list flag repeats and accumulates; any other value flag, --token and --summary included, refuses a second value). A complete Record<InputName, ...> test runs one repeated-argv case per input.
  • --private-report artifact is refused before parsing (the artifact upload needs the Actions runner); report-public-key is not a flag. input-token-missing and input-repository-not-slug are reworded with the terminal's remedy; the --repository help says the flag is required outside Actions.
  • The Io (src/cli/io.ts) is consola with one reporter: annotations and the debug trace on stderr as level: message, logs on stdout (stderr under --json), outputs collected and flushed as name=value lines or one JSON object. Every writer, commander's messages included, goes through one redacting stream boundary registered with the token before the parse; the stream honors the target's backpressure.
  • src/cli/commands.ts ends every run where the action does (concludeRun, concludeMerge, or the failure tail failRun produces).
  • tsdown builds two entries; the shared code lands in one hashed chunk both index.js and cli.js import. lib/pkg/cli.js keeps the shebang and is chmod +x by tsdown; its empty .d.ts is removed after the build.
  • The package smoke runs the installed node_modules/.bin/gsac --help (asserting every subcommand), gsac validate on a valid file (exit 0), and on an invalid file (exit 1, the control).
  • .github/scripts/changed-sections.ts lists src/cli/ and src/cli.ts as all-selecting, and its import scanner tolerates the shebang line.
  • snapshot and init arrive with the snapshot mode (task 77).

Reviewer note (Copilot): report a finding only with a concrete input and the wrong output it produces.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 2 warning(s).

File Size Tier Cap
.github/workflows/post-green.yml:54 205 chars warn 150
.github/workflows/post-green.yml:55 193 chars warn 150

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

@Vivswan
Vivswan force-pushed the feat/library-package branch 2 times, most recently from d45b6fa to a3d764a Compare September 12, 2026 01:39
@Vivswan
Vivswan force-pushed the feat/cli branch 2 times, most recently from 33d86eb to f9b7e3d Compare September 12, 2026 02:19
@Vivswan Vivswan closed this Sep 12, 2026
@Vivswan Vivswan reopened this Sep 12, 2026
@Vivswan
Vivswan force-pushed the feat/library-package branch from 379c9b1 to 6ea8aa1 Compare September 12, 2026 03:28
Base automatically changed from feat/library-package to main September 12, 2026 04:23
Copilot AI balanced review requested due to automatic review settings September 12, 2026 05:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Overlapping masked values can expose a secret suffix in CLI output.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a packaged Node CLI that exposes the library's apply, check, merge, validate, and permissions workflows.

Changes:

  • Adds CLI parsing, execution, output formatting, and secret redaction.
  • Publishes github-settings-as-code and gsac binaries.
  • Adds CLI tests, package smoke coverage, architecture rules, and documentation.
File summaries
File Description
src/cli.ts Adds the executable entrypoint.
src/cli/program.ts Defines commands and global options.
src/cli/commands.ts Connects commands to library flows.
src/cli/inputs.ts Maps action inputs to CLI flags.
src/cli/io.ts Implements terminal output and masking.
src/flows/inputs.ts Declares and parses list inputs.
tsdown.config.ts Builds library and CLI entries.
package.json Adds binaries and runtime dependencies.
bun.lock Locks new CLI dependencies.
architecture.yml Adds the CLI architecture layer.
AGENTS.md Documents CLI layering guidance.
.github/scripts/package-smoke.ts Smoke-tests the packaged CLI.
.github/scripts/changed-sections.ts Makes CLI changes select all sections.
test/scripts/changed-sections.test.ts Tests shebang import scanning.
test/package-json.test.ts Pins binary and dependency metadata.
test/cli/streams.ts Adds in-memory test streams.
test/cli/program.test.ts Tests command behavior and exits.
test/cli/io.test.ts Tests output routing and masking.
test/cli/inputs.test.ts Tests argv-to-config parity.
test/cli/backpressure.test.ts Tests stream backpressure behavior.
test/docs/readme.test.ts Includes the CLI guide link.
test/docs/guides.test.ts Requires the CLI guide.
README.md Introduces terminal usage.
docs/start/cli.md Documents commands, flags, and output.
docs/reference/library.md Links library users to the CLI.
docs/reference/architecture.md Documents the CLI layer.
docs/README.md Adds CLI documentation navigation.
Review details
  • Files reviewed: 26/27 changed files
  • Comments generated: 1
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/cli/io.ts Outdated
Copilot AI review requested due to automatic review settings September 12, 2026 05:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Overlapping masked token values can leak a token suffix in CLI output.

Review details

Suppressed comments (1)

src/cli/io.ts:65

  • Overlapping masked values can expose the suffix of a token because shorter values are replaced first. For --token ghp_abc --token ghp_abcdef, the masks are registered in that order, so Commander's echoed second value becomes ***def instead of ***. Replace longer masks first so one masked value cannot partially consume another.
    for (const value of registry.masked()) {
      out = out.replaceAll(value, "***");
    }
  • Files reviewed: 26/27 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Copilot AI review requested due to automatic review settings September 12, 2026 05:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Check and apply incorrectly treat repeated --settings-file flags as accumulative before rejecting them later.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 27/28 changed files
  • Comments generated: 1
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Comment thread src/cli/inputs.ts
Copilot AI review requested due to automatic review settings September 12, 2026 06:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation, packaging, documentation, and comprehensive tests are consistent with the stated CLI behavior.

Review details
  • Files reviewed: 27/28 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

The npm package gains a command line built by tsdown from src/cli.ts to lib/pkg/cli.js, with the bin names github-settings-as-code and gsac. It is a second consumer of the public entry: the new cli layer in architecture.yml has the action's one edge, src/index.ts.

check, apply, and merge mirror the action's modes: the subcommand is the mode input and every other input the mode reads is a --kebab-case flag generated from INPUT_DECLS, so the help text and the inputs reference share one declaration. The read port is built from commander's parsed options and handed to the library's parseConfig, and test/cli/inputs.test.ts pins argv-to-config equal to a record-backed port shaped like the runner's, over every RunConfig arm and the rejections. validate and permissions read a settings file alone; snapshot and init name the snapshot mode this library build does not carry and exit 1.

The Io over consola prints annotations to stderr as level: message, log lines to stdout, outputs as name=value lines or one JSON object under --json, and appends the summary to --summary's file; every channel is masked, and the token is registered before anything can echo it.

Packaging: bin in package.json, the three runtime dependencies pinned, a second tsdown entry, the package smoke running the installed gsac --help and validate under Node, and the changed-sections selector listing the cli paths. The smoke's consumers read validateSettings as the Result it returns on this base.

Docs: docs/start/cli.md, a CLI section in the library reference, the README paragraph and table row, and the AGENTS.md bullet for the layer.
…en the README cap

The help usage line names github-settings-as-code (gsac in the description) instead of commander's default "cli". A repeated flag now accumulates only for the inputs parseConfig splits (LIST_INPUTS, pinned against the library's own reading of each pair); every other value flag, --token and --summary included, rejects a second value with commander's message naming the flag, so no argv can form a joined value the action never receives. The README line budget is 101, one over the file.
…rtifact channel, drop the snapshot placeholders

Which inputs are lists is now one declaration: InputDecl gains `list`, parseConfig reads such inputs only through its list() port (a name without the attribute cannot be split), and the CLI lets exactly those flags repeat. A complete Record over every input runs one repeated-argv case per name, so a new list input cannot be missed.

The snapshot and init subcommands are gone until the snapshot mode exists; no build could run them. `--private-report artifact` is refused before parsing (the upload needs the Actions runner) and report-public-key is not a flag. The no-token and not-a-slug problems name --token, --repository, and the environment variables; the --repository help says the flag is required outside Actions.

Tests assert whole outcomes: exact option sets and descriptions per subcommand, the fold's own shape in the merged file, the whole --json object with exit code and stderr, and where an unnamed summary goes. The package smoke gains a failing control (validate on an invalid file exits 1). The redacting stream honors the target's backpressure, with a guard test seen failing on the old code; the bin's empty declaration file is removed after the build; docs and AGENTS.md trimmed to what the code cannot show.
…ument, assert the full refusal line

failRun takes a describe parameter defaulting to describeProblem, so the command line hands it describeCliProblem instead of re-implementing the failure tail; the artifact channel's refusal is an input-unsupported-value problem whose allowed list is derived from the channels. The merge test compares the whole merged document the fold writes, and the single-value refusal test asserts the exact stderr line.
…secrets leave no fragment

Replacing one masked value after another left a remainder whenever two values overlapped in the text: a prefix of a longer token printed ***DEF, and "ABC" with "BCD" across "ABCD" printed ***D. The redactor now locates every occurrence of every masked value in the original text, merges overlapping or touching ranges, and replaces each merged range with one ***, for both streams and the summary file.
Copilot AI review requested due to automatic review settings September 12, 2026 06:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The CLI behavior and packaging are comprehensively tested; only a minor crash-message issue remains.

Review details
  • Files reviewed: 27/28 changed files
  • Comments generated: 1
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

Comment thread src/cli/program.ts
…nted, not at re-running

The unexpected-error line asked for a --verbose re-run even when the stack was already on the terminal. The remedy now depends on the flag: without it, re-run for the stack; with it, the stack above is the report to file. Both branches are pinned over the same crash.
Copilot AI review requested due to automatic review settings September 12, 2026 07:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Bare invocation silently succeeds, and file-only read failures recommend an unavailable input.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/cli/program.ts:114

  • Invoking the binary without a subcommand exits successfully without doing anything. For gsac (or gsac --json), Commander runs no action, exitCode remains 0, and the user gets no usage or error output. Add a root action that prints help and returns a nonzero code when no subcommand was selected.
    src/cli/commands.ts:72
  • The file-only commands point users to an option they do not accept when a file cannot be read. For gsac validate missing.yml, this role produces set the "settings-file" input if it lives elsewhere, but validate and permissions only accept the positional <file>. Render unreadable-file problems for these commands with advice to correct the positional path instead.
  • Files reviewed: 27/28 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

@Vivswan
Vivswan marked this pull request as ready for review September 12, 2026 07:09
@Vivswan
Vivswan merged commit 364a9c6 into main Sep 12, 2026
39 checks passed
@Vivswan
Vivswan deleted the feat/cli branch September 12, 2026 07:12
Vivswan added a commit that referenced this pull request Sep 12, 2026
…shared parser

Main's CLI (#152) is total over the parsed modes: its dispatcher and per-mode flag split are typed over RunConfig and MODES, so the snapshot arm needs a home. The dispatcher concludes a snapshot config the way the action does, inputsForMode derives each mode's flags from the exported per-mode input lists (MERGE_ONLY_INPUTS and SNAPSHOT_ONLY_INPUTS now exported), and the program gains the snapshot subcommand with its guide row. The README folds its two closing sentences into one to stay inside the front-door line budget.
Vivswan added a commit that referenced this pull request Sep 12, 2026
…shared parser

Main's CLI (#152) is total over the parsed modes: its dispatcher and per-mode flag split are typed over RunConfig and MODES, so the snapshot arm needs a home. The dispatcher concludes a snapshot config the way the action does, inputsForMode derives each mode's flags from the exported per-mode input lists (MERGE_ONLY_INPUTS and SNAPSHOT_ONLY_INPUTS now exported), and the program gains the snapshot subcommand with its guide row. The README folds its two closing sentences into one to stay inside the front-door line budget.
Vivswan added a commit that referenced this pull request Sep 12, 2026
…shared parser

Main's CLI (#152) is total over the parsed modes: its dispatcher and per-mode flag split are typed over RunConfig and MODES, so the snapshot arm needs a home. The dispatcher concludes a snapshot config the way the action does, inputsForMode derives each mode's flags from the exported per-mode input lists (MERGE_ONLY_INPUTS and SNAPSHOT_ONLY_INPUTS now exported), and the program gains the snapshot subcommand with its guide row.
Vivswan added a commit that referenced this pull request Sep 12, 2026
…shared parser

Main's CLI (#152) is total over the parsed modes: its dispatcher and per-mode flag split are typed over RunConfig and MODES, so the snapshot arm needs a home. The dispatcher concludes a snapshot config the way the action does, inputsForMode derives each mode's flags from the exported per-mode input lists (MERGE_ONLY_INPUTS and SNAPSHOT_ONLY_INPUTS now exported), and the program gains the snapshot subcommand with its guide row.
Vivswan added a commit that referenced this pull request Sep 12, 2026
…shared parser

Main's CLI (#152) is total over the parsed modes: its dispatcher and per-mode flag split are typed over RunConfig and MODES, so the snapshot arm needs a home. The dispatcher concludes a snapshot config the way the action does, inputsForMode derives each mode's flags from the exported per-mode input lists (MERGE_ONLY_INPUTS and SNAPSHOT_ONLY_INPUTS now exported), and the program gains the snapshot subcommand with its guide row.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants