Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/plans/deploy-v1-credentials-runtime-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Source spec: `docs/plans/deploy-v1-credentials-and-runtime-spec.md`
- [x] Compute markup only for `relay_managed`.
- [x] Emit monthly soft-cap warning over $100.
- [x] Wire CLI `--harness-source byok --byok-key` to BYOK route before deploy.
- [x] Wire CLI `--harness-source plan` to managed credential route before deploy.
- [x] Wire CLI `--harness-source managed` to managed credential route before deploy (`plan` remains a legacy alias).
- [x] Preserve CLI `--harness-source oauth` behavior.
- [x] Add cloud BYOK route tests.
- [x] Add cloud managed route tests.
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/deploy-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,18 @@ test('parseDeployArgs: --reconnect is repeatable and comma-aware', () => {
assert.deepEqual(parsed.reconnectProviders, ['slack', 'github', 'linear']);
});

test('parseDeployArgs: --harness-source managed is accepted', () => {
const parsed = parseDeployArgs(['./persona.json', '--harness-source', 'managed']);

assert.equal(parsed.harnessSource, 'managed');
});

test('parseDeployArgs: legacy --harness-source plan normalizes to managed', () => {
const parsed = parseDeployArgs(['./persona.json', '--harness-source=plan']);

assert.equal(parsed.harnessSource, 'managed');
});

test('parseDeployArgs: malformed --input exits with clean error', () => {
const trap = trapExit();
try {
Expand Down
15 changes: 11 additions & 4 deletions packages/cli/src/deploy-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ Flags:
--dry-run Validate the persona and exit before any side effects
--cloud-url <url> Override the workforce cloud base URL
--no-prompt Fail instead of prompting for cloud setup
--harness-source <source> Cloud harness source: plan, byok, or oauth
--harness-source <source> Cloud harness source: managed, byok, or oauth
(legacy alias: plan)
--byok-key <key> API key for --harness-source byok
--on-exists <choice> Existing cloud persona behavior: cancel, update, or destroy
--input <key>=<value> Override a declared persona input (repeatable)
Expand Down Expand Up @@ -262,7 +263,6 @@ Flags:
-h, --help Print this message
`;

const HARNESS_SOURCES = ['plan', 'byok', 'oauth'] as const;
const ON_EXISTS_CHOICES = ['update', 'destroy', 'cancel'] as const;

export function parseDeployArgs(args: readonly string[]): DeployOptions {
Expand Down Expand Up @@ -317,9 +317,9 @@ export function parseDeployArgs(args: readonly string[]): DeployOptions {
noPrompt = true;
noConnect = true;
} else if (a === '--harness-source') {
harnessSource = expectChoice('--harness-source', expectValue('--harness-source', args[++i]), HARNESS_SOURCES);
harnessSource = expectHarnessSourceFlag(expectValue('--harness-source', args[++i]));
} else if (a.startsWith('--harness-source=')) {
harnessSource = expectChoice('--harness-source', expectInlineValue('--harness-source', a.slice('--harness-source='.length)), HARNESS_SOURCES);
harnessSource = expectHarnessSourceFlag(expectInlineValue('--harness-source', a.slice('--harness-source='.length)));
} else if (a === '--byok-key') {
byokKey = expectValue('--byok-key', args[++i]);
} else if (a.startsWith('--byok-key=')) {
Expand Down Expand Up @@ -415,6 +415,13 @@ function expectChoice<T extends string>(flag: string, value: string, allowed: re
return value as T;
}

function expectHarnessSourceFlag(value: string): DeployOptions['harnessSource'] {
const normalized = value.trim().toLowerCase();
if (normalized === 'plan') return 'managed';
if (normalized === 'managed' || normalized === 'byok' || normalized === 'oauth') return normalized;
die(`--harness-source: expected one of managed|byok|oauth (legacy alias: plan); got "${value}"`);
}

function parseLoginArgs(args: readonly string[]): { workspace?: string; cloudUrl?: string } {
let workspace: string | undefined;
let cloudUrl: string | undefined;
Expand Down
18 changes: 17 additions & 1 deletion packages/deploy/src/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,23 @@ test('deploy --dry-run rejects useSubscription when cloud mode is not selected',
}
});

test('deploy --dry-run rejects useSubscription with workforce plan credentials', async () => {
test('deploy --dry-run rejects useSubscription with workforce managed credentials', async () => {
const { personaPath, cleanup } = await withTempPersona(
basePersonaJson({ useSubscription: true })
);
const io = createBufferedIO();
try {
await assert.rejects(
deploy({ personaPath, mode: 'cloud', dryRun: true, harnessSource: 'managed', io }),
/use --harness-source oauth/
);
assert.ok(!io.messages.find((m) => m.message.startsWith('workspace:')));
} finally {
await cleanup();
}
});

test('deploy --dry-run rejects useSubscription with legacy plan alias', async () => {
const { personaPath, cleanup } = await withTempPersona(
basePersonaJson({ useSubscription: true })
);
Expand Down
2 changes: 1 addition & 1 deletion packages/deploy/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ function validateSubscriptionSupport(
args: {
mode: DeployMode;
subscription?: ProviderSubscriptionResolver;
harnessSource?: 'plan' | 'byok' | 'oauth';
harnessSource?: 'managed' | 'plan' | 'byok' | 'oauth';
}
): void {
if (!persona.useSubscription || args.subscription) return;
Expand Down
20 changes: 18 additions & 2 deletions packages/deploy/src/modes/cloud-subscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ function subscriptionArgs(
// Step 2 — subscription-source tests (tests 1–3)
// ---------------------------------------------------------------------------

test('validateCloudSubscriptionSupport throws when harnessSource is plan', () => {
test('validateCloudSubscriptionSupport throws when harnessSource is managed', () => {
assert.throws(
() => validateCloudSubscriptionSupport({ persona: persona(), harnessSource: 'plan' }),
() => validateCloudSubscriptionSupport({ persona: persona(), harnessSource: 'managed' }),
(err: Error) => {
assert.ok(
err.message.includes('useSubscription:true'),
Expand All @@ -137,6 +137,13 @@ test('validateCloudSubscriptionSupport throws when harnessSource is plan', () =>
);
});

test('validateCloudSubscriptionSupport throws when harnessSource is legacy plan alias', () => {
assert.throws(
() => validateCloudSubscriptionSupport({ persona: persona(), harnessSource: 'plan' }),
/useSubscription:true/
);
});

test('validateCloudSubscriptionSupport throws when WORKFORCE_DEPLOY_HARNESS_SOURCE=plan', async () => {
await withEnv({ WORKFORCE_DEPLOY_HARNESS_SOURCE: 'plan' }, async () => {
assert.throws(
Expand All @@ -146,6 +153,15 @@ test('validateCloudSubscriptionSupport throws when WORKFORCE_DEPLOY_HARNESS_SOUR
});
});

test('validateCloudSubscriptionSupport throws when WORKFORCE_DEPLOY_HARNESS_SOURCE=managed', async () => {
await withEnv({ WORKFORCE_DEPLOY_HARNESS_SOURCE: 'managed' }, async () => {
assert.throws(
() => validateCloudSubscriptionSupport({ persona: persona() }),
/useSubscription:true/
);
});
});

test('ensureCloudSubscriptionReady oauth leg throws "credentials are not connected" under noPrompt when no connected row', async () => {
// Default source is oauth when no harnessSource supplied. With no connected
// entry in /cloud-agents and noPrompt:true, must throw the subscription
Expand Down
Loading
Loading