Skip to content
Open
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
245 changes: 245 additions & 0 deletions packages/core/src/v3/externalDeploymentId.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { describe, expect, it } from "vitest";
import {
discoverPlatformCommitSha,
isAutomaticSkewProtectionEnabled,
normalizeExternalDeploymentId,
PLATFORM_COMMIT_SHA_ENV_VARS,
resolveExternalDeploymentId,
} from "./externalDeploymentId.js";

function reader(vars: Record<string, string | undefined>) {
return (name: string) => vars[name];
}

const SHA = "fa1eade47b73733d6312d5abfad33ce9e4068081";

describe("normalizeExternalDeploymentId", () => {
it("trims surrounding whitespace", () => {
expect(normalizeExternalDeploymentId(` ${SHA} `)).toBe(SHA);
});

it.each([undefined, "", " ", "\t\n"])("treats %j as absent", (value) => {
expect(normalizeExternalDeploymentId(value)).toBeUndefined();
});

it("accepts exactly 128 characters", () => {
expect(normalizeExternalDeploymentId("a".repeat(128))).toBe("a".repeat(128));
});

it("skips a value longer than 128 characters rather than sending it to be rejected", () => {
expect(normalizeExternalDeploymentId("a".repeat(129))).toBeUndefined();
});

it("measures the length limit after trimming", () => {
expect(normalizeExternalDeploymentId(` ${"a".repeat(128)} `)).toBe("a".repeat(128));
});
});

describe("isAutomaticSkewProtectionEnabled", () => {
it.each([
["1", true],
["true", true],
["TRUE", true],
["True", true],
[" 1 ", true],
["0", false],
["false", false],
["", false],
["yes", false],
["on", false],
["2", false],
[undefined, false],
])("reads %j as %s", (value, expected) => {
expect(
isAutomaticSkewProtectionEnabled(reader({ TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION: value }))
).toBe(expected);
});
});

describe("discoverPlatformCommitSha", () => {
it("returns undefined when nothing is set", () => {
expect(discoverPlatformCommitSha(reader({}))).toBeUndefined();
});

it.each(PLATFORM_COMMIT_SHA_ENV_VARS)("reads %s", (name) => {
expect(discoverPlatformCommitSha(reader({ [name]: SHA }))).toBe(SHA);
});

it("prefers a hosting variable over a CI variable, because it describes the deployment that is running", () => {
expect(
discoverPlatformCommitSha(
reader({ VERCEL_GIT_COMMIT_SHA: "vercel-sha", GITHUB_SHA: "github-sha" })
)
).toBe("vercel-sha");
});

it("prefers a CI variable over the generic tier", () => {
expect(
discoverPlatformCommitSha(reader({ GITHUB_SHA: "github-sha", GIT_HASH: "generic-sha" }))
).toBe("github-sha");
});

it("falls back to the generic tier when nothing named is set", () => {
expect(discoverPlatformCommitSha(reader({ COMMIT_HASH: "generic-sha" }))).toBe("generic-sha");
});

it("honours the full hosting order", () => {
const order = [
"VERCEL_GIT_COMMIT_SHA",
"RAILWAY_GIT_COMMIT_SHA",
"RENDER_GIT_COMMIT",
"CF_PAGES_COMMIT_SHA",
"WORKERS_CI_COMMIT_SHA",
"COMMIT_REF",
"AWS_COMMIT_ID",
"HEROKU_BUILD_COMMIT",
"HEROKU_SLUG_COMMIT",
"KOYEB_GIT_SHA",
];

const vars: Record<string, string> = Object.fromEntries(order.map((n) => [n, n]));

for (const expected of order) {
expect(discoverPlatformCommitSha(reader(vars))).toBe(expected);
delete vars[expected];
}
});

it("skips an empty value and keeps looking", () => {
expect(discoverPlatformCommitSha(reader({ VERCEL_GIT_COMMIT_SHA: "", GITHUB_SHA: SHA }))).toBe(
SHA
);
});

it("skips an over-long value and keeps looking, rather than sending something that will be rejected", () => {
expect(
discoverPlatformCommitSha(reader({ VERCEL_GIT_COMMIT_SHA: "a".repeat(129), GITHUB_SHA: SHA }))
).toBe(SHA);
});

it("never reads CACHED_COMMIT_REF, which is the previous build's SHA", () => {
expect(PLATFORM_COMMIT_SHA_ENV_VARS).not.toContain("CACHED_COMMIT_REF");
expect(discoverPlatformCommitSha(reader({ CACHED_COMMIT_REF: SHA }))).toBeUndefined();
});
});

