Skip to content

feat(template): upgrade a deployed template instance, and roll it back one step - #367

Merged
CarmenDou merged 10 commits into
mainfrom
feat/template-upgrade-apply
Oct 11, 2026
Merged

CarmenDou merged 10 commits into
mainfrom
feat/template-upgrade-apply

Conversation

@CarmenDou

@CarmenDou CarmenDou commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

insta template upgrade <service> moves a service deployed from a template to the version the
registry serves now, and insta template rollback <service> returns it one step.

Both address a service name rather than a deployment id, because that is what a person and an
agent already have in hand. The CLI resolves the deployment from the service.

Paired with insta-platform #645 and insta-console #601.

The agent-facing command reference this repository requires as a mirror is updated in
InsForge/instacloud-skills#170, which documents both commands and every flag added here.
It lives in the superproject's skills submodule, so it cannot be a file in this PR.

How

upgrade prints the plan before it does anything: the version move, and per field the live value
against the new one. A field changed by hand after deploying is marked so, because the upgrade will
overwrite it. Then it asks, and --yes or no TTY skips the question.

The plan the user saw is the plan that runs. The CLI sends back the version and digest from it, so
a republish in between answers 409 and nothing happens, with a sentence telling the user to look
again.

--set NAME=value supplies a required variable the new version adds. Variables the instance
already holds are never asked for again, and generated secrets are recovered by the platform rather
than transiting the CLI.

rollback prints its warnings before the confirmation: what the older version restores, that a
setting it does not declare keeps its current value, that a public access change on a storage
service reaches that bucket on every branch, and that data the app migrated under the newer version
is not restored. It takes --set too, because an upgrade can drop a variable the older version
still requires.

Coded 409s reach the user as sentences rather than a bare conflict. Two of them name the deployment
or the version to act on, and those keep the platform's own wording instead of being replaced with
ours, which is what KEEP_PLATFORM_SENTENCE is for.

Verify

  • npm run typecheck and npm test, 103 files and 2141 tests
  • the plan printer, the version pin, the --set paths, the destroy-without-asking rule off a TTY
    and each coded 409 all have cases
  • the no-step-back case covers both of its causes, never upgraded and already rolled back, and
    asserts the platform sentence survives for each

🤖 Generated with Claude Code


Summary by cubic

Adds insta template upgrade <service> and insta template rollback <service> so a deployed template service can move to the version the registry serves now, or one step back. Both commands take a service name rather than a deployment id, show what will change before running, and confirm before acting.

  • upgrade prints the plan — the version move and, per field, the live value versus the new one, with hand-changed fields marked as being overwritten — then sends back the version and digest it showed, so a republish in between answers 409 and nothing runs.
  • An added service is shown as created by the upgrade with its field rows printed as (none) → <new value> plus its required variables; a dropped one is left running. A skipped service now reads [skipped: …] instead of [pending] in deployment output and no longer counts against the healthy total.
  • --set NAME=value supplies a required variable the new version adds; variables the instance already holds are never re-asked, and generated secrets are recovered by the platform. A rejected --set never echoes the value, which is where a credential sits.
  • rollback prints its warnings before confirmation: what the older version restores, that undeclared settings keep their current value, that a public-access change on a storage service reaches that bucket on every branch, and that app-migrated data is not restored. It takes --set too, and off a TTY it fails without --yes.
  • Coded 409s are raised as sentences instead of a bare conflict; two keep the platform's wording because it names the deployment or version to act on.

Written for commit 549f58d. Summary will update on new commits.

View guided diff Turn on auto-fix

The upgrade applies every field the manifest declares for a service that
already exists, always-on, public access and the volume mount path among
them. The `--help` text still carried the superseded sentence saying those
three were left alone, which is the opposite of what runs, and on
`--yes --json` no plan is printed, so that text is all an agent ever reads.

Rollback goes through the same planner, so it writes those fields too
whenever the OLDER manifest declares them, and it prints no plan at all.
Its two warnings were the whole disclosure and named only image, start
command, port and variables. They now name every setting, with the
qualifier that a field the older version is silent about keeps its current
value.

