Skip to content

feat(cli): add [deployments] subdomain to config-as-code#125

Closed
tonychang04 wants to merge 1 commit into
mainfrom
feat/insforge-toml-deployments-subdomain
Closed

feat(cli): add [deployments] subdomain to config-as-code#125
tonychang04 wants to merge 1 commit into
mainfrom
feat/insforge-toml-deployments-subdomain

Conversation

@tonychang04
Copy link
Copy Markdown
Contributor

@tonychang04 tonychang04 commented May 13, 2026

Summary

Adds [deployments] subdomain = \"...\" to insforge.toml, completing the config-as-code surface for the deployment slug feature (InsForge#1259, already merged).

  • insforge config export pulls the live custom slug from /api/metadata and emits [deployments] when one is set
  • insforge config apply diffs the TOML against live state and PUTs /api/deployments/slug for changes
  • Per-section capability gate skips the write cleanly on backends that don't expose the slice

Version-skew handling

The capability probe is presence-only on the new metadata.deployments slice from #1259, so all three rollout permutations land in the right place:

Backend state Metadata response Apply behavior
Pre-#1259 (any topology) no deployments key lands in skipped[] with named warning, no PUT
Post-#1259 self-host getConfigMetadata returns undefined → key omitted same — slice absent = self-host signal
Post-#1259 cloud, no slug deployments: { customSlug: null } proceeds, PUTs new slug
Post-#1259 cloud, slug set deployments: { customSlug: \"foo\" } proceeds, PUTs change or no-op

