Skip to content

Commit ed665b2

Browse files
authored
fix(vercel-api): send nowConfig and projectSettings to honor vercel.json (#350)
* docs(track): add build-exit-255-20260423 Track for issue #336 (build.sh exits with 255 on v42.2.0). Root cause confirmed via source comparison of @vercel/client@17.2.65 and vercel@50.0.0: VercelApiClient does not send nowConfig or projectSettings in the deployment body, so the server never sees the user's vercel.json buildCommand and falls back to framework auto-detection. Refs #336 * chore(track): build-exit-255-20260423 구현 시작 * feat(project-config): add readVercelJson to load vercel.json from working directory Reads vercel.json from the resolved working directory, returns parsed object on success, null when absent, and throws a clear error naming the file on invalid JSON. Part of #336 — v42.2.0 API deployment does not send nowConfig, so the server never sees the user's buildCommand. This is the first building block toward restoring CLI parity. * feat(project-config): add readNodeVersion to read engines.node from package.json Reads package.json from the resolved working directory and returns engines.node as a string when present, or undefined otherwise. Mirrors vercel@50.0.0 CLI behavior at packages/cli/src/commands/deploy/index.ts:541-558. Unreadable or malformed package.json returns undefined rather than throwing, because the field is an optional hint for projectSettings and should not fail the whole deployment. Part of #336. * feat(project-config): add buildProjectConfig to assemble nowConfig and projectSettings Produces the { nowConfig, projectSettings } subset the Vercel deployment API expects. When vercel.json is present, its contents become nowConfig (with images stripped). For zero-config projects (no 'builds' field) the function also fills projectSettings.rootDirectory and sourceFilesOutsideRootDirectory from the action's rootDirectory input. projectSettings.nodeVersion is populated from package.json engines.node whenever present. This mirrors vercel@50.0.0 CLI behavior at packages/cli/src/commands/deploy/index.ts:494-571. Part of #336. * fix(vercel-api): send nowConfig and projectSettings to Vercel API Closes the CLI-parity gap introduced in v42.2.0. When deploying via the API path, read vercel.json and package.json from the working directory and include: - nowConfig: full vercel.json contents (with images stripped) - projectSettings.rootDirectory and sourceFilesOutsideRootDirectory (zero-config projects only) - projectSettings.nodeVersion (from package.json engines.node) Without these fields, the Vercel server never sees the user's buildCommand/installCommand/outputDirectory and falls back to framework auto-detection, causing projects with custom build scripts (e.g. ./build.sh for Hugo) to fail with exit 255. Refs vercel@50.0.0 CLI at packages/cli/src/commands/deploy/index.ts:494-571. Fixes #336 * test(integration): verify nowConfig deployment path with vercel.json Add two integration tests that exercise the real deploy() code path: - Deploy a tmp project that ships vercel.json with buildCommand. Asserts that the filesystem-read path does not crash and does not surface a vercel.json-related error — regression guard for #336. - Deploy with malformed vercel.json. Asserts the action fails fast with a message naming the file (FR-5). Also add GITHUB_REPOSITORY and GITHUB_ACTOR defaults to the integration global-setup so the suite runs locally, not only in GitHub Actions CI (where these are injected automatically). * build: rebuild dist/index.js with nowConfig/projectSettings fix Bundles the src changes from #336 into the distribution file that GitHub Actions actually executes. Verified dist/index.js contains the new buildProjectConfig / readVercelJson / readNodeVersion symbols. Part of #336. * docs(readme): note vercel.json is honored by the API deployment path Call out the #336 fix under the API Deployment Inputs section so users upgrading from v42.2.0 know custom buildCommand / installCommand / outputDirectory values in vercel.json are now sent to the Vercel API. * docs(track): mark build-exit-255-20260423 implementation tasks complete * fix(vercel): apply review suggestions from iteration 1 - Extract applyConditionalFlags and applyProjectConfig helpers from buildDeploymentOptions so each function stays under the 50 LOC limit per AGENTS.md. - Sanitize __proto__ / constructor / prototype keys from nowConfig in addition to the existing 'images' strip. Defense-in-depth against prototype-pollution via a crafted vercel.json, for any downstream code that forwards nowConfig via [[Set]] semantics. - Add regression tests: EISDIR on vercel.json path re-throws (locks the 'only ENOENT is treated as absent' contract) and __proto__ keys are stripped from nowConfig. - Rebuild dist/ with the refactor. - Ignore .claude/agent-memory/ (machine-local agent state). * chore(track): build-exit-255-20260423 PR 제출 완료 * chore(review): apply AI review suggestions from iteration 2 - README: reword version note to be version-neutral (copilot) - project-config: guard against non-object JSON at top level of vercel.json (cubic) - project-config test: drop toHaveProperty(__proto__) check that walks the prototype chain; the Object.getOwnPropertyNames check covers the invariant correctly (cubic) - Add regression tests for array/string/number/boolean/null vercel.json - Rebuild dist/ * chore: regenerate pnpm-lock.yaml and rebuild dist The check-dist CI workflow failed with: ERR_PNPM_BROKEN_LOCKFILE: expected a single document in the stream Regenerating the lockfile from scratch with pnpm@10.15.0 produced a slightly shorter (-226 lines) file that installs cleanly on CI. Rebuild dist/ with the fresh node_modules to keep check-dist in sync. * chore: trigger CI on 01260ec * build: rebuild dist
1 parent 49bc6f3 commit ed665b2

17 files changed

Lines changed: 4413 additions & 3145 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,7 @@ typings/
7272
.next
7373

7474
# please plugin runtime state
75-
.please/state/
75+
.please/state/
76+
77+
# Claude Code agent-persistent memory (machine-local, not project artifacts)
78+
.claude/agent-memory/

.please/docs/tracks.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
{"id":"prebuilt-project-id-bug-20260408","type":"bugfix","status":"review","phase":"finalize","issue":"#330","created":"2026-04-08","section":"completed"}
22
{"id":"relative-working-dir-20260423","type":"bugfix","status":"review","phase":"finalize","issue":"#341","pr":"#349","created":"2026-04-23","section":"completed"}
3+
{"id":"build-exit-255-20260423","type":"bugfix","status":"review","phase":"finalize","issue":"#336","pr":"#350","created":"2026-04-23","section":"completed"}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Bug Investigation Report: Error on v42.2.0 Deployment (Issue #336)
2+
3+
## 1. Reproduction Status
4+
5+
**Status**: Root cause confirmed by source comparison of `@vercel/client@17.2.65` and `vercel@50.0.0` (pinned versions in `package.json`). Live reproduction not yet executed — to be done as task T-01 during implementation.
6+
7+
**Reported Symptom**:
8+
9+
```
10+
Command "./build.sh" exited with 255
11+
```
12+
13+
Triggered on v42.2.0 but not on v42.1.0 for the same repository and workflow (reference: `petems/petersouter.xyz` Hugo site with a custom `build.sh` in `vercel.json`'s `buildCommand`).
14+
15+
**Reproduction Path**:
16+
17+
1. `src/index.ts:269``createVercelClient(config)` returns a `VercelApiClient` because `vercel-args` is empty (new default path in v42.2.0).
18+
2. `src/vercel.ts:9-16` — Routing selects API client over CLI client.
19+
3. `src/vercel-api.ts:144-202``VercelApiClient.deploy()` calls `@vercel/client`'s `createDeployment` with `clientOptions` (from `buildClientOptions`) and `deploymentOptions` (from `buildDeploymentOptions`).
20+
4. `src/vercel-api.ts:68-116``buildDeploymentOptions()` populates only: `meta`, `gitMetadata`, `autoAssignCustomDomains`, `target`, `env`, `build.env`, `regions`, `public`, `customEnvironmentSlugOrId`, `name`, `project`. **It does NOT populate `nowConfig` or `projectSettings`.**
21+
5. `@vercel/client` sends a POST to `/v13/deployments` with only the provided fields plus the file manifest. The server has no knowledge of the user's `vercel.json` `buildCommand`.
22+
6. **Failure point** — Vercel's remote builder applies framework auto-detection (Hugo detected by the uploaded `config.toml`/`hugo.toml`), runs a default Hugo build command (or a pre-registered dashboard command), and `./build.sh` is either not executed or is executed in a context that fails. Exit code 255 is the wrapper exit code Vercel's builder returns on non-zero build-command exit.
23+
24+
## 2. Root Cause Analysis
25+
26+
**Confirmed Root Cause**: The `VercelApiClient` does NOT send `nowConfig` (the contents of the user's `vercel.json`) or `projectSettings` in the deployment body. The Vercel CLI — which the action used exclusively in v42.1.0 and earlier — always sends both. Without them, the server-side build does not receive the user's `buildCommand`, `installCommand`, `outputDirectory`, `rootDirectory`, etc., and falls back to framework auto-detection. Combined with `skipAutoDetectionConfirmation: true` (unconditionally set in `src/vercel-api.ts:63`), the server proceeds silently with whatever it detects, overriding the user's intent.
27+
28+
**Evidence from CLI source** (`vercel@50.0.0`, `packages/cli/src/commands/deploy/index.ts:494-571`):
29+
30+
```typescript
31+
createArgs: CreateArgs = {
32+
name,
33+
env: deploymentEnv as Dictionary<string>,
34+
build: { env: deploymentBuildEnv as Dictionary<string> },
35+
forceNew: parsedArguments.flags['--force'],
36+
// ...
37+
nowConfig: {
38+
...localConfig, // <-- vercel.json contents
39+
images: undefined,
40+
},
41+
regions,
42+
meta,
43+
gitMetadata,
44+
target,
45+
skipAutoDetectionConfirmation: autoConfirm,
46+
// ...
47+
};
48+
49+
if (!localConfig.builds || localConfig.builds.length === 0) {
50+
// Only add projectSettings for zero config deployments
51+
createArgs.projectSettings = {
52+
sourceFilesOutsideRootDirectory,
53+
rootDirectory,
54+
...localConfigurationOverrides,
55+
};
56+
}
57+
// ...
58+
if (!createArgs.projectSettings) createArgs.projectSettings = {};
59+
createArgs.projectSettings.nodeVersion = nodeVersion; // from package.json engines.node
60+
```
61+
62+
The CLI populates **both** `nowConfig` and `projectSettings`. The vercel-action populates **neither**.
63+
64+
**Evidence from action source** (`src/vercel-api.ts:68-116`):
65+
66+
```typescript
67+
function buildDeploymentOptions(config: ActionConfig, deployContext: DeploymentContext): DeploymentOptions {
68+
const options: DeploymentOptions = {
69+
meta: { /* ... */ },
70+
gitMetadata: buildGitMetadata(deployContext),
71+
autoAssignCustomDomains: config.autoAssignCustomDomains,
72+
}
73+
if (config.target === 'production') { options.target = 'production' }
74+
if (Object.keys(config.env).length > 0) { options.env = config.env }
75+
if (Object.keys(config.buildEnv).length > 0) { options.build = { env: config.buildEnv } }
76+
// regions, public, customEnvironmentSlugOrId, name, project ...
77+
return options
78+
}
79+
```
80+
81+
No `nowConfig`. No `projectSettings`. Also no read of `vercel.json` anywhere in the action codebase (confirmed via grep for `vercel.json` in `src/`).
82+
83+
**Type support** (`@vercel/client@17.2.65`, `packages/client/src/types.ts:181-205`):
84+
85+
```typescript
86+
export interface DeploymentOptions {
87+
// ...
88+
projectSettings?: ProjectSettings;
89+
// ...
90+
}
91+
```
92+
93+
`projectSettings` is a typed DeploymentOptions field. `nowConfig` is accepted by the API but not in the DeploymentOptions type — the CLI passes it via a broader `createArgs` shape.
94+
95+
**Why the mode/executable-bit hypothesis is wrong**: `@vercel/client` DOES preserve POSIX mode. `packages/client/src/utils/hashes.ts:54-79` reads `mode = stat.mode` via `fs.lstat`. `packages/client/src/utils/index.ts:380-420` includes `mode: file.mode` in the `PreparedFile` manifest sent to the server. So executable bits ARE preserved end-to-end.
96+
97+
**Supporting evidence (sibling v42.2.0 regressions)**: All trace to the same missing `nowConfig`/`projectSettings`:
98+
99+
- #341 (`working-directory` must be absolute) — `@vercel/client` requires absolute paths (`packages/client/src/create-deployment.ts:77-91`). The CLI resolves to absolute before calling; the action passes through raw.
100+
- #342 (project name falls back to repo name) — without `nowConfig.name`, the client uses `getDefaultName()` (`packages/client/src/deploy.ts:114-131`) which picks the last path segment.
101+
- #343 (alias assignment fails on preview) — alias/autoAlias config lives in `nowConfig` via `vercel.json`; the server falls back without it.
102+
- #345 (paid-team / CLI-version complaints) — when framework auto-detection replaces the user's `buildCommand`, builds hit different server-side code paths that have different eligibility checks.
103+
104+
## 3. Proposed Solutions
105+
106+
### Solution 1: Send `nowConfig` and `projectSettings` from the action (Recommended)
107+
108+
Mirror what the CLI does in `packages/cli/src/commands/deploy/index.ts:494-571`:
109+
110+
1. Read `vercel.json` from `workingDirectory` (or `rootDirectory`). Pass its contents as `nowConfig` in DeploymentOptions.
111+
2. Read `package.json`'s `engines.node` and build `projectSettings.nodeVersion`.
112+
3. When `localConfig.builds` is absent (zero-config), populate `projectSettings.rootDirectory` / `projectSettings.sourceFilesOutsideRootDirectory` / `projectSettings.localConfigurationOverrides`.
113+
4. Pass them through via `Object.assign(options, { nowConfig, projectSettings })` (same pattern used for `project` on line 112).
114+
115+
**Pros**: Mirrors CLI behavior exactly — fixes #336 and reduces divergence for #341/#342/#343. Narrow surface.
116+
**Cons**: Introduces dependency on filesystem reads of `vercel.json`/`package.json`. Need to handle missing/invalid config gracefully.
117+
118+
### Solution 2: Defer to the CLI for deployments that have `vercel.json` (Fallback only)
119+
120+
If the action detects `vercel.json` in the working directory, route to `VercelCliClient` instead of `VercelApiClient`.
121+
122+
**Pros**: Cheap to implement; keeps CLI as the "reliable" path.
123+
**Cons**: Defeats the purpose of the API migration. Doesn't address the underlying gap. Users would hit other CLI-only issues (#345 paid-team requirement, telemetry, etc.). Strong preference **against**.
124+
125+
### Solution 3: Conditionally skip `skipAutoDetectionConfirmation`
126+
127+
Set `skipAutoDetectionConfirmation: true` only when the project is being created fresh (no `vercel-project-id`). For existing linked projects, let the server respect stored settings.
128+
129+
**Pros**: Single-line change.
130+
**Cons**: Doesn't fix the root gap — the server still doesn't see the user's `vercel.json` via `nowConfig`. Partial fix only.
131+
132+
## 4. Testing Requirements
133+
134+
1. **Bug scenario (TR-1)** — Integration test: seed a project via `emulate.dev` with `vercel.json` containing `"buildCommand": "./build.sh"` and an executable `build.sh`. Run `VercelApiClient.deploy()`. Assert: the emulator receives a POST body that includes `nowConfig.buildCommand === "./build.sh"`.
135+
2. **Edge case (TR-2)** — Missing `vercel.json`: deploy must succeed with empty `nowConfig` (no regression on projects that don't ship `vercel.json`).
136+
3. **Edge case (TR-3)** — Invalid/unparseable `vercel.json`: action must fail fast with a clear error, not hang or silently drop config.
137+
4. **Edge case (TR-4)**`working-directory` provided: `vercel.json` must be read relative to `workingDirectory`, not `process.cwd()`.
138+
5. **Regression (TR-5)** — Existing API deployment tests (no `vercel.json`) continue to pass.
139+
6. **Regression (TR-6)** — CLI path (`vercel-args` provided) remains green.
140+
7. **Unit (TR-7)**`buildDeploymentOptions()` returns a DeploymentOptions with `nowConfig` and `projectSettings` populated when inputs include a `vercel.json`.
141+
142+
## 5. Similar Code Patterns
143+
144+
- `src/vercel-api.ts:107-113` — existing "push extra fields" pattern via `Object.assign(options, { project: config.vercelProjectId })`. Same pattern applies to `nowConfig`/`projectSettings`.
145+
- `src/vercel-api.ts:63``skipAutoDetectionConfirmation = true`. Relevant to Solution 3 and to the broader interaction.
146+
- `src/__integration__/vercel-api.test.ts` — existing API deployment integration tests against `emulate.dev`; new tests can follow the same structure.
147+
- Related recent fixes in the v42.x line: `1d5e098` (project ID fix — same `Object.assign` pattern), `5dc8cc2` (scope loop), `bfe8c4e` (scope for inspect/alias). The v42.2.0 migration has consistently missed CLI-parity edge cases.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"track_id": "build-exit-255-20260423",
3+
"type": "bugfix",
4+
"status": "review",
5+
"created_at": "2026-04-23T12:32:18Z",
6+
"updated_at": "2026-04-23T23:10:00Z",
7+
"issue": "#336",
8+
"pr": "#350",
9+
"project": ""
10+
}

0 commit comments

Comments
 (0)