-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(sdk): allow opting out of TRIGGER_VERSION locking via version: null
#3413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
jorgehermo9
wants to merge
1
commit into
triggerdotdev:main
from
jorgehermo9:feat/opt-out-trigger-version-locking
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 runWithConfig uses global state, not AsyncLocalStorage — concurrent scopes can collide
The
runWithConfigmethod atpackages/core/src/v3/apiClientManager/index.ts:115-126stores scoped config in a global variable viaregisterGlobal, not viaAsyncLocalStorage. If two concurrent async operations both callrunWithConfig(e.g., two simultaneousauth.withAuthblocks), the second call overwrites the global config, and the first call'sfinallyblock then restores the wrong original. This is a pre-existing design issue (not introduced by this PR), but the newversionfield is now also subject to this race. In practice, this is likely fine for server-side usage where each request runs in isolation, but it's worth noting for library consumers running multiple scoped operations concurrently in the same process.(Refers to lines 115-126)
Was this helpful? React with 👍 or 👎 to provide feedback.