Also: one lookup serves both verbs, so a service that came from no template
no longer tells someone who ran `rollback` there is nothing to upgrade.
… a skipped service

The platform's final-review pass changed four things in the contract this
command speaks. Three of them are reachable from here.

Two new coded 409s arrive where only a bare `conflict (HTTP 409)` would have
been printed. `template_superseded` (the instance has already moved off this
deployment) and `template_version_content_changed` (the recorded manifest for
that version is not the content this instance ran) both name a deployment or a
version in the platform's own sentence, and that name is the whole of what
makes them actionable, so they are printed as given with only the next step
appended, rather than replaced by wording of ours. `template_superseded` is
raised by the preview too, which is a GET and so never went through
postWithAdvice — the preview now gets the same reading as the POST.

`template rollback` gains `--set NAME=value`, matching `template upgrade`'s
flag. Before this, a version that dropped a required variable could not be
rolled back at all: the upgrade's env write is an authoritative replace, so it
deleted that value, the older manifest still requires it, and the platform's
400 naming it was a dead end. Rollback prints no plan, so those names exist
only in that 400 — the command prompts from it on a terminal and, with no TTY
or `--yes`, fails with the `--set NAME=value` list, having rolled nothing back.

A deployment's service entry can now carry `skipped: true`: a service the user
deleted, which the run stepped over. Its `state` stays `pending`, which under
the existing marks read as "not reached yet", so it gets its own mark and names
the reason. It is also out of `partialMessage`'s healthy count — a run that did
everything it could should not report a shortfall for a service that was not
there — while staying in the list, because the count is what raises the
question.

Descriptions corrected for what the upgrade now actually does: it creates a
volume a new version declares on an existing service rather than only rewriting
a mount path, and a public-access change on a storage bucket reaches that bucket
on every branch that has a copy.
`template_no_step_back` has two causes now: the instance was never upgraded,
or it was already rolled back and the way forward is an upgrade. Only the
platform can tell them apart, and the second names the version to return to.

The CLI replaced that sentence with one of its own, so it printed "never
upgraded" for both, which is the wrong one in the case a user reaches by
rolling back twice. Moved the code into KEEP_PLATFORM_SENTENCE, which exists
for exactly this: codes whose platform sentence names the thing to act on.
The test that pinned the replaced wording now asserts both causes survive.

@cubic-dev-ai cubic-dev-ai 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.

5 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/template-upgrade.test.ts">

<violation number="1" location="test/template-upgrade.test.ts:274">
P2: This assertion depends on the test runner not having a TTY: with both streams attached to a terminal, the mocked confirmation returns true and rollback proceeds instead of rejecting. Set both `isTTY` flags to false around the assertion, then restore them.</violation>
</file>

<file name="src/commands/template.ts">

<violation number="1" location="src/commands/template.ts:279">
P3: When every service in a partial run is marked `skipped`, `ran.length` is 0 and the header prints `0/0 services healthy`. Since this change intentionally excludes skipped services from the run, special-case the all-skipped case (for example "no services were deployed — all were deleted before the run") so the count never renders as 0/0.</violation>

<violation number="2" location="src/commands/template.ts:488">
P2: An added service skips its `missing_variables` lines here, so the plan says only that the service is added while execution still asks for its required values. Print those variables before continuing.</violation>

<violation number="3" location="src/commands/template.ts:491">
P2: A null target value appears as literal `null` in the change plan, unlike a null live value. Render `f.next ?? '(none)'` so the reviewed target is clear.</violation>

<violation number="4" location="src/commands/template.ts:593">
P1: A piped upgrade skips this confirmation and still posts unless `--yes` is checked. Reject non-TTY runs without `--yes`, as rollback does, rather than applying changes unattended.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/commands/template.ts
const tty = !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY
const variables = missing.length ? await resolveVariables(missing, given, { tty, ask }) : given

if (!opts.yes && tty) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: A piped upgrade skips this confirmation and still posts unless --yes is checked. Reject non-TTY runs without --yes, as rollback does, rather than applying changes unattended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/template.ts, line 593:

