You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: GitOps loop — plan on PR, gated apply, scheduled drift detection #24
Wrap the CLI in the GitOps loop that makes desired-state code actually reproducible in practice: every change is a PR with a visible plan, applies are gated, and drift is detected continuously instead of when someone remembers to run ct plan. Lives mostly in eqrm/ct-structure; ct-cli grows the machine-facing affordances. Part of the Phase 6 epic. Depends on environments + ct-structure bootstrap.
Scope
ct-cli (tool side):
Terraform-style --detailed-exitcode for ct plan: 0 = no changes, 1 = error, 2 = changes present. INCOMPLETE stays exit 1 (src/commands/plan.ts:102-113). Drift-only vs config-diff distinction surfaced in --json (summary.drifted separate from summary.hasChanges; per-item changes[].source = config/drift/config+drift). Tests: tests/plan-detailed-exitcode-command.test.ts (9 passing).
Non-interactive auth: already exists — CT_HOST + CT_LOGINTOKEN env vars bypass the Keychain (src/auth/tokenStore.ts). Remaining: document the CI usage in the workflow README.
Plan output rendering suitable for a PR comment. renderPlan/renderPermissionPlan use picocolors, which drops ANSI when stdout isn't a TTY or NO_COLOR is set; +/~/-/!/? prefixes carry the meaning with no color. Documented in README §"Posting a plan as a PR comment" incl. the CI=true-forces-color gotcha (set NO_COLOR=1) and a full GitHub Actions capture-into-<details> example.
ct-structure (repo side):
PR workflow: ct plan --env dev + --env prod, post/update the plan as a PR comment, fail the check on plan errors.
Apply workflow: merge to main → ct apply --env dev automatically; ct apply --env prod behind a GitHub Environment with required reviewers (the confirmation prompt's CI equivalent).
Scheduled drift job (nightly): ct plan --detailed-exitcode per env; on exit 2, open/update a "drift detected" issue with the plan output; close it when clean.
Secrets: per-env CT token as environment-scoped repo secrets.
Acceptance criteria
A structure change lands as: PR with visible plans → review → merge → dev applied → prod applied after approval. No local CLI run needed.
Manual change in the CT UI to a managed resource → drift issue appears within 24h, with the exact field diff.
ct plan --detailed-exitcode exit codes covered by tests (tests/plan-detailed-exitcode-command.test.ts: 0/1/2 incl. INCOMPLETE-stays-1 and permission-only-changes→2).
Status (autonomous wave, 2026-07-09): both halves are merged.
Tool side (PR feat(plan): --detailed-exitcode, drift-aware --json, CI usage docs #63): ct plan --detailed-exitcode (0/1/2, INCOMPLETE stays 1, permission-only changes → 2, covered by tests), --json gains per-change source: config|drift|config+drift + summary (attribution traced correct in review), NO_COLOR guidance (picocolors force-colors under CI=true), README CI-usage docs.
Repo side (eqrm/ct-structure#2): plan.yml (sticky PR comment, both envs, fails only on exit 1), apply.yml (dev auto → prod behind production environment gate with --confirm-env prod; state write-back commits each env's ct-state file back to main even on partial apply failure, rebase-retry push, paths-ignore retrigger guard), drift.yml (nightly + manual; exit 2 → create/update a drift issue with source attribution; exit 0 → close; hard-fails on unexpected exit codes). Three review rounds; zero outstanding findings.
Remaining (user setup, then a supervised first run — exact steps in eqrm/ct-structure#2's body): repo secrets CT_DEV_TOKEN + CT_PROD_TOKEN, GitHub Environment production with required reviewers, branch-protection bypass for the Actions bot's state write-back pushes. Acceptance (drift issue within 24h, merge-to-applied with no local CLI) is then provable live.
✅ production GitHub Environment created via API, deployment branch policy = main only.
❌ Required reviewers are impossible on this billing plan: environment protection rules on PRIVATE repos need GitHub Enterprise Cloud (Free/Pro/Team = public repos only; API returns 422 — verified live). Adaptation in ct-structure PR feat: Phase 2 — Read/Adopt (ct adopt) + state file #4: apply-prod runs only on manual workflow_dispatch (the dispatch is the human gate, honestly documented as weaker), with a success()-guarded gate so a failed dev rehearsal can never unlock prod.
🐛 PR feat: Phase 2 — Read/Adopt (ct adopt) + state file #4 also fixes a blocker its own CI run exposed: workflows couldn't npm ci the private ct-cli git dependency (no SSH key; GITHUB_TOKEN is repo-scoped) → an auth step rewrites ssh→token-https via a new CT_CLI_READ_TOKEN secret.
Remaining (user): create secrets CT_DEV_TOKEN, CT_PROD_TOKEN, CT_CLI_READ_TOKEN (fine-grained PAT, contents:read on eqrm/ct-cli) → PR #4's plan check should go green → merge PR #4 → supervised first run of each workflow. Setup checklist is in the PR body / README.
Handoff state (2026-07-10): everything mergeable is merged except one PR, and one secret is missing.
✅ Tool side complete (PR feat(plan): --detailed-exitcode, drift-aware --json, CI usage docs #63). ✅ Workflows authored + 3 review rounds (ct-structure#2, merged). ✅ production environment created via API (deployment branch policy = main). ✅ Secrets CT_DEV_TOKEN + CT_PROD_TOKEN set.
❌ CT_CLI_READ_TOKEN missing (fine-grained PAT, contents:read on eqrm/ct-cli) — without it no workflow can npm ci the private git dep, so ct-structure PR feat: Phase 2 — Read/Adopt (ct adopt) + state file #4 (dispatch-gated prod apply — required reviewers need Enterprise Cloud on private repos — + the CI dep-auth step, all review findings fixed incl. the insteadOf --add correction) cannot go green.
Non-interactive auth (CT_HOST/CT_LOGINTOKEN) documented in README §CI usage.
PR-comment rendering documented (README §"Posting a plan as a PR comment"), including the CI=true picocolors gotcha and a GitHub Actions example that captures the plan into a <details> block.
Everything remaining on #24 is ct-structure-side (separate private repo): the plan/apply/drift workflows and env-scoped secrets. Per the roadmap that is handoff item #3 — it needs the CT_CLI_READ_TOKEN fine-grained PAT secret so ct-structure can npm install ct-cli, then ct-structure PR #4 goes green. No further ct-cli code changes are required for #24.
ct-cli now publishes to GitHub Packages — merged PR #83 (87eb5d9). This is the no-PAT cross-repo path (your GitHub-Packages recollection was right; it applies to registries, not raw git deps).
Validated live: the release.yml run on the merge (29094017769) went fully green — semantic-release reached analyzeCommits, i.e. the new @semantic-release/npmverifyConditions (GitHub Packages auth) passed. The ci: commit is non-releasing, so nothing published yet; the package first publishes on the next releasable feat/fix merged to main.
Consumer side (eqrm/ct-structure — separate PR there):
package.json: depend on @eqrm/ct-cli instead of the ssh://…/ct-cli.git git URL.
workflows: permissions: { packages: read, contents: read }; actions/setup-node with registry-url: https://npm.pkg.github.com, scope: '@eqrm'; npm ci with env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}. No PAT.
one-time click: in ct-cli's package settings, grant eqrm/ct-structureActions access to the @eqrm/ct-cli package (after it first publishes).
Reviewed by an agent across 7 wiring dimensions (version bump, plugin order, auth/whoami blast-radius, dist packaging, no-public-npm-leak, repository field, YAML) — all clean. One pre-existing follow-up filed: the GitHub Release ct-cli.tgz asset stays at 0.1.0.
GitOps loop is live and validated end-to-end (2026-07-11).
The last blocker — ct-structure's CI could not install the private ct-cli dependency — is resolved by consuming @eqrm/ct-cli from GitHub Packages with the built-in GITHUB_TOKEN (no PAT). Chain: #84/PR #85 fixed the release tarball and triggered the first Packages publish (@eqrm/ct-cli@1.2.2) → eqrm/ct-structure#8 switched the consumer + all three workflows to it → #7 (dev state) and #4 (prod gate) merged in a safe order so no push ever triggered an unsupervised apply.
All three workflows proven live:
plan.yml — verified on eqrm/ct-structure#8: posts the sticky dual-env plan comment; passes on exit 0/2.
apply.yml — first apply-dev ran on merge: healed one real dev drift (a stray ct-cli bug: dynamic ruleset round-trip assumption unverified — CT-recomputed fields would cause perpetual diffs #36 test probe on group #1698 → the config's real ruleset), apply-prodskipped (dispatch-gated). Prod apply is manual workflow_dispatch — required-reviewer env protection needs Enterprise Cloud on private repos (422, verified live), so the dispatch is the documented (weaker) human gate; if: success() && workflow_dispatch keeps a failed dev rehearsal from ever unlocking prod.
drift.yml — manual dispatch: dev and prod both clean (exit 0), no false drift issues opened.
Acceptance: PR → plan comment → merge → dev applied ✅; drift detection live (nightly + on-demand, proven green) ✅; --detailed-exitcode tests ✅; prod behind a gate ✅ (dispatch, not reviewer — honest caveat). No PAT anywhere; secrets are CT_DEV_TOKEN + CT_PROD_TOKEN only.
Tool side was already complete (PR #63). Nothing further required.
Activity
2000game commentedon Jul 9, 2026
Status (autonomous wave, 2026-07-09): both halves are merged.
ct plan --detailed-exitcode(0/1/2, INCOMPLETE stays 1, permission-only changes → 2, covered by tests),--jsongains per-changesource: config|drift|config+drift+ summary (attribution traced correct in review), NO_COLOR guidance (picocolors force-colors under CI=true), README CI-usage docs.productionenvironment gate with--confirm-env prod; state write-back commits each env's ct-state file back to main even on partial apply failure, rebase-retry push, paths-ignore retrigger guard), drift.yml (nightly + manual; exit 2 → create/update adriftissue with source attribution; exit 0 → close; hard-fails on unexpected exit codes). Three review rounds; zero outstanding findings.Remaining (user setup, then a supervised first run — exact steps in eqrm/ct-structure#2's body): repo secrets CT_DEV_TOKEN + CT_PROD_TOKEN, GitHub Environment
productionwith required reviewers, branch-protection bypass for the Actions bot's state write-back pushes. Acceptance (drift issue within 24h, merge-to-applied with no local CLI) is then provable live.2000game commentedon Jul 10, 2026
Live setup update (2026-07-10):
productionGitHub Environment created via API, deployment branch policy =mainonly.apply-prodruns only on manualworkflow_dispatch(the dispatch is the human gate, honestly documented as weaker), with asuccess()-guarded gate so a failed dev rehearsal can never unlock prod.npm cithe private ct-cli git dependency (no SSH key; GITHUB_TOKEN is repo-scoped) → an auth step rewrites ssh→token-https via a newCT_CLI_READ_TOKENsecret.Remaining (user): create secrets
CT_DEV_TOKEN,CT_PROD_TOKEN,CT_CLI_READ_TOKEN(fine-grained PAT, contents:read on eqrm/ct-cli) → PR #4's plan check should go green → merge PR #4 → supervised first run of each workflow. Setup checklist is in the PR body / README.2000game commentedon Jul 10, 2026
Handoff state (2026-07-10): everything mergeable is merged except one PR, and one secret is missing.
productionenvironment created via API (deployment branch policy = main). ✅ Secrets CT_DEV_TOKEN + CT_PROD_TOKEN set.npm cithe private git dep, so ct-structure PR feat: Phase 2 — Read/Adopt (ct adopt) + state file #4 (dispatch-gated prod apply — required reviewers need Enterprise Cloud on private repos — + the CI dep-auth step, all review findings fixed incl. the insteadOf --add correction) cannot go green.2000game commentedon Jul 10, 2026
ct-cli tool-side is complete — all three tool checkboxes ticked. Verified in this session:
--detailed-exitcodeimplemented (src/commands/plan.ts), 9 exit-code tests pass (tests/plan-detailed-exitcode-command.test.ts), incl. INCOMPLETE→1, permission-only-changes→2, drift-alone→0.CT_HOST/CT_LOGINTOKEN) documented in README §CI usage.CI=truepicocolors gotcha and a GitHub Actions example that captures the plan into a<details>block.Everything remaining on #24 is ct-structure-side (separate private repo): the plan/apply/drift workflows and env-scoped secrets. Per the roadmap that is handoff item #3 — it needs the
CT_CLI_READ_TOKENfine-grained PAT secret so ct-structure cannpm installct-cli, then ct-structure PR #4 goes green. No further ct-cli code changes are required for #24.Merge pull request #83 from eqrm/feat/publish-github-packages-24
2000game commentedon Jul 10, 2026
ct-cli now publishes to GitHub Packages — merged PR #83 (
87eb5d9). This is the no-PAT cross-repo path (your GitHub-Packages recollection was right; it applies to registries, not raw git deps).Validated live: the release.yml run on the merge (29094017769) went fully green — semantic-release reached
analyzeCommits, i.e. the new@semantic-release/npmverifyConditions(GitHub Packages auth) passed. Theci:commit is non-releasing, so nothing published yet; the package first publishes on the next releasablefeat/fixmerged to main.Consumer side (eqrm/ct-structure — separate PR there):
package.json: depend on@eqrm/ct-cliinstead of thessh://…/ct-cli.gitgit URL.permissions: { packages: read, contents: read };actions/setup-nodewithregistry-url: https://npm.pkg.github.com,scope: '@eqrm';npm ciwithenv: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}. No PAT.eqrm/ct-structureActions access to the@eqrm/ct-clipackage (after it first publishes).Reviewed by an agent across 7 wiring dimensions (version bump, plugin order, auth/whoami blast-radius, dist packaging, no-public-npm-leak, repository field, YAML) — all clean. One pre-existing follow-up filed: the GitHub Release
ct-cli.tgzasset stays at0.1.0.2000game commentedon Jul 11, 2026
GitOps loop is live and validated end-to-end (2026-07-11).
The last blocker — ct-structure's CI could not install the private ct-cli dependency — is resolved by consuming
@eqrm/ct-clifrom GitHub Packages with the built-inGITHUB_TOKEN(no PAT). Chain: #84/PR #85 fixed the release tarball and triggered the first Packages publish (@eqrm/ct-cli@1.2.2) → eqrm/ct-structure#8 switched the consumer + all three workflows to it → #7 (dev state) and #4 (prod gate) merged in a safe order so no push ever triggered an unsupervised apply.All three workflows proven live:
apply-devran on merge: healed one real dev drift (a stray ct-cli bug: dynamic ruleset round-trip assumption unverified — CT-recomputed fields would cause perpetual diffs #36 test probe on group #1698 → the config's real ruleset),apply-prodskipped (dispatch-gated). Prod apply is manualworkflow_dispatch— required-reviewer env protection needs Enterprise Cloud on private repos (422, verified live), so the dispatch is the documented (weaker) human gate;if: success() && workflow_dispatchkeeps a failed dev rehearsal from ever unlocking prod.Acceptance: PR → plan comment → merge → dev applied ✅; drift detection live (nightly + on-demand, proven green) ✅;
--detailed-exitcodetests ✅; prod behind a gate ✅ (dispatch, not reviewer — honest caveat). No PAT anywhere; secrets areCT_DEV_TOKEN+CT_PROD_TOKENonly.Tool side was already complete (PR #63). Nothing further required.