describe("resolveExternalDeploymentId", () => {
it("returns nothing when no source yields a value", () => {
expect(resolveExternalDeploymentId({ read: reader({}) })).toBeUndefined();
});

it("honours a per-call id above everything else", () => {
expect(
resolveExternalDeploymentId({
explicit: "per-call",
clientConfig: "per-client",
read: reader({
TRIGGER_EXTERNAL_DEPLOYMENT_ID: "per-env",
TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION: "1",
VERCEL_GIT_COMMIT_SHA: "discovered",
}),
})
).toBe("per-call");
});

it("honours a per-client id above the environment and discovery", () => {
expect(
resolveExternalDeploymentId({
clientConfig: "per-client",
read: reader({
TRIGGER_EXTERNAL_DEPLOYMENT_ID: "per-env",
TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION: "1",
VERCEL_GIT_COMMIT_SHA: "discovered",
}),
})
).toBe("per-client");
});

it("honours TRIGGER_EXTERNAL_DEPLOYMENT_ID above discovery", () => {
expect(
resolveExternalDeploymentId({
read: reader({
TRIGGER_EXTERNAL_DEPLOYMENT_ID: "per-env",
TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION: "1",
VERCEL_GIT_COMMIT_SHA: "discovered",
}),
})
).toBe("per-env");
});

it("honours an explicit id with no opt-in variable at all — the gate is on discovery, not pinning", () => {
expect(
resolveExternalDeploymentId({
read: reader({ TRIGGER_EXTERNAL_DEPLOYMENT_ID: "per-env" }),
})
).toBe("per-env");
});

it("honours a per-call id with no opt-in variable", () => {
expect(resolveExternalDeploymentId({ explicit: "per-call", read: reader({}) })).toBe(
"per-call"
);
});

it("discovers when the opt-in is exactly 1", () => {
expect(
resolveExternalDeploymentId({
read: reader({
TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION: "1",
VERCEL_GIT_COMMIT_SHA: SHA,
}),
})
).toBe(SHA);
});

it.each(["0", "", "false", "yes", undefined])(
"discovers nothing when the opt-in reads %j",
(gate) => {
expect(
resolveExternalDeploymentId({
read: reader({
TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION: gate,
VERCEL_GIT_COMMIT_SHA: SHA,
}),
})
).toBeUndefined();
}
);

it("normalises whatever it resolves, whichever tier produced it", () => {
expect(resolveExternalDeploymentId({ explicit: ` ${SHA} `, read: reader({}) })).toBe(SHA);
expect(resolveExternalDeploymentId({ clientConfig: ` ${SHA} `, read: reader({}) })).toBe(SHA);
expect(
resolveExternalDeploymentId({ read: reader({ TRIGGER_EXTERNAL_DEPLOYMENT_ID: ` ${SHA} ` }) })
).toBe(SHA);
});

it("falls through a blank higher tier to a usable lower one", () => {
expect(
resolveExternalDeploymentId({
explicit: " ",
clientConfig: "",
read: reader({ TRIGGER_EXTERNAL_DEPLOYMENT_ID: SHA }),
})
).toBe(SHA);
});

it("reads the environment on every call, so a variable appearing later is picked up", () => {
const vars: Record<string, string | undefined> = {};
const read = reader(vars);

expect(resolveExternalDeploymentId({ read })).toBeUndefined();

vars.TRIGGER_EXTERNAL_DEPLOYMENT_ID = SHA;

expect(resolveExternalDeploymentId({ read })).toBe(SHA);
});

it("reads nothing at all when the reader refuses, which is how a non-inheriting SDK scope behaves", () => {
expect(
resolveExternalDeploymentId({
read: () => undefined,
})
).toBeUndefined();
});
});
103 changes: 103 additions & 0 deletions packages/core/src/v3/externalDeploymentId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
export const EXTERNAL_DEPLOYMENT_ID_ENV_VAR = "TRIGGER_EXTERNAL_DEPLOYMENT_ID";