<comment>A piped upgrade skips this confirmation and still posts unless `--yes` is checked. Reject non-TTY runs without `--yes`, as rollback does, rather than applying changes unattended.</comment>

<file context>
@@ -458,3 +466,202 @@ export async function templateDeploy(target: string, opts: TemplateDeployOpts =
+  const tty = !opts.yes && !!process.stdin.isTTY && !!process.stdout.isTTY
+  const variables = missing.length ? await resolveVariables(missing, given, { tty, ask }) : given
+
+  if (!opts.yes && tty) {
+    const go = await clack.confirm({ message: `Upgrade ${service} from ${plan.from_version} to ${plan.to_version}?` })
+    if (clack.isCancel(go)) throw new CliCancel()
</file context>
Suggested change
if (!opts.yes && tty) {
if (!opts.yes) {
if (!tty) throw new Error('upgrade needs --yes without a terminal to confirm on')


it('rollback without a terminal or --yes rolls back nothing and fails', async () => {
const { api, posts } = fakeApi()
await expect(templateRollback('n8n', {}, deps(api))).rejects.toThrow(/--yes/)

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: This assertion depends on the test runner not having a TTY: with both streams attached to a terminal, the mocked confirmation returns true and rollback proceeds instead of rejecting. Set both isTTY flags to false around the assertion, then restore them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At test/template-upgrade.test.ts, line 274:

<comment>This assertion depends on the test runner not having a TTY: with both streams attached to a terminal, the mocked confirmation returns true and rollback proceeds instead of rejecting. Set both `isTTY` flags to false around the assertion, then restore them.</comment>

<file context>
@@ -0,0 +1,277 @@
+
+  it('rollback without a terminal or --yes rolls back nothing and fails', async () => {
+    const { api, posts } = fakeApi()
+    await expect(templateRollback('n8n', {}, deps(api))).rejects.toThrow(/--yes/)
+    expect(posts).toEqual([])
+  })
</file context>

Comment thread src/commands/template.ts
if (s.added) { lines.push(` ${who} added by ${plan.to_version}`); continue }
for (const f of s.fields) {
const drift = f.drifted ? ' (you changed this, the upgrade will overwrite it)' : ''
lines.push(` ${who} ${f.label} ${f.live ?? '(none)'} → ${f.next}${drift}`)

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: A null target value appears as literal null in the change plan, unlike a null live value. Render f.next ?? '(none)' so the reviewed target is clear.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/template.ts, line 491:

<comment>A null target value appears as literal `null` in the change plan, unlike a null live value. Render `f.next ?? '(none)'` so the reviewed target is clear.</comment>

<file context>
@@ -458,3 +466,202 @@ export async function templateDeploy(target: string, opts: TemplateDeployOpts =
+    if (s.added) { lines.push(`  ${who}  added by ${plan.to_version}`); continue }
+    for (const f of s.fields) {
+      const drift = f.drifted ? '  (you changed this, the upgrade will overwrite it)' : ''
+      lines.push(`  ${who}  ${f.label}  ${f.live ?? '(none)'} → ${f.next}${drift}`)
+    }
+    for (const name of s.missing_variables) {
</file context>
Suggested change
lines.push(` ${who} ${f.label} ${f.live ?? '(none)'} → ${f.next}${drift}`)
lines.push(` ${who} ${f.label} ${f.live ?? '(none)'} → ${f.next ?? '(none)'}${drift}`)

Comment thread src/commands/template.ts Outdated
for (const s of plan.services) {
const who = s.service_name ?? s.key
if (s.gone) { lines.push(` ${who} no longer exists, skipped`); continue }
if (s.added) { lines.push(` ${who} added by ${plan.to_version}`); continue }

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: An added service skips its missing_variables lines here, so the plan says only that the service is added while execution still asks for its required values. Print those variables before continuing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/template.ts, line 488:

<comment>An added service skips its `missing_variables` lines here, so the plan says only that the service is added while execution still asks for its required values. Print those variables before continuing.</comment>

<file context>
@@ -458,3 +466,202 @@ export async function templateDeploy(target: string, opts: TemplateDeployOpts =
+  for (const s of plan.services) {
+    const who = s.service_name ?? s.key
+    if (s.gone) { lines.push(`  ${who}  no longer exists, skipped`); continue }
+    if (s.added) { lines.push(`  ${who}  added by ${plan.to_version}`); continue }
+    for (const f of s.fields) {
+      const drift = f.drifted ? '  (you changed this, the upgrade will overwrite it)' : ''
</file context>
Suggested change
if (s.added) { lines.push(` ${who} added by ${plan.to_version}`); continue }
if (s.added) {
lines.push(` ${who} added by ${plan.to_version}`)
for (const name of s.missing_variables) {
lines.push(` ${who} ${name} is required by ${plan.to_version} and has no value, pass --set ${name}=...`)
}
continue
}

Comment thread src/commands/template.ts
const healthy = ran.filter((s) => s?.state === 'healthy').length
return [
`template deployment finished partial: ${healthy}/${services.length} services healthy`,
`template deployment finished partial: ${healthy}/${ran.length} services healthy`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: When every service in a partial run is marked skipped, ran.length is 0 and the header prints 0/0 services healthy. Since this change intentionally excludes skipped services from the run, special-case the all-skipped case (for example "no services were deployed — all were deleted before the run") so the count never renders as 0/0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/template.ts, line 279:

<comment>When every service in a partial run is marked `skipped`, `ran.length` is 0 and the header prints `0/0 services healthy`. Since this change intentionally excludes skipped services from the run, special-case the all-skipped case (for example "no services were deployed — all were deleted before the run") so the count never renders as 0/0.</comment>

<file context>
@@ -266,9 +270,13 @@ export function serviceStateLines(dep: any): string[] {
+  const healthy = ran.filter((s) => s?.state === 'healthy').length
   return [
-    `template deployment finished partial: ${healthy}/${services.length} services healthy`,
+    `template deployment finished partial: ${healthy}/${ran.length} services healthy`,
     ...serviceStateLines(dep),
     ...(dep?.error ? [`  ${dep.error}`] : []),
</file context>
Suggested change
`template deployment finished partial: ${healthy}/${ran.length} services healthy`,
`template deployment finished partial: ${ran.length ? `${healthy}/${ran.length} services healthy` : 'no services were deployed — all were deleted before the run'}`,

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Summary

The implementation is generally sound, but the required agent-facing command reference has not been updated.

Requirements context

I assessed the change against the PR description, the existing template command conventions, AGENTS.md, the repository development guide, and the README command surface. The linked platform #645 and console #601 content was unavailable from this checkout, so their contracts could only be evaluated through the PR description and client implementation.

Findings

Critical

  • The new template upgrade and template rollback commands and their flags are absent from the required skills/insta/cli-reference.md update. The repository explicitly classifies this mirror as non-negotiable and says a command is only half-complete without it; this PR changes the command surface but contains only CLI source/tests. Please update the superproject reference or identify the companion change that does so. (AGENTS.md:8-17, .claude/skills/developing-insta-cli/SKILL.md:36-38, src/index.ts:576-587)

Suggestion

  • Update the local command summary as well: it still says insta template only browses and deploys via list, info, and deploy, so users reading the repository documentation will not discover either new operation. (README.md:240)

  • Add direct upgrade-variable coverage. The tests verify plan pinning and JSON behavior, and extensively exercise rollback --set, but do not demonstrate that upgrade forwards a supplied value, prompts for a newly required variable on a TTY, or refuses before POST when that value is unavailable non-interactively. (test/template-upgrade.test.ts:88-110, test/template-upgrade.test.ts:224-263)

  • The development guide asks every new leaf command to state its MCP reachability decision in the PR description. This is explicitly non-blocking, but neither new command has that decision recorded. (.claude/skills/developing-insta-cli/SKILL.md:65-79, src/index.ts:576-587)

Information

  • Software engineering/functionality: service-name resolution, pinned version/digest submission, refusal handling, confirmation rules, approval gates, coded conflicts, rollback retry behavior, and deployment polling follow existing patterns. (src/commands/template.ts:527-666)

  • Security: no new dependency, shell/SQL execution, authorization bypass, or explicit secret logging was found; supplied variables remain confined to API request bodies. (src/commands/template.ts:587-603, src/commands/template.ts:642-658)

  • Performance: no material concern found. Each operation adds one bounded service-list lookup, one preview for upgrades, one mutation, and the existing bounded deployment poll; there is no new N+1 or blocking synchronous work. (src/commands/template.ts:548-571, src/commands/template.ts:601-608, src/commands/template.ts:647-663)

  • git diff --check passes. I could not independently run the typecheck or tests because node_modules is absent and the read-only review prohibited installing dependencies; npm run typecheck stopped at tsc: not found. (package.json:35-40)

Verdict

Request changes because an explicit, non-negotiable command-documentation requirement is unmet.

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Summary

The commands largely follow existing CLI conventions, but the upgrade flow does not yet guarantee that the displayed plan is the plan applied.

Requirements context

I assessed this against the PR description, command help, AGENTS.md, and .claude/skills/developing-insta-cli/SKILL.md. The linked platform/console PR contents were not available in this checkout, so no additional requirements were inferred from them; the separate command-reference update was accepted as described but could not be verified here.

Findings

Critical

  1. The preview is not pinned to the live service state it describes. The GET obtains live values for the plan, but the POST sends only the template version and digest. If a service setting changes while the confirmation is open, that change does not affect either pin, and the upgrade can overwrite a value that was never displayed or marked as drifted. This violates the stated guarantee that “the plan the user saw is the plan that runs.” Have the platform return a plan token or live-state revision/hash and submit it with the POST, rejecting stale plans. src/commands/template.ts:569-573, src/commands/template.ts:599-603

  2. Added services omit the settings and required variables that will be applied. The continue after printing “added by …” bypasses both the field loop and missing_variables loop. Consequently, an added service’s image, port, access, volume settings, and required variables are not listed even though the command help promises every declared setting is shown before application. Print the added-service heading without skipping its details, and add a regression test for an added service with fields and a missing variable. src/commands/template.ts:485-495, src/index.ts:576-580

Suggestion

  1. Update the repository’s command summary, which still lists only template list, info, and deploy. README.md:240

  2. The development guide asks every new leaf command to state its MCP reachability decision in the PR description. Neither new command has that decision documented; this is explicitly non-blocking. .claude/skills/developing-insta-cli/SKILL.md:65-79

Information

  • Software engineering: the tests cover the main request paths, JSON modes, coded conflicts, missing variables, and skipped services. test/template-upgrade.test.ts:1-277, test/template.test.ts:1117-1131
  • Security: no security-relevant issue found; --set names are validated and values are sent in JSON without being printed. src/commands/template.ts:141-149, src/commands/template.ts:587-603
  • Performance: no material concern found; the new flow adds bounded lookup/preview requests and reuses the existing deployment poller. src/commands/template.ts:548-571, src/commands/template.ts:606-608
  • git diff --check passed. The required typecheck/tests could not be rerun because dependencies are absent in the review checkout (tsc: not found); installing them would violate the read-only constraint. AGENTS.md:15

Verdict

Request changes because both Critical findings affect the advertised safety and completeness of the upgrade plan.

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Summary

The new commands largely follow existing CLI patterns, but the implementation does not yet provide the promised plan for added services and can disclose mistyped --set secrets.

Requirements context

I assessed the change against the PR description, command help, the repository’s command conventions, and the stated paired platform/console/skills PR contracts. No additional upgrade/rollback design document exists in this checkout; the linked repositories were not accessible from this environment. The repository requires command-reference mirroring, which the description identifies as InsForge/instacloud-skills#170.

Findings

Critical

  1. Added services skip the per-field plan the command promises. upgradePlanLines emits only added by <version> and immediately continues, so it omits that service’s image, port, public access, volume configuration, variables, and even missing_variables. This conflicts with the command help’s guarantee that every declared setting is listed before application and prevents the user from reviewing material resource/access changes before confirmation. Render the added-service fields and non-secret variable requirements before continuing, and add coverage using added: true; the current variable-plan test covers only an existing service. (src/commands/template.ts:485-495, src/index.ts:576-580, test/template-upgrade.test.ts:42-46)

  2. A malformed --set can disclose its value. Both new commands accept secret-like values through --set, but the shared parser includes the complete rejected pair in its error. A common typo such as a lowercase variable name therefore writes the supplied credential to stderr and potentially CI/agent logs. Report only the variable name or redact everything after the first =; extend the existing malformed-input test to assert that the value is absent. (src/commands/template.ts:141-146, src/commands/template.ts:557-558, src/commands/template.ts:615-616, test/template-upgrade.test.ts:265-269)

Suggestion

(none)

Information

  • Software engineering: The main success, refusal, JSON, missing-variable, rollback-confirmation, skipped-service, and coded-409 paths have focused tests and follow the repository’s dependency-injection conventions. I could not execute the suite or typecheck because this read-only checkout has no installed dependencies (tsc: not found).
  • Functionality: Beyond the added-service plan omission above, I found no additional correctness issue in service resolution, version/digest pinning, confirmation behavior, or deployment polling.
  • Security: No new dependency, authorization bypass, shell execution, or unsafe URL construction was introduced; the secret-bearing error described above is the security blocker.
  • Performance: No performance concerns found. Each operation performs a bounded number of setup requests followed by the existing bounded deployment poll, with no new N+1 or synchronous hot-path work.

Verdict

Request changes because the PR has two Critical findings: incomplete disclosure before an upgrade and possible secret exposure.

… value

The upgrade plan printed `added by <version>` for a service the new version
introduces and then skipped the rest of that service: its field rows and its
`missing_variables` were never rendered, so the command's own promise that
every declared setting is listed before anything runs was false exactly where
it mattered most, a service that does not exist yet and will be created and
billed. The added line now says the service is created by this upgrade and the
loop falls through to whatever rows the plan carries for it, which today is its
required variables and, the day the planner sends them, its fields.

The help text said "the same services are reused" and promised a per-setting
plan for all of them. It now separates the three cases the platform actually
reports: an existing service is reused and every declared setting is applied
and listed first, an added service is created and named with the variables it
requires rather than a per-setting comparison (there is no live service to
compare against), and a service the new version no longer declares is left
running and named as dropped.

parseSetFlags put the whole rejected pair in its error. `--set` is how these
commands take secrets, and a lowercase name is a common typo, so one typo wrote
a live credential to stderr and from there into CI and agent logs. The parser
now shows the name and hides everything after the first `=`; a pair with no `=`
is not split at all, so none of it is shown. Redacting here rather than at the
three call sites means a fourth caller cannot reintroduce the leak.

@cubic-dev-ai cubic-dev-ai 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.

1 existing issue remains and 1 new issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/commands/template.ts">

<violation number="1" location="src/commands/template.ts:155">
P2: Malformed names are echoed without validation, so `--set hunter2-live-key=x` leaks `hunter2-live-key` to stderr despite being rejected. Show the prefix only when it matches the allowed variable-name pattern; otherwise redact the whole token.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/commands/template.ts
function redactSetPair(pair: string): string {
const eq = pair.indexOf('=')
// Nothing of a pair with no `=`: it was never split, so the whole token may be the value itself.
return eq >= 0 ? `${pair.slice(0, eq)}=<value hidden>` : '<value hidden: no NAME= in front of it>'

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: Malformed names are echoed without validation, so --set hunter2-live-key=x leaks hunter2-live-key to stderr despite being rejected. Show the prefix only when it matches the allowed variable-name pattern; otherwise redact the whole token.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/template.ts, line 155:

<comment>Malformed names are echoed without validation, so `--set hunter2-live-key=x` leaks `hunter2-live-key` to stderr despite being rejected. Show the prefix only when it matches the allowed variable-name pattern; otherwise redact the whole token.</comment>

<file context>
@@ -142,12 +142,19 @@ export function parseSetFlags(pairs: string[]): Record<string, string> {
+function redactSetPair(pair: string): string {
+  const eq = pair.indexOf('=')
+  // Nothing of a pair with no `=`: it was never split, so the whole token may be the value itself.
+  return eq >= 0 ? `${pair.slice(0, eq)}=<value hidden>` : '<value hidden: no NAME= in front of it>'
+}
+
</file context>
Suggested change
return eq >= 0 ? `${pair.slice(0, eq)}=<value hidden>` : '<value hidden: no NAME= in front of it>'
const name = pair.slice(0, eq)
return eq >= 0 && /^[A-Z][A-Z0-9_]{0,63}$/.test(name) ? `${name}=<value hidden>` : '<value hidden: no valid NAME= in front of it>'

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Summary

The new template upgrade and one-step rollback commands match the stated contract and fit the existing CLI architecture, with no blocking findings.

Requirements context

I reviewed against the PR description, the repository guidance in AGENTS.md and .claude/skills/developing-insta-cli/SKILL.md, existing template command conventions, and the paired agent-facing documentation in instacloud-skills#170. The linked platform and console PRs were not publicly accessible, so their API implementation details could not be independently inspected.

Findings

Critical

(none)

Suggestion

  • test/template-upgrade.test.ts:104-125 — Add direct upgrade-flow coverage for forwarding --set, rejecting an unresolved required variable under --yes/without a TTY, and declining the confirmation. The implementation at src/commands/template.ts:595-605 appears correct and reuses tested helpers, but the current upgrade tests only assert an empty variables object, leaving core advertised behavior without an integration-style regression test.

Information

  • src/commands/template.ts:556-673 — Functionality follows the documented flow: service-to-deployment resolution, pinned version/digest application, refusal handling, confirmation rules, approval gates, rollback variable recovery, and bounded deployment polling.
  • src/commands/template.ts:141-155 — Security review found no newly exposed secrets or unsafe shell/SQL execution. Invalid --set values are now redacted, request paths use trusted identifiers or encode the branch, and no authorization behavior is bypassed.
  • src/commands/template.ts:572-616 — Performance review found no material concern: upgrade adds one service-list lookup and one preview request before using the existing bounded polling loop; there are no N+1 operations, blocking hot-path work, or new dependencies.
  • src/index.ts:576-587 — Command registration and flag shapes follow repository conventions, and the required agent-facing mirror exists in the paired documentation PR.
  • test/template-upgrade.test.ts:1-298 — git diff --check passed. I could not execute typecheck or tests locally because this read-only checkout has no installed typescript or vitest; the reported CI verification therefore remains the executable evidence.

Verdict

Approved: zero Critical findings. The testing suggestion is non-blocking.

…tually prints

The command help said an added service is named with the variables it requires and not a
per-setting comparison. upgradePlanLines prints the added-by line and then every field as
`(none) -> <new value>`, which is what insta-platform #645 made the plan carry. Same sentence as
the one corrected in insta-skills #170, which this one was copied from.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/index.ts">

<violation number="1" location="src/index.ts:576">
P3: The plan prints `→`, not `->`; use the rendered arrow so the help matches the actual added-service rows.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/index.ts
.option('-y, --yes', 'non-interactive: missing required variables fail with a --set list instead of prompting')
.option('--json')
.action(guard((target, o) => template.templateDeploy(target, o)))
tpl.command('upgrade <service>').description('Move a service deployed from a template to the version the registry serves now. Shows what will change and asks before it runs. A service that already exists is reused, and every setting the new version declares is applied: image, start command, port, always-on, public access, the volume and its mount path, and the variables, each of them listed in the plan first. A service the new version ADDS is created by the upgrade: the plan names it as added and then lists every setting it will be created with, each written `(none) -> <new value>` because there is no live service to compare against, followed by the variables it requires. A service the new version no longer declares is left running, named in the plan as dropped. A volume the new version adds is attached at your plan\'s size and mounted where the manifest says; an existing one is never resized. For a storage bucket, a public-access change reaches that bucket on every branch that has a copy. Refused with a 409 when the template was republished since the plan, or when this instance has already moved off this deployment')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The plan prints →, not ->; use the rendered arrow so the help matches the actual added-service rows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/index.ts, line 576:

<comment>The plan prints `→`, not `->`; use the rendered arrow so the help matches the actual added-service rows.</comment>

<file context>
@@ -573,7 +573,7 @@ tpl.command('deploy <code-or-dir-or-url>').description('Deploy a template onto a
   .option('--json')
   .action(guard((target, o) => template.templateDeploy(target, o)))
-tpl.command('upgrade <service>').description('Move a service deployed from a template to the version the registry serves now. Shows what will change and asks before it runs. A service that already exists is reused, and every setting the new version declares is applied: image, start command, port, always-on, public access, the volume and its mount path, and the variables, each of them listed in the plan first. A service the new version ADDS is created by the upgrade: the plan names it as added, with the variables it requires, and not a per-setting comparison, because there is no live service to compare it against. A service the new version no longer declares is left running, named in the plan as dropped. A volume the new version adds is attached at your plan\'s size and mounted where the manifest says; an existing one is never resized. For a storage bucket, a public-access change reaches that bucket on every branch that has a copy. Refused with a 409 when the template was republished since the plan, or when this instance has already moved off this deployment')
+tpl.command('upgrade <service>').description('Move a service deployed from a template to the version the registry serves now. Shows what will change and asks before it runs. A service that already exists is reused, and every setting the new version declares is applied: image, start command, port, always-on, public access, the volume and its mount path, and the variables, each of them listed in the plan first. A service the new version ADDS is created by the upgrade: the plan names it as added and then lists every setting it will be created with, each written `(none) -> <new value>` because there is no live service to compare against, followed by the variables it requires. A service the new version no longer declares is left running, named in the plan as dropped. A volume the new version adds is attached at your plan\'s size and mounted where the manifest says; an existing one is never resized. For a storage bucket, a public-access change reaches that bucket on every branch that has a copy. Refused with a 409 when the template was republished since the plan, or when this instance has already moved off this deployment')
   .option('--branch <b>', 'branch the service is on (default: current)')
   .option('--set <NAME=value>', 'set a template variable the new version requires (repeatable)', (v: string, prev: string[]) => [...prev, v], [] as string[])
</file context>
Suggested change
tpl.command('upgrade <service>').description('Move a service deployed from a template to the version the registry serves now. Shows what will change and asks before it runs. A service that already exists is reused, and every setting the new version declares is applied: image, start command, port, always-on, public access, the volume and its mount path, and the variables, each of them listed in the plan first. A service the new version ADDS is created by the upgrade: the plan names it as added and then lists every setting it will be created with, each written `(none) -> <new value>` because there is no live service to compare against, followed by the variables it requires. A service the new version no longer declares is left running, named in the plan as dropped. A volume the new version adds is attached at your plan\'s size and mounted where the manifest says; an existing one is never resized. For a storage bucket, a public-access change reaches that bucket on every branch that has a copy. Refused with a 409 when the template was republished since the plan, or when this instance has already moved off this deployment')
tpl.command('upgrade <service>').description('Move a service deployed from a template to the version the registry serves now. Shows what will change and asks before it runs. A service that already exists is reused, and every setting the new version declares is applied: image, start command, port, always-on, public access, the volume and its mount path, and the variables, each of them listed in the plan first. A service the new version ADDS is created by the upgrade: the plan names it as added and then lists every setting it will be created with, each written `(none) → <new value>` because there is no live service to compare against, followed by the variables it requires. A service the new version no longer declares is left running, named in the plan as dropped. A volume the new version adds is attached at your plan\'s size and mounted where the manifest says; an existing one is never resized. For a storage bucket, a public-access change reaches that bucket on every branch that has a copy. Refused with a 409 when the template was republished since the plan, or when this instance has already moved off this deployment')

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM - approved.

@CarmenDou
CarmenDou merged commit 501bdee into main Oct 11, 2026
3 checks passed
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