Track: cli-default-experimental-api-20260430 Spec: spec.md
- Source: /please:plan
- Track: cli-default-experimental-api-20260430
- Created: 2026-04-30
- Approach: Add
experimental-apiboolean input. Validate mutual exclusion withvercel-argsat config-parse time. Move routing default to CLI increateVercelClient()and gate the API path behindexperimentalApi. Emit acore.warningwhen the API path is selected.
Make Vercel CLI the default deployment client and gate the @vercel/client API path behind an explicit opt-in. The API client depends on an internal Vercel package without semver guarantees, so users should opt in consciously.
createVercelClient()insrc/vercel.ts:9-16currently selects the API client whenvercelArgsis empty. This plan inverts that default.getActionConfig()insrc/config.ts:81-117parses all action inputs. The newexperimental-apiinput is parsed here, and the mutual-exclusion check lives here too.ActionConfiginsrc/types.ts:55-83carries the parsed config across the codebase.- Existing tests in
src/__tests__/vercel.test.ts:495-515already cover the routing matrix and need to be updated to the new semantics. - The previous track
api-based-deployment-20260329is the immediate predecessor and added the typed inputs (target,prebuilt, etc.) that remain available regardless of routing default.
Validation lives at config-parse time; routing lives at the factory. Splitting these two concerns keeps each file's responsibility narrow:
getActionConfig()is the single place that reads inputs and produces a typed config. Cross-input validation (mutual exclusion) belongs here so it fails fast before any side effects (env var setup, octokit creation, deployment context fetching) run.createVercelClient()only decides which client to instantiate. It assumes the config it receives is already valid, which keeps the routing logic boolean-simple and purely a function ofexperimentalApi.
The factory does not re-check vercelArgs against experimentalApi; that invariant is enforced upstream. This avoids defensive duplication and surfaces the misconfiguration earlier in the run.
The experimental warning is emitted in the factory (not the constructor) because the factory is the single point that proves "API path was actually chosen for this run." Constructors of VercelApiClient are also instantiated by tests and would emit noise.
- T001 Add
experimental-apiboolean input toaction.ymlwith default'false'and a description explaining it gates opt-in API mode and is mutually exclusive withvercel-args(file: action.yml) - T002 Add
experimentalApi: booleanfield toActionConfiginterface (file: src/types.ts) (depends on T001) - T003 Parse the new input in
getActionConfig()and add the mutual-exclusion error: throw a clearErrorwhen bothexperimental-api === 'true'andvercel-argsis non-empty (file: src/config.ts) (depends on T002) - T004 Add unit tests in
src/__tests__/config.test.tsfor: (a)experimentalApi=falsedefault, (b)experimentalApi=trueparses correctly, (c) mutual-exclusion error message and stack when both are set (file: src/tests/config.test.ts) (depends on T003)
- T005 Update
createVercelClient()insrc/vercel.ts: route toVercelCliClientby default; route toVercelApiClientonly whenconfig.experimentalApi === true; emitcore.warningdescribing the experimental nature and how to opt out when API path is taken (file: src/vercel.ts) (depends on T003) - T006 Update existing routing tests in
src/__tests__/vercel.test.ts(thedescribe('createVercelClient', ...)block): replace the two existing cases with the new four-case matrix from spec AC-1 — verify warning text in the API case, verifycore.infotext in the CLI case, verify bothvercelArgs=""andvercelArgs="--prod"route toVercelCliClientwhenexperimentalApi=false(file: src/tests/vercel.test.ts) (depends on T005) - T007 [P] Update
createConfigtest helper insrc/__tests__/vercel.test.tsto includeexperimentalApi: falseso the helper produces a validActionConfigafter T002 (file: src/tests/vercel.test.ts) (depends on T002)
- T008 Replace the empty
descriptionofvercel-argsinaction.ymlwith a real description noting it is for ad-hoc CLI passthrough; reword thedeprecationMessageofscopeso it no longer claims scope is "only for CLI when vercel-args is provided" (since CLI is now the default regardless of vercel-args). Keep zeit-/now- deprecations untouched (file: action.yml) (depends on T001)
- T009 Add a new "Deployment Mode" section to README.md near the top of the inputs documentation: explain CLI is the default, document the
experimental-apiopt-in, surface the experimental warning, document the mutual-exclusion rule (file: README.md) (depends on T008) - T010 [P] Add a migration note to README.md (or a release-notes section) explicitly calling out that users who relied on the previous API-default must now set
experimental-api: trueto keep that behavior (file: README.md) (depends on T009)
- T011 Update or add an integration test in
src/__integration__/that runs the action twice against the emulator: once with default inputs (CLI path) and once withexperimental-api: true(API path), verifying both produce a validpreview-url(file: src/integration/vercel-api.test.ts) (depends on T005) - T012 Run the full quality gate locally:
pnpm lint,pnpm typecheck,pnpm build,pnpm test,pnpm test:integration. Fix any failures uncovered (depends on T006, T007, T011)
T001 ──► T002 ──► T003 ──► T004
│
└─► T005 ──► T006 ──► T012
│ │
│ └────► T011 ──► T012
│
└─► T007 [P, depends on T002] ──► T012
T001 ──► T008 ──► T009 ──► T010 [P]
[P] tasks are parallelizable with their siblings under the same dependency root.
| File | Change |
|---|---|
action.yml |
Add experimental-api input; add real description to vercel-args; reword scope deprecation |
src/types.ts |
Add experimentalApi: boolean to ActionConfig |
src/config.ts |
Parse experimental-api; add mutual-exclusion validation in getActionConfig() |
src/vercel.ts |
Invert routing default; emit experimental warning |
src/__tests__/config.test.ts |
Tests for new input parsing and mutual-exclusion error |
src/__tests__/vercel.test.ts |
Update test config helper; rewrite routing matrix tests |
src/__integration__/vercel-api.test.ts |
Add CLI-default and experimental-api opt-in cases |
README.md |
Deployment Mode section + migration note |
pnpm lint— no warningspnpm typecheck— passespnpm test— all unit tests pass, including the new four-case routing matrixpnpm test:integration— emulator tests pass for both default (CLI) andexperimental-api: true(API) pathspnpm build—dist/index.jsregenerated successfully- Manual: invoke the action with default inputs and confirm
core.info('Using CLI-based deployment')is logged (no warning) - Manual: invoke the action with
experimental-api: trueand confirmcore.warning(...)is logged exactly once - Manual: invoke the action with both
experimental-api: trueandvercel-args: --prodand confirm a clear config error is thrown before any deployment side effects
- 2026-04-30 — Phase 1 complete: T001 added
experimental-apiinput; T002 addedexperimentalApi: booleantoActionConfig; T003 added parsing + mutual-exclusion validation; T004 added 7 config tests (defaults, parsing, error message) - 2026-04-30 — Phase 2 complete: T005 inverted routing default in
createVercelClient()and emitscore.warningwhen API path is taken; T006 rewrotedescribe('createVercelClient', ...)as the four-case AC-1 matrix; T007 updatedcreateConfigtest helper - 2026-04-30 — Phase 3 complete: T008 replaced empty
vercel-argsdescription, rewordedscopedeprecation message - 2026-04-30 — Phase 4 complete: T009 replaced "Migration to API-based Deployment" with "Deployment Mode" section in README; T010 added migration note for users on the previous v42 API default
- 2026-04-30 — Phase 5 complete: T011 added factory-routing integration tests +
experimentalApifield to integrationcreateConfig; T012 quality gate (lint, typecheck, build, full test suite + integration suite) all green — 244 unit + 19 integration tests pass
| Date | Decision | Rationale |
|---|---|---|
| 2026-04-30 | Validate mutual exclusion at config-parse time (not in createVercelClient) |
Fails fast before any side effects; one source of truth; easier to unit-test in isolation |
| 2026-04-30 | Emit experimental warning in createVercelClient(), not in VercelApiClient constructor |
Co-located with routing decision; constructor is also called by tests and would produce warning noise |
| 2026-04-30 | Field naming: experimentalApi (camelCase) and experimental-api (kebab-case input) |
Matches existing convention (vercelOrgId / vercel-org-id, autoAssignCustomDomains / auto-assign-custom-domains) |
| 2026-04-30 | Treat as semver MINOR (per user direction), not MAJOR | No public API contract changes; only default behavior shifts. Document the migration in README and release notes |
| 2026-04-30 | Hard-fail on mutual exclusion rather than silent precedence | Surfaces misconfiguration immediately; avoids surprising deployments where one input is silently ignored |
- The spec's FR-6 mentioned removing a
deprecationMessagefromvercel-args, but inspection ofaction.yml:7-10showsvercel-argshas nodeprecationMessageat all — only an empty description. The real cleanup target is thescopeinput (line 44), whose deprecation message references "CLI-based deployments (when vercel-args is provided)" — that conditional clause is now misleading because CLI is the default regardless ofvercel-args. T008 captures this corrected scope. - Existing tests in
src/__tests__/vercel.test.ts:495-515already provide a routing test scaffold; this plan rewrites them rather than creating new files. src/__tests__/vercel.test.ts:27-55createConfighelper does not currently includegithubDeployment/githubDeploymentEnvironmentfields — it produces aPartial<ActionConfig>cast as full. T007 will update it for the newexperimentalApifield; whether to also fix the missing legacy fields is out of scope for this track.