Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bea9ebfb0 | |||
| 639525935c | |||
| 92c7341bd3 | |||
| 9c61a25402 | |||
| 972b5e34b8 | |||
| 47ca24aab8 | |||
| 8578c9b281 | |||
| 8bb81ec73f | |||
| c46e3d528e | |||
| 93bc32ca87 | |||
| 36397d0cb1 | |||
| 90bf01fb23 | |||
| 15ed74b63e | |||
| 392453e8a6 | |||
| e5f0aafb14 | |||
| 05cdc6993c | |||
| 569dcf4ef4 | |||
| 87e46d497b | |||
| 022bbe1371 | |||
| 3e0881c2e9 | |||
| ec3c300cac | |||
| 3f182cd145 | |||
| 09e6f58e84 | |||
| aa79b01f7e | |||
| f040417440 | |||
| 5231381717 | |||
| c6b8a5a64b | |||
| 55fa386bdd | |||
| 7c1339c108 | |||
| 4cfb52ef65 | |||
| f7d32b83b1 | |||
| 0979a521bd | |||
| e30beb779f | |||
| 99a63d4cd5 | |||
| 6744617879 | |||
| d62979914f | |||
| fc02e95f2a | |||
| 8688822723 | |||
| 0d829f9530 | |||
| 6496539c01 | |||
| 79a5c36d6e | |||
| 2f0d65a490 | |||
| 92d98e5499 | |||
| a9810b0fd8 | |||
| f9ec66c562 | |||
| 2cd8658190 | |||
| e5895424db | |||
| a724a6fa01 | |||
| ba7bbdbbdb | |||
| 7746302b51 | |||
| 6558e5f8eb | |||
| 9f07df8abc | |||
| 8ad599f90e | |||
| 78dac99d72 | |||
| d6786002b2 | |||
| 9d529e9f17 | |||
| d48eb9bac8 | |||
| 0591db5f2b | |||
| a64064965d | |||
| 41ec450bee | |||
| 9a9bdc20c9 | |||
| 6652edf7a7 | |||
| 369bc38560 | |||
| 27f7132509 | |||
| b6fc68aaae | |||
| 3421f5ec0b | |||
| 15097a3a67 | |||
| 1b7bb90852 | |||
| 7e68c02636 | |||
| 01e01376f8 | |||
| db9163e775 | |||
| e9bdd0c7e5 | |||
| 7cb977ff2c | |||
| 2940b07430 | |||
| c46e3c97b5 | |||
| 16cb1c46ac | |||
| 2b0d68a0b6 | |||
| 2088fa392d | |||
| f1ffc2e6c8 | |||
| c0b9394099 | |||
| 937b61913d | |||
| 5cf90da722 | |||
| da6ce3c8d5 | |||
| 07054f13d1 | |||
| c50a94b962 | |||
| bb38261947 | |||
| 0d4e3e70c1 | |||
| bf1482fdb9 | |||
| e2d3358271 | |||
| 6270abaab8 | |||
| 6c71ddbe67 | |||
| fafda21e3b | |||
| dc2d68107f | |||
| b65e7f7a7a | |||
| 7d9cec8652 | |||
| a591e1118f | |||
| 4d2ea86d6c | |||
| 366ae5ca7d | |||
| 157973a720 | |||
| 1353d66ca1 | |||
| 1c5ee5d94c |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/shopify": patch
|
||||
---
|
||||
|
||||
improved error messages when a shopify webhook fails to register
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/core-apps": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"webapp",
|
||||
"emails",
|
||||
"proxy",
|
||||
"yalt",
|
||||
"@trigger.dev/database",
|
||||
"coordinator",
|
||||
"docker-provider",
|
||||
"kubernetes-provider"
|
||||
"kubernetes-provider",
|
||||
"@trigger.dev/otlp-importer"
|
||||
],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fixed empty env vars overriding in dev runs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Fixes for continuing after waits
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
New Build System
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Removed the folder/filepath from Attempt spans
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/build": patch
|
||||
---
|
||||
|
||||
Fix issue with emitDecoratorMetadata and tsconfigs with extends
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/react": patch
|
||||
---
|
||||
|
||||
Fix for shared queryKey between useRunDetails and useRunStatuses
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Fix issues that could result in unreezable state run crashes. Details:
|
||||
- Never checkpoint between attempts
|
||||
- Some messages and socket data now include attempt numbers
|
||||
- Remove attempt completion replays
|
||||
- Additional prod entry point logging
|
||||
- Fail runs that receive deprecated (pre-lazy attempt) execute messages
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/nestjs": patch
|
||||
---
|
||||
|
||||
fix: [nestjs integration] fastify HTTP adapter detection now works correctly for response headers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Rollback to try and fix some dependent attempt issues
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fix entry point paths on windows
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Support self-hosters pushing to a custom registry when running deploy
|
||||
+4
-32
@@ -8,40 +8,12 @@
|
||||
"proxy": "0.0.11",
|
||||
"webapp": "1.0.0",
|
||||
"yalt": "0.0.1",
|
||||
"@trigger.dev/airtable": "2.3.18",
|
||||
"@trigger.dev/github": "2.3.18",
|
||||
"@trigger.dev/linear": "2.3.18",
|
||||
"@trigger.dev/openai": "2.3.18",
|
||||
"@trigger.dev/plain": "2.3.18",
|
||||
"@trigger.dev/replicate": "2.3.18",
|
||||
"@trigger.dev/resend": "2.3.18",
|
||||
"@trigger.dev/sendgrid": "2.3.18",
|
||||
"@trigger.dev/shopify": "2.3.18",
|
||||
"@trigger.dev/slack": "2.3.18",
|
||||
"@trigger.dev/stripe": "2.3.18",
|
||||
"@trigger.dev/supabase": "2.3.18",
|
||||
"@trigger.dev/typeform": "2.3.18",
|
||||
"@trigger.dev/astro": "2.3.18",
|
||||
"@trigger.dev/cli": "2.3.18",
|
||||
"trigger.dev": "2.3.18",
|
||||
"@trigger.dev/core": "2.3.18",
|
||||
"@trigger.dev/core-apps": "0.0.0",
|
||||
"@trigger.dev/core-backend": "2.3.18",
|
||||
"@trigger.dev/database": "0.0.1",
|
||||
"emails": "1.0.0",
|
||||
"@trigger.dev/eslint-plugin": "2.3.18",
|
||||
"@trigger.dev/express": "2.3.18",
|
||||
"@trigger.dev/hono": "2.3.18",
|
||||
"@trigger.dev/integration-kit": "2.3.18",
|
||||
"@trigger.dev/nestjs": "2.3.18",
|
||||
"@trigger.dev/nextjs": "2.3.18",
|
||||
"@trigger.dev/otlp-importer": "2.3.11",
|
||||
"@trigger.dev/react": "2.3.18",
|
||||
"@trigger.dev/remix": "2.3.18",
|
||||
"@trigger.dev/sveltekit": "2.3.18",
|
||||
"@trigger.dev/testing": "2.3.18",
|
||||
"@trigger.dev/sdk": "2.3.18",
|
||||
"@trigger.dev/yalt": "2.3.18"
|
||||
"@trigger.dev/sdk": "2.3.18"
|
||||
},
|
||||
"changesets": [
|
||||
"afraid-sheep-joke",
|
||||
@@ -50,7 +22,6 @@
|
||||
"beige-pears-explode",
|
||||
"beige-pens-dance",
|
||||
"big-tomatoes-deliver",
|
||||
"blue-pumas-whisper",
|
||||
"breezy-gorillas-mate",
|
||||
"brown-boats-bathe",
|
||||
"brown-spies-burn",
|
||||
@@ -65,6 +36,7 @@
|
||||
"curly-monkeys-tell",
|
||||
"dry-walls-check",
|
||||
"dull-mangos-press",
|
||||
"early-impalas-itch",
|
||||
"eight-pumas-float",
|
||||
"eleven-paws-join",
|
||||
"famous-boats-tease",
|
||||
@@ -79,11 +51,11 @@
|
||||
"funny-swans-destroy",
|
||||
"gorgeous-cycles-guess",
|
||||
"gorgeous-gorillas-compete",
|
||||
"green-bags-wink",
|
||||
"green-pens-battle",
|
||||
"hot-buckets-behave",
|
||||
"hot-fishes-retire",
|
||||
"hot-wasps-sin",
|
||||
"hungry-sloths-promise",
|
||||
"itchy-chairs-itch",
|
||||
"khaki-apricots-design",
|
||||
"khaki-poems-lay",
|
||||
@@ -91,11 +63,11 @@
|
||||
"late-icons-lie",
|
||||
"late-steaks-behave",
|
||||
"lazy-files-lay",
|
||||
"lemon-jobs-repair",
|
||||
"lemon-sloths-hide",
|
||||
"light-bulldogs-press",
|
||||
"light-dragons-complain",
|
||||
"little-crabs-cross",
|
||||
"long-feet-invent",
|
||||
"long-fireants-search",
|
||||
"long-hounds-wave",
|
||||
"loud-actors-remember",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/build": patch
|
||||
---
|
||||
|
||||
Add support for prisma typed sql
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/build": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Added support for custom esbuild plugins
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fix --project-ref when running deploy
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/otlp-importer": patch
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Move to our global system from AsyncLocalStorage for the current task context storage
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/core-apps": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Support custom config file names & paths
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fixed stuck runs when a child run fails with a process exit
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
"trigger.dev": major
|
||||
"@trigger.dev/core": major
|
||||
"@trigger.dev/otlp-importer": major
|
||||
"@trigger.dev/sdk": major
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Fix for when a log flush times out and the process is checkpointed
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add remote forced externals system, in case we come across another package that cannot be bundled (spurred on by header-generator)
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Remove msw and retry.interceptFetch
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Add support for tasks located in subdirectories inside trigger dirs
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
fix node10 moduleResolution in @trigger.dev/core
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/build": patch
|
||||
---
|
||||
|
||||
Added new @trigger.dev/build package that currently has all the build extensions
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Only import import-in-the-middle hook if there are instrumented packages
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Support for custom conditions
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"verbatimModuleSyntax": false,
|
||||
"jsx": "react",
|
||||
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"removeComments": false,
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"experimentalDecorators": false,
|
||||
"downlevelIteration": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
"pretty": true,
|
||||
"customConditions": ["@triggerdotdev/source"]
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ packages/\*\*/dist
|
||||
.vercel
|
||||
.cache
|
||||
.output
|
||||
.trigger
|
||||
apps/\*\*/public/build
|
||||
|
||||
cypress/screenshots
|
||||
|
||||
@@ -9,12 +9,13 @@ on:
|
||||
type: string
|
||||
jobs:
|
||||
cli-v3:
|
||||
name: "🧪 CLI v3 tests"
|
||||
name: "🧪 CLI v3 tests (${{ matrix.os }} - ${{ matrix.package-manager }})"
|
||||
if: inputs.package == 'cli-v3' || inputs.package == ''
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [buildjet-8vcpu-ubuntu-2204, windows-latest]
|
||||
package-manager: ["npm", "pnpm", "yarn"]
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
@@ -46,76 +47,8 @@ jobs:
|
||||
run: corepack enable
|
||||
|
||||
- name: Run E2E Tests
|
||||
shell: bash
|
||||
run: |
|
||||
PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
|
||||
webapp:
|
||||
name: "🧪 Webapp tests"
|
||||
if: inputs.package == 'webapp' || inputs.package == ''
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME || vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN || vars.DOCKERHUB_TOKEN }}
|
||||
LOG=debug PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
# Setup environment variables
|
||||
cp ./.env.example ./.env
|
||||
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
cd apps/webapp && pnpm run build:server
|
||||
cd ../..
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
pnpm --filter @trigger.dev/otlp-importer generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Execute tests
|
||||
pnpm run docker
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run test:e2e
|
||||
|
||||
# Cleanup
|
||||
pnpm run docker:stop
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@@ -2,13 +2,7 @@ name: 🤖 PR Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**.mdx"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -27,8 +21,40 @@ jobs:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
# e2e:
|
||||
# uses: ./.github/workflows/e2e.yml
|
||||
# with:
|
||||
# package: webapp
|
||||
# secrets: inherit
|
||||
e2e:
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
with:
|
||||
package: cli-v3
|
||||
secrets: inherit
|
||||
|
||||
preview-release:
|
||||
name: Preview Release
|
||||
needs: [typecheck, units, e2e]
|
||||
if: |
|
||||
github.repository == 'triggerdotdev/trigger.dev'
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 🏗️ Build
|
||||
run: pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
|
||||
- name: ⚡ publish preview release
|
||||
run: npx pkg-pr-new publish --no-template $(ls -d ./packages/*)
|
||||
|
||||
@@ -49,12 +49,6 @@ jobs:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
with:
|
||||
package: cli-v3
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
needs: [typecheck, units]
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
|
||||
@@ -38,17 +38,15 @@ jobs:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🏗️ Build
|
||||
run: pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
|
||||
|
||||
@@ -22,11 +22,6 @@ jobs:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -34,4 +29,7 @@ jobs:
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck --filter webapp
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: 🔎 Check exports
|
||||
run: pnpm run check-exports
|
||||
|
||||
@@ -22,21 +22,6 @@ jobs:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: ⎔ Setup Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: ⎔ Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.0.15"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
||||
+5
-2
@@ -12,7 +12,6 @@ coverage
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
dist
|
||||
packages/**/dist
|
||||
|
||||
@@ -54,4 +53,8 @@ apps/**/public/build
|
||||
/playwright/.cache/
|
||||
|
||||
.cosine
|
||||
.trigger/
|
||||
.trigger
|
||||
.tshy*
|
||||
.yarn
|
||||
*.tsbuildinfo
|
||||
/packages/cli-v3/src/package.json
|
||||
|
||||
@@ -7,5 +7,4 @@ tailwind.css
|
||||
**/.react-email/
|
||||
**/storybook-static/
|
||||
**/.changeset/
|
||||
**/build/
|
||||
**/dist/
|
||||
Vendored
+1
-2
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"astro-build.astro-vscode",
|
||||
"denoland.vscode-deno"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+37
-6
@@ -23,17 +23,16 @@
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug v2 job catalog",
|
||||
"command": "pnpm run byo-auth",
|
||||
"envFile": "${workspaceFolder}/references/job-catalog/.env",
|
||||
"cwd": "${workspaceFolder}/references/job-catalog",
|
||||
"name": "Debug V3 init CLI",
|
||||
"command": "pnpm exec triggerdev init",
|
||||
"cwd": "${workspaceFolder}/references/init-shell",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 Dev CLI",
|
||||
"command": "pnpm exec triggerdev dev --log-level debug",
|
||||
"command": "pnpm exec triggerdev dev",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
@@ -41,7 +40,23 @@
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 Deploy CLI",
|
||||
"command": "pnpm exec triggerdev deploy",
|
||||
"command": "pnpm exec triggerdev deploy --self-hosted --load-image",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 list-profiles CLI",
|
||||
"command": "pnpm exec triggerdev list-profiles --log-level debug",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 update CLI",
|
||||
"command": "pnpm exec triggerdev update",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
@@ -60,6 +75,22 @@
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug CLI e2e tests",
|
||||
"command": "MOD=otel-telemetry-loader pnpm run test:e2e",
|
||||
"cwd": "${workspaceFolder}/packages/cli-v3",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "debug v3 hello-world dev",
|
||||
"command": "pnpm exec triggerdev dev",
|
||||
"cwd": "${workspaceFolder}/references/hello-world",
|
||||
"sourceMaps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"deno.enablePaths": ["references/deno-reference", "runtime_tests/tests/deno"],
|
||||
"debug.toolBarLocation": "commandCenter"
|
||||
"debug.toolBarLocation": "commandCenter",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"search.exclude": {
|
||||
"**/node_modules/**": true,
|
||||
"packages/cli-v3/e2e": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"execa": "^8.0.1",
|
||||
"nanoid": "^5.0.6",
|
||||
"prom-client": "^15.1.0",
|
||||
|
||||
@@ -12,7 +12,11 @@ export class ChaosMonkey {
|
||||
private chaosEventRate = 0.2;
|
||||
private delayInSeconds = 45;
|
||||
|
||||
constructor(private enabled = false) {
|
||||
constructor(
|
||||
private enabled = false,
|
||||
private disableErrors = false,
|
||||
private disableDelays = false
|
||||
) {
|
||||
if (this.enabled) {
|
||||
console.log("🍌 Chaos monkey enabled");
|
||||
}
|
||||
@@ -32,8 +36,8 @@ export class ChaosMonkey {
|
||||
|
||||
async call({
|
||||
$,
|
||||
throwErrors = true,
|
||||
addDelays = true,
|
||||
throwErrors = !this.disableErrors,
|
||||
addDelays = !this.disableDelays,
|
||||
}: {
|
||||
$?: Execa$<string>;
|
||||
throwErrors?: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExponentialBackoff } from "@trigger.dev/core-apps/backoff";
|
||||
import { isExecaChildProcess, testDockerCheckpoint } from "@trigger.dev/core-apps/checkpoints";
|
||||
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
|
||||
import { ExponentialBackoff } from "@trigger.dev/core/v3/apps";
|
||||
import { testDockerCheckpoint, isExecaChildProcess } from "@trigger.dev/core/v3/apps";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import { $ } from "execa";
|
||||
import { nanoid } from "nanoid";
|
||||
import fs from "node:fs/promises";
|
||||
@@ -17,6 +17,7 @@ type CheckpointAndPushOptions = {
|
||||
projectRef: string;
|
||||
deploymentVersion: string;
|
||||
shouldHeartbeat?: boolean;
|
||||
attemptNumber?: number;
|
||||
};
|
||||
|
||||
type CheckpointAndPushResult =
|
||||
@@ -258,6 +259,7 @@ export class Checkpointer {
|
||||
leaveRunning = true, // This mirrors kubernetes behaviour more accurately
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
attemptNumber,
|
||||
}: CheckpointAndPushOptions): Promise<CheckpointAndPushResult> {
|
||||
this.#logger.log("Checkpointing with backoff", {
|
||||
runId,
|
||||
@@ -297,6 +299,7 @@ export class Checkpointer {
|
||||
leaveRunning,
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
attemptNumber,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
@@ -359,6 +362,7 @@ export class Checkpointer {
|
||||
leaveRunning = true, // This mirrors kubernetes behaviour more accurately
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
attemptNumber,
|
||||
}: CheckpointAndPushOptions): Promise<CheckpointAndPushResult> {
|
||||
await this.init();
|
||||
|
||||
@@ -367,6 +371,7 @@ export class Checkpointer {
|
||||
leaveRunning,
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
attemptNumber,
|
||||
};
|
||||
|
||||
if (!this.#dockerMode && !this.#canCheckpoint) {
|
||||
@@ -418,13 +423,14 @@ export class Checkpointer {
|
||||
this.#logger.log("Checkpointing:", { options });
|
||||
|
||||
const containterName = this.#getRunContainerName(runId);
|
||||
const containterNameWithAttempt = this.#getRunContainerName(runId, attemptNumber);
|
||||
|
||||
// Create checkpoint (docker)
|
||||
if (this.#dockerMode) {
|
||||
try {
|
||||
if (this.opts.forceSimulate || !this.#canCheckpoint) {
|
||||
this.#logger.log("Simulating checkpoint");
|
||||
this.#logger.debug(await $$`docker pause ${containterName}`);
|
||||
this.#logger.debug(await $$`docker pause ${containterNameWithAttempt}`);
|
||||
} else {
|
||||
if (this.simulateCheckpointFailure) {
|
||||
if (performance.now() < this.simulateCheckpointFailureSeconds * 1000) {
|
||||
@@ -435,11 +441,11 @@ export class Checkpointer {
|
||||
|
||||
if (leaveRunning) {
|
||||
this.#logger.debug(
|
||||
await $$`docker checkpoint create --leave-running ${containterName} ${exportLocation}`
|
||||
await $$`docker checkpoint create --leave-running ${containterNameWithAttempt} ${exportLocation}`
|
||||
);
|
||||
} else {
|
||||
this.#logger.debug(
|
||||
await $$`docker checkpoint create ${containterName} ${exportLocation}`
|
||||
await $$`docker checkpoint create ${containterNameWithAttempt} ${exportLocation}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -581,7 +587,7 @@ export class Checkpointer {
|
||||
return this.#failedCheckpoints.has(runId);
|
||||
}
|
||||
|
||||
#getRunContainerName(suffix: string) {
|
||||
return `task-run-${suffix}`;
|
||||
#getRunContainerName(suffix: string, attemptNumber?: number) {
|
||||
return `task-run-${suffix}${attemptNumber && attemptNumber > 1 ? `-att${attemptNumber}` : ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { ZodNamespace } from "@trigger.dev/core/v3/zodNamespace";
|
||||
import { ZodSocketConnection } from "@trigger.dev/core/v3/zodSocket";
|
||||
import { HttpReply, getTextBody } from "@trigger.dev/core-apps/http";
|
||||
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
|
||||
import { HttpReply, getTextBody } from "@trigger.dev/core/v3/apps";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import { ChaosMonkey } from "./chaosMonkey";
|
||||
import { Checkpointer } from "./checkpointer";
|
||||
|
||||
@@ -49,7 +49,11 @@ const PLATFORM_SECRET = process.env.PLATFORM_SECRET || "coordinator-secret";
|
||||
const SECURE_CONNECTION = ["1", "true"].includes(process.env.SECURE_CONNECTION ?? "false");
|
||||
|
||||
const logger = new SimpleLogger(`[${NODE_NAME}]`);
|
||||
const chaosMonkey = new ChaosMonkey(!!process.env.CHAOS_MONKEY_ENABLED);
|
||||
const chaosMonkey = new ChaosMonkey(
|
||||
!!process.env.CHAOS_MONKEY_ENABLED,
|
||||
!!process.env.CHAOS_MONKEY_DISABLE_ERRORS,
|
||||
!!process.env.CHAOS_MONKEY_DISABLE_DELAYS
|
||||
);
|
||||
|
||||
class TaskCoordinator {
|
||||
#httpServer: ReturnType<typeof createServer>;
|
||||
@@ -290,6 +294,7 @@ class TaskCoordinator {
|
||||
setSocketDataFromHeader("projectRef", "x-trigger-project-ref");
|
||||
setSocketDataFromHeader("runId", "x-trigger-run-id");
|
||||
setSocketDataFromHeader("attemptFriendlyId", "x-trigger-attempt-friendly-id", false);
|
||||
setSocketDataFromHeader("attemptNumber", "x-trigger-attempt-number", false);
|
||||
setSocketDataFromHeader("envId", "x-trigger-env-id");
|
||||
setSocketDataFromHeader("deploymentId", "x-trigger-deployment-id");
|
||||
setSocketDataFromHeader("deploymentVersion", "x-trigger-deployment-version");
|
||||
@@ -306,6 +311,10 @@ class TaskCoordinator {
|
||||
onConnection: async (socket, handler, sender) => {
|
||||
const logger = new SimpleLogger(`[prod-worker][${socket.id}]`);
|
||||
|
||||
const getAttemptNumber = () => {
|
||||
return socket.data.attemptNumber ? parseInt(socket.data.attemptNumber) : undefined;
|
||||
};
|
||||
|
||||
const crashRun = async (error: { name: string; message: string; stack?: string }) => {
|
||||
try {
|
||||
this.#platformSocket?.send("RUN_CRASHED", {
|
||||
@@ -381,6 +390,10 @@ class TaskCoordinator {
|
||||
socket.data.attemptFriendlyId = attemptFriendlyId;
|
||||
};
|
||||
|
||||
const updateAttemptNumber = (attemptNumber: string | number) => {
|
||||
socket.data.attemptNumber = String(attemptNumber);
|
||||
};
|
||||
|
||||
this.#platformSocket?.send("LOG", {
|
||||
metadata: socket.data,
|
||||
text: "connected",
|
||||
@@ -430,6 +443,7 @@ class TaskCoordinator {
|
||||
});
|
||||
|
||||
updateAttemptFriendlyId(executionAck.payload.execution.attempt.id);
|
||||
updateAttemptNumber(executionAck.payload.execution.attempt.number);
|
||||
} catch (error) {
|
||||
logger.error("Error", { error });
|
||||
|
||||
@@ -505,11 +519,17 @@ class TaskCoordinator {
|
||||
|
||||
updateAttemptFriendlyId(message.attemptFriendlyId);
|
||||
|
||||
this.#platformSocket?.send("READY_FOR_RESUME", message);
|
||||
if (message.version === "v2") {
|
||||
updateAttemptNumber(message.attemptNumber);
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("READY_FOR_RESUME", { ...message, version: "v1" });
|
||||
});
|
||||
|
||||
// MARK: RUN COMPLETED
|
||||
socket.on("TASK_RUN_COMPLETED", async ({ completion, execution }, callback) => {
|
||||
socket.on("TASK_RUN_COMPLETED", async (message, callback) => {
|
||||
const { completion, execution } = message;
|
||||
|
||||
logger.log("completed task", { completionId: completion.id });
|
||||
|
||||
// Cancel all in-progress checkpoints (if any)
|
||||
@@ -518,8 +538,10 @@ class TaskCoordinator {
|
||||
await chaosMonkey.call({ throwErrors: false });
|
||||
|
||||
const completeWithoutCheckpoint = (shouldExit: boolean) => {
|
||||
const supportsRetryCheckpoints = message.version === "v1";
|
||||
|
||||
this.#platformSocket?.send("TASK_RUN_COMPLETED", {
|
||||
version: "v1",
|
||||
version: supportsRetryCheckpoints ? "v1" : "v2",
|
||||
execution,
|
||||
completion,
|
||||
});
|
||||
@@ -546,6 +568,15 @@ class TaskCoordinator {
|
||||
|
||||
if (completion.retry.delay < this.#delayThresholdInMs) {
|
||||
completeWithoutCheckpoint(false);
|
||||
|
||||
// Prevents runs that fail fast from never sending a heartbeat
|
||||
this.#sendRunHeartbeat(socket.data.runId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.version === "v2") {
|
||||
completeWithoutCheckpoint(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -681,6 +712,7 @@ class TaskCoordinator {
|
||||
runId: socket.data.runId,
|
||||
projectRef: socket.data.projectRef,
|
||||
deploymentVersion: socket.data.deploymentVersion,
|
||||
attemptNumber: getAttemptNumber(),
|
||||
});
|
||||
|
||||
if (!checkpoint) {
|
||||
@@ -752,6 +784,7 @@ class TaskCoordinator {
|
||||
runId: socket.data.runId,
|
||||
projectRef: socket.data.projectRef,
|
||||
deploymentVersion: socket.data.deploymentVersion,
|
||||
attemptNumber: getAttemptNumber(),
|
||||
});
|
||||
|
||||
if (!checkpoint) {
|
||||
@@ -821,6 +854,7 @@ class TaskCoordinator {
|
||||
runId: socket.data.runId,
|
||||
projectRef: socket.data.projectRef,
|
||||
deploymentVersion: socket.data.deploymentVersion,
|
||||
attemptNumber: getAttemptNumber(),
|
||||
});
|
||||
|
||||
if (!checkpoint) {
|
||||
@@ -905,6 +939,7 @@ class TaskCoordinator {
|
||||
}
|
||||
|
||||
updateAttemptFriendlyId(createAttempt.executionPayload.execution.attempt.id);
|
||||
updateAttemptNumber(createAttempt.executionPayload.execution.attempt.number);
|
||||
|
||||
callback({
|
||||
success: true,
|
||||
@@ -924,6 +959,10 @@ class TaskCoordinator {
|
||||
if (message.attemptFriendlyId) {
|
||||
updateAttemptFriendlyId(message.attemptFriendlyId);
|
||||
}
|
||||
|
||||
if (message.attemptNumber) {
|
||||
updateAttemptNumber(message.attemptNumber);
|
||||
}
|
||||
});
|
||||
},
|
||||
onDisconnect: async (socket, handler, sender, logger) => {
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@trigger.dev/core/v3": ["../../packages/core/src/v3"],
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"],
|
||||
"@trigger.dev/core-apps": ["../../packages/core-apps/src"],
|
||||
"@trigger.dev/core-apps/*": ["../../packages/core-apps/src/*"]
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"execa": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,15 +5,16 @@ import {
|
||||
TaskOperationsCreateOptions,
|
||||
TaskOperationsIndexOptions,
|
||||
TaskOperationsRestoreOptions,
|
||||
} from "@trigger.dev/core-apps/provider";
|
||||
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
|
||||
import { isExecaChildProcess, testDockerCheckpoint } from "@trigger.dev/core-apps/checkpoints";
|
||||
} from "@trigger.dev/core/v3/apps";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import { isExecaChildProcess, testDockerCheckpoint } from "@trigger.dev/core/v3/apps";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import { PostStartCauses, PreStopCauses } from "@trigger.dev/core/v3";
|
||||
|
||||
const MACHINE_NAME = process.env.MACHINE_NAME || "local";
|
||||
const COORDINATOR_PORT = process.env.COORDINATOR_PORT || 8020;
|
||||
const COORDINATOR_HOST = process.env.COORDINATOR_HOST || "127.0.0.1";
|
||||
const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "host";
|
||||
|
||||
const OTEL_EXPORTER_OTLP_ENDPOINT =
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://0.0.0.0:4318";
|
||||
@@ -90,7 +91,7 @@ class DockerTaskOperations implements TaskOperations {
|
||||
logger.debug(
|
||||
await execa("docker", [
|
||||
"run",
|
||||
"--network=host",
|
||||
`--network=${DOCKER_NETWORK}`,
|
||||
"--rm",
|
||||
`--env=INDEX_TASKS=true`,
|
||||
`--env=TRIGGER_SECRET_KEY=${opts.apiKey}`,
|
||||
@@ -109,11 +110,11 @@ class DockerTaskOperations implements TaskOperations {
|
||||
async create(opts: TaskOperationsCreateOptions) {
|
||||
await this.init();
|
||||
|
||||
const containerName = this.#getRunContainerName(opts.runId);
|
||||
const containerName = this.#getRunContainerName(opts.runId, opts.nextAttemptNumber);
|
||||
|
||||
const runArgs = [
|
||||
"run",
|
||||
"--network=host",
|
||||
`--network=${DOCKER_NETWORK}`,
|
||||
"--detach",
|
||||
`--env=TRIGGER_ENV_ID=${opts.envId}`,
|
||||
`--env=TRIGGER_RUN_ID=${opts.runId}`,
|
||||
@@ -150,7 +151,7 @@ class DockerTaskOperations implements TaskOperations {
|
||||
async restore(opts: TaskOperationsRestoreOptions) {
|
||||
await this.init();
|
||||
|
||||
const containerName = this.#getRunContainerName(opts.runId);
|
||||
const containerName = this.#getRunContainerName(opts.runId, opts.attemptNumber);
|
||||
|
||||
if (!this.#canCheckpoint || this.opts.forceSimulate) {
|
||||
logger.log("Simulating restore");
|
||||
@@ -195,8 +196,8 @@ class DockerTaskOperations implements TaskOperations {
|
||||
return `task-index-${suffix}`;
|
||||
}
|
||||
|
||||
#getRunContainerName(suffix: string) {
|
||||
return `task-run-${suffix}`;
|
||||
#getRunContainerName(suffix: string, attemptNumber?: number) {
|
||||
return `task-run-${suffix}${attemptNumber && attemptNumber > 1 ? `-att${attemptNumber}` : ""}`;
|
||||
}
|
||||
|
||||
async #sendPostStart(containerName: string): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
@@ -9,9 +9,7 @@
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@trigger.dev/core/v3": ["../../packages/core/src/v3"],
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"],
|
||||
"@trigger.dev/core-apps": ["../../packages/core-apps/src"],
|
||||
"@trigger.dev/core-apps/*": ["../../packages/core-apps/src/*"]
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^0.20.0",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"p-queue": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -6,15 +6,14 @@ import {
|
||||
TaskOperationsIndexOptions,
|
||||
TaskOperationsPrePullDeploymentOptions,
|
||||
TaskOperationsRestoreOptions,
|
||||
} from "@trigger.dev/core-apps/provider";
|
||||
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
|
||||
} from "@trigger.dev/core/v3/apps";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import {
|
||||
MachinePreset,
|
||||
PostStartCauses,
|
||||
PreStopCauses,
|
||||
EnvironmentType,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { randomUUID } from "crypto";
|
||||
import { TaskMonitor } from "./taskMonitor";
|
||||
import { PodCleaner } from "./podCleaner";
|
||||
import { UptimeHeartbeat } from "./uptimeHeartbeat";
|
||||
@@ -41,7 +40,7 @@ type Namespace = {
|
||||
};
|
||||
};
|
||||
|
||||
type ComputeResources = {
|
||||
type ResourceQuantities = {
|
||||
[K in "cpu" | "memory" | "ephemeral-storage"]?: string;
|
||||
};
|
||||
|
||||
@@ -102,7 +101,7 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
resources: {
|
||||
limits: {
|
||||
cpu: "1",
|
||||
memory: "1G",
|
||||
memory: "2G",
|
||||
"ephemeral-storage": "2Gi",
|
||||
},
|
||||
},
|
||||
@@ -139,10 +138,12 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
}
|
||||
|
||||
async create(opts: TaskOperationsCreateOptions) {
|
||||
const containerName = this.#getRunContainerName(opts.runId, opts.nextAttemptNumber);
|
||||
|
||||
await this.#createPod(
|
||||
{
|
||||
metadata: {
|
||||
name: this.#getRunContainerName(opts.runId),
|
||||
name: containerName,
|
||||
namespace: this.#namespace.metadata.name,
|
||||
labels: {
|
||||
...this.#getSharedLabels(opts),
|
||||
@@ -157,22 +158,14 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
terminationGracePeriodSeconds: 60 * 60,
|
||||
containers: [
|
||||
{
|
||||
name: this.#getRunContainerName(opts.runId),
|
||||
name: containerName,
|
||||
image: opts.image,
|
||||
ports: [
|
||||
{
|
||||
containerPort: 8000,
|
||||
},
|
||||
],
|
||||
resources: {
|
||||
requests: {
|
||||
...this.#defaultResourceRequests,
|
||||
},
|
||||
limits: {
|
||||
...this.#defaultResourceLimits,
|
||||
...this.#getResourcesFromMachineConfig(opts.machine),
|
||||
},
|
||||
},
|
||||
resources: this.#getResourcesForMachine(opts.machine),
|
||||
lifecycle: {
|
||||
preStop: {
|
||||
exec: {
|
||||
@@ -211,7 +204,7 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
await this.#createPod(
|
||||
{
|
||||
metadata: {
|
||||
name: `${this.#getRunContainerName(opts.runId)}-${randomUUID().slice(0, 8)}`,
|
||||
name: `${this.#getRunContainerName(opts.runId)}-${opts.checkpointId.slice(-8)}`,
|
||||
namespace: this.#namespace.metadata.name,
|
||||
labels: {
|
||||
...this.#getSharedLabels(opts),
|
||||
@@ -263,15 +256,7 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
containerPort: 8000,
|
||||
},
|
||||
],
|
||||
resources: {
|
||||
requests: {
|
||||
...this.#defaultResourceRequests,
|
||||
},
|
||||
limits: {
|
||||
...this.#defaultResourceLimits,
|
||||
...this.#getResourcesFromMachineConfig(opts.machine),
|
||||
},
|
||||
},
|
||||
resources: this.#getResourcesForMachine(opts.machine),
|
||||
lifecycle: {
|
||||
postStart: {
|
||||
exec: {
|
||||
@@ -412,13 +397,13 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
};
|
||||
}
|
||||
|
||||
get #defaultResourceRequests(): ComputeResources {
|
||||
get #defaultResourceRequests(): ResourceQuantities {
|
||||
return {
|
||||
"ephemeral-storage": "2Gi",
|
||||
};
|
||||
}
|
||||
|
||||
get #defaultResourceLimits(): ComputeResources {
|
||||
get #defaultResourceLimits(): ResourceQuantities {
|
||||
return {
|
||||
"ephemeral-storage": "10Gi",
|
||||
};
|
||||
@@ -484,13 +469,33 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
};
|
||||
}
|
||||
|
||||
#getResourcesFromMachineConfig(preset: MachinePreset): ComputeResources {
|
||||
#getResourceRequestsForMachine(preset: MachinePreset): ResourceQuantities {
|
||||
return {
|
||||
cpu: `${preset.cpu * 0.75}`,
|
||||
memory: `${preset.memory}G`,
|
||||
};
|
||||
}
|
||||
|
||||
#getResourceLimitsForMachine(preset: MachinePreset): ResourceQuantities {
|
||||
return {
|
||||
cpu: `${preset.cpu}`,
|
||||
memory: `${preset.memory}G`,
|
||||
};
|
||||
}
|
||||
|
||||
#getResourcesForMachine(preset: MachinePreset): k8s.V1ResourceRequirements {
|
||||
return {
|
||||
requests: {
|
||||
...this.#defaultResourceRequests,
|
||||
...this.#getResourceRequestsForMachine(preset),
|
||||
},
|
||||
limits: {
|
||||
...this.#defaultResourceLimits,
|
||||
...this.#getResourceLimitsForMachine(preset),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#getLifecycleCommand<THookType extends "postStart" | "preStop">(
|
||||
type: THookType,
|
||||
cause: THookType extends "postStart" ? PostStartCauses : PreStopCauses
|
||||
@@ -514,8 +519,8 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
return `task-index-${suffix}`;
|
||||
}
|
||||
|
||||
#getRunContainerName(suffix: string) {
|
||||
return `task-run-${suffix}`;
|
||||
#getRunContainerName(suffix: string, attemptNumber?: number) {
|
||||
return `task-run-${suffix}${attemptNumber && attemptNumber > 1 ? `-att${attemptNumber}` : ""}`;
|
||||
}
|
||||
|
||||
#getPrePullContainerName(suffix: string) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
|
||||
type PodCleanerOptions = {
|
||||
runtimeEnv: "local" | "kubernetes";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
|
||||
import { EXIT_CODE_ALREADY_HANDLED, EXIT_CODE_CHILD_NONZERO } from "@trigger.dev/core-apps/process";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import { EXIT_CODE_ALREADY_HANDLED, EXIT_CODE_CHILD_NONZERO } from "@trigger.dev/core/v3/apps";
|
||||
import { setTimeout } from "timers/promises";
|
||||
import PQueue from "p-queue";
|
||||
import type { Prettify } from "@trigger.dev/core/v3";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
|
||||
type UptimeHeartbeatOptions = {
|
||||
runtimeEnv: "local" | "kubernetes";
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@trigger.dev/core/v3": ["../../packages/core/src/v3"],
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"],
|
||||
"@trigger.dev/core-apps": ["../../packages/core-apps/src"],
|
||||
"@trigger.dev/core-apps/*": ["../../packages/core-apps/src/*"]
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
"zod": "3.22.3",
|
||||
"zod-error": "1.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/primitives/Tooltip";
|
||||
import {
|
||||
useIsImpersonating,
|
||||
useOptionalOrganization,
|
||||
useOrganization,
|
||||
} from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject, useProject } from "~/hooks/useProject";
|
||||
import { useIsImpersonating, useOptionalOrganization } from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject } from "~/hooks/useProject";
|
||||
import { useHasAdminAccess, useUser } from "~/hooks/useUser";
|
||||
|
||||
export function AdminDebugTooltip({ children }: { children: React.ReactNode }) {
|
||||
export function AdminDebugTooltip({ children }: { children?: React.ReactNode }) {
|
||||
const hasAdminAccess = useHasAdminAccess();
|
||||
const isImpersonating = useIsImpersonating();
|
||||
|
||||
|
||||
@@ -60,6 +60,23 @@ export function UsageBar({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
<motion.div
|
||||
initial={{ width: usagePercentage / startFactor + "%" }}
|
||||
animate={{ width: usagePercentage + "%" }}
|
||||
transition={{ duration: 1.5, type: "spring" }}
|
||||
style={{ width: `${usagePercentage}%` }}
|
||||
className={cn(
|
||||
"absolute h-3 rounded-l-sm",
|
||||
tierLimit && current > tierLimit ? "bg-green-700" : "bg-green-600"
|
||||
)}
|
||||
>
|
||||
<Legend
|
||||
text="Used:"
|
||||
value={formatCurrency(current, false)}
|
||||
position="topRow1"
|
||||
percentage={usagePercentage}
|
||||
/>
|
||||
</motion.div>
|
||||
{tierLimit !== undefined && (
|
||||
<motion.div
|
||||
initial={{ width: tierRunLimitPercentage / startFactor + "%" }}
|
||||
@@ -92,23 +109,7 @@ export function UsageBar({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
<motion.div
|
||||
initial={{ width: usagePercentage / startFactor + "%" }}
|
||||
animate={{ width: usagePercentage + "%" }}
|
||||
transition={{ duration: 1.5, type: "spring" }}
|
||||
style={{ width: `${usagePercentage}%` }}
|
||||
className={cn(
|
||||
"absolute h-3 rounded-l-sm",
|
||||
tierLimit && current > tierLimit ? "bg-rose-600" : "bg-green-600"
|
||||
)}
|
||||
>
|
||||
<Legend
|
||||
text="Used:"
|
||||
value={formatCurrency(current, false)}
|
||||
position="topRow1"
|
||||
percentage={usagePercentage}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ width: usageCappedToLimitPercentage / startFactor + "%" }}
|
||||
animate={{ width: usageCappedToLimitPercentage + "%" }}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CursorArrowRaysIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
RectangleStackIcon,
|
||||
ServerStackIcon,
|
||||
ShieldCheckIcon,
|
||||
SignalIcon,
|
||||
@@ -19,9 +20,9 @@ import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { MatchedOrganization } from "~/hooks/useOrganizations";
|
||||
import { MatchedProject } from "~/hooks/useProject";
|
||||
import { User } from "~/models/user.server";
|
||||
import { type MatchedOrganization } from "~/hooks/useOrganizations";
|
||||
import { type MatchedProject } from "~/hooks/useProject";
|
||||
import { type User } from "~/models/user.server";
|
||||
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
projectTriggersPath,
|
||||
v3ApiKeysPath,
|
||||
v3BillingPath,
|
||||
v3ConcurrencyPath,
|
||||
v3DeploymentsPath,
|
||||
v3EnvironmentVariablesPath,
|
||||
v3ProjectAlertsPath,
|
||||
@@ -63,8 +65,7 @@ import { StepContentContainer } from "../StepContentContainer";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { FreePlanUsage } from "../billing/v2/FreePlanUsage";
|
||||
import { Badge } from "../primitives/Badge";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
@@ -80,19 +81,10 @@ import {
|
||||
import { StepNumber } from "../primitives/StepNumber";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { MenuCount, SideMenuItem } from "./SideMenuItem";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
|
||||
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
|
||||
type SideMenuProject = Pick<
|
||||
MatchedProject,
|
||||
| "id"
|
||||
| "name"
|
||||
| "slug"
|
||||
| "hasInactiveExternalTriggers"
|
||||
| "jobCount"
|
||||
| "httpEndpointCount"
|
||||
| "version"
|
||||
>;
|
||||
type SideMenuProject = Pick<MatchedProject, "id" | "name" | "slug" | "version">;
|
||||
|
||||
type SideMenuProps = {
|
||||
user: SideMenuUser;
|
||||
@@ -107,6 +99,9 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
const currentPlan = useCurrentPlan();
|
||||
const { isManagedCloud } = useFeatures();
|
||||
|
||||
const isV3Project = project.version === "V3";
|
||||
const isFreeV3User = currentPlan?.v3Subscription?.isPaying === false;
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (borderRef.current) {
|
||||
@@ -164,7 +159,6 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
icon="integration"
|
||||
to={organizationIntegrationsPath(organization)}
|
||||
data-action="integrations"
|
||||
hasWarning={organization.hasUnconfiguredIntegrations}
|
||||
/>
|
||||
)}
|
||||
<SideMenuItem
|
||||
@@ -195,6 +189,11 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
to={v3BillingPath(organization)}
|
||||
iconColor="text-blue-600"
|
||||
data-action="billing"
|
||||
badge={
|
||||
currentPlan?.v3Subscription?.isPaying
|
||||
? currentPlan?.v3Subscription?.plan?.title
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -218,14 +217,25 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
</div>
|
||||
<div className="m-2">
|
||||
{project.version === "V2" && (
|
||||
<Callout variant={"info"}>
|
||||
<Paragraph variant="small">
|
||||
This is a v2 project.{" "}
|
||||
<TextLink href="https://trigger.dev/docs/v3/upgrading-from-v2">
|
||||
Upgrade to v3
|
||||
<div className="flex flex-col gap-3 rounded border border-success/50 bg-success/10 p-3">
|
||||
<Paragraph variant="small/bright">
|
||||
This is a v2 project. V2 will be deprecated on January 31, 2025.{" "}
|
||||
<TextLink
|
||||
className="text-text-bright underline decoration-text-dimmed underline-offset-2 transition hover:text-text-bright hover:decoration-text-bright"
|
||||
to="https://trigger.dev/blog/v2-end-of-life-announcement"
|
||||
>
|
||||
Learn more
|
||||
</TextLink>
|
||||
.
|
||||
</Paragraph>
|
||||
</Callout>
|
||||
<LinkButton
|
||||
variant="primary/medium"
|
||||
to="https://trigger.dev/docs/v3/upgrading-from-v2"
|
||||
fullWidth
|
||||
>
|
||||
Upgrade to v3
|
||||
</LinkButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 border-t border-grid-bright p-1">
|
||||
@@ -319,7 +329,7 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{currentPlan?.v3Subscription?.isPaying === false && (
|
||||
{isV3Project && isFreeV3User && (
|
||||
<FreePlanUsage
|
||||
to={v3BillingPath(organization)}
|
||||
percentage={currentPlan.v3Usage.usagePercentage}
|
||||
@@ -374,10 +384,10 @@ function ProjectSelector({
|
||||
title={
|
||||
<div className="flex w-full items-center justify-between text-text-bright">
|
||||
<span className="grow truncate text-left">{p.name}</span>
|
||||
{p.version === "V2" ? (
|
||||
<MenuCount count={p.jobCount} />
|
||||
) : (
|
||||
<Badge variant="v3">v3</Badge>
|
||||
{p.version === "V2" && (
|
||||
<Badge variant="small" className="normal-case">
|
||||
v2
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
@@ -396,7 +406,7 @@ function ProjectSelector({
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
<div className="border-t border-charcoal-800 p-1">
|
||||
<div className="border-t border-charcoal-700 p-1">
|
||||
<PopoverMenuItem to={newOrganizationPath()} title="New Organization" icon="plus" />
|
||||
</div>
|
||||
</PopoverContent>
|
||||
@@ -486,7 +496,6 @@ function V2ProjectSideMenu({
|
||||
name="Jobs"
|
||||
icon="job"
|
||||
iconColor="text-indigo-500"
|
||||
count={project.jobCount}
|
||||
to={projectPath(organization, project)}
|
||||
data-action="jobs"
|
||||
/>
|
||||
@@ -502,7 +511,6 @@ function V2ProjectSideMenu({
|
||||
iconColor="text-amber-500"
|
||||
to={projectTriggersPath(organization, project)}
|
||||
data-action="triggers"
|
||||
hasWarning={project.hasInactiveExternalTriggers}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Events"
|
||||
@@ -514,7 +522,6 @@ function V2ProjectSideMenu({
|
||||
name="HTTP endpoints"
|
||||
icon="http-endpoint"
|
||||
iconColor="text-pink-500"
|
||||
count={project.httpEndpointCount}
|
||||
to={projectHttpEndpointsPath(organization, project)}
|
||||
data-action="httpendpoints"
|
||||
/>
|
||||
@@ -589,6 +596,7 @@ function V3ProjectSideMenu({
|
||||
to={v3EnvironmentVariablesPath(organization, project)}
|
||||
data-action="environment variables"
|
||||
/>
|
||||
|
||||
<SideMenuItem
|
||||
name="Deployments"
|
||||
icon={ServerStackIcon}
|
||||
@@ -605,6 +613,13 @@ function V3ProjectSideMenu({
|
||||
data-action="alerts"
|
||||
/>
|
||||
)}
|
||||
<SideMenuItem
|
||||
name="Concurrency limits"
|
||||
icon={RectangleStackIcon}
|
||||
iconColor="text-indigo-500"
|
||||
to={v3ConcurrencyPath(organization, project)}
|
||||
data-action="concurrency"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Project settings"
|
||||
icon="settings"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AnchorHTMLAttributes } from "react";
|
||||
import { type AnchorHTMLAttributes } from "react";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { IconNames } from "../primitives/NamedIcon";
|
||||
import { type IconNames } from "../primitives/NamedIcon";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||
@@ -13,7 +13,7 @@ export function SideMenuItem({
|
||||
name,
|
||||
to,
|
||||
hasWarning,
|
||||
count,
|
||||
badge,
|
||||
target,
|
||||
subItem = false,
|
||||
}: {
|
||||
@@ -22,7 +22,7 @@ export function SideMenuItem({
|
||||
name: string;
|
||||
to: string;
|
||||
hasWarning?: string | boolean;
|
||||
count?: number;
|
||||
badge?: string;
|
||||
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
|
||||
subItem?: boolean;
|
||||
}) {
|
||||
@@ -47,20 +47,20 @@ export function SideMenuItem({
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{name}
|
||||
<div className="flex items-center gap-1">
|
||||
{count !== undefined && count > 0 && <MenuCount count={count} />}
|
||||
{badge !== undefined && <MenuCount count={badge} />}
|
||||
{typeof hasWarning === "string" ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
|
||||
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-error" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-1 border border-rose-500 bg-rose-500/20 backdrop-blur-xl">
|
||||
<TooltipContent className="flex items-center gap-1 border border-error bg-error/20 backdrop-blur-xl">
|
||||
{hasWarning}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
|
||||
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-error" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,8 +68,10 @@ export function SideMenuItem({
|
||||
);
|
||||
}
|
||||
|
||||
export function MenuCount({ count }: { count: number | string }) {
|
||||
function MenuCount({ count }: { count: number | string }) {
|
||||
return (
|
||||
<div className="rounded-full bg-charcoal-900 px-2 py-1 text-xxs text-text-dimmed">{count}</div>
|
||||
<div className="rounded-full bg-charcoal-900 px-2 py-1 text-xxs uppercase tracking-wider text-text-dimmed">
|
||||
{count}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ const variants = {
|
||||
"grid place-items-center rounded-full px-[0.4rem] h-4 tracking-wider text-xxs bg-background-dimmed text-text-dimmed uppercase whitespace-nowrap",
|
||||
outline:
|
||||
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider text-xxs border border-dimmed text-text-dimmed uppercase whitespace-nowrap",
|
||||
v3: "grid place-items-center rounded-full px-[0.4rem] h-5 tracking-wider text-xxs bg-charcoal-750 text-primary whitespace-nowrap",
|
||||
"outline-rounded":
|
||||
"grid place-items-center rounded-full px-1 h-4 tracking-wider text-xxs border border-blue-500 text-blue-500 uppercase whitespace-nowrap",
|
||||
};
|
||||
|
||||
@@ -245,12 +245,12 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
|
||||
type ButtonPropsType = Pick<
|
||||
JSX.IntrinsicElements["button"],
|
||||
"type" | "disabled" | "onClick" | "name" | "value" | "form"
|
||||
"type" | "disabled" | "onClick" | "name" | "value" | "form" | "autoFocus"
|
||||
> &
|
||||
React.ComponentProps<typeof ButtonContent>;
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
({ type, disabled, onClick, ...props }, ref) => {
|
||||
({ type, disabled, autoFocus, onClick, ...props }, ref) => {
|
||||
const innerRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
|
||||
|
||||
@@ -276,6 +276,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
value={props.value}
|
||||
ref={innerRef}
|
||||
form={props.form}
|
||||
autoFocus={autoFocus}
|
||||
>
|
||||
<ButtonContent {...props} />
|
||||
</button>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ChevronDownIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ButtonContentPropsType, LinkButton } from "./Buttons";
|
||||
import { Paragraph, ParagraphVariant } from "./Paragraph";
|
||||
import { type ButtonContentPropsType, LinkButton } from "./Buttons";
|
||||
import { Paragraph, type ParagraphVariant } from "./Paragraph";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
@@ -41,7 +41,7 @@ function PopoverSectionHeader({
|
||||
variant?: ParagraphVariant;
|
||||
}) {
|
||||
return (
|
||||
<Paragraph variant={variant} className="bg-charcoal-900 px-2.5 py-1.5">
|
||||
<Paragraph variant={variant} className="bg-charcoal-750 px-2.5 py-1.5">
|
||||
{title}
|
||||
</Paragraph>
|
||||
);
|
||||
@@ -71,7 +71,10 @@ function PopoverMenuItem({
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
TrailingIcon={isSelected ? "check" : undefined}
|
||||
className={isSelected ? "bg-charcoal-750 group-hover:bg-charcoal-800" : undefined}
|
||||
className={cn(
|
||||
"group-hover:bg-charcoal-700",
|
||||
isSelected ? "bg-charcoal-750 group-hover:bg-charcoal-600/50" : undefined
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</LinkButton>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Form, useFetcher, useNavigation, useSubmit } from "@remix-run/react";
|
||||
import { Form, useNavigation, useSubmit } from "@remix-run/react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson";
|
||||
import { JSONEditor } from "~/components/code/JSONEditor";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { DialogContent, DialogDescription, DialogHeader } from "~/components/primitives/Dialog";
|
||||
import { DialogContent, DialogHeader } from "~/components/primitives/Dialog";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
@@ -27,7 +27,7 @@ export function ReplayRunDialog({ runFriendlyId, failedRedirect }: ReplayRunDial
|
||||
|
||||
function ReplayContent({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) {
|
||||
const fetcher = useTypedFetcher<typeof loader>();
|
||||
const isLoading = fetcher.state !== "idle";
|
||||
const isLoading = fetcher.state === "loading";
|
||||
|
||||
useEffect(() => {
|
||||
fetcher.load(`/resources/taskruns/${runFriendlyId}/replay`);
|
||||
@@ -98,6 +98,7 @@ function ReplayForm({
|
||||
<Header3 spacing>Payload</Header3>
|
||||
<div className="mb-3 max-h-[70vh] overflow-y-auto rounded-sm border border-grid-dimmed bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<JSONEditor
|
||||
autoFocus
|
||||
defaultValue={currentJson.current}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
|
||||
@@ -67,23 +67,17 @@ const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
|
||||
EXPIRED: "Task has surpassed its ttl and won't be executed",
|
||||
};
|
||||
|
||||
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY", "DELAYED"];
|
||||
export const QUEUED_STATUSES = [
|
||||
"PENDING",
|
||||
"WAITING_FOR_DEPLOY",
|
||||
"DELAYED",
|
||||
] satisfies TaskRunStatus[];
|
||||
|
||||
export const RUNNING_STATUSES: TaskRunStatus[] = [
|
||||
export const RUNNING_STATUSES = [
|
||||
"EXECUTING",
|
||||
"RETRYING_AFTER_FAILURE",
|
||||
"WAITING_TO_RESUME",
|
||||
];
|
||||
|
||||
export const FINISHED_STATUSES: TaskRunStatus[] = [
|
||||
"COMPLETED_SUCCESSFULLY",
|
||||
"CANCELED",
|
||||
"COMPLETED_WITH_ERRORS",
|
||||
"INTERRUPTED",
|
||||
"SYSTEM_FAILURE",
|
||||
"CRASHED",
|
||||
"EXPIRED",
|
||||
];
|
||||
] satisfies TaskRunStatus[];
|
||||
|
||||
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
|
||||
return taskRunStatusDescriptions[status];
|
||||
|
||||
@@ -114,6 +114,7 @@ const EnvironmentSchema = z.object({
|
||||
DEPOT_TOKEN: z.string().optional(),
|
||||
DEPOT_PROJECT_ID: z.string().optional(),
|
||||
DEPOT_ORG_ID: z.string().optional(),
|
||||
DEPOT_REGION: z.string().default("us-east-1"),
|
||||
CONTAINER_REGISTRY_ORIGIN: z.string().optional(),
|
||||
CONTAINER_REGISTRY_USERNAME: z.string().optional(),
|
||||
CONTAINER_REGISTRY_PASSWORD: z.string().optional(),
|
||||
|
||||
Vendored
+4
@@ -13,3 +13,7 @@ interface Array<T> {
|
||||
export type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
|
||||
export type EnsureProperty<T, K extends keyof T> = T & {
|
||||
[P in K]-?: NonNullable<T[P]>;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { prisma } from "~/db.server";
|
||||
import { SearchParams } from "~/routes/admin._index";
|
||||
import {
|
||||
clearImpersonationId,
|
||||
commitImpersonationSession,
|
||||
setImpersonationId,
|
||||
} from "~/services/impersonation.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
@@ -32,6 +39,7 @@ export async function adminGetUsers(userId: string, { page, search }: SearchPara
|
||||
select: {
|
||||
title: true,
|
||||
slug: true,
|
||||
deletedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -120,6 +128,7 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se
|
||||
title: true,
|
||||
v2Enabled: true,
|
||||
v3Enabled: true,
|
||||
deletedAt: true,
|
||||
members: {
|
||||
select: {
|
||||
user: {
|
||||
@@ -217,3 +226,26 @@ export async function setV3Enabled(userId: string, id: string, v3Enabled: boolea
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function redirectWithImpersonation(request: Request, userId: string, path: string) {
|
||||
const user = await requireUser(request);
|
||||
if (!user.admin) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
const session = await setImpersonationId(userId, request);
|
||||
|
||||
return redirect(path, {
|
||||
headers: { "Set-Cookie": await commitImpersonationSession(session) },
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearImpersonation(request: Request, path: string) {
|
||||
const session = await clearImpersonationId(request);
|
||||
|
||||
return redirect(path, {
|
||||
headers: {
|
||||
"Set-Cookie": await commitImpersonationSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +116,20 @@ export async function disconnectSession(environmentId: string) {
|
||||
return session;
|
||||
}
|
||||
|
||||
type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
|
||||
export async function findLatestSession(environmentId: string) {
|
||||
const session = await prisma.runtimeEnvironmentSession.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
type: true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { JobRun, Task, TaskAttempt } from "@trigger.dev/database";
|
||||
import type { JobRun, Task, TaskAttempt, TaskTriggerSource } from "@trigger.dev/database";
|
||||
import { CachedTask, ServerTask } from "@trigger.dev/core";
|
||||
import { PrismaClientOrTransaction, sqlDatabaseSchema } from "~/db.server";
|
||||
|
||||
export type TaskWithAttempts = Task & {
|
||||
attempts: TaskAttempt[];
|
||||
@@ -116,3 +117,22 @@ function prepareTaskForCaching(task: TaskForCaching): CachedTask {
|
||||
function calculateCachedTaskSize(task: CachedTask): number {
|
||||
return JSON.stringify(task).length;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param prisma An efficient query to get all task identifiers for a project.
|
||||
* It has indexes for fast performance.
|
||||
* It does NOT care about versions, so includes all tasks ever created.
|
||||
*/
|
||||
export function getAllTaskIdentifiers(prisma: PrismaClientOrTransaction, projectId: string) {
|
||||
return prisma.$queryRaw<
|
||||
{
|
||||
slug: string;
|
||||
triggerSource: TaskTriggerSource;
|
||||
}[]
|
||||
>`
|
||||
SELECT DISTINCT(slug), "triggerSource"
|
||||
FROM ${sqlDatabaseSchema}."BackgroundWorkerTask"
|
||||
WHERE "projectId" = ${projectId}
|
||||
ORDER BY slug ASC;`;
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
identifier: K,
|
||||
payload: z.infer<TMessageCatalog[K]>,
|
||||
options?: ZodWorkerEnqueueOptions
|
||||
): Promise<GraphileJob> {
|
||||
): Promise<GraphileJob | undefined> {
|
||||
const task = this.#tasks[identifier];
|
||||
|
||||
const optionsWithoutTx = removeUndefinedKeys(omit(options ?? {}, ["tx"]));
|
||||
@@ -439,11 +439,9 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
identifier,
|
||||
payload,
|
||||
spec,
|
||||
error: JSON.stringify(rows.error),
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
`Failed to add job to queue, zod parsing error: ${JSON.stringify(rows.error)}`
|
||||
);
|
||||
return { job: undefined, durationInMs: Math.floor(durationInMs) };
|
||||
}
|
||||
|
||||
const job = rows.data[0];
|
||||
|
||||
@@ -14,8 +14,6 @@ import { Job } from "@trigger.dev/database";
|
||||
import { BasePresenter } from "./v3/basePresenter.server";
|
||||
|
||||
export class JobPresenter extends BasePresenter {
|
||||
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
jobSlug,
|
||||
@@ -96,15 +94,6 @@ export class JobPresenter extends BasePresenter {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
runs: {
|
||||
where: {
|
||||
isTest: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
slug: jobSlug,
|
||||
@@ -196,9 +185,6 @@ export class JobPresenter extends BasePresenter {
|
||||
|
||||
const projectRootPath = projectPath({ slug: organizationSlug }, { slug: projectSlug });
|
||||
|
||||
//we exclude test runs from this count
|
||||
const hasRealRuns = job._count.runs > 0;
|
||||
|
||||
return {
|
||||
id: job.id,
|
||||
slug: job.slug,
|
||||
@@ -214,9 +200,6 @@ export class JobPresenter extends BasePresenter {
|
||||
? `${projectRootPath}/${alias.version.triggerLink}`
|
||||
: undefined,
|
||||
},
|
||||
noRunsHelp: hasRealRuns
|
||||
? undefined
|
||||
: this.#getNoRunsHelp(alias.version.triggerHelp, projectRootPath),
|
||||
integrations,
|
||||
hasIntegrationsRequiringAction: integrations.some((i) => i.setupStatus === "MISSING_FIELDS"),
|
||||
lastRun,
|
||||
@@ -224,19 +207,4 @@ export class JobPresenter extends BasePresenter {
|
||||
environments,
|
||||
};
|
||||
}
|
||||
|
||||
#getNoRunsHelp(data: Prisma.JsonValue, projectPath: string) {
|
||||
const triggerHelp = TriggerHelpSchema.nullish().parse(data);
|
||||
if (!triggerHelp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (triggerHelp.noRuns) {
|
||||
triggerHelp.noRuns.link = triggerHelp.noRuns.link
|
||||
? `${projectPath}/${triggerHelp.noRuns.link}`
|
||||
: undefined;
|
||||
|
||||
return triggerHelp.noRuns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,29 +186,10 @@ export class OrganizationsPresenter {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
_count: {
|
||||
select: {
|
||||
jobs: {
|
||||
where: {
|
||||
internal: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
version: true,
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
integrations: {
|
||||
where: {
|
||||
setupStatus: "MISSING_FIELDS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -221,10 +202,8 @@ export class OrganizationsPresenter {
|
||||
id: project.id,
|
||||
slug: project.slug,
|
||||
name: project.name,
|
||||
jobCount: project._count.jobs,
|
||||
version: project.version,
|
||||
})),
|
||||
hasUnconfiguredIntegrations: org._count.integrations > 0,
|
||||
runsEnabled: org.runsEnabled,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -28,22 +28,6 @@ export class ProjectPresenter {
|
||||
deletedAt: true,
|
||||
version: true,
|
||||
externalRef: true,
|
||||
_count: {
|
||||
select: {
|
||||
sources: {
|
||||
where: {
|
||||
active: false,
|
||||
},
|
||||
},
|
||||
jobs: {
|
||||
where: {
|
||||
internal: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
httpEndpoints: true,
|
||||
},
|
||||
},
|
||||
environments: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -81,9 +65,6 @@ export class ProjectPresenter {
|
||||
updatedAt: project.updatedAt,
|
||||
deletedAt: project.deletedAt,
|
||||
version: project.version,
|
||||
hasInactiveExternalTriggers: project._count.sources > 0,
|
||||
jobCount: project._count.jobs,
|
||||
httpEndpointCount: project._count.httpEndpoints,
|
||||
environments: sortEnvironments(
|
||||
project.environments.map((environment) => ({
|
||||
...displayableEnvironment(environment, userId),
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
EventSpecificationSchema,
|
||||
StyleSchema,
|
||||
} from "@trigger.dev/core";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { $replica, PrismaClient, prisma } from "~/db.server";
|
||||
import { isRunCompleted, runBasicStatus } from "~/models/jobRun.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
import { taskListToTree } from "~/utils/taskListToTree";
|
||||
@@ -110,7 +110,7 @@ export class RunPresenter {
|
||||
}
|
||||
|
||||
query({ id, userId }: RunOptions) {
|
||||
return this.#prismaClient.jobRun.findFirst({
|
||||
return $replica.jobRun.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
number: true,
|
||||
@@ -198,6 +198,7 @@ export class RunPresenter {
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
take: 1000,
|
||||
},
|
||||
runConnections: {
|
||||
select: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RedactSchema, StyleSchema } from "@trigger.dev/core";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { $replica, PrismaClient, prisma } from "~/db.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
import { Redactor } from "~/utils/redactor";
|
||||
|
||||
@@ -16,7 +16,7 @@ export class TaskDetailsPresenter {
|
||||
}
|
||||
|
||||
public async call({ id, userId }: DetailsProps) {
|
||||
const task = await this.#prismaClient.task.findFirst({
|
||||
const task = await $replica.task.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
displayKey: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { ProjectAlertChannel } from "@trigger.dev/database";
|
||||
import { type ProjectAlertChannel } from "@trigger.dev/database";
|
||||
import { decryptSecret } from "~/services/secrets/secretStore.server";
|
||||
import { env } from "~/env.server";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
ProjectAlertChannel,
|
||||
ProjectAlertChannelType,
|
||||
ProjectAlertType,
|
||||
type ProjectAlertChannel,
|
||||
type ProjectAlertChannelType,
|
||||
type ProjectAlertType,
|
||||
} from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import { z } from "zod";
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
} from "~/models/projectAlert.server";
|
||||
import { decryptSecret } from "~/services/secrets/secretStore.server";
|
||||
|
||||
export const ApiAlertType = z.enum(["attempt_failure", "deployment_failure", "deployment_success"]);
|
||||
export const ApiAlertType = z.enum([
|
||||
"run_failure",
|
||||
"attempt_failure",
|
||||
"deployment_failure",
|
||||
"deployment_success",
|
||||
]);
|
||||
|
||||
export type ApiAlertType = z.infer<typeof ApiAlertType>;
|
||||
|
||||
@@ -72,6 +77,8 @@ export class ApiAlertChannelPresenter {
|
||||
|
||||
public static alertTypeToApi(alertType: ProjectAlertType): ApiAlertType {
|
||||
switch (alertType) {
|
||||
case "TASK_RUN":
|
||||
return "run_failure";
|
||||
case "TASK_RUN_ATTEMPT":
|
||||
return "attempt_failure";
|
||||
case "DEPLOYMENT_FAILURE":
|
||||
@@ -85,6 +92,8 @@ export class ApiAlertChannelPresenter {
|
||||
|
||||
public static alertTypeFromApi(alertType: ApiAlertType): ProjectAlertType {
|
||||
switch (alertType) {
|
||||
case "run_failure":
|
||||
return "TASK_RUN";
|
||||
case "attempt_failure":
|
||||
return "TASK_RUN_ATTEMPT";
|
||||
case "deployment_failure":
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { QUEUED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { Prisma, sqlDatabaseSchema } from "~/db.server";
|
||||
import { type Project } from "~/models/project.server";
|
||||
import {
|
||||
displayableEnvironment,
|
||||
type DisplayableInputEnvironment,
|
||||
} from "~/models/runtimeEnvironment.server";
|
||||
import { type User } from "~/models/user.server";
|
||||
import { getLimit } from "~/services/platform.v3.server";
|
||||
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";
|
||||
import { concurrencyTracker } from "~/v3/services/taskRunConcurrencyTracker.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
export type Environment = Awaited<
|
||||
ReturnType<ConcurrencyPresenter["environmentConcurrency"]>
|
||||
>[number];
|
||||
|
||||
export class ConcurrencyPresenter extends BasePresenter {
|
||||
public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) {
|
||||
const project = await this._replica.project.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
environments: {
|
||||
select: {
|
||||
id: true,
|
||||
apiKey: true,
|
||||
pkApiKey: true,
|
||||
type: true,
|
||||
slug: true,
|
||||
updatedAt: true,
|
||||
orgMember: {
|
||||
select: {
|
||||
user: { select: { id: true, name: true, displayName: true } },
|
||||
},
|
||||
},
|
||||
maximumConcurrencyLimit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
slug: projectSlug,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new Error(`Project not found: ${projectSlug}`);
|
||||
}
|
||||
|
||||
return {
|
||||
environments: this.environmentConcurrency(
|
||||
project.id,
|
||||
userId,
|
||||
filterOrphanedEnvironments(project.environments)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async environmentConcurrency(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
environments: (DisplayableInputEnvironment & { maximumConcurrencyLimit: number })[]
|
||||
) {
|
||||
const environmentConcurrency = await concurrencyTracker.environmentConcurrentRunCounts(
|
||||
projectId,
|
||||
environments.map((env) => env.id)
|
||||
);
|
||||
|
||||
const queued = await this._replica.$queryRaw<
|
||||
{
|
||||
runtimeEnvironmentId: string;
|
||||
count: BigInt;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
"runtimeEnvironmentId",
|
||||
COUNT(*)
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" as tr
|
||||
WHERE
|
||||
tr."projectId" = ${projectId}
|
||||
AND tr."status" = ANY(ARRAY[${Prisma.join(QUEUED_STATUSES)}]::\"TaskRunStatus\"[])
|
||||
GROUP BY
|
||||
tr."runtimeEnvironmentId";`;
|
||||
|
||||
const sortedEnvironments = sortEnvironments(environments).map((environment) => ({
|
||||
...displayableEnvironment(environment, userId),
|
||||
concurrencyLimit: environment.maximumConcurrencyLimit,
|
||||
concurrency: environmentConcurrency[environment.id] ?? 0,
|
||||
queued: Number(queued.find((q) => q.runtimeEnvironmentId === environment.id)?.count ?? 0),
|
||||
}));
|
||||
|
||||
return sortedEnvironments;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
import {
|
||||
DeploymentErrorData,
|
||||
ExternalBuildData,
|
||||
TaskMetadataFailedToParseData,
|
||||
groupTaskMetadataIssuesByTask,
|
||||
prepareDeploymentError,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { WorkerDeployment, WorkerDeploymentStatus } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { WorkerDeployment } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import { safeJsonParse } from "~/utils/json";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
export type ErrorData = {
|
||||
@@ -164,75 +161,12 @@ export class DeploymentPresenter {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedErrorData = DeploymentErrorData.safeParse(errorData);
|
||||
const deploymentError = DeploymentErrorData.safeParse(errorData);
|
||||
|
||||
if (!parsedErrorData.success) {
|
||||
if (!deploymentError.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedErrorData.data.name === "TaskMetadataParseError") {
|
||||
const errorJson = safeJsonParse(parsedErrorData.data.stack);
|
||||
|
||||
if (errorJson) {
|
||||
const parsedError = TaskMetadataFailedToParseData.safeParse(errorJson);
|
||||
|
||||
if (parsedError.success) {
|
||||
return {
|
||||
name: parsedErrorData.data.name,
|
||||
message: parsedErrorData.data.message,
|
||||
stack: createTaskMetadataFailedErrorStack(parsedError.data),
|
||||
stderr: parsedErrorData.data.stderr,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: parsedErrorData.data.name,
|
||||
message: parsedErrorData.data.message,
|
||||
stderr: parsedErrorData.data.stderr,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
name: parsedErrorData.data.name,
|
||||
message: parsedErrorData.data.message,
|
||||
stderr: parsedErrorData.data.stderr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: parsedErrorData.data.name,
|
||||
message: parsedErrorData.data.message,
|
||||
stack: parsedErrorData.data.stack,
|
||||
stderr: parsedErrorData.data.stderr,
|
||||
};
|
||||
return prepareDeploymentError(deploymentError.data);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskMetadataFailedErrorStack(
|
||||
data: z.infer<typeof TaskMetadataFailedToParseData>
|
||||
): string {
|
||||
const stack = [];
|
||||
|
||||
const groupedIssues = groupTaskMetadataIssuesByTask(data.tasks, data.zodIssues);
|
||||
|
||||
for (const key in groupedIssues) {
|
||||
const taskWithIssues = groupedIssues[key];
|
||||
|
||||
if (!taskWithIssues) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stack.push("\n");
|
||||
stack.push(` ❯ ${taskWithIssues.exportName} in ${taskWithIssues.filePath}`);
|
||||
|
||||
for (const issue of taskWithIssues.issues) {
|
||||
if (issue.path) {
|
||||
stack.push(` x ${issue.path} ${issue.message}`);
|
||||
} else {
|
||||
stack.push(` x ${issue.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stack.join("\n");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { filterOrphanedEnvironments } from "~/utils/environmentSort";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
|
||||
type EditScheduleOptions = {
|
||||
@@ -68,9 +70,11 @@ export class EditSchedulePresenter {
|
||||
},
|
||||
});
|
||||
|
||||
const possibleEnvironments = project.environments.map((environment) => {
|
||||
return displayableEnvironment(environment, userId);
|
||||
});
|
||||
const possibleEnvironments = filterOrphanedEnvironments(project.environments).map(
|
||||
(environment) => {
|
||||
return displayableEnvironment(environment, userId);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
possibleTasks: possibleTasks.map((task) => task.slug),
|
||||
@@ -115,13 +119,16 @@ export class EditSchedulePresenter {
|
||||
return {
|
||||
...schedule,
|
||||
cron: schedule.generatorExpression,
|
||||
environments: schedule.instances.map((instance) => {
|
||||
environments: schedule.instances.flatMap((instance) => {
|
||||
const environment = possibleEnvironments.find((env) => env.id === instance.environmentId);
|
||||
if (!environment) {
|
||||
throw new Error(`Environment with id ${instance.environmentId} not found`);
|
||||
logger.error(
|
||||
`EditSchedulePresenter: environment with id ${instance.environmentId} not found`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return environment;
|
||||
return [environment];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import { sortEnvironments } from "~/utils/environmentSort";
|
||||
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";
|
||||
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
||||
|
||||
type Result = Awaited<ReturnType<EnvironmentVariablesPresenter["call"]>>;
|
||||
@@ -78,11 +78,11 @@ export class EnvironmentVariablesPresenter {
|
||||
where: {
|
||||
project: {
|
||||
slug: projectSlug,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sortedEnvironments = sortEnvironments(environments);
|
||||
const sortedEnvironments = sortEnvironments(filterOrphanedEnvironments(environments));
|
||||
|
||||
const repository = new EnvironmentVariablesRepository(this.#prismaClient);
|
||||
const variables = await repository.getProject(project.id);
|
||||
@@ -104,12 +104,12 @@ export class EnvironmentVariablesPresenter {
|
||||
}, {} as Record<string, { value: string | undefined; environment: { type: string; id: string } }>),
|
||||
};
|
||||
}),
|
||||
environments: sortedEnvironments.filter(
|
||||
(e) => e.orgMember?.userId === userId || e.orgMember === null
|
||||
).map((environment) => ({
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
})),
|
||||
environments: sortedEnvironments
|
||||
.filter((e) => e.orgMember?.userId === userId || e.orgMember === null)
|
||||
.map((environment) => ({
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
})),
|
||||
hasStaging: environments.some((environment) => environment.type === "STAGING"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Prisma, type TaskRunStatus, type TaskTriggerSource } from "@trigger.dev/database";
|
||||
import { Prisma, type TaskRunStatus } from "@trigger.dev/database";
|
||||
import parse from "parse-duration";
|
||||
import { type Direction } from "~/components/runs/RunStatuses";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { isCancellableRunStatus } from "~/v3/taskStatus";
|
||||
import { getAllTaskIdentifiers } from "~/models/task.server";
|
||||
import { isCancellableRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
export type RunListOptions = {
|
||||
@@ -97,16 +97,7 @@ export class RunListPresenter extends BasePresenter {
|
||||
});
|
||||
|
||||
//get all possible tasks
|
||||
const possibleTasksAsync = this._replica.$queryRaw<
|
||||
{
|
||||
slug: string;
|
||||
triggerSource: TaskTriggerSource;
|
||||
}[]
|
||||
>`
|
||||
SELECT DISTINCT(slug), "triggerSource"
|
||||
FROM ${sqlDatabaseSchema}."BackgroundWorkerTask"
|
||||
WHERE "projectId" = ${project.id}
|
||||
ORDER BY slug ASC;`;
|
||||
const possibleTasksAsync = getAllTaskIdentifiers(this._replica, project.id);
|
||||
|
||||
//get possible bulk actions
|
||||
const bulkActionsAsync = this._replica.bulkActionGroup.findMany({
|
||||
@@ -313,7 +304,7 @@ WHERE
|
||||
throw new Error(`Environment not found for TaskRun ${run.id}`);
|
||||
}
|
||||
|
||||
const hasFinished = FINISHED_STATUSES.includes(run.status);
|
||||
const hasFinished = isFinalRunStatus(run.status);
|
||||
|
||||
const startedAt = run.startedAt ?? run.lockedAt;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3";
|
||||
import { createTreeFromFlatItems, flattenTree } from "~/components/primitives/TreeView/TreeView";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { eventRepository } from "~/v3/eventRepository.server";
|
||||
import { isFinalRunStatus } from "~/v3/taskStatus";
|
||||
|
||||
type Result = Awaited<ReturnType<RunPresenter["call"]>>;
|
||||
export type Run = Result["run"];
|
||||
@@ -35,6 +35,8 @@ export class RunPresenter {
|
||||
spanId: true,
|
||||
friendlyId: true,
|
||||
status: true,
|
||||
completedAt: true,
|
||||
logsDeletedAt: true,
|
||||
runtimeEnvironment: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -75,7 +77,9 @@ export class RunPresenter {
|
||||
traceId: run.traceId,
|
||||
spanId: run.spanId,
|
||||
status: run.status,
|
||||
isFinished: FINISHED_STATUSES.includes(run.status),
|
||||
isFinished: isFinalRunStatus(run.status),
|
||||
completedAt: run.completedAt,
|
||||
logsDeletedAt: run.logsDeletedAt,
|
||||
environment: {
|
||||
id: run.runtimeEnvironment.id,
|
||||
organizationId: run.runtimeEnvironment.organizationId,
|
||||
@@ -134,7 +138,9 @@ export class RunPresenter {
|
||||
traceId: run.traceId,
|
||||
spanId: run.spanId,
|
||||
status: run.status,
|
||||
isFinished: FINISHED_STATUSES.includes(run.status),
|
||||
isFinished: isFinalRunStatus(run.status),
|
||||
completedAt: run.completedAt,
|
||||
logsDeletedAt: run.logsDeletedAt,
|
||||
environment: {
|
||||
id: run.runtimeEnvironment.id,
|
||||
organizationId: run.runtimeEnvironment.organizationId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Context, MachinePresetName, prettyPrintPacket } from "@trigger.dev/core/v3";
|
||||
import { FINISHED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { MachinePresetName, prettyPrintPacket, TaskRunError } from "@trigger.dev/core/v3";
|
||||
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { eventRepository } from "~/v3/eventRepository.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { machineDefinition } from "@trigger.dev/platform/v3";
|
||||
import { machinePresetFromName } from "~/v3/machinePresets.server";
|
||||
import { FINAL_ATTEMPT_STATUSES, isFinalRunStatus } from "~/v3/taskStatus";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
type Result = Awaited<ReturnType<SpanPresenter["call"]>>;
|
||||
export type Span = NonNullable<NonNullable<Result>["span"]>;
|
||||
@@ -57,6 +57,7 @@ export class SpanPresenter extends BasePresenter {
|
||||
async getRun(spanId: string) {
|
||||
const run = await this._replica.taskRun.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
traceId: true,
|
||||
//metadata
|
||||
number: true,
|
||||
@@ -81,6 +82,8 @@ export class SpanPresenter extends BasePresenter {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
queuedAt: true,
|
||||
completedAt: true,
|
||||
logsDeletedAt: true,
|
||||
//idempotency
|
||||
idempotencyKey: true,
|
||||
//delayed
|
||||
@@ -111,17 +114,6 @@ export class SpanPresenter extends BasePresenter {
|
||||
payload: true,
|
||||
payloadType: true,
|
||||
maxAttempts: true,
|
||||
//finished attempt
|
||||
attempts: {
|
||||
select: {
|
||||
output: true,
|
||||
outputType: true,
|
||||
error: true,
|
||||
},
|
||||
where: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
},
|
||||
project: {
|
||||
include: {
|
||||
organization: true,
|
||||
@@ -143,9 +135,27 @@ export class SpanPresenter extends BasePresenter {
|
||||
return;
|
||||
}
|
||||
|
||||
const finishedAttempt = run.attempts.at(0);
|
||||
const isFinished = isFinalRunStatus(run.status);
|
||||
|
||||
const finishedAttempt = isFinished
|
||||
? await this._replica.taskRunAttempt.findFirst({
|
||||
select: {
|
||||
output: true,
|
||||
outputType: true,
|
||||
error: true,
|
||||
},
|
||||
where: {
|
||||
status: { in: FINAL_ATTEMPT_STATUSES },
|
||||
taskRunId: run.id,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
const output =
|
||||
finishedAttempt === undefined
|
||||
finishedAttempt === null
|
||||
? undefined
|
||||
: finishedAttempt.outputType === "application/store"
|
||||
? `/resources/packets/${run.runtimeEnvironment.id}/${finishedAttempt.output}`
|
||||
@@ -160,6 +170,19 @@ export class SpanPresenter extends BasePresenter {
|
||||
? await prettyPrintPacket(run.payload, run.payloadType ?? undefined)
|
||||
: undefined;
|
||||
|
||||
let error: TaskRunError | undefined = undefined;
|
||||
if (finishedAttempt?.error) {
|
||||
const result = TaskRunError.safeParse(finishedAttempt.error);
|
||||
if (result.success) {
|
||||
error = result.data;
|
||||
} else {
|
||||
error = {
|
||||
type: "CUSTOM_ERROR",
|
||||
raw: JSON.stringify(finishedAttempt.error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const span = await eventRepository.getSpan(spanId, run.traceId);
|
||||
|
||||
const context = {
|
||||
@@ -213,6 +236,8 @@ export class SpanPresenter extends BasePresenter {
|
||||
updatedAt: run.updatedAt,
|
||||
delayUntil: run.delayUntil,
|
||||
expiredAt: run.expiredAt,
|
||||
completedAt: run.completedAt,
|
||||
logsDeletedAt: run.logsDeletedAt,
|
||||
ttl: run.ttl,
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
version: run.lockedToVersion?.version,
|
||||
@@ -237,14 +262,14 @@ export class SpanPresenter extends BasePresenter {
|
||||
costInCents: run.costInCents,
|
||||
totalCostInCents: run.costInCents + run.baseCostInCents,
|
||||
usageDurationMs: run.usageDurationMs,
|
||||
isFinished: FINISHED_STATUSES.includes(run.status),
|
||||
isFinished,
|
||||
isRunning: RUNNING_STATUSES.includes(run.status),
|
||||
payload,
|
||||
payloadType: run.payloadType,
|
||||
output,
|
||||
outputType: finishedAttempt?.outputType ?? "application/json",
|
||||
error,
|
||||
links: span?.links,
|
||||
events: span?.events,
|
||||
context: JSON.stringify(context, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Prisma } from "@trigger.dev/database";
|
||||
import type {
|
||||
RuntimeEnvironmentType,
|
||||
TaskTriggerSource,
|
||||
TaskRunStatus as TaskRunStatusType,
|
||||
} from "@trigger.dev/database";
|
||||
import { Prisma } from "@trigger.dev/database";
|
||||
import { QUEUED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import type { Organization } from "~/models/organization.server";
|
||||
@@ -20,6 +20,7 @@ import { logger } from "~/services/logger.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { TaskRunStatus } from "~/database-types";
|
||||
import { CURRENT_DEPLOYMENT_LABEL } from "~/consts";
|
||||
import { concurrencyTracker } from "~/v3/services/taskRunConcurrencyTracker.server";
|
||||
|
||||
export type Task = {
|
||||
slug: string;
|
||||
@@ -114,7 +115,7 @@ export class TaskListPresenter extends BasePresenter {
|
||||
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" tasks ON tasks."workerId" = workers.id
|
||||
ORDER BY slug ASC;`;
|
||||
|
||||
//group by the task identifier (task.slug). Add the latestRun and add all the environments.
|
||||
//group by the task identifier (task.slug).
|
||||
const outputTasks = tasks.reduce((acc, task) => {
|
||||
const environment = project.environments.find((env) => env.id === task.runtimeEnvironmentId);
|
||||
if (!environment) {
|
||||
@@ -251,51 +252,40 @@ export class TaskListPresenter extends BasePresenter {
|
||||
return {};
|
||||
}
|
||||
|
||||
const statuses = await this._replica.$queryRaw<
|
||||
const concurrencies = await concurrencyTracker.taskConcurrentRunCounts(projectId, tasks);
|
||||
|
||||
const queued = await this._replica.$queryRaw<
|
||||
{
|
||||
taskIdentifier: string;
|
||||
status: TaskRunStatusType;
|
||||
count: BigInt;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
tr."taskIdentifier",
|
||||
tr."status",
|
||||
tr."taskIdentifier",
|
||||
COUNT(*)
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" as tr
|
||||
WHERE
|
||||
tr."taskIdentifier" IN (${Prisma.join(tasks)})
|
||||
AND tr."projectId" = ${projectId}
|
||||
AND tr."status" IN ('PENDING', 'WAITING_FOR_DEPLOY', 'EXECUTING', 'RETRYING_AFTER_FAILURE', 'WAITING_TO_RESUME')
|
||||
AND tr."status" = ANY(ARRAY[${Prisma.join(QUEUED_STATUSES)}]::\"TaskRunStatus\"[])
|
||||
GROUP BY
|
||||
tr."taskIdentifier",
|
||||
tr."status"
|
||||
tr."taskIdentifier"
|
||||
ORDER BY
|
||||
tr."taskIdentifier" ASC,
|
||||
tr."status" ASC;`;
|
||||
tr."taskIdentifier" ASC`;
|
||||
|
||||
return statuses.reduce((acc, a) => {
|
||||
let existingTask = acc[a.taskIdentifier];
|
||||
//create an object combining the queued and concurrency counts
|
||||
const result: Record<string, { queued: number; running: number }> = {};
|
||||
for (const task of tasks) {
|
||||
const concurrency = concurrencies[task] ?? 0;
|
||||
const queuedCount = queued.find((q) => q.taskIdentifier === task)?.count ?? 0;
|
||||
|
||||
if (!existingTask) {
|
||||
existingTask = {
|
||||
queued: 0,
|
||||
running: 0,
|
||||
};
|
||||
|
||||
acc[a.taskIdentifier] = existingTask;
|
||||
}
|
||||
|
||||
if (QUEUED_STATUSES.includes(a.status)) {
|
||||
existingTask.queued += Number(a.count);
|
||||
}
|
||||
if (RUNNING_STATUSES.includes(a.status)) {
|
||||
existingTask.running += Number(a.count);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, { queued: number; running: number }>);
|
||||
result[task] = {
|
||||
queued: Number(queuedCount),
|
||||
running: concurrency,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async #getAverageDurations(tasks: string[], projectId: string) {
|
||||
|
||||
@@ -105,18 +105,18 @@ export class UsagePresenter extends BasePresenter {
|
||||
//usage by task
|
||||
const tasks = this._replica.$queryRaw<TaskUsageItem[]>`
|
||||
SELECT
|
||||
tr."taskIdentifier",
|
||||
COUNT(*) AS "runCount",
|
||||
AVG(tr."usageDurationMs") AS "averageDuration",
|
||||
SUM(tr."usageDurationMs") AS "totalDuration",
|
||||
AVG(tr."costInCents") / 100.0 AS "averageCost",
|
||||
SUM(tr."costInCents") / 100.0 AS "totalCost",
|
||||
SUM(tr."baseCostInCents") / 100.0 AS "totalBaseCost"
|
||||
tr."taskIdentifier",
|
||||
COUNT(*) AS "runCount",
|
||||
AVG(tr."usageDurationMs") AS "averageDuration",
|
||||
SUM(tr."usageDurationMs") AS "totalDuration",
|
||||
AVG(tr."costInCents") / 100.0 AS "averageCost",
|
||||
SUM(tr."costInCents") / 100.0 AS "totalCost",
|
||||
SUM(tr."baseCostInCents") / 100.0 AS "totalBaseCost"
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" tr
|
||||
JOIN ${sqlDatabaseSchema}."Project" pr ON pr.id = tr."projectId"
|
||||
JOIN ${sqlDatabaseSchema}."Organization" org ON org.id = pr."organizationId"
|
||||
JOIN ${sqlDatabaseSchema}."RuntimeEnvironment" env ON env."projectId" = pr.id
|
||||
JOIN ${sqlDatabaseSchema}."RuntimeEnvironment" env ON env."id" = tr."runtimeEnvironmentId"
|
||||
WHERE
|
||||
env.type <> 'DEVELOPMENT'
|
||||
AND tr."createdAt" > ${startOfMonth}
|
||||
@@ -132,7 +132,7 @@ export class UsagePresenter extends BasePresenter {
|
||||
averageDuration: Number(item.averageDuration),
|
||||
averageCost: Number(item.averageCost) + env.CENTS_PER_RUN / 100,
|
||||
totalDuration: Number(item.totalDuration),
|
||||
totalCost: Number(item.totalCost + item.totalBaseCost),
|
||||
totalCost: Number(item.totalCost) + Number(item.totalBaseCost),
|
||||
}))
|
||||
.sort((a, b) => b.totalCost - a.totalCost);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { clearImpersonation } from "~/models/admin.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
return clearImpersonation(request, "/admin");
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { $replica } from "~/db.server";
|
||||
import { clearImpersonation, redirectWithImpersonation } from "~/models/admin.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const user = await requireUser(request);
|
||||
if (!user.admin) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const path = params["*"];
|
||||
const organizationSlug = params.organizationSlug;
|
||||
|
||||
logger.debug("Impersonating user", { path, organizationSlug });
|
||||
|
||||
if (!organizationSlug) {
|
||||
logger.debug("Exiting impersonation mode");
|
||||
return clearImpersonation(request, "/admin");
|
||||
}
|
||||
|
||||
const org = await $replica.organization.findFirst({
|
||||
where: {
|
||||
slug: organizationSlug,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
members: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
confirmedBasicDetails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
logger.debug("Organization not found", { organizationSlug });
|
||||
return clearImpersonation(request, "/admin");
|
||||
}
|
||||
|
||||
const firstValidMember = org.members.find((m) => m.user.confirmedBasicDetails);
|
||||
|
||||
if (!firstValidMember) {
|
||||
logger.debug("No valid members found", { organizationSlug });
|
||||
return clearImpersonation(request, "/admin");
|
||||
}
|
||||
|
||||
return redirectWithImpersonation(
|
||||
request,
|
||||
firstValidMember.user.id,
|
||||
`/orgs/${organizationSlug}/${path}`
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user