diff --git a/.changeset/opt-out-trigger-version-locking.md b/.changeset/opt-out-trigger-version-locking.md new file mode 100644 index 00000000000..dd72543dd8f --- /dev/null +++ b/.changeset/opt-out-trigger-version-locking.md @@ -0,0 +1,26 @@ +--- +"@trigger.dev/core": minor +"@trigger.dev/sdk": minor +--- + +Allow opting out of `TRIGGER_VERSION` locking per-call and per-scope (fixes #3380). + +`TriggerOptions.version` now accepts `null` in addition to a version string, and `ApiClientConfiguration` gains a `version?: string | null` field that applies to every trigger inside an `auth.withAuth(...)` scope. Passing `null` explicitly unpins the call: `lockToVersion` is omitted from the request and the server resolves to the current deployed version, ignoring the `TRIGGER_VERSION` environment variable. + +Precedence (highest first): per-call `version` option, scoped `version` in `ApiClientConfiguration`, `TRIGGER_VERSION` env var. `undefined` at any level falls through to the next level; only `null` explicitly unpins. + +Use cases: +- Cross-project triggers where the ambient `TRIGGER_VERSION` (e.g., injected by the Vercel integration for your "main" project) does not apply to a sibling project. +- One-off calls that should always run on the current deployed version regardless of the runtime environment. + +```ts +// Scoped: every trigger inside this scope resolves to the current deployed version +await auth.withAuth({ secretKey, version: null }, async () => { + await tasks.trigger("some-task", payload); +}); + +// Per-call: only this call is unpinned +await tasks.trigger("some-task", payload, { version: null }); +``` + +The existing string-pin behavior and `TRIGGER_VERSION` fallback are unchanged. diff --git a/packages/core/src/v3/apiClientManager/index.ts b/packages/core/src/v3/apiClientManager/index.ts index 96a4bc8e534..efffb01a5c0 100644 --- a/packages/core/src/v3/apiClientManager/index.ts +++ b/packages/core/src/v3/apiClientManager/index.ts @@ -62,7 +62,13 @@ export class APIClientManagerAPI { const requestOptions = this.#getConfig()?.requestOptions; const futureFlags = this.#getConfig()?.future; - return new ApiClient(this.baseURL, this.accessToken, this.branchName, requestOptions, futureFlags); + return new ApiClient( + this.baseURL, + this.accessToken, + this.branchName, + requestOptions, + futureFlags + ); } clientOrThrow(config?: ApiClientConfiguration): ApiClient { @@ -80,6 +86,32 @@ export class APIClientManagerAPI { return new ApiClient(baseURL, accessToken, branchName, requestOptions, futureFlags); } + /** + * Resolves the value to send as `lockToVersion` on a trigger request. + * + * Precedence (highest first): + * 1. Per-call `version` option (a string pins; `null` explicitly unpins). + * 2. Scoped `version` in `ApiClientConfiguration` (via `runWithConfig` / `auth.withAuth`). + * 3. `TRIGGER_VERSION` environment variable. + * + * Returns `undefined` when the result should not be sent (unpinned, server resolves + * to the current deployed version). Returns a version string when the request should + * be pinned to that specific version. `undefined` at any level falls through to the + * next level; only `null` explicitly unpins. + */ + resolveLockToVersion(callVersion?: string | null): string | undefined { + if (callVersion !== undefined) { + return callVersion === null ? undefined : callVersion; + } + + const scopedVersion = this.#getConfig()?.version; + if (scopedVersion !== undefined) { + return scopedVersion === null ? undefined : scopedVersion; + } + + return getEnvVar("TRIGGER_VERSION"); + } + runWithConfig Promise>( config: ApiClientConfiguration, fn: R diff --git a/packages/core/src/v3/apiClientManager/types.ts b/packages/core/src/v3/apiClientManager/types.ts index 8cdb185146d..63998c6ce79 100644 --- a/packages/core/src/v3/apiClientManager/types.ts +++ b/packages/core/src/v3/apiClientManager/types.ts @@ -14,6 +14,20 @@ export type ApiClientConfiguration = { * The preview branch name (for preview environments) */ previewBranch?: string; + /** + * Controls the `lockToVersion` applied to task triggers in this scope. + * + * - A version string (e.g. `"20250208.1"`) pins every trigger in the scope to that version. + * - `null` explicitly unpins: `lockToVersion` is omitted from the request and the server + * resolves to the current deployed version. Ignores the `TRIGGER_VERSION` environment + * variable. Use this when triggering into a project where the ambient `TRIGGER_VERSION` + * does not apply (for example, cross-project triggers). + * - Omitted (`undefined`) preserves the default behavior: per-call `version` option, then + * the `TRIGGER_VERSION` environment variable. + * + * A per-call `TriggerOptions.version` always wins over this scoped value. + */ + version?: string | null; requestOptions?: ApiRequestOptions; future?: ApiClientFutureFlags; }; diff --git a/packages/core/src/v3/types/tasks.ts b/packages/core/src/v3/types/tasks.ts index d04d088ef1a..ad2e80164a7 100644 --- a/packages/core/src/v3/types/tasks.ts +++ b/packages/core/src/v3/types/tasks.ts @@ -895,16 +895,25 @@ export type TriggerOptions = { * but you can specify a specific version to run here. You can also set the TRIGGER_VERSION environment * variables to run a specific version for all tasks. * + * Pass `null` to explicitly unpin this call: `lockToVersion` is omitted from the request and the + * server resolves to the current deployed version, ignoring the `TRIGGER_VERSION` environment + * variable. Useful when triggering into a project where the ambient `TRIGGER_VERSION` does not + * apply (for example, cross-project triggers). + * * @example * * ```ts + * // Pin to a specific version * await myTask.trigger({ foo: "bar" }, { version: "20250208.1" }); + * + * // Explicitly use the current deployed version, ignoring TRIGGER_VERSION + * await myTask.trigger({ foo: "bar" }, { version: null }); * ``` * * Note that this option is only available for `trigger` and NOT `triggerAndWait` (and their batch counterparts). The "wait" versions will always be locked * to the same version as the parent task that is triggering the child tasks. */ - version?: string; + version?: string | null; /** * Specify the region to run the task in. This overrides the default region set for your project in the dashboard. diff --git a/packages/core/test/apiClientManager.test.ts b/packages/core/test/apiClientManager.test.ts new file mode 100644 index 00000000000..50757eef0f5 --- /dev/null +++ b/packages/core/test/apiClientManager.test.ts @@ -0,0 +1,105 @@ +import { apiClientManager } from "../src/v3/apiClientManager-api.js"; + +const originalEnv = process.env.TRIGGER_VERSION; + +describe("APIClientManagerAPI.resolveLockToVersion", () => { + beforeEach(() => { + delete process.env.TRIGGER_VERSION; + }); + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.TRIGGER_VERSION; + } else { + process.env.TRIGGER_VERSION = originalEnv; + } + apiClientManager.disable(); + }); + + describe("without a scope override", () => { + it("returns undefined when no call version is given and TRIGGER_VERSION is unset", () => { + expect(apiClientManager.resolveLockToVersion()).toBeUndefined(); + }); + + it("falls back to TRIGGER_VERSION when no call version is given", () => { + process.env.TRIGGER_VERSION = "20250101.1"; + expect(apiClientManager.resolveLockToVersion()).toBe("20250101.1"); + }); + + it("prefers a per-call version string over TRIGGER_VERSION", () => { + process.env.TRIGGER_VERSION = "20250101.1"; + expect(apiClientManager.resolveLockToVersion("20250202.1")).toBe("20250202.1"); + }); + + it("returns undefined when per-call version is null, even if TRIGGER_VERSION is set", () => { + process.env.TRIGGER_VERSION = "20250101.1"; + expect(apiClientManager.resolveLockToVersion(null)).toBeUndefined(); + }); + }); + + describe("inside a scope with a version string", () => { + it("uses the scoped version when no call version is given", async () => { + process.env.TRIGGER_VERSION = "20250101.1"; + + await apiClientManager.runWithConfig({ version: "20250303.1" }, async () => { + expect(apiClientManager.resolveLockToVersion()).toBe("20250303.1"); + }); + }); + + it("lets a per-call version string win over the scope", async () => { + await apiClientManager.runWithConfig({ version: "20250303.1" }, async () => { + expect(apiClientManager.resolveLockToVersion("20250404.1")).toBe("20250404.1"); + }); + }); + + it("lets a per-call null win over the scope", async () => { + await apiClientManager.runWithConfig({ version: "20250303.1" }, async () => { + expect(apiClientManager.resolveLockToVersion(null)).toBeUndefined(); + }); + }); + }); + + describe("inside a scope with version: null", () => { + it("ignores TRIGGER_VERSION when no call version is given", async () => { + process.env.TRIGGER_VERSION = "20250101.1"; + + await apiClientManager.runWithConfig({ version: null }, async () => { + expect(apiClientManager.resolveLockToVersion()).toBeUndefined(); + }); + }); + + it("lets a per-call version string win over the null scope", async () => { + await apiClientManager.runWithConfig({ version: null }, async () => { + expect(apiClientManager.resolveLockToVersion("20250505.1")).toBe("20250505.1"); + }); + }); + }); + + describe("scope without a version key", () => { + it("falls back to TRIGGER_VERSION", async () => { + process.env.TRIGGER_VERSION = "20250101.1"; + + await apiClientManager.runWithConfig({ accessToken: "tr_test_xyz" }, async () => { + expect(apiClientManager.resolveLockToVersion()).toBe("20250101.1"); + }); + }); + + it("still respects a per-call null", async () => { + process.env.TRIGGER_VERSION = "20250101.1"; + + await apiClientManager.runWithConfig({ accessToken: "tr_test_xyz" }, async () => { + expect(apiClientManager.resolveLockToVersion(null)).toBeUndefined(); + }); + }); + }); + + describe("scope with version: undefined explicitly", () => { + it("treats explicit undefined as 'no key' and falls back to TRIGGER_VERSION", async () => { + process.env.TRIGGER_VERSION = "20250101.1"; + + await apiClientManager.runWithConfig({ version: undefined }, async () => { + expect(apiClientManager.resolveLockToVersion()).toBe("20250101.1"); + }); + }); + }); +}); diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index c69bceeb535..ea96d30eed0 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -10,7 +10,6 @@ import { createErrorTaskError, defaultRetryOptions, flattenIdempotencyKey, - getEnvVar, getIdempotencyKeyOptions, getSchemaParseFn, InitOutput, @@ -650,7 +649,7 @@ export async function batchTriggerById( machine: item.options?.machine, priority: item.options?.priority, region: item.options?.region, - lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"), + lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version), debounce: item.options?.debounce, }, }; @@ -1166,7 +1165,7 @@ export async function batchTriggerTasks( machine: item.options?.machine, priority: item.options?.priority, region: item.options?.region, - lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"), + lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version), debounce: item.options?.debounce, }, }; @@ -1830,7 +1829,7 @@ async function* transformBatchItemsStream( machine: item.options?.machine, priority: item.options?.priority, region: item.options?.region, - lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"), + lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version), debounce: item.options?.debounce, }, }; @@ -1933,7 +1932,7 @@ async function* transformBatchByTaskItemsStream( machine: item.options?.machine, priority: item.options?.priority, region: item.options?.region, - lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"), + lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version), debounce: item.options?.debounce, }, }; @@ -2146,7 +2145,7 @@ async function trigger_internal( machine: options?.machine, priority: options?.priority, region: options?.region, - lockToVersion: options?.version ?? getEnvVar("TRIGGER_VERSION"), + lockToVersion: apiClientManager.resolveLockToVersion(options?.version), debounce: options?.debounce, }, }, @@ -2232,7 +2231,7 @@ async function batchTrigger_internal( machine: item.options?.machine, priority: item.options?.priority, region: item.options?.region, - lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"), + lockToVersion: apiClientManager.resolveLockToVersion(item.options?.version), }, }; })