Skip to content

feat: bootstrap phase 6 reports and offers repository security settings - #27

Merged
TzuH-Hsu merged 6 commits into
mainfrom
feat/26-security-phase
Sep 5, 2026
Merged

feat: bootstrap phase 6 reports and offers repository security settings#27
TzuH-Hsu merged 6 commits into
mainfrom
feat/26-security-phase

Conversation

@TzuH-Hsu

@TzuH-Hsu TzuH-Hsu commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Adds bootstrap phase 6: reports repository visibility and four security settings, and offers to enable the ones that are free. Renumbers phases 6/7/8 to 7/8/9.

Stacked PR — base is fix/24-repo-settings (#25), not main. Both touch phase_repo_settings's neighbourhood in scripts/bootstrap.sh and the same headings in docs/setup/bootstrap.md. Merge #25 first; GitHub will retarget this to main automatically. The diff shown here is this change alone.

Related issue

Closes #26

Validation

  • L0 static — markdownlint, yamllint, offline lychee (46 OK, 0 errors), actionlint, gitleaks, check-skills, check-local-md — all green
  • L1 unit — make test (placeholder; nothing covers bootstrap.sh, see RISK)
  • L2 integration — bash -n clean, shellcheck -s bash clean, API shapes verified read-only, --dry-run run against this repo
  • L3 e2e / preview — see RISK

Every API shape was verified read-only before the phase was written, not assumed:

Probe Result
gh api --help key[subkey]=value nested syntax is documented
GET .../vulnerability-alerts HTTP/2.0 204 No Content, exit 0 when enabled
GET .../automated-security-fixes {"enabled":true,"paused":false}
GET repos/{o}/{r} --jq '[...]' public enabled enabled

The nested-syntax one is load-bearing: it means the security_and_analysis PATCH stays a normal run_or_dry gh api call, visible under --dry-run, instead of needing a piped JSON body that would bypass the dry-run choke point.

Dry run against this repo (public, all four already on — so the correct behaviour is four ok lines, zero prompts, zero writes):

→ Phase 6: security settings
✓ repository visibility: public
✓ secret scanning + push protection: already enabled
✓ Dependabot alerts: enabled
✓ Dependabot security updates: enabled

And the summary confirms all ten phases record exactly once with the new numbering:

✓ 5. Repo settings
✓ 6. Security
✓ 7. Actions permission
- 8. Ruleset
- 9. De-template

Risk / rollback

RISK: L3 not run — the branches that matter most cannot be exercised against this
repository, because it is public and already has all four settings enabled. The
untested paths are: the enable-write on a public repo with the settings OFF, and
the private-repo path where the phase must issue ZERO writes. Both need scratch
repos and are added to the release exit checklist in
docs/template/architecture.md as a new step 3 requiring two scratch repos, one
public and one private. The private-repo assertion is the no-spend guarantee and
is the single most important thing to check before this ships.
Rollback: revert this commit. The phase is purely additive; reverting restores
the previous 8-phase numbering and removes all security handling.

Checklist

  • Conventional Commit PR title
  • Linked issue using "Closes #N"
  • No secrets, no *.local.md files committed
  • Documentation updated where affected — docs/setup/bootstrap.md (new ### 6. Security, renumbering, troubleshooting entry), SECURITY.md (new section), docs/template/architecture.md (release checklist)

The design rule, which is the part to review

The only settings this phase ever enables are free by construction. Anything with a billing consequence is reported and handed back as a MANUAL step.

  • Public repo → offers secret scanning + push protection. Free, reversible. Prompted with default yes, so --yes enables — no more aggressive than phase 7, which already grants Actions the power to create and approve PRs under --yes.
  • Private or internalnever writes, under any flag. Secret scanning there needs a paid GitHub Advanced Security / Secret Protection seat. A setup script must not commit an adopter's account to a per-committer charge. It reports state, explains the licensing, emits a MANUAL step.
  • Dependabot alerts + security updates → free everywhere, so offered regardless of visibility. .github/dependabot.yml:9-10 has always asserted both are on; this is the first thing that checks.
  • Visibility → never changed, never offered. Private → public erases stars and watchers and publishes all Actions history. The phase comment says so explicitly, because "detect whether the repo is public" is exactly the phrasing that invites someone to add that prompt later.

Two states the GitHub UI blurs, kept distinct

Unreadable ≠ disabled. security_and_analysis is only populated for callers with admin permission — it comes back null otherwise. Reporting that as "disabled" would be a lie that reads as reassuring in the wrong direction, so it degrades to warn + manual with an explicit "the token may lack admin" message, and a matching troubleshooting entry.

Enabled ≠ unpaused. automated-security-fixes returns paused as well as enabled. Enabled-but-paused means no fix PR will ever open, so it gets its own warn rather than being folded into "enabled".

Structural notes

  • Follows phase 2's explicit-exit-code idiom throughout — gh api prints HTTP error bodies to stdout, so || true would capture the error body as data.
  • Unlike phases 3/5/8, this phase runs several independent checks, so it accumulates $result and calls record_phase once at the end (the pattern phase_issue_types uses). run_phase's contract wants one record per exit path, not one per check.
  • Idempotency is structural: every branch reads first and only prompts when a setting is actually off. The dry run above is the proof — zero prompts on an already-configured repo.
  • No --skip-security flag: the phase never writes without a prompt and never writes anything with a cost, so there is nothing to skip.

One documented false alarm

GitHub's "Setting repository visibility" doc lists "all push rulesets will be disabled" among the consequences of going public. An adopter reading that will reasonably fear for their main protection. .github/rulesets/main-branch.json is "target": "branch", not "push", so it is unaffected. One sentence in the phase-6 docs says so — the rare case where restating an external fact earns its keep, because the value is in the negation.

TzuH-Hsu and others added 2 commits September 2, 2026 12:05
…efaults

Each default contradicted something the repository already documents.

squash_merge_commit_title=PR_TITLE. AGENTS.md, CONTRIBUTING.md, pr-authoring
and branch-and-commit all state the PR title becomes the commit message on
main. GitHub's COMMIT_OR_PR_TITLE default makes that false whenever a PR has
exactly one commit -- visible in this repo's own log, where #7 and #9 carry
the (#N) suffix and #11, #13 and #15 do not.

squash_merge_commit_message=PR_BODY. The COMMIT_MESSAGES default concatenates
every branch commit message into the main commit body, and release-please
deliberately parses that body for further Conventional Commits and
BREAKING-CHANGE footers. Already live here: f98cbf9's body carries
"* chore: trigger CI on release PR", harmless only because chore is
release-please's hidden bucket. A branch commit reading "fix: wip" would have
produced a phantom changelog entry or an unintended bump.

allow_rebase_merge=false. The comment justifying rebase claimed release-please
merges its own PR. It does not -- ADR-0002, the release-please workflow header
and the release-management skill all require a human merge. Nor has rebase
ever been used: the history has no merge commits, all eight merged PRs were
squash-merged, and both release PRs cut their tags that way.

allow_update_branch=true. The ruleset sets
strict_required_status_checks_policy=false, so the "Update branch" button is
not offered at all without it.

Also corrects skills/release-management/SKILL.md, which instructed
`gh pr merge --merge`. allow_merge_commit=false has been set since day one, so
that command has always returned HTTP 405 -- the documented release procedure
was broken. Its parenthetical was wrong too: release-please does not merge.

Accepted cost: with PR_BODY an unmodified PR template lands verbatim in the
main commit message. BLANK was rejected -- it drops Co-authored-by trailers
and discards the RISK/rollback record. PR_BODY reduces the misparse surface
but does not eliminate it; a Conventional-Commit-shaped line in a PR body is
still parsed.

Closes #24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bootstrap did nothing about security settings -- grep for secret, scanning,
visibility, vulnerability or dependabot in the script returned nothing. Two
consequences. Converting a repo private to public grants ACCESS to secret
scanning but does not enable it, and push protection in particular must be
switched on explicitly, which is how a public repo ends up without it. And
.github/dependabot.yml asserts Dependabot alerts and security updates are
enabled while nothing verifies that.

The write path is bounded by cost, not by capability: the only settings this
phase ever enables are free by construction.

- Public repo: offers secret scanning and push protection, both free.
- Private or internal: never writes them, under any flag. There they need a
  paid Advanced Security / Secret Protection seat, and a setup script must not
  commit an adopter's account to a per-committer charge. It reports the state
  and emits a MANUAL step.
- Dependabot alerts and automated security fixes: free everywhere, so offered
  regardless of visibility.
- Repository visibility is never changed and never offered. Private to public
  erases stars and watchers and publishes all Actions history -- a one-way
  door, and the phase comment says so, because "detect whether the repo is
  public" invites someone to add that prompt later.

Two states the GitHub UI blurs are kept distinct. Unreadable settings mean the
token lacks admin, not that the setting is off, so security_and_analysis
coming back null degrades to warn plus MANUAL rather than reporting "disabled".
And automated-security-fixes returns {"enabled":..,"paused":..}: enabled but
paused means no fix PR ever opens, so it is reported separately.

Verified read-only before writing the phase: gh api documents the
key[subkey]=value nested syntax, so the security_and_analysis PATCH stays a
normal run_or_dry call visible under --dry-run; /vulnerability-alerts returns
204 enabled and 404 disabled; /automated-security-fixes returns enabled and
paused.

Unlike phases 3/5/8 this phase runs several independent checks, so it
accumulates a result and calls record_phase once at the end -- the pattern
phase_issue_types uses, matching run_phase's one-record-per-exit-path
contract.

Phases 6/7/8 renumber to 7/8/9 across the script and docs. Because every read
runs for real under --dry-run, bootstrap --dry-run now doubles as a zero-risk
security audit.

Closes #26

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TzuH-Hsu TzuH-Hsu added this to the v0.3.0 milestone Sep 2, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T13:00:37.767356Z 8f97592 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a43bc8fb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/bootstrap.sh
Comment on lines +763 to +766
if [ "$visibility" = "public" ]; then
if [ "$secret_scanning" = "enabled" ] && [ "$push_protection" = "enabled" ]; then
ok "secret scanning + push protection: already enabled"
else

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat unreadable secret-scanning state as unknown

When a caller can read the public repository but lacks permission to view security_and_analysis, the query produces public\tunknown\tunknown, so facts is nonempty and this branch treats both unknown values as disabled. The script then prompts to enable the settings—or attempts the PATCH automatically under --yes—instead of reporting the unreadable state and issuing the documented manual step. Add an explicit unknown path before deciding whether either setting is off.

Useful? React with 👍 / 👎.

Comment thread docs/setup/bootstrap.md
updates". On a private repo the first two require a Secret Protection licence;
the Dependabot pair are free everywhere.

### 7. Actions PR permission

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Point Actions troubleshooting to phase 7

After renumbering the Actions permission step to phase 7, the troubleshooting section at lines 268–271 still says the failure means phase 6 was skipped or declined and tells users to accept the phase 6 prompt. Phase 6 is now Security, so this recovery guidance directs users to the wrong prompt; update both references to phase 7.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 978df54611

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/bootstrap.sh
if run_or_dry gh api -X PATCH "repos/${REPO}" \
-f 'security_and_analysis[secret_scanning][status]=enabled' \
-f 'security_and_analysis[secret_scanning_push_protection][status]=enabled'; then
ok "secret scanning + push protection enabled"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve disabled states in dry-run output

When any security setting is disabled and the script runs with --dry-run (especially with --yes), run_or_dry performs no mutation but returns success, so this line reports the protections as enabled and the phase summary remains green. The same fall-through occurs for Dependabot alerts at line 807 and security updates at line 836. This makes the documented security-audit mode misleading: its final status contradicts the real reads and can leave an operator believing disabled protections are active.

Useful? React with 👍 / 👎.

1. An unreadable secret-scanning state was treated as disabled. security_and
   _analysis is only populated for callers with admin on the repo, so a token
   without it yields "public\tunknown\tunknown" -- a non-empty facts line that
   fell through to the else branch and prompted to enable, or PATCHed outright
   under --yes. That acts on a guess and reports a state never observed, which
   is the opposite of the unknown-is-not-disabled rule this phase was written
   around. There is now an explicit unknown branch that warns, emits a manual
   step, and writes nothing.

2. The Actions-permission troubleshooting entry still said phase 6 after the
   renumber moved it to 7, so the recovery text pointed at the Security prompt
   instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 656e4ef627

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/bootstrap.sh
Comment on lines +802 to +806
else
warn "repository is ${visibility}: secret scanning (${secret_scanning}), push protection (${push_protection})"
warn " on a private/internal repo these need a paid GitHub Advanced Security / Secret Protection seat"
warn " bootstrap will not enable them for you — that is a billing decision, not a setup step"
manual "Private repo: decide whether to license GitHub Secret Protection, then enable secret scanning + push protection in Settings → Advanced Security"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip the warning for enabled private protections

When a private or internal repository already has licensed Secret Protection and both statuses read enabled, this unconditional branch still marks phase 6 as a warning and queues an instruction to license and enable them. A fully configured repository therefore cannot receive a clean idempotent audit and the operator is told to perform an already-completed action; handle the both-enabled state before emitting the visibility warning.

Useful? React with 👍 / 👎.

@TzuH-Hsu

TzuH-Hsu commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Codex P2s addressed

1. Unreadable state treated as disabled. Correct, and it broke the rule this phase was written around. security_and_analysis is only populated for callers with admin, so a token without it produced public\tunknown\tunknown — a non-empty facts line that fell through to the enable branch and prompted, or PATCHed outright under --yes. There is now an explicit unknown branch that warns, emits a manual step, and writes nothing.

Verified with a gh stub that logs every invocation, counting real PATCH calls rather than grepping output:

State result security PATCH calls
public, both enabled ok 0
public, unreadable warn 0
public, disabled ok 1
private, disabled warn 0

2. Stale phase-6 reference. Fixed — the Actions-permission troubleshooting entry now says phase 7 in both places.

@TzuH-Hsu
TzuH-Hsu deleted the branch main September 5, 2026 12:50
@TzuH-Hsu TzuH-Hsu closed this Sep 5, 2026
@TzuH-Hsu TzuH-Hsu reopened this Sep 5, 2026
@TzuH-Hsu
TzuH-Hsu changed the base branch from fix/24-repo-settings to main September 5, 2026 12:55
@TzuH-Hsu
TzuH-Hsu merged commit 8a62e5c into main Sep 5, 2026
1 check passed
@TzuH-Hsu
TzuH-Hsu deleted the feat/26-security-phase branch September 5, 2026 12:57
@github-actions github-actions Bot mentioned this pull request Sep 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f97592e96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/bootstrap.sh
# wording covers "or not visible" rather than asserting the wrong one.
if gh api "repos/${REPO}/vulnerability-alerts" >/dev/null 2>&1; then
ok "Dependabot alerts: enabled"
elif confirm "Enable Dependabot alerts? (free on every plan; .github/dependabot.yml assumes it)" "y"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve unreadable Dependabot states

When the token can read the repository but lacks admin access, this GET can fail with 403 exactly as the preceding comment notes, yet the failure immediately triggers an enable prompt and, under --yes, an unauthorized PUT. Declining also emits a manual enable step even though the setting may already be enabled. The automated-security-fixes check at lines 832–845 has the same problem, so both failed reads should be reported as unknown rather than treated as disabled.

Useful? React with 👍 / 👎.

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.

Bootstrap does nothing about secret scanning, push protection or Dependabot alerts

1 participant