Skip to content

feat: rulesets, persistent state, issue-driven repo provisioning#21

Merged
avrabe merged 1 commit intomainfrom
feat/rulesets-and-issue-driven-provisioning
Apr 25, 2026
Merged

feat: rulesets, persistent state, issue-driven repo provisioning#21
avrabe merged 1 commit intomainfrom
feat/rulesets-and-issue-driven-provisioning

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented Apr 25, 2026

Summary

  • Rulesets API replaces legacy branch protection. New src/rulesets.js uses POST /repos/{owner}/{repo}/rulesets targeting ~DEFAULT_BRANCH, which applies on empty repos. Kills the 5×2s retry race on repository.created. Translates the existing branch_protection.default config block into a ruleset payload — no config rewrite required. Falls back to legacy branch protection on 404/422/403.
  • Issue-driven repo provisioning (Cut A — Temper as the brain). New issues.opened handler watches a configurable controller repo (e.g. pulseengine/repo-requests), validates the issue-form body (src/provisioning.js), enqueues a provision-repo task. The task creates the repo (template-based when requested), applies full configuration, comments the URL on the source issue, and closes it with state_reason: completed. Issue-form starter under docs/controller-repo-template/.
  • Persistent state replaces in-memory Maps. New src/persistent-kv.js (SQLite-backed KV with TTL) backs both webhook idempotency and AI review rate limits. PM2 restart no longer re-processes webhooks or re-reviews PRs. merge-strategy.js's 1-hour revert is now a persistent task (revert-merge-settings) instead of setTimeout.
  • Scheduler wired from registerApp. Was exported from src/app.js but never imported in index.js — meaning every queued task in production sat unprocessed. Now uses an installation-token factory so each tick gets a fresh, installation-scoped Octokit. New handlers: revert-merge-settings, reconcile-repo, provision-repo.

Empty-repo bootstrap

repository.created no longer bails when the default branch is missing. Rulesets, merge settings, and labels are applied immediately; branch-scoped work (templates, codeowners, dependabot) is deferred to a reconcile-repo task that fires after the first push.

Files

File Why
src/rulesets.js New: list-then-create-or-update by name, idempotent. Translator from branch_protection.default.
src/persistent-kv.js New: SQLite-backed TTL'd KV.
src/provisioning.js New: parse issue-form body, validate, create repo (template/plain), comment + close source issue.
src/app.js Wires issues.opened, initPersistence, initScheduler from registerApp. Per-tick installation-token factory.
src/repository.js New skipBranchScopedWork mode for empty repos.
src/merge-strategy.js setTimeout → persistent task.
src/idempotency.js, src/ai-review.js Optional persistent backend; in-memory fallback for tests.
src/scheduler.js Accepts factory or octokit (back-compat).
src/schema.js Validates new rulesets and controller_repo sections.
config.yml New rulesets: (enabled by default) and controller_repo: (disabled by default) sections.
docs/controller-repo-template/ Starter issue-form schema + README. Operator fills in pulseengine-specific fields.

Test plan

  • All 698 tests pass (was 654; +44 covering rulesets, persistent-kv, provisioning, issues.opened, and the new skipBranchScopedWork path)
  • npm run lint clean
  • Coverage above thresholds: 84.27% lines / 86.5% funcs / 78.08% branches
  • After merge + self-update: create a new repo in pulseengine — verify the ruleset appears immediately and the 5×2s retry log line is gone
  • After merge: run /allow-merge-commit on a PR with signed commits, kill the bot mid-revert, restart, confirm the persistent task still flips merge settings back
  • Confirm AI review fires on this PR — the user noted it hasn't been seen working recently. Zero AI review comments on PRs fix: consolidate CI fixes, require PR reviews, and update README #13fix: repo creation race condition + AI review timeouts #20. If this PR also gets none, Ollama is down on the netcup VM and we'll fix in a follow-up.

Risk & rollout

  • Risk: medium. Rulesets are enabled by default in this commit but fall_back_to_legacy: true means a 404/422/403 falls through to the legacy branch-protection path. Controller-repo provisioning is off by default (controller_repo.enabled: false).
  • Rollout: self-update on merge. After deploy:
    1. Set controller_repo.enabled: true in config.local.yml.
    2. Create pulseengine/repo-requests from docs/controller-repo-template/.
    3. (Optional) Run /sync-all-repos to re-apply configuration via the new ruleset path on existing repos.

Checklist

  • Code follows repo standards
  • Docs updated (CHANGELOG, controller-repo README)
  • No secrets committed
  • data/ and .claude/ added to .gitignore

🤖 Generated with Claude Code

## Summary
- **Rulesets API replaces legacy branch protection.** New `src/rulesets.js`
  uses `POST /repos/{owner}/{repo}/rulesets` targeting `~DEFAULT_BRANCH`,
  which applies on empty repos. Kills the 5×2s retry race on
  `repository.created`. Translates the existing `branch_protection.default`
  config block into a ruleset payload — no config rewrite required.
  Falls back to legacy branch protection when rulesets are unavailable.
- **Issue-driven repo provisioning (Cut A).** New `issues.opened` handler
  watches a configurable controller repo (e.g. `pulseengine/repo-requests`),
  validates the issue-form body (`src/provisioning.js`), enqueues a
  `provision-repo` task. Handler creates the repo (template-based when
  requested), applies full configuration, comments the URL on the source
  issue, closes the issue with `state_reason: completed`. Issue form
  schema starter under `docs/controller-repo-template/`.
- **Persistent state replaces in-memory Maps.** New `src/persistent-kv.js`
  (SQLite-backed KV with TTL) backs both webhook idempotency and AI review
  rate limits. PM2 restart no longer re-processes webhooks or re-reviews
  PRs. `merge-strategy.js` 1-hour revert is now a persistent task
  (`revert-merge-settings`) instead of `setTimeout`.
- **Scheduler wired from `registerApp`.** Was exported but never imported
  in `index.js`. Now uses an installation-token factory so every tick gets
  a fresh, installation-scoped Octokit. New handlers: `revert-merge-settings`,
  `reconcile-repo`, `provision-repo`.

## Empty-repo bootstrap
`repository.created` no longer bails when the default branch is missing.
Rulesets, merge settings, and labels are applied immediately; branch-scoped
work (templates, codeowners, dependabot) is deferred to a `reconcile-repo`
task that fires after the first push.

## Test plan
- [x] All 698 tests pass (was 654; added 44 covering rulesets, persistent-kv,
      provisioning, issues.opened, and the new `skipBranchScopedWork` path)
- [x] eslint clean (`npm run lint`)
- [x] Coverage above thresholds: 84.27% lines / 86.5% funcs / 78.08% branches
- [ ] After merge + self-update: create a new repo in pulseengine — verify
      ruleset appears immediately and the 5×2s retry log is gone
- [ ] After merge: `/allow-merge-commit` — kill the bot mid-revert, restart,
      verify the merge setting still reverts via the persistent task store

## Risk & rollout
- Risk: medium. Rulesets are opt-in via `rulesets.enabled: true` (default in
  this commit); legacy fall-back kicks in on 404/422/403. Controller repo
  is opt-in via `controller_repo.enabled: false` (default off).
- Rollout: self-update on merge. After deploy, set
  `controller_repo.enabled: true` and create `pulseengine/repo-requests`
  with the form from `docs/controller-repo-template/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avrabe avrabe merged commit c01f18d into main Apr 25, 2026
5 checks passed
@avrabe avrabe deleted the feat/rulesets-and-issue-driven-provisioning branch April 25, 2026 20:05
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.

1 participant