Skip to content

Commit 802047e

Browse files
authored
fix: skip non-upload-triggered worker versions in skew-protection (#1270)
Worker versions created by metadata-only operations (e.g. Cloudflare API secret updates with triggered_by=secret) do not include the static assets bundle. Previously, such a version could be picked as the "latest" target when replacing the "current" sentinel in the deployment mapping, causing /_next/static/* requests to return 404 on past deployments. listWorkerVersions now skips versions whose metadata.annotations["workers/triggered_by"] is not in {upload, version_upload}. Versions with no annotation are kept for backward compatibility. Closes #1230
1 parent dd78941 commit 802047e

3 files changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
fix: skip non-upload-triggered worker versions when building skew-protection deployment mapping
6+
7+
Worker versions created by metadata-only operations (e.g. Cloudflare API secret updates) do not include the static assets bundle. Previously, such versions could become the "latest" target in the skew-protection mapping, causing `/_next/static/*` requests to return 404 on past deployments. Versions are now filtered to those with `workers/triggered_by` in `{upload, version_upload}`.
8+
9+
Closes #1230

packages/cloudflare/src/cli/commands/skew-protection.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,89 @@ describe("skew protection", () => {
4949
},
5050
]);
5151
});
52+
53+
test("listWorkerVersions filters out non-upload-triggered versions", async () => {
54+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
55+
const client: any = {
56+
workers: {
57+
scripts: {
58+
versions: {
59+
list: () => [],
60+
},
61+
},
62+
},
63+
};
64+
65+
const now = Date.now();
66+
67+
client.workers.scripts.versions.list = vi.fn().mockReturnValue([
68+
{
69+
id: "secret-version",
70+
metadata: {
71+
created_on: new Date(now),
72+
annotations: { "workers/triggered_by": "secret" },
73+
},
74+
},
75+
{
76+
id: "upload-version",
77+
metadata: {
78+
created_on: new Date(now - 1000),
79+
annotations: { "workers/triggered_by": "upload" },
80+
},
81+
},
82+
{
83+
id: "version-upload",
84+
metadata: {
85+
created_on: new Date(now - 2000),
86+
annotations: { "workers/triggered_by": "version_upload" },
87+
},
88+
},
89+
{
90+
id: "legacy-no-annotation",
91+
metadata: { created_on: new Date(now - 3000) },
92+
},
93+
]);
94+
95+
expect(await listWorkerVersions("scriptName", { client, accountId: "accountId" })).toMatchObject([
96+
{ id: "upload-version", createdOnMs: now - 1000 },
97+
{ id: "version-upload", createdOnMs: now - 2000 },
98+
{ id: "legacy-no-annotation", createdOnMs: now - 3000 },
99+
]);
100+
});
101+
102+
test("listWorkerVersions returns [] when the newest versions are all secret-triggered", async () => {
103+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
104+
const client: any = {
105+
workers: {
106+
scripts: {
107+
versions: {
108+
list: () => [],
109+
},
110+
},
111+
},
112+
};
113+
114+
const now = Date.now();
115+
116+
client.workers.scripts.versions.list = vi.fn().mockReturnValue([
117+
{
118+
id: "secret-A",
119+
metadata: {
120+
created_on: new Date(now),
121+
annotations: { "workers/triggered_by": "secret" },
122+
},
123+
},
124+
{
125+
id: "secret-B",
126+
metadata: {
127+
created_on: new Date(now - 1000),
128+
annotations: { "workers/triggered_by": "secret" },
129+
},
130+
},
131+
]);
132+
133+
expect(await listWorkerVersions("scriptName", { client, accountId: "accountId" })).toEqual([]);
134+
});
52135
});
53136
});
54137

packages/cloudflare/src/cli/commands/skew-protection.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const MAX_NUMBER_OF_VERSIONS = 20;
4040
const MAX_VERSION_AGE_DAYS = 7;
4141
const MS_PER_DAY = 24 * 3600 * 1000;
4242

43+
/** Worker-version trigger types that produce a full upload (assets + code). */
44+
const UPLOAD_TRIGGER_TYPES = new Set(["upload", "version_upload"]);
45+
4346
/**
4447
* Compute the deployment mapping for a deployment.
4548
*
@@ -244,12 +247,19 @@ export async function listWorkerVersions(
244247
})) {
245248
const id = version.id;
246249
const createdOn = version.metadata?.created_on;
250+
const triggeredBy = (version.metadata as any)?.annotations?.["workers/triggered_by"];
247251

248252
if (id && createdOn) {
249253
const createdOnMs = new Date(createdOn).getTime();
250254
if (createdOnMs < afterTimeMs) {
251255
break;
252256
}
257+
// Skip metadata-only versions (e.g. secret/service_token triggers)
258+
// that lack the static assets bundle. Versions with no annotation
259+
// are kept for backward compatibility.
260+
if (triggeredBy !== undefined && !UPLOAD_TRIGGER_TYPES.has(triggeredBy)) {
261+
continue;
262+
}
253263
versions.push({ id, createdOnMs });
254264
if (versions.length >= maxNumberOfVersions) {
255265
break;

0 commit comments

Comments
 (0)