A 409 from a taken slug surfaces as CLIError(\"Slug is already taken\") via ossFetch, and aborts the apply mid-batch — earlier successful changes in the same run stay applied.

TOML semantics

  • subdomain = \"my-app\" → set the slug
  • subdomain = \"\" → clear the slug (TOML has no null; the diff layer normalizes "" → null for the PUT body)
  • absent section → default-keep (live state preserved)

Export deliberately omits [deployments] when no slug is set so a fresh export doesn't imply "clear on apply."

Test plan

  • npm test — 287 pass, 0 fail
  • npm run lint — clean
  • Capability probe tested for all three version-skew cases
  • Apply path tested: set, clear (empty-string normalized to null), no-op when match, skip when slice absent
  • Export path tested: cloud+slug emits, cloud+null omits, self-host adds to skipped
  • TOML parser tested: string, empty-string, type rejection
  • Manual E2E against a cloud project (will run after first review)

Version bumped 0.1.76 → 0.1.77.

🤖 Generated with Claude Code


Summary by cubic

Add [deployments] subdomain to insforge.toml and wire insforge config export/apply to manage deployment slugs. Lets users set or clear custom subdomains from code, with safe gating on older or self-hosted backends.

  • New Features
    • Export reads /api/metadata.deployments and emits [deployments] only when a slug exists.
    • Apply diffs TOML vs live and PUTs /api/deployments/slug with { slug: string | null }.
    • Empty subdomain = "" means clear (normalized to null); absent section keeps live state.
    • Capability probe skips writes when metadata.deployments is missing; change is reported as skipped.
    • 409 conflicts surface as "Slug is already taken".

Written for commit 54ea7ee. Summary will update on new commits.

Note

Add [deployments].subdomain field to config-as-code export and apply

  • Adds an optional [deployments] section to the config schema, TOML serialization, diff, and format utilities, with subdomain as the only field.
  • The export command includes subdomain when the backend exposes the deployments slice and a custom slug is set; it records deployments.subdomain as skipped when the slice is absent.
  • The apply command diffs the subdomain against the live value and issues PUT /api/deployments/slug with { slug: string | null } to apply changes, treating an empty string in TOML as a null (clear) operation.
  • metadataSupports gates this field on the presence of the deployments object in raw metadata, so older backends silently skip it.

Macroscope summarized 54ea7ee.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for configuring and managing deployment subdomains through config commands.
    • Deployment subdomain settings can now be applied and exported alongside other configurations.
  • Tests

    • Added comprehensive test coverage for deployment subdomain configuration and edge cases.
  • Chores

    • Version bumped to 0.1.77.

Review Change Stack

Mirrors the SMTP/auth pattern: per-section capability probe gates the
write so old/self-host backends never receive a PUT they can't honor.

Wire:
- [deployments] subdomain = "my-app"  → PUT /api/deployments/slug { slug: "my-app" }
- [deployments] subdomain = ""        → PUT { slug: null } (clear)
- absent section                      → default-keep (live state preserved)

Version skew handled in metadataSupports() via presence-only probe of the
new metadata slice exposed in InsForge#1259:
- pre-#1259 backend (any topology) → no `deployments` key → skip with named warning
- post-#1259 self-host             → getConfigMetadata returns undefined → same skip
- post-#1259 cloud (slug null/set) → slice present → proceed

Empty-string in TOML normalizes to null in the diff/apply layer (TOML
lacks a null literal). Export emits [deployments] only when a slug is
actually set, to avoid implying "clear on apply" on a fresh export.

CLI bumped 0.1.76 → 0.1.77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Walkthrough

This PR extends the CLI's config-as-code system to support the deployments.subdomain field alongside existing auth.allowed_redirect_urls support. Changes span schema types, TOML serialization, diff detection, backend capability probing, export/apply command integration, and a version increment.

Changes

Deployments Subdomain Config Feature

Layer / File(s) Summary
Config Schema & Type Contracts
src/lib/config-schema.ts, src/lib/config-diff.ts
DeploymentsConfig interface with subdomain?: string | null is added to InsforgeConfig; DiffChange converted to discriminated union supporting both auth and deployments diffs with appropriate value types; validateConfig extended with validateDeployments function for type enforcement.
TOML Parsing & Serialization
src/lib/config-toml.ts, src/lib/config-toml.test.ts
[deployments] section support added to TOML stringify with conditional emission of subdomain only when non-empty string; tests verify string parsing, empty-string-as-clear signal, and omission of unset values.
Diff Detection & Capability Probing
src/lib/config-diff.ts, src/lib/config-capabilities.ts, src/lib/config-capabilities.test.ts
diffConfig now detects deployments.subdomain changes with empty-string-to-null normalization; RawMetadata adds optional deployments field; metadataSupports probes backend support via raw.deployments presence check; tests verify support detection across metadata shapes.
Diff Formatting
src/lib/config-format.ts
formatChange special-cases deployments.subdomain to display null values as '(unset)' for clarity.
Export Command
src/commands/config/export.ts, src/commands/config/export.test.ts
Export reads deployments.customSlug from /api/metadata, conditionally maps to config.deployments.subdomain when present and non-empty, records as skipped when slice absent; tests verify skipped reporting and section emission across metadata configurations.
Apply Command
src/commands/config/apply.ts, src/commands/config/apply.test.ts
Apply reads deployments metadata to construct live config; handles deployments.subdomain changes via PUT /api/deployments/slug with the slug value (allowing null); tests cover PUT emission, empty-string clearing, no-op skipping, and backend-absence cases.
Package Version
package.json
Version incremented from 0.1.76 to 0.1.77.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • InsForge/CLI#109: Introduced the foundational config-as-code system (config-capabilities, config-diff, config-toml, config-format, and command integration) that this PR extends with support for the new deployments.subdomain field.

Suggested reviewers

  • jwfing
  • Fermionic-Lyu

Poem

🐰 A subdomain hops into place,
From metadata to TOML's space,
Diffs detected, format clean,
Apply and export—the schema's scene,
Version bumped, the feature's done! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a [deployments] subdomain section to insforge.toml config-as-code.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/insforge-toml-deployments-subdomain

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/config-capabilities.ts`:
- Around line 53-56: The current capability check that marks deployments
supported uses the expression "raw?.deployments !== undefined && raw.deployments
!== null && typeof raw.deployments === 'object'", which treats arrays as valid;
update this to explicitly exclude arrays (e.g., replace the typeof check with a
plain-object guard such as "!Array.isArray(raw.deployments) && typeof
raw.deployments === 'object'" or call a small helper
isPlainObject(raw.deployments)) so that raw.deployments (referenced in the
boolean expression) must be a non-null plain object before reporting the
capability as supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dcb1dfa8-7393-4877-b0c1-8ffad3f78b53

📥 Commits

Reviewing files that changed from the base of the PR and between 1924f64 and 54ea7ee.

📒 Files selected for processing (12)
  • package.json
  • src/commands/config/apply.test.ts
  • src/commands/config/apply.ts
  • src/commands/config/export.test.ts
  • src/commands/config/export.ts
  • src/lib/config-capabilities.test.ts
  • src/lib/config-capabilities.ts
  • src/lib/config-diff.ts
  • src/lib/config-format.ts
  • src/lib/config-schema.ts
  • src/lib/config-toml.test.ts
  • src/lib/config-toml.ts

Comment on lines +53 to +56
raw?.deployments !== undefined &&
raw.deployments !== null &&
typeof raw.deployments === 'object'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Treat non-plain deployments values as unsupported.
On Line 55, typeof raw.deployments === 'object' also accepts arrays, which can incorrectly mark capability as supported on malformed metadata.

Suggested patch
   return (
     raw?.deployments !== undefined &&
     raw.deployments !== null &&
-    typeof raw.deployments === 'object'
+    typeof raw.deployments === 'object' &&
+    !Array.isArray(raw.deployments)
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raw?.deployments !== undefined &&
raw.deployments !== null &&
typeof raw.deployments === 'object'
);
raw?.deployments !== undefined &&
raw.deployments !== null &&
typeof raw.deployments === 'object' &&
!Array.isArray(raw.deployments)
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/config-capabilities.ts` around lines 53 - 56, The current capability
check that marks deployments supported uses the expression "raw?.deployments !==
undefined && raw.deployments !== null && typeof raw.deployments === 'object'",
which treats arrays as valid; update this to explicitly exclude arrays (e.g.,
replace the typeof check with a plain-object guard such as
"!Array.isArray(raw.deployments) && typeof raw.deployments === 'object'" or call
a small helper isPlainObject(raw.deployments)) so that raw.deployments
(referenced in the boolean expression) must be a non-null plain object before
reporting the capability as supported.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 13, 2026

Greptile Summary

This PR wires [deployments] subdomain into the config-as-code surface: config export pulls the live custom slug from /api/metadata and writes it to TOML, and config apply diffs the TOML value against live state and PUTs /api/deployments/slug for changes. The capability probe (presence of the deployments key in the metadata response) correctly gates all writes, so self-host and pre-#1259 backends are handled cleanly.

  • Export path: omits [deployments] when the backend lacks the slice or customSlug is null, avoiding a spurious "clear on apply" signal. Correctly adds deployments.subdomain to skipped[] only when the slice is absent.
  • Apply path: populates live.deployments only when raw.deployments is present (consistent with the capability gate), normalizes empty-string to null before the PUT, and surfaces 409 slug-conflicts as CLIError.
  • Three stale doc comments — in config-diff.ts, config-schema.ts, and config-toml.ts — reference auth-only scope or old section ordering and should be updated now that deployments is wired.

Confidence Score: 4/5

Safe to merge; all four version-skew permutations are covered by tests and the implementation logic is sound.

The core apply/export logic, capability gating, and empty-string-to-null normalization are all correct and well-tested. The only findings are three doc comments that were not updated to reflect that deployments is now a wired section rather than a future one.

src/lib/config-diff.ts, src/lib/config-schema.ts, and src/lib/config-toml.ts each carry a stale comment worth cleaning up before the next contributor adds a new config section.

Important Files Changed

Filename Overview
src/commands/config/apply.ts Correctly conditionally populates live.deployments only when raw.deployments is present; adds applyChange handler for deployments.subdomain with proper null-slug semantics.
src/commands/config/export.ts Correctly skips [deployments] section when backend slice is absent and omits subdomain when null (no slug set), avoiding the "clear on apply" trap.
src/lib/config-diff.ts Extends DiffChange union to include deployments.subdomain; adds diff logic with correct empty-string-to-null normalization. JSDoc still claims "v1 scope: auth.allowed_redirect_urls only" — stale after this change.
src/lib/config-capabilities.ts Presence-only probe for deployments.subdomain is correct; consistent with existing auth probe pattern. Self-host/pre-#1259 backends that omit the key correctly return false.
src/lib/config-schema.ts Adds DeploymentsConfig and validateDeployments correctly. Top-of-file comment still lists deployments as a future section now that it's wired.
src/lib/config-toml.ts Serialization correctly skips [deployments] when subdomain is null/empty. JSDoc ordering comment still says "project_id → auth", missing the new deployments section.
src/lib/config-format.ts Adds special-case display for null subdomain (shows "(unset)" instead of literal "null"). Correct and consistent.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant Backend as Backend /api

    Note over User,Backend: config export
    User->>CLI: insforge config export
    CLI->>Backend: GET /api/metadata
    Backend-->>CLI: "{ auth: {...}, deployments?: { customSlug } }"
    alt deployments slice present AND customSlug is non-null string
        CLI->>CLI: "config.deployments = { subdomain: customSlug }"
        CLI-->>User: "writes [deployments] subdomain = "..." to TOML"
    else deployments slice present, customSlug is null
        CLI-->>User: "omits [deployments] section (no-slug = default URL)"
    else "deployments slice absent (self-host / pre-#1259)"
        CLI-->>User: omits [deployments], adds deployments.subdomain to skipped[]
    end

    Note over User,Backend: config apply
    User->>CLI: insforge config apply
    CLI->>Backend: GET /api/metadata
    Backend-->>CLI: "{ auth: {...}, deployments?: { customSlug } }"
    CLI->>CLI: build live config, run diffConfig(live, file)
    loop for each DiffChange
        alt "metadataSupports(raw, change) == false"
            CLI-->>User: skip with named warning
        else deployments.subdomain change
            CLI->>Backend: "PUT /api/deployments/slug { slug: value | null }"
            Backend-->>CLI: 200 OK or 409 Slug taken
        end
    end
    CLI-->>User: "{ applied: [...], skipped: [...] }"
Loading

Comments Outside Diff (3)

  1. src/lib/config-diff.ts, line 36-40 (link)

    P2 The JSDoc still says "v1 scope: auth.allowed_redirect_urls only" but this function now also handles deployments.subdomain. Leaving this stale will mislead future contributors adding the next section.

  2. src/lib/config-schema.ts, line 3-7 (link)

    P2 The module comment still lists deployments as an upcoming future section, but it's now wired in this PR. The parenthetical is now inaccurate.

  3. src/lib/config-toml.ts, line 14-17 (link)

    P2 The JSDoc ordering comment omits deployments, so it no longer reflects the actual section order emitted by this function.

Reviews (1): Last reviewed commit: "feat(cli): add [deployments] subdomain s..." | Re-trigger Greptile

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 12 files

Copy link
Copy Markdown
Member

@jwfing jwfing left a comment

Choose a reason for hiding this comment

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

Code Review — feat(cli): add [deployments] subdomain to config-as-code

Summary. Solid, well-scoped addition that wires insforge config export/apply to manage custom deployment slugs, with correct capability gating for version-skew safety and comprehensive test coverage across all documented rollout permutations.


Requirements context

No /docs/superpowers/ directory exists in this repo — assessing against the PR description and the linked PR InsForge#1259 as the source of intent. The implementation matches the stated spec on all four version-skew cases (pre-#1259 / post-#1259 self-host / cloud+no-slug / cloud+slug-set).


Findings

Critical

(none)


Suggestion

[Functionality] Missing test for 409 "slug already taken" error pathapply.ts:144-153, apply.test.ts

The PR description explicitly calls out: "A 409 from a taken slug surfaces as CLIError("Slug is already taken") via ossFetch, and aborts the apply mid-batch." This is a distinct, user-visible failure mode that users will hit in practice (slug collisions are common), yet there's no test covering it. The existing ossFetchMock infrastructure in apply.test.ts should make it straightforward to mock a 409 response and assert the error message propagates. Worth adding before the manual E2E pass.


Information

[Software engineering] Stale JSDoc in config-diff.ts:36

v1 scope: auth.allowed_redirect_urls only. Default-keep for absent fields

Now covers deployments.subdomain too. Minor doc drift.


[Software engineering] Stale comment in config-toml.ts:17

Section ordering is deterministic (project_id → auth)

Three sections are now serialized. Should read project_id → auth → deployments.


[Functionality] liveDeployments = live.deployments ?? {} double-gate patternconfig-diff.ts:66-68

When the backend doesn't expose deployments, live.deployments is undefined, so liveDeployments becomes {} and fromV becomes null. This means diffConfig still emits a change (null → TOML value) even when the backend lacks the slice. The capability gate in apply.ts:97 catches it correctly — but the path is subtle. The existing comment partially explains it; a one-line note linking the two gates (// capability gate in apply.ts filters this out before it reaches applyChange) would help the next reader avoid the same double-take.

This is purely informational — the logic is correct and safe.


Verdict

Approved (informational; explicit GitHub approval is a separate human action). No blocking issues found. The one Suggestion (409 test) is worth addressing before or after the manual E2E, but it doesn't block merge given the rest of the coverage is thorough.

@tonychang04
Copy link
Copy Markdown
Contributor Author

Folding into #123 — both sections share the same config-as-code scaffolding (diff/apply/export/capability probe), so a single review is cheaper than two. The deployments work is now on #123 along with the SMTP changes.

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