export const AUTOMATIC_SKEW_PROTECTION_ENV_VAR = "TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION";

export const EXTERNAL_DEPLOYMENT_ID_MAX_LENGTH = 128;

export type EnvVarReader = (name: string) => string | undefined;

export const PLATFORM_COMMIT_SHA_ENV_VARS = [
"VERCEL_GIT_COMMIT_SHA",
"RAILWAY_GIT_COMMIT_SHA",
"RENDER_GIT_COMMIT",
"CF_PAGES_COMMIT_SHA",
"WORKERS_CI_COMMIT_SHA",
"COMMIT_REF",
"AWS_COMMIT_ID",
"HEROKU_BUILD_COMMIT",
"HEROKU_SLUG_COMMIT",
"KOYEB_GIT_SHA",

"GITHUB_SHA",
"CI_COMMIT_SHA",
"CIRCLE_SHA1",
"BITBUCKET_COMMIT",
"BUILDKITE_COMMIT",
"BUILD_SOURCEVERSION",
"COMMIT_SHA",
"DRONE_COMMIT_SHA",
"GIT_COMMIT",
"BUILD_VCS_NUMBER",
"TRAVIS_COMMIT",

"COMMIT_SHA",
"COMMIT_HASH",
"GIT_COMMIT",
"GIT_SHA",
"GIT_HASH",
] as const;

export function normalizeExternalDeploymentId(value: string | undefined): string | undefined {
if (typeof value !== "string") {
return undefined;
}

const trimmed = value.trim();

if (trimmed === "" || trimmed.length > EXTERNAL_DEPLOYMENT_ID_MAX_LENGTH) {
return undefined;
}

return trimmed;
}

export function isAutomaticSkewProtectionEnabled(read: EnvVarReader): boolean {
const raw = read(AUTOMATIC_SKEW_PROTECTION_ENV_VAR);

if (typeof raw !== "string") {
return false;
}

const normalized = raw.trim().toLowerCase();

return normalized === "1" || normalized === "true";
}

export function discoverPlatformCommitSha(read: EnvVarReader): string | undefined {
for (const name of PLATFORM_COMMIT_SHA_ENV_VARS) {
const candidate = normalizeExternalDeploymentId(read(name));

if (candidate) {
return candidate;
}
}

return undefined;
}

export type ResolveExternalDeploymentIdOptions = {
explicit?: string;
clientConfig?: string;
read: EnvVarReader;
};

export function resolveExternalDeploymentId({
explicit,
clientConfig,
read,
}: ResolveExternalDeploymentIdOptions): string | undefined {
const fromCall = normalizeExternalDeploymentId(explicit);
if (fromCall) return fromCall;

const fromClient = normalizeExternalDeploymentId(clientConfig);
if (fromClient) return fromClient;

const fromEnv = normalizeExternalDeploymentId(read(EXTERNAL_DEPLOYMENT_ID_ENV_VAR));
if (fromEnv) return fromEnv;

if (isAutomaticSkewProtectionEnabled(read)) {
return discoverPlatformCommitSha(read);
}

return undefined;
}
1 change: 1 addition & 0 deletions packages/core/src/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type { ApiPromise, OffsetLimitPagePromise, CursorPagePromise } from "./ap
export * from "./apiClient/errors.js";
export * from "./clock-api.js";
export * from "./errors.js";
export * from "./externalDeploymentId.js";
export * from "./limits.js";
export * from "./logger-api.js";
export * from "./runtime-api.js";
Expand Down
Loading
Loading