Skip to content

Commit 64420fe

Browse files
committed
Merge remote-tracking branch 'origin/master' into amondnet/vercel-build-step
# Conflicts: # .please/docs/tracks.jsonl # dist/index.js # dist/index.js.map
2 parents fb288ab + 31671aa commit 64420fe

17 files changed

Lines changed: 3587 additions & 83 deletions

File tree

.please/docs/knowledge/tech-stack.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
| `axios` | HTTP requests for alias domain management |
2323
| `vercel` (v50.0.0) | Vercel CLI — deployment engine (fallback when `vercel-args` provided) |
2424
| `common-tags` | Template literal tag functions for comment formatting |
25+
| `semver` | Normalizes `package.json` `engines.node` into Vercel's `NN.x` API enum (see #359) |
2526

2627
## Dev Tooling
2728

.please/docs/tracks.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
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"}
33
{"id":"build-exit-255-20260423","type":"bugfix","status":"review","phase":"finalize","issue":"#336","pr":"#350","created":"2026-04-23","section":"completed"}
4+
{"id":"fix-vercel-validation-20260430","type":"bugfix","status":"review","phase":"finalize","issue":"#359","pr":"#364","created":"2026-04-30","section":"completed"}
45
{"id":"auto-vercel-build-20260430","type":"feature","status":"review","phase":"finalize","issue":"#360","pr":"#361","created":"2026-04-30","section":"completed"}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Bug Investigation Report: v42.2.1 Deployment Validation Errors
2+
3+
> Track: fix-vercel-validation-20260430
4+
> Issue: [#359](https://github.com/amondnet/vercel-action/issues/359)
5+
6+
## 1. Reproduction Status
7+
8+
**Reproduced** — both errors confirmed via static code-path tracing through `src/`, `dist/index.js` (bundled `@vercel/client`), and the Vercel CLI deploy code referenced by PR #350.
9+
10+
### Reproduction Path — Bug 1 (`projectSettings.nodeVersion` rejected)
11+
12+
1. `src/index.ts` reads action inputs and builds `ActionConfig`, then calls `client.deploy()`.
13+
2. `src/project-config.ts:51-52``readNodeVersion()` returns `parsed.engines?.node` as-is (e.g. `">=24.0.0"`).
14+
3. `src/project-config.ts:114-116``buildProjectConfig()` assigns `projectSettings.nodeVersion = nodeVersion` verbatim.
15+
4. `src/vercel-api.ts:138-140``applyProjectConfig()` sets `options.projectSettings = projectConfig.projectSettings`.
16+
5. `src/vercel-api.ts:177``createDeployment(clientOptions, deploymentOptions)` is called.
17+
6. `dist/index.js:35126-35129``@vercel/client.postDeployment` does `JSON.stringify({ ...deploymentOptions, files })`, sending `projectSettings.nodeVersion = ">=24.0.0"` to the REST API.
18+
7. **Failure**: Vercel REST API validates `projectSettings.nodeVersion` against the enum `["24.x","22.x","20.x"]` and returns `400 bad_request`.
19+
20+
### Reproduction Path — Bug 2 (`nowConfig` rejected)
21+
22+
1. `src/project-config.ts:98-100``buildProjectConfig()` sets `result.nowConfig = sanitizeNowConfig(vercelJson)`.
23+
2. `src/vercel-api.ts:135-137``applyProjectConfig()` does `Object.assign(options, { nowConfig: projectConfig.nowConfig })`.
24+
3. `src/vercel-api.ts:177``createDeployment(clientOptions, deploymentOptions)` is called.
25+
4. `dist/index.js:35126-35129``postDeployment` raw-spreads `deploymentOptions` into the JSON body, so `nowConfig` becomes a top-level REST API field.
26+
5. **Failure**: Vercel REST API rejects `nowConfig` as `additionalProperty` (`400 bad_request`).
27+
28+
## 2. Root Cause Analysis
29+
30+
### Bug 1
31+
**Problem Location**: `src/project-config.ts:38-58`, function `readNodeVersion()`, lines 51-52.
32+
33+
**Root Cause**: The function returns `parsed.engines?.node` verbatim. The Vercel REST API's `projectSettings.nodeVersion` field is an enum (`"24.x" | "22.x" | "20.x"`), not a free-form semver range. Any user value that is not already in canonical `NN.x` form (e.g., `">=24.0.0"`, `"^20.0.0"`, `"24.0.0"`, `">=18"`) is rejected with HTTP 400.
34+
35+
The Vercel CLI normalizes this via `@vercel/build-utils` `getSupportedNodeVersion()` (visible at `dist/index.js:32044`). It runs `semver.intersects(o.range, engineRange)` against an internal `NODE_VERSIONS` array and returns the canonical `NN.x` string. The action does not perform this step.
36+
37+
```typescript
38+
// src/project-config.ts:46-58 — current
39+
export function readNodeVersion(workingDirectory: string): string | undefined {
40+
const filePath = path.join(resolveWorkingDir(workingDirectory), 'package.json')
41+
let raw: string
42+
try { raw = readFileSync(filePath, 'utf8') }
43+
catch { return undefined }
44+
45+
try {
46+
const parsed = JSON.parse(raw) as { engines?: { node?: unknown } }
47+
const node = parsed.engines?.node
48+
return typeof node === 'string' ? node : undefined // ← returns ">=24.0.0" unchanged
49+
}
50+
catch { return undefined }
51+
}
52+
```
53+
54+
### Bug 2
55+
**Problem Location**: `src/vercel-api.ts:130-141`, function `applyProjectConfig()`, lines 135-137.
56+
57+
**Root Cause**: The function injects `nowConfig` as a top-level field on `DeploymentOptions`. `@vercel/client.postDeployment` (`dist/index.js:35101`) raw-spreads `DeploymentOptions` into the JSON body. The Vercel REST API rejects `nowConfig` as `additionalProperty`.
58+
59+
The comment claiming the API accepts `nowConfig` is incorrect:
60+
61+
```typescript
62+
// src/vercel-api.ts:130-141 — current
63+
function applyProjectConfig(options: DeploymentOptions, config: ActionConfig): void {
64+
// nowConfig is accepted by the REST API but not declared in DeploymentOptions ← FALSE
65+
// (it's documented at https://vercel.com/docs/configuration#system-properties)
66+
const projectConfig = buildProjectConfig(config)
67+
if (projectConfig.nowConfig) {
68+
Object.assign(options, { nowConfig: projectConfig.nowConfig }) // ← rejected as additional property
69+
}
70+
if (projectConfig.projectSettings) {
71+
options.projectSettings = projectConfig.projectSettings
72+
}
73+
}
74+
```
75+
76+
The Vercel CLI's deploy command (referenced by PR #350 as the parity target, lines 494-571 in the CLI source) does **not** send `nowConfig`. Instead, it copies a select set of `vercel.json` keys (`buildCommand`, `installCommand`, `outputDirectory`, `framework`, `devCommand`) into `projectSettings`.
77+
78+
## 3. Proposed Solutions
79+
80+
### Bug 1 — Solution 1: Inline semver normalization (Recommended)
81+
82+
Use the `semver` package (already bundled at `dist/index.js:21921` as a transitive dep) to map any input to the closest matching `NN.x` from a static list of supported versions.
83+
84+
```typescript
85+
import semver from 'semver'
86+
87+
const VERCEL_NODE_VERSIONS = ['24.x', '22.x', '20.x'] as const
88+
89+
export function normalizeNodeVersion(input: string | undefined): string | undefined {
90+
if (!input) return undefined
91+
if ((VERCEL_NODE_VERSIONS as readonly string[]).includes(input)) return input
92+
for (const version of VERCEL_NODE_VERSIONS) {
93+
if (semver.intersects(input, version)) return version
94+
}
95+
return undefined
96+
}
97+
```
98+
99+
**Pros**: No new dependency; deterministic; matches Vercel CLI behavior. Easy to update when Vercel adds/removes supported versions.
100+
**Cons**: We hardcode the supported version list. If Vercel adds Node 26 silently, we miss it until we update.
101+
102+
### Bug 1 — Solution 2: Import from `@vercel/build-utils`
103+
104+
Reuse `getSupportedNodeVersion` directly from `@vercel/build-utils` (transitive via `@vercel/client`).
105+
106+
**Pros**: Always in sync with Vercel.
107+
**Cons**: We pin to an internal API of a transitive dep — fragile across upgrades. Same risk we already documented for `@actions/http-client`. Reaching into `@vercel/build-utils` adds bundle weight and a stronger coupling than is warranted.
108+
109+
### Bug 2 — Solution 1: Remove `nowConfig`, expand vercel.json keys into `projectSettings` (Recommended)
110+
111+
```typescript
112+
// src/project-config.ts — replacement for nowConfig assignment in buildProjectConfig
113+
const PROJECT_SETTINGS_KEYS = [
114+
'buildCommand',
115+
'installCommand',
116+
'outputDirectory',
117+
'framework',
118+
'devCommand',
119+
] as const
120+
121+
if (vercelJson) {
122+
for (const key of PROJECT_SETTINGS_KEYS) {
123+
if (key in vercelJson) {
124+
projectSettings[key] = vercelJson[key] as string | null
125+
}
126+
}
127+
}
128+
// Drop result.nowConfig entirely.
129+
```
130+
131+
**Pros**: Mirrors actual Vercel CLI behavior; honors `vercel.json` for the keys the API documents under `projectSettings`; small diff; preserves prototype-pollution safeguards because we only copy whitelisted keys.
132+
**Cons**: We hardcode the key list — adding support for new keys requires a code change.
133+
134+
### Bug 2 — Solution 2: Keep `nowConfig` but unwrap inside `applyProjectConfig`
135+
136+
Spread `nowConfig` keys directly into `projectSettings` and drop the `nowConfig` wrapper.
137+
138+
**Pros**: Minimal whitelist; user-driven keys propagate.
139+
**Cons**: We have no idea which `vercel.json` keys the API actually accepts as `projectSettings` — this is a footgun. The whitelist approach is safer.
140+
141+
## 4. Testing Requirements
142+
143+
1. **Bug 1 scenarios**: Unit tests for the new `normalizeNodeVersion` (or the updated `readNodeVersion`) covering: `"24.x"` (passthrough), `">=24.0.0"`, `"^20.0.0"`, `"24.0.0"`, `">=18"`, `">=99.0.0"` (no match → undefined), `"not-a-version"`, `undefined`.
144+
2. **Bug 2 scenarios**: Unit test asserting `nowConfig` is absent from `DeploymentOptions` for any combination of `vercel.json` contents.
145+
3. **PR #350 regression**: Test asserting Hugo-style `vercel.json` with `"buildCommand": "./build.sh"` results in `projectSettings.buildCommand === "./build.sh"`.
146+
4. **Existing assertion update**: `src/__tests__/vercel-api.test.ts:379-398` currently asserts `deployOpts.nowConfig.buildCommand === './build.sh'` — must be updated to assert `deployOpts.projectSettings.buildCommand === './build.sh'` and `deployOpts.nowConfig === undefined`.
147+
5. **Integration**: Update `src/__integration__/vercel-api.test.ts` with a deployment whose `package.json` has `engines.node: ">=24.0.0"` and a `vercel.json` containing `buildCommand`; confirm the emulator receives the canonical-shape payload.
148+
149+
## 5. Similar Code Patterns
150+
151+
- **`src/vercel-api.ts:122-124`** — uses `Object.assign(options, { project: config.vercelProjectId })` for the `project` field. This is safe; `project` is an accepted Vercel API field. Leave it alone.
152+
- **No other locations** in `src/` read `package.json` engines or `vercel.json` and forward values to the Vercel API. The bug surface is entirely localized to `src/project-config.ts` + `src/vercel-api.ts:applyProjectConfig`.
153+
154+
## 6. Recent Changes
155+
156+
| Commit | PR | Date | Relevance |
157+
|---|---|---|---|
158+
| `4b001a4` | #352 | 2026-04-24 | Release v42.2.1 |
159+
| `ed665b2` | #350 | 2026-04-24 | **Introduces both bugs** — adds `readNodeVersion()` without normalization and `nowConfig` top-level injection |
160+
| `353227b` | #325 | (prior) | Migrated to API-based deployment via `@vercel/client` (background) |
161+
162+
## 7. Test Coverage Gaps (current state)
163+
164+
- `src/__tests__/project-config.test.ts` — covers `readNodeVersion` only with the canonical `"20.x"` value. No semver-range, exact-version, or unsupported-input test exists.
165+
- `src/__tests__/vercel-api.test.ts:379-398` — explicitly **asserts the broken behavior** (`deployOpts.nowConfig.buildCommand === './build.sh'`). This test must be updated when Bug 2 is fixed.
166+
- `src/__integration__/vercel-api.test.ts` — does not assert absence of `nowConfig` in the request body; would not have caught either regression.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"track_id": "fix-vercel-validation-20260430",
3+
"type": "bugfix",
4+
"status": "review",
5+
"created_at": "2026-04-30T00:00:00Z",
6+
"updated_at": "2026-04-30T04:35:00Z",
7+
"issue": "#359",
8+
"pr": "#364",
9+
"project": "",
10+
"project_item_id": ""
11+
}

0 commit comments

Comments
 (0)