From b8c55756a68779b85f695501ed7fdd644a8e7487 Mon Sep 17 00:00:00 2001 From: Nicholas Yang Date: Thu, 2 Feb 2023 13:23:22 -0500 Subject: [PATCH 1/4] chore: Removed `Cargo.lock` from CI for PRs (#3558) We remove this requirement so that PRs for just turborepo code + Cargo.lock or just turbopack code + Cargo.lock can only run the corresponding Turbopack/Turborepo workflows, thus saving time. Since the workflows are still run on main, we can catch dependency errors before shipping. --- .github/workflows/test.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6fbbc77dd40ca..da837619cd98c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,6 +58,14 @@ jobs: .github/actions/** .github/workflows/test.yml + - name: Root cargo related changes + id: cargo + uses: technote-space/get-diff-action@v6 + with: + PATTERNS: | + Cargo.* + rust-toolchain + - name: Rust related changes id: rust uses: technote-space/get-diff-action@v6 @@ -65,10 +73,10 @@ jobs: PATTERNS: | pnpm-lock.yaml package.json + Cargo.** crates/** shim/** xtask/** - Cargo.* .cargo/** rust-toolchain !**.md @@ -83,7 +91,6 @@ jobs: package.json crates/** xtask/** - Cargo.* .cargo/** rust-toolchain !crates/turborepo/** @@ -102,7 +109,6 @@ jobs: crates/turborepo/** crates/turborepo-lib/** crates/turbo-updater/** - Cargo.* .cargo/** rust-toolchain !**.md @@ -154,6 +160,8 @@ jobs: outputs: rust: ${{ steps.ci.outputs.diff != '' || steps.rust.outputs.diff != '' }} + # We only test workspace dependency changes on main, not on PRs to speed up CI + cargo_on_main: ${{ steps.ci.outputs.diff != '' || (steps.cargo.outputs.diff != '' && github.event_name == 'push' && github.ref == 'refs/heads/main') }} turbopack: ${{ steps.ci.outputs.diff != '' || steps.turbopack.outputs.diff != '' }} turborepo: ${{ steps.ci.outputs.diff != '' || steps.turborepo.outputs.diff != '' }} rust_bench: ${{ steps.ci.outputs.diff != '' || steps.rust_bench.outputs.diff != '' }} @@ -517,7 +525,8 @@ jobs: turborepo_rust_test: needs: [determine_jobs, rust_prepare] - if: needs.determine_jobs.outputs.turborepo == 'true' + # We test dependency changes only on main + if: needs.determine_jobs.outputs.turborepo == 'true' || needs.determine_jobs.outputs.cargo_on_main == 'true' strategy: fail-fast: false matrix: @@ -556,7 +565,7 @@ jobs: turbopack_rust_test: needs: [determine_jobs, rust_prepare] - if: needs.determine_jobs.outputs.turbopack == 'true' + if: needs.determine_jobs.outputs.turbopack == 'true' || needs.determine_jobs.outputs.cargo_on_main == 'true' strategy: fail-fast: false matrix: @@ -621,7 +630,7 @@ jobs: turbopack_rust_test_bench: needs: [determine_jobs, rust_prepare] - if: needs.determine_jobs.outputs.turbopack == 'true' + if: needs.determine_jobs.outputs.turbopack == 'true' || needs.determine_jobs.outputs.cargo_on_main == 'true' strategy: fail-fast: false matrix: From ccd8300133eff8186f0217ce5236031c94350f75 Mon Sep 17 00:00:00 2001 From: Mehul Kar Date: Thu, 2 Feb 2023 11:55:43 -0800 Subject: [PATCH 2/4] Capture engine/run unit tests with prysk (#3592) This converts unit tests exercising the task graph to integration tests. The purpose of this change is to make it easier to land lazy loading task definitions, which breaks all these unit tests, due to their use of the AddTask API to setup the test. Additionally, the lazy loading feature will require that these tests have access to the file system to read from config file, which would make them closer to integration tests already. Note that the new tests are here are: - not 1:1 with the old ones, but attempt to capture the scenarios in spirit. - asserted with `--graph`, because parallel execution makes it a bit more challenging to assert against logs. - combined with existing topological-deps tests --- .../task-dependencies/complex.t | 109 ++++ .../complex/app-a/package.json | 10 + .../complex/app-b/package.json | 11 + .../complex/lib-a/package.json | 10 + .../complex/lib-b/package.json | 10 + .../complex/lib-c/package.json | 6 + .../complex/lib-d/package.json | 7 + .../task-dependencies/complex/package.json | 11 + .../task-dependencies/complex/turbo.json | 30 + .../task-dependencies/overwriting.t | 41 ++ .../overwriting/package.json | 7 + .../task-dependencies/overwriting/turbo.json | 11 + .../overwriting/workspace-a/package.json | 7 + .../overwriting/workspace-b/package.json | 7 + .../setup.sh | 3 +- .../run.t => task-dependencies/topological.t} | 18 +- .../topological}/.gitignore | 0 .../topological}/apps/my-app/package.json | 0 .../topological}/package.json | 0 .../topological}/packages/util/package.json | 0 .../topological}/turbo.json | 0 .../task-dependencies/workspace-tasks.t | 54 ++ .../workspace-tasks/package.json | 10 + .../workspace-tasks/turbo.json | 22 + .../workspace-tasks/workspace-a/package.json | 9 + .../workspace-tasks/workspace-b/package.json | 6 + .../topological_deps/graph.t | 18 - cli/internal/core/engine_test.go | 595 ------------------ cli/internal/run/run_test.go | 96 --- 29 files changed, 397 insertions(+), 711 deletions(-) create mode 100644 cli/integration_tests/task-dependencies/complex.t create mode 100644 cli/integration_tests/task-dependencies/complex/app-a/package.json create mode 100644 cli/integration_tests/task-dependencies/complex/app-b/package.json create mode 100644 cli/integration_tests/task-dependencies/complex/lib-a/package.json create mode 100644 cli/integration_tests/task-dependencies/complex/lib-b/package.json create mode 100644 cli/integration_tests/task-dependencies/complex/lib-c/package.json create mode 100644 cli/integration_tests/task-dependencies/complex/lib-d/package.json create mode 100644 cli/integration_tests/task-dependencies/complex/package.json create mode 100644 cli/integration_tests/task-dependencies/complex/turbo.json create mode 100644 cli/integration_tests/task-dependencies/overwriting.t create mode 100644 cli/integration_tests/task-dependencies/overwriting/package.json create mode 100644 cli/integration_tests/task-dependencies/overwriting/turbo.json create mode 100644 cli/integration_tests/task-dependencies/overwriting/workspace-a/package.json create mode 100644 cli/integration_tests/task-dependencies/overwriting/workspace-b/package.json rename cli/integration_tests/{topological_deps => task-dependencies}/setup.sh (61%) rename cli/integration_tests/{topological_deps/run.t => task-dependencies/topological.t} (60%) rename cli/integration_tests/{topological_deps/monorepo => task-dependencies/topological}/.gitignore (100%) rename cli/integration_tests/{topological_deps/monorepo => task-dependencies/topological}/apps/my-app/package.json (100%) rename cli/integration_tests/{topological_deps/monorepo => task-dependencies/topological}/package.json (100%) rename cli/integration_tests/{topological_deps/monorepo => task-dependencies/topological}/packages/util/package.json (100%) rename cli/integration_tests/{topological_deps/monorepo => task-dependencies/topological}/turbo.json (100%) create mode 100644 cli/integration_tests/task-dependencies/workspace-tasks.t create mode 100644 cli/integration_tests/task-dependencies/workspace-tasks/package.json create mode 100644 cli/integration_tests/task-dependencies/workspace-tasks/turbo.json create mode 100644 cli/integration_tests/task-dependencies/workspace-tasks/workspace-a/package.json create mode 100644 cli/integration_tests/task-dependencies/workspace-tasks/workspace-b/package.json delete mode 100644 cli/integration_tests/topological_deps/graph.t delete mode 100644 cli/internal/core/engine_test.go delete mode 100644 cli/internal/run/run_test.go diff --git a/cli/integration_tests/task-dependencies/complex.t b/cli/integration_tests/task-dependencies/complex.t new file mode 100644 index 0000000000000..41f73c9bb487c --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex.t @@ -0,0 +1,109 @@ + +Setup + $ . ${TESTDIR}/../setup.sh + $ . ${TESTDIR}/setup.sh $(pwd) complex + +# Workspace Graph: +# app-a -> lib-a +# \ +# -> lib-b -> lib-d +# / +# app-b -> +# \ ->lib-c +# app-a depends on lib-a +# app-b depends on lib-b, lib-c +# lib-a depends on lib-b +# lib-b depends on lib-d + +We can scope the run to specific packages + $ ${TURBO} run build1 --filter=app-b --graph + + digraph { + \tcompound = "true" (esc) + \tnewrank = "true" (esc) + \tsubgraph "root" { (esc) + \t\t"[root] ___ROOT___#build1" -> "[root] ___ROOT___" (esc) + \t\t"[root] app-b#build1" -> "[root] lib-b#build1" (esc) + \t\t"[root] app-b#build1" -> "[root] lib-c#build1" (esc) + \t\t"[root] lib-b#build1" -> "[root] lib-d#build1" (esc) + \t\t"[root] lib-c#build1" -> "[root] ___ROOT___#build1" (esc) + \t\t"[root] lib-d#build1" -> "[root] ___ROOT___#build1" (esc) + \t} (esc) + } + +Can't depend on unknown tasks + $ ${TURBO} run build2 + ERROR run failed: error preparing engine: Could not find task "workspace-a#custom" in pipeline + Turbo error: error preparing engine: Could not find task "workspace-a#custom" in pipeline + [1] + +Can't depend on tasks from unknown packages + $ ${TURBO} run build3 + ERROR run failed: error preparing engine: Could not find task "unknown#custom" in pipeline + Turbo error: error preparing engine: Could not find task "unknown#custom" in pipeline + [1] + + +Complex dependency chain + $ ${TURBO} run test --graph + + digraph { + \tcompound = "true" (esc) + \tnewrank = "true" (esc) + \tsubgraph "root" { (esc) + \t\t"[root] ___ROOT___#build0" -> "[root] ___ROOT___#prepare" (esc) + \t\t"[root] ___ROOT___#prepare" -> "[root] ___ROOT___" (esc) + \t\t"[root] app-a#prepare" -> "[root] ___ROOT___" (esc) + \t\t"[root] app-a#test" -> "[root] app-a#prepare" (esc) + \t\t"[root] app-a#test" -> "[root] lib-a#build0" (esc) + \t\t"[root] app-b#prepare" -> "[root] ___ROOT___" (esc) + \t\t"[root] app-b#test" -> "[root] app-b#prepare" (esc) + \t\t"[root] app-b#test" -> "[root] lib-b#build0" (esc) + \t\t"[root] app-b#test" -> "[root] lib-c#build0" (esc) + \t\t"[root] lib-a#build0" -> "[root] lib-a#prepare" (esc) + \t\t"[root] lib-a#build0" -> "[root] lib-b#build0" (esc) + \t\t"[root] lib-a#prepare" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-a#test" -> "[root] lib-a#prepare" (esc) + \t\t"[root] lib-a#test" -> "[root] lib-b#build0" (esc) + \t\t"[root] lib-b#build0" -> "[root] lib-b#prepare" (esc) + \t\t"[root] lib-b#build0" -> "[root] lib-d#build0" (esc) + \t\t"[root] lib-b#prepare" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-b#test" -> "[root] lib-b#prepare" (esc) + \t\t"[root] lib-b#test" -> "[root] lib-d#build0" (esc) + \t\t"[root] lib-c#build0" -> "[root] ___ROOT___#build0" (esc) + \t\t"[root] lib-c#build0" -> "[root] lib-c#prepare" (esc) + \t\t"[root] lib-c#prepare" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-c#test" -> "[root] ___ROOT___#build0" (esc) + \t\t"[root] lib-c#test" -> "[root] lib-c#prepare" (esc) + \t\t"[root] lib-d#build0" -> "[root] ___ROOT___#build0" (esc) + \t\t"[root] lib-d#build0" -> "[root] lib-d#prepare" (esc) + \t\t"[root] lib-d#prepare" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-d#test" -> "[root] ___ROOT___#build0" (esc) + \t\t"[root] lib-d#test" -> "[root] lib-d#prepare" (esc) + \t} (esc) + } + + +Check that --only only runs leaf tasks + $ ${TURBO} run test --only --graph + + digraph { + \tcompound = "true" (esc) + \tnewrank = "true" (esc) + \tsubgraph "root" { (esc) + \t\t"[root] app-a#test" -> "[root] ___ROOT___" (esc) + \t\t"[root] app-b#test" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-a#test" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-b#test" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-c#test" -> "[root] ___ROOT___" (esc) + \t\t"[root] lib-d#test" -> "[root] ___ROOT___" (esc) + \t} (esc) + } + +Can't depend on itself + $ ${TURBO} run build4 + ERROR run failed: error preparing engine: Invalid task dependency graph: + .*#build4 depends on itself (re) + Turbo error: error preparing engine: Invalid task dependency graph: + .*#build4 depends on itself (re) + [1] diff --git a/cli/integration_tests/task-dependencies/complex/app-a/package.json b/cli/integration_tests/task-dependencies/complex/app-a/package.json new file mode 100644 index 0000000000000..1b1065a49f07a --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/app-a/package.json @@ -0,0 +1,10 @@ +{ + "name": "app-a", + "scripts": { + "build": "echo 'build app-a'", + "test": "echo 'test app-a'" + }, + "dependencies": { + "lib-a": "*" + } +} diff --git a/cli/integration_tests/task-dependencies/complex/app-b/package.json b/cli/integration_tests/task-dependencies/complex/app-b/package.json new file mode 100644 index 0000000000000..5b28eec0f17f7 --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/app-b/package.json @@ -0,0 +1,11 @@ +{ + "name": "app-b", + "scripts": { + "build": "echo 'build app-b'", + "test": "echo 'test app-b'" + }, + "dependencies": { + "lib-b": "*", + "lib-c": "*" + } +} diff --git a/cli/integration_tests/task-dependencies/complex/lib-a/package.json b/cli/integration_tests/task-dependencies/complex/lib-a/package.json new file mode 100644 index 0000000000000..7507e85ce1b0b --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/lib-a/package.json @@ -0,0 +1,10 @@ +{ + "name": "lib-a", + "scripts": { + "build": "echo 'build lib-a'", + "test": "echo 'test lib-a'" + }, + "dependencies": { + "lib-b": "*" + } +} diff --git a/cli/integration_tests/task-dependencies/complex/lib-b/package.json b/cli/integration_tests/task-dependencies/complex/lib-b/package.json new file mode 100644 index 0000000000000..5288e96887e92 --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/lib-b/package.json @@ -0,0 +1,10 @@ +{ + "name": "lib-b", + "scripts": { + "build": "echo 'build lib-b'", + "test": "echo 'test lib-b'" + }, + "dependencies": { + "lib-d": "*" + } +} diff --git a/cli/integration_tests/task-dependencies/complex/lib-c/package.json b/cli/integration_tests/task-dependencies/complex/lib-c/package.json new file mode 100644 index 0000000000000..d52930425c5d3 --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/lib-c/package.json @@ -0,0 +1,6 @@ +{ + "name": "lib-c", + "scripts": { + "build": "echo 'build lib-c'" + } +} diff --git a/cli/integration_tests/task-dependencies/complex/lib-d/package.json b/cli/integration_tests/task-dependencies/complex/lib-d/package.json new file mode 100644 index 0000000000000..94d86aa0d4a5b --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/lib-d/package.json @@ -0,0 +1,7 @@ +{ + "name": "lib-d", + "scripts": { + "build": "echo 'build lib-d'", + "test": "echo 'test lib-d'" + } +} diff --git a/cli/integration_tests/task-dependencies/complex/package.json b/cli/integration_tests/task-dependencies/complex/package.json new file mode 100644 index 0000000000000..ea0a7089dc497 --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/package.json @@ -0,0 +1,11 @@ +{ + "name": "unknown-dependency", + "workspaces": [ + "app-a", + "app-b", + "lib-a", + "lib-b", + "lib-c", + "lib-d" + ] +} diff --git a/cli/integration_tests/task-dependencies/complex/turbo.json b/cli/integration_tests/task-dependencies/complex/turbo.json new file mode 100644 index 0000000000000..3c4147e684a0d --- /dev/null +++ b/cli/integration_tests/task-dependencies/complex/turbo.json @@ -0,0 +1,30 @@ +{ + "pipeline": { + "build0": { + "dependsOn": ["^build0", "prepare"] + }, + "test": { + "dependsOn": ["^build0", "prepare"] + }, + "prepare": {}, + "side-quest": { + "dependsOn": ["prepare"] + }, + + "build1": { + "dependsOn": ["^build1"] + }, + + "build2": { + "dependsOn": ["workspace-a#custom"] + }, + + "build3": { + "dependsOn": ["unknown#custom"] + }, + + "build4": { + "dependsOn": ["build4"] + } + } +} diff --git a/cli/integration_tests/task-dependencies/overwriting.t b/cli/integration_tests/task-dependencies/overwriting.t new file mode 100644 index 0000000000000..f4cabdb95b361 --- /dev/null +++ b/cli/integration_tests/task-dependencies/overwriting.t @@ -0,0 +1,41 @@ + +Setup + $ . ${TESTDIR}/../setup.sh + $ . ${TESTDIR}/setup.sh $(pwd) overwriting + +Test + $ ${TURBO} run build > tmp.log + $ cat tmp.log | grep "Packages in scope" -A2 + \xe2\x80\xa2 Packages in scope: workspace-a, workspace-b (esc) + \xe2\x80\xa2 Running build in 2 packages (esc) + \xe2\x80\xa2 Remote caching disabled (esc) + +# workspace-a#generate ran + $ cat tmp.log | grep "workspace-a:generate" + workspace-a:generate: cache miss, executing 38a94e8f72e48200 + workspace-a:generate: + workspace-a:generate: > generate + workspace-a:generate: > echo 'generate workspace-a' + workspace-a:generate: + workspace-a:generate: generate workspace-a +workspace-a#build ran + $ cat tmp.log | grep "workspace-a:build" + workspace-a:build: cache miss, executing cdf4f35b3e5ff342 + workspace-a:build: + workspace-a:build: > build + workspace-a:build: > echo 'build workspace-a' + workspace-a:build: + workspace-a:build: build workspace-a + +workspace-b#generate DID NOT run + $ cat tmp.log | grep "workspace-b:generate" + [1] + +workspace-b#build ran + $ cat tmp.log | grep "workspace-b:build" + workspace-b:build: cache miss, executing 35d38d74bf7b33db + workspace-b:build: + workspace-b:build: > build + workspace-b:build: > echo 'build workspace-b' + workspace-b:build: + workspace-b:build: build workspace-b diff --git a/cli/integration_tests/task-dependencies/overwriting/package.json b/cli/integration_tests/task-dependencies/overwriting/package.json new file mode 100644 index 0000000000000..015e98a7c5d6d --- /dev/null +++ b/cli/integration_tests/task-dependencies/overwriting/package.json @@ -0,0 +1,7 @@ +{ + "name": "overwriting", + "workspaces": [ + "workspace-a", + "workspace-b" + ] +} diff --git a/cli/integration_tests/task-dependencies/overwriting/turbo.json b/cli/integration_tests/task-dependencies/overwriting/turbo.json new file mode 100644 index 0000000000000..d16b6d0cb2a57 --- /dev/null +++ b/cli/integration_tests/task-dependencies/overwriting/turbo.json @@ -0,0 +1,11 @@ +{ + "pipeline": { + "build": { + "dependsOn": ["generate"] + }, + "generate": {}, + "workspace-b#build": { + "dependsOn": [] + } + } +} diff --git a/cli/integration_tests/task-dependencies/overwriting/workspace-a/package.json b/cli/integration_tests/task-dependencies/overwriting/workspace-a/package.json new file mode 100644 index 0000000000000..83a75937b0dc7 --- /dev/null +++ b/cli/integration_tests/task-dependencies/overwriting/workspace-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "workspace-a", + "scripts": { + "build": "echo 'build workspace-a'", + "generate": "echo 'generate workspace-a'" + } +} diff --git a/cli/integration_tests/task-dependencies/overwriting/workspace-b/package.json b/cli/integration_tests/task-dependencies/overwriting/workspace-b/package.json new file mode 100644 index 0000000000000..594de49c21e1f --- /dev/null +++ b/cli/integration_tests/task-dependencies/overwriting/workspace-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "workspace-b", + "scripts": { + "build": "echo 'build workspace-b'", + "generate": "echo 'generate workspace-b'" + } +} diff --git a/cli/integration_tests/topological_deps/setup.sh b/cli/integration_tests/task-dependencies/setup.sh similarity index 61% rename from cli/integration_tests/topological_deps/setup.sh rename to cli/integration_tests/task-dependencies/setup.sh index 864b7a64a37a6..f18ac540914a3 100644 --- a/cli/integration_tests/topological_deps/setup.sh +++ b/cli/integration_tests/task-dependencies/setup.sh @@ -2,5 +2,6 @@ SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]}) TARGET_DIR=$1 -cp -a ${SCRIPT_DIR}/monorepo/. ${TARGET_DIR}/ +MONOREPO_DIR=$2 +cp -a ${SCRIPT_DIR}/${MONOREPO_DIR}/. ${TARGET_DIR}/ ${SCRIPT_DIR}/../setup_git.sh ${TARGET_DIR} diff --git a/cli/integration_tests/topological_deps/run.t b/cli/integration_tests/task-dependencies/topological.t similarity index 60% rename from cli/integration_tests/topological_deps/run.t rename to cli/integration_tests/task-dependencies/topological.t index 0015b6477cf8d..b9ca96337f136 100644 --- a/cli/integration_tests/topological_deps/run.t +++ b/cli/integration_tests/task-dependencies/topological.t @@ -1,6 +1,6 @@ Setup $ . ${TESTDIR}/../setup.sh - $ . ${TESTDIR}/setup.sh $(pwd) + $ . ${TESTDIR}/setup.sh $(pwd) topological Check my-app#build output $ ${TURBO} run build @@ -24,3 +24,19 @@ Check my-app#build output Cached: 0 cached, 2 total Time:\s*[\.0-9]+m?s (re) + + +Graph + $ ${TURBO} run build --graph + + digraph { + \tcompound = "true" (esc) + \tnewrank = "true" (esc) + \tsubgraph "root" { (esc) + \t\t"[root] //#build" -> "[root] ___ROOT___" (esc) + \t\t"[root] ___ROOT___#build" -> "[root] ___ROOT___" (esc) + \t\t"[root] my-app#build" -> "[root] util#build" (esc) + \t\t"[root] util#build" -> "[root] ___ROOT___#build" (esc) + \t} (esc) + } + diff --git a/cli/integration_tests/topological_deps/monorepo/.gitignore b/cli/integration_tests/task-dependencies/topological/.gitignore similarity index 100% rename from cli/integration_tests/topological_deps/monorepo/.gitignore rename to cli/integration_tests/task-dependencies/topological/.gitignore diff --git a/cli/integration_tests/topological_deps/monorepo/apps/my-app/package.json b/cli/integration_tests/task-dependencies/topological/apps/my-app/package.json similarity index 100% rename from cli/integration_tests/topological_deps/monorepo/apps/my-app/package.json rename to cli/integration_tests/task-dependencies/topological/apps/my-app/package.json diff --git a/cli/integration_tests/topological_deps/monorepo/package.json b/cli/integration_tests/task-dependencies/topological/package.json similarity index 100% rename from cli/integration_tests/topological_deps/monorepo/package.json rename to cli/integration_tests/task-dependencies/topological/package.json diff --git a/cli/integration_tests/topological_deps/monorepo/packages/util/package.json b/cli/integration_tests/task-dependencies/topological/packages/util/package.json similarity index 100% rename from cli/integration_tests/topological_deps/monorepo/packages/util/package.json rename to cli/integration_tests/task-dependencies/topological/packages/util/package.json diff --git a/cli/integration_tests/topological_deps/monorepo/turbo.json b/cli/integration_tests/task-dependencies/topological/turbo.json similarity index 100% rename from cli/integration_tests/topological_deps/monorepo/turbo.json rename to cli/integration_tests/task-dependencies/topological/turbo.json diff --git a/cli/integration_tests/task-dependencies/workspace-tasks.t b/cli/integration_tests/task-dependencies/workspace-tasks.t new file mode 100644 index 0000000000000..feb22346f5698 --- /dev/null +++ b/cli/integration_tests/task-dependencies/workspace-tasks.t @@ -0,0 +1,54 @@ + +Setup + $ . ${TESTDIR}/../setup.sh + $ . ${TESTDIR}/setup.sh $(pwd) workspace-tasks + +Test that root tasks are included in the graph. In this case, "//#build" task should be there + $ ${TURBO} run build1 --graph + + digraph { + \tcompound = "true" (esc) + \tnewrank = "true" (esc) + \tsubgraph "root" { (esc) + \t\t"[root] //#build1" -> "[root] ___ROOT___" (esc) + \t\t"[root] workspace-a#build1" -> "[root] ___ROOT___" (esc) + \t\t"[root] workspace-b#build1" -> "[root] ___ROOT___" (esc) + \t} (esc) + } + + +Can depend on root tasks + $ ${TURBO} run build2 --graph + + digraph { + \tcompound = "true" (esc) + \tnewrank = "true" (esc) + \tsubgraph "root" { (esc) + \t\t"[root] //#exists" -> "[root] ___ROOT___" (esc) + \t\t"[root] ___ROOT___#build2" -> "[root] //#exists" (esc) + \t\t"[root] workspace-a#build2" -> "[root] //#exists" (esc) + \t\t"[root] workspace-a#build2" -> "[root] workspace-b#build2" (esc) + \t\t"[root] workspace-b#build2" -> "[root] //#exists" (esc) + \t\t"[root] workspace-b#build2" -> "[root] ___ROOT___#build2" (esc) + \t} (esc) + } + +Can't depend on a missing root task + $ ${TURBO} run build3 --graph + ERROR run failed: error preparing engine: //#not-exists needs an entry in turbo.json before it can be depended on because it is a task run from the root package + Turbo error: error preparing engine: //#not-exists needs an entry in turbo.json before it can be depended on because it is a task run from the root package + [1] + +Package tasks can depend on things + $ ${TURBO} run special --graph + + digraph { + \tcompound = "true" (esc) + \tnewrank = "true" (esc) + \tsubgraph "root" { (esc) + \t\t"[root] ___ROOT___#build4" -> "[root] ___ROOT___" (esc) + \t\t"[root] workspace-a#special" -> "[root] workspace-b#build4" (esc) + \t\t"[root] workspace-b#build4" -> "[root] ___ROOT___#build4" (esc) + \t} (esc) + } + diff --git a/cli/integration_tests/task-dependencies/workspace-tasks/package.json b/cli/integration_tests/task-dependencies/workspace-tasks/package.json new file mode 100644 index 0000000000000..3951359aed3bd --- /dev/null +++ b/cli/integration_tests/task-dependencies/workspace-tasks/package.json @@ -0,0 +1,10 @@ +{ + "name": "root-tasks", + "scripts": { + "test": "echo 'root test'" + }, + "workspaces": [ + "workspace-a", + "workspace-b" + ] +} diff --git a/cli/integration_tests/task-dependencies/workspace-tasks/turbo.json b/cli/integration_tests/task-dependencies/workspace-tasks/turbo.json new file mode 100644 index 0000000000000..fe53b0720a930 --- /dev/null +++ b/cli/integration_tests/task-dependencies/workspace-tasks/turbo.json @@ -0,0 +1,22 @@ +{ + "pipeline": { + "build1": {}, + "//#build1": {}, + + "build2": { + "dependsOn": ["^build2", "//#exists"] + }, + "//#exists": {}, + + "build3": { + "dependsOn": ["//#not-exists"] + }, + + "build4": { + "dependsOn": ["^build4"] + }, + "workspace-a#special": { + "dependsOn": ["^build4"] + } + } +} diff --git a/cli/integration_tests/task-dependencies/workspace-tasks/workspace-a/package.json b/cli/integration_tests/task-dependencies/workspace-tasks/workspace-a/package.json new file mode 100644 index 0000000000000..306e7487cb6c1 --- /dev/null +++ b/cli/integration_tests/task-dependencies/workspace-tasks/workspace-a/package.json @@ -0,0 +1,9 @@ +{ + "name": "workspace-a", + "scripts": { + "build": "echo 'build workspace-a'" + }, + "dependencies": { + "workspace-b": "*" + } +} diff --git a/cli/integration_tests/task-dependencies/workspace-tasks/workspace-b/package.json b/cli/integration_tests/task-dependencies/workspace-tasks/workspace-b/package.json new file mode 100644 index 0000000000000..558ea00a54201 --- /dev/null +++ b/cli/integration_tests/task-dependencies/workspace-tasks/workspace-b/package.json @@ -0,0 +1,6 @@ +{ + "name": "workspace-b", + "scripts": { + "build": "echo 'build workspace-b'" + } +} diff --git a/cli/integration_tests/topological_deps/graph.t b/cli/integration_tests/topological_deps/graph.t deleted file mode 100644 index b757c4f2bb177..0000000000000 --- a/cli/integration_tests/topological_deps/graph.t +++ /dev/null @@ -1,18 +0,0 @@ -Setup - $ . ${TESTDIR}/../setup.sh - $ . ${TESTDIR}/setup.sh $(pwd) - -Graph - $ ${TURBO} run build --graph - - digraph { - \tcompound = "true" (esc) - \tnewrank = "true" (esc) - \tsubgraph "root" { (esc) - \t\t"[root] //#build" -> "[root] ___ROOT___" (esc) - \t\t"[root] ___ROOT___#build" -> "[root] ___ROOT___" (esc) - \t\t"[root] my-app#build" -> "[root] util#build" (esc) - \t\t"[root] util#build" -> "[root] ___ROOT___#build" (esc) - \t} (esc) - } - diff --git a/cli/internal/core/engine_test.go b/cli/internal/core/engine_test.go deleted file mode 100644 index a7bff2293bcee..0000000000000 --- a/cli/internal/core/engine_test.go +++ /dev/null @@ -1,595 +0,0 @@ -package core - -import ( - "fmt" - "strings" - "testing" - - "github.com/vercel/turbo/cli/internal/fs" - "github.com/vercel/turbo/cli/internal/graph" - "github.com/vercel/turbo/cli/internal/util" - "gotest.tools/v3/assert" - - "github.com/pyr-sh/dag" -) - -func testVisitor(taskID string) error { - fmt.Println(taskID) - return nil -} - -func TestEngineDefault(t *testing.T) { - var workspaceGraph dag.AcyclicGraph - workspaceGraph.Add("a") - workspaceGraph.Add("b") - workspaceGraph.Add("c") - workspaceGraph.Connect(dag.BasicEdge("c", "b")) - workspaceGraph.Connect(dag.BasicEdge("c", "a")) - - buildTask := fs.TaskDefinition{ - TopologicalDependencies: []string{"build"}, - TaskDependencies: []string{"prepare"}, - } - - testTask := fs.TaskDefinition{ - TopologicalDependencies: []string{"build"}, - TaskDependencies: []string{"prepare"}, - } - - prepareTask := fs.TaskDefinition{} - sideQuestTask := fs.TaskDefinition{TaskDependencies: []string{"prepare"}} - - pipeline := map[string]fs.TaskDefinition{ - "build": buildTask, - "test": testTask, - "prepare": prepareTask, - "side-quest": sideQuestTask, - } - - p := NewEngine(&graph.CompleteGraph{ - WorkspaceGraph: workspaceGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - WorkspaceInfos: graph.WorkspaceInfos{ - "a": &fs.PackageJSON{}, - "b": &fs.PackageJSON{}, - "c": &fs.PackageJSON{}, - }, - }) - - p.AddTask(&Task{Name: "build", TaskDefinition: buildTask}) - p.AddTask(&Task{Name: "test", TaskDefinition: testTask}) - p.AddTask(&Task{Name: "prepare"}) - p.AddTask(&Task{Name: "side-quest", TaskDefinition: sideQuestTask}) // not in the build/test tree - - if _, ok := p.Tasks["build"]; !ok { - t.Fatal("AddTask is not adding tasks (build)") - } - - if _, ok := p.Tasks["test"]; !ok { - t.Fatal("AddTask is not adding tasks (test)") - } - - err := p.Prepare(&EngineBuildingOptions{ - Packages: []string{"a", "b", "c"}, - TaskNames: []string{"test"}, - TasksOnly: false, - }) - - if err != nil { - t.Fatalf("%v", err) - } - - errs := p.Execute(testVisitor, EngineExecutionOptions{ - Concurrency: 10, - }) - - for _, err := range errs { - t.Fatalf("%v", err) - } - - actual := strings.TrimSpace(p.TaskGraph.String()) - expected := strings.TrimSpace(leafStringAll) - if actual != expected { - t.Fatalf("bad: \n\nactual---\n%s\n\n expected---\n%s", actual, expected) - } -} - -func TestUnknownDependency(t *testing.T) { - g := dag.AcyclicGraph{} - g.Add("a") - g.Add("b") - g.Add("c") - graph := graph.CompleteGraph{WorkspaceGraph: g} - p := NewEngine(&graph) - - err := p.AddDep("unknown#custom", "build") - if err == nil { - t.Error("expected error for unknown package, got nil") - } - err = p.AddDep("a#custom", "build") - if err != nil { - t.Errorf("expected no error for package task with known package, got %v", err) - } -} - -func TestDependenciesOnUnspecifiedPackages(t *testing.T) { - // app1 -> libA - // \ - // > libB -> libD - // / - // app2 < - // \ libC - // - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app1") - workspaceGraph.Add("app2") - workspaceGraph.Add("libA") - workspaceGraph.Add("libB") - workspaceGraph.Add("libC") - workspaceGraph.Add("libD") - workspaceGraph.Connect(dag.BasicEdge("libA", "libB")) - workspaceGraph.Connect(dag.BasicEdge("libB", "libD")) - workspaceGraph.Connect(dag.BasicEdge("app0", "libA")) - workspaceGraph.Connect(dag.BasicEdge("app1", "libA")) - workspaceGraph.Connect(dag.BasicEdge("app2", "libB")) - workspaceGraph.Connect(dag.BasicEdge("app2", "libC")) - - buildTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - testTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - - pipeline := fs.Pipeline{"build": buildTask, "test": testTask} - - p := NewEngine(&graph.CompleteGraph{ - WorkspaceGraph: workspaceGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - WorkspaceInfos: graph.WorkspaceInfos{ - "app1": &fs.PackageJSON{}, - "app2": &fs.PackageJSON{}, - "libA": &fs.PackageJSON{}, - "libB": &fs.PackageJSON{}, - "libC": &fs.PackageJSON{}, - "libD": &fs.PackageJSON{}, - }, - }) - - p.AddTask(&Task{Name: "build", TaskDefinition: buildTask}) - p.AddTask(&Task{Name: "test", TaskDefinition: testTask}) - - // We're only requesting one package ("scope"), - // but the combination of that package and task causes - // dependencies to also get run. This is the equivalent of - // turbo run test --filter=app2 - err := p.Prepare(&EngineBuildingOptions{ - Packages: []string{"app2"}, - TaskNames: []string{"test"}, - }) - if err != nil { - t.Fatalf("failed to prepare engine: %v", err) - } - errs := p.Execute(testVisitor, EngineExecutionOptions{ - Concurrency: 10, - }) - for _, err := range errs { - t.Fatalf("error executing tasks: %v", err) - } - expected := ` -___ROOT___ -app2#test - libB#build - libC#build -libB#build - libD#build -libC#build - ___ROOT___ -libD#build - ___ROOT___ -` - expected = strings.TrimSpace(expected) - actual := strings.TrimSpace(p.TaskGraph.String()) - if actual != expected { - t.Errorf("task graph got:\n%v\nwant:\n%v", actual, expected) - } -} - -func TestRunPackageTask(t *testing.T) { - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app1") - workspaceGraph.Add("libA") - workspaceGraph.Connect(dag.BasicEdge("app1", "libA")) - - buildTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - specialTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - pipeline := fs.Pipeline{ - "build": buildTask, - "app1#special": specialTask, - } - - p := NewEngine(&graph.CompleteGraph{ - WorkspaceGraph: workspaceGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - WorkspaceInfos: graph.WorkspaceInfos{ - "app1": &fs.PackageJSON{}, - "libA": &fs.PackageJSON{}, - }, - }) - - p.AddTask(&Task{ - Name: "app1#special", - TaskDefinition: specialTask, - }) - p.AddTask(&Task{ - Name: "build", - TaskDefinition: buildTask, - }) - // equivalent to "turbo run special", without an entry for - // "special" in turbo.json. Only "app1#special" is defined. - err := p.Prepare(&EngineBuildingOptions{ - Packages: []string{"app1", "libA"}, - TaskNames: []string{"special"}, - }) - assert.NilError(t, err, "Prepare") - errs := p.Execute(testVisitor, EngineExecutionOptions{ - Concurrency: 10, - }) - for _, err := range errs { - assert.NilError(t, err, "Execute") - } - actual := strings.TrimSpace(p.TaskGraph.String()) - expected := strings.TrimSpace(` -___ROOT___ -app1#special - libA#build -libA#build - ___ROOT___`) - assert.Equal(t, expected, actual) -} - -func TestRunWithNoTasksFound(t *testing.T) { - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app") - workspaceGraph.Add("lib") - workspaceGraph.Connect(dag.BasicEdge("app", "lib")) - - p := NewEngine(&graph.CompleteGraph{WorkspaceGraph: workspaceGraph}) - - err := p.Prepare(&EngineBuildingOptions{ - Packages: []string{"app", "lib"}, - TaskNames: []string{"build"}, - }) - // should not fail because we have no tasks in the engine - assert.NilError(t, err, "Prepare") -} - -func TestIncludeRootTasks(t *testing.T) { - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app1") - workspaceGraph.Add("libA") - workspaceGraph.Connect(dag.BasicEdge("app1", "libA")) - - buildTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - testTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - rootTestTask := fs.TaskDefinition{} - pipeline := fs.Pipeline{ - "build": buildTask, - "test": testTask, - "//#test": rootTestTask, - } - p := NewEngine(&graph.CompleteGraph{ - WorkspaceGraph: workspaceGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - WorkspaceInfos: graph.WorkspaceInfos{ - util.RootPkgName: &fs.PackageJSON{}, - "app1": &fs.PackageJSON{}, - "libA": &fs.PackageJSON{}, - }, - }) - - p.AddTask(&Task{Name: "build", TaskDefinition: buildTask}) - p.AddTask(&Task{Name: "test", TaskDefinition: testTask}) - p.AddTask(&Task{Name: util.RootTaskID("test")}) - - err := p.Prepare(&EngineBuildingOptions{ - Packages: []string{util.RootPkgName, "app1", "libA"}, - TaskNames: []string{"build", "test"}, - }) - if err != nil { - t.Fatalf("failed to prepare engine: %v", err) - } - errs := p.Execute(testVisitor, EngineExecutionOptions{ - Concurrency: 10, - }) - for _, err := range errs { - t.Fatalf("error executing tasks: %v", err) - } - actual := strings.TrimSpace(p.TaskGraph.String()) - expected := fmt.Sprintf(` -%v#test - ___ROOT___ -___ROOT___ -app1#build - libA#build -app1#test - libA#build -libA#build - ___ROOT___ -libA#test - ___ROOT___ -`, util.RootPkgName) - expected = strings.TrimSpace(expected) - if actual != expected { - t.Errorf("task graph got:\n%v\nwant:\n%v", actual, expected) - } -} - -func TestDependOnRootTask(t *testing.T) { - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app1") - workspaceGraph.Add("libA") - workspaceGraph.Connect(dag.BasicEdge("app1", "libA")) - - buildTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - rootTask := fs.TaskDefinition{} - - pipeline := fs.Pipeline{ - "build": buildTask, - "//#root-task": rootTask, - } - - p := NewEngine(&graph.CompleteGraph{ - WorkspaceGraph: workspaceGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - WorkspaceInfos: graph.WorkspaceInfos{ - util.RootPkgName: &fs.PackageJSON{}, - "app1": &fs.PackageJSON{}, - "libA": &fs.PackageJSON{}, - }, - }) - - p.AddTask(&Task{Name: "build", TaskDefinition: buildTask}) - p.AddTask(&Task{Name: "//#root-task"}) - err := p.AddDep("//#root-task", "libA#build") - assert.NilError(t, err, "AddDep") - - err = p.Prepare(&EngineBuildingOptions{ - Packages: []string{"app1"}, - TaskNames: []string{"build"}, - }) - assert.NilError(t, err, "Prepare") - errs := p.Execute(testVisitor, EngineExecutionOptions{ - Concurrency: 10, - }) - for _, err := range errs { - assert.NilError(t, err, "Execute") - } - actual := strings.TrimSpace(p.TaskGraph.String()) - expected := fmt.Sprintf(`%v#root-task - ___ROOT___ -___ROOT___ -app1#build - libA#build -libA#build - %v#root-task`, util.RootPkgName, util.RootPkgName) - assert.Equal(t, expected, actual) -} - -func TestDependOnMissingRootTask(t *testing.T) { - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app1") - workspaceGraph.Add("libA") - workspaceGraph.Connect(dag.BasicEdge("app1", "libA")) - - p := NewEngine(&graph.CompleteGraph{WorkspaceGraph: workspaceGraph}) - - p.AddTask(&Task{ - Name: "build", - TaskDefinition: fs.TaskDefinition{TopologicalDependencies: []string{"build"}}, - }) - err := p.AddDep("//#root-task", "libA#build") - assert.NilError(t, err, "AddDep") - - err = p.Prepare(&EngineBuildingOptions{ - Packages: []string{"app1"}, - TaskNames: []string{"build"}, - }) - if err == nil { - t.Error("expected an error depending on non-existent root task") - } -} - -func TestDependOnMultiplePackageTasks(t *testing.T) { - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app1") - workspaceGraph.Add("libA") - workspaceGraph.Connect(dag.BasicEdge("app1", "libA")) - - buildTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - compileTask := fs.TaskDefinition{TopologicalDependencies: []string{"build"}} - - pipeline := fs.Pipeline{ - "build": buildTask, - "compile": compileTask, - } - p := NewEngine(&graph.CompleteGraph{ - WorkspaceGraph: workspaceGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - WorkspaceInfos: graph.WorkspaceInfos{ - "app1": &fs.PackageJSON{}, - "libA": &fs.PackageJSON{}, - }, - }) - - p.AddTask(&Task{Name: "build", TaskDefinition: buildTask}) - p.AddTask(&Task{Name: "compile", TaskDefinition: compileTask}) - - err := p.AddDep("app1#build", "libA#build") - assert.NilError(t, err, "AddDep") - - err = p.AddDep("app1#compile", "libA#build") - assert.NilError(t, err, "AddDep") - - err = p.Prepare(&EngineBuildingOptions{ - Packages: []string{"app1"}, - TaskNames: []string{"build"}, - }) - assert.NilError(t, err, "Prepare") - - actual := strings.TrimSpace(p.TaskGraph.String()) - expected := strings.TrimSpace(` -app1#build - libA#build -app1#compile - libA#build -libA#build - app1#build - app1#compile`) - expected = strings.TrimSpace(expected) - if actual != expected { - t.Errorf("task graph got:\n%v\nwant:\n%v", actual, expected) - } -} - -func TestDependOnUnenabledRootTask(t *testing.T) { - workspaceGraph := dag.AcyclicGraph{} - workspaceGraph.Add("app1") - workspaceGraph.Add("libA") - workspaceGraph.Connect(dag.BasicEdge("app1", "libA")) - - p := NewEngine(&graph.CompleteGraph{WorkspaceGraph: workspaceGraph}) - - p.AddTask(&Task{ - Name: "build", - TaskDefinition: fs.TaskDefinition{TopologicalDependencies: []string{"build"}}, - }) - p.AddTask(&Task{ - Name: "foo", - }) - err := p.AddDep("//#foo", "libA#build") - assert.NilError(t, err, "AddDep") - - err = p.Prepare(&EngineBuildingOptions{ - Packages: []string{"app1"}, - TaskNames: []string{"build"}, - }) - if err == nil { - t.Error("expected an error depending on un-enabled root task") - } -} - -func TestEngineTasksOnly(t *testing.T) { - var workspaceGraph dag.AcyclicGraph - workspaceGraph.Add("a") - workspaceGraph.Add("b") - workspaceGraph.Add("c") - workspaceGraph.Connect(dag.BasicEdge("c", "b")) - workspaceGraph.Connect(dag.BasicEdge("c", "a")) - - buildTask := fs.TaskDefinition{ - TopologicalDependencies: []string{"build"}, - TaskDependencies: []string{"prepare"}, - } - testTask := fs.TaskDefinition{ - TopologicalDependencies: []string{"build"}, - TaskDependencies: []string{"prepare"}, - } - prepareTask := fs.TaskDefinition{} - - pipeline := map[string]fs.TaskDefinition{ - "build": buildTask, - "test": testTask, - "prepare": prepareTask, - } - - p := NewEngine(&graph.CompleteGraph{ - WorkspaceGraph: workspaceGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - WorkspaceInfos: graph.WorkspaceInfos{ - "a": &fs.PackageJSON{}, - "b": &fs.PackageJSON{}, - "c": &fs.PackageJSON{}, - }, - }) - - p.AddTask(&Task{ - Name: "build", - TaskDefinition: buildTask, - }) - p.AddTask(&Task{ - Name: "test", - TaskDefinition: testTask, - }) - p.AddTask(&Task{ - Name: "prepare", - TaskDefinition: prepareTask, - }) - - if _, ok := p.Tasks["build"]; !ok { - t.Fatal("AddTask is not adding tasks (build)") - } - - if _, ok := p.Tasks["test"]; !ok { - t.Fatal("AddTask is not adding tasks (test)") - } - - err := p.Prepare(&EngineBuildingOptions{ - Packages: []string{"a", "b", "c"}, - TaskNames: []string{"test"}, - TasksOnly: true, - }) - - if err != nil { - t.Fatalf("%v", err) - } - - errs := p.Execute(testVisitor, EngineExecutionOptions{ - Concurrency: 10, - }) - - for _, err := range errs { - t.Fatalf("%v", err) - } - - actual := strings.TrimSpace(p.TaskGraph.String()) - expected := strings.TrimSpace(leafStringOnly) - if actual != expected { - t.Fatalf("bad: \n\nactual---\n%s\n\n expected---\n%s", actual, expected) - } -} - -const leafStringAll = ` -___ROOT___ -a#build - a#prepare -a#prepare - ___ROOT___ -a#test - a#prepare -b#build - b#prepare -b#prepare - ___ROOT___ -b#test - b#prepare -c#prepare - ___ROOT___ -c#test - a#build - b#build - c#prepare -` - -const leafStringOnly = ` -___ROOT___ -a#test - ___ROOT___ -b#test - ___ROOT___ -c#test - ___ROOT___ -` diff --git a/cli/internal/run/run_test.go b/cli/internal/run/run_test.go deleted file mode 100644 index 28700d231c0d4..0000000000000 --- a/cli/internal/run/run_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package run - -import ( - "testing" - - "github.com/pyr-sh/dag" - "github.com/vercel/turbo/cli/internal/fs" - "github.com/vercel/turbo/cli/internal/graph" - "github.com/vercel/turbo/cli/internal/util" -) - -func Test_dontSquashTasks(t *testing.T) { - topoGraph := &dag.AcyclicGraph{} - topoGraph.Add("a") - topoGraph.Add("b") - // no dependencies between packages - - pipeline := map[string]fs.TaskDefinition{ - "build": { - Outputs: fs.TaskOutputs{}, - TaskDependencies: []string{"generate"}, - }, - "generate": { - Outputs: fs.TaskOutputs{Inclusions: []string{}, Exclusions: []string{}}, - }, - "b#build": { - Outputs: fs.TaskOutputs{Inclusions: []string{}, Exclusions: []string{}}, - }, - } - filteredPkgs := make(util.Set) - filteredPkgs.Add("a") - filteredPkgs.Add("b") - rs := &runSpec{ - FilteredPkgs: filteredPkgs, - Targets: []string{"build"}, - Opts: &Opts{}, - } - - workspaceInfos := make(graph.WorkspaceInfos) - workspaceInfos["a"] = &fs.PackageJSON{ - Name: "a", - Scripts: map[string]string{}, - } - - completeGraph := &graph.CompleteGraph{ - WorkspaceGraph: *topoGraph, - Pipeline: pipeline, - WorkspaceInfos: workspaceInfos, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - } - - engine, err := buildTaskGraphEngine(completeGraph, rs) - if err != nil { - t.Fatalf("failed to build task graph: %v", err) - } - toRun := engine.TaskGraph.Vertices() - // 4 is the 3 tasks + root - if len(toRun) != 4 { - t.Errorf("expected 4 tasks, got %v", len(toRun)) - } - for task := range pipeline { - if _, ok := engine.Tasks[task]; !ok { - t.Errorf("expected to find task %v in the task graph, but it is missing", task) - } - } -} - -func Test_taskSelfRef(t *testing.T) { - topoGraph := &dag.AcyclicGraph{} - topoGraph.Add("a") - // no dependencies between packages - - pipeline := map[string]fs.TaskDefinition{ - "build": { - TaskDependencies: []string{"build"}, - }, - } - filteredPkgs := make(util.Set) - filteredPkgs.Add("a") - rs := &runSpec{ - FilteredPkgs: filteredPkgs, - Targets: []string{"build"}, - Opts: &Opts{}, - } - - completeGraph := &graph.CompleteGraph{ - WorkspaceGraph: *topoGraph, - Pipeline: pipeline, - TaskDefinitions: map[string]*fs.TaskDefinition{}, - } - - _, err := buildTaskGraphEngine(completeGraph, rs) - if err == nil { - t.Fatalf("expected to failed to build task graph: %v", err) - } -} From 8bcf4a7ef3cf25668124291b1832048ab22cd8df Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 2 Feb 2023 14:59:19 -0500 Subject: [PATCH 3/4] Add print_task_invalidation feature (#3537) I used this to track down the infinite looping behavior #3525 and #3526. Running `cargo run --features print_task_invalidation --bin next-dev` will print something similar to: ``` invalidated Task { id: 16875, name: ConditionalContentSource::get } Task { id: 16875, name: ConditionalContentSource::get } invalidates: Task { id: 16873, name: CombinedContentSource::get } Task { id: 16832, name: CombinedContentSource::get } Task { id: 16815, name: CombinedContentSource::get } Task { id: 16807, name: CombinedContentSource::get } Task { id: 15384, name: get_from_source } invalidated Task { id: 16873, name: CombinedContentSource::get } invalidated Task { id: 16832, name: CombinedContentSource::get } invalidated Task { id: 16815, name: CombinedContentSource::get } invalidated Task { id: 16807, name: CombinedContentSource::get } invalidated Task { id: 15384, name: get_from_source } invalidated Task { id: 71537, name: DiskFileSystem::read } invalidated Task { id: 71517, name: DiskFileSystem::write } invalidated Task { id: 72217, name: DiskFileSystem::read } invalidated Task { id: 72214, name: DiskFileSystem::write } --- crates/turbo-tasks-memory/Cargo.toml | 1 + .../turbo-tasks-memory/src/memory_backend.rs | 2 +- crates/turbo-tasks-memory/src/output.rs | 37 +++++------------ crates/turbo-tasks-memory/src/task.rs | 41 +++++++++++++++++-- crates/turbo-tasks/src/backend.rs | 14 +++++++ 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/crates/turbo-tasks-memory/Cargo.toml b/crates/turbo-tasks-memory/Cargo.toml index f62b9a523aa44..00710f11b4092 100644 --- a/crates/turbo-tasks-memory/Cargo.toml +++ b/crates/turbo-tasks-memory/Cargo.toml @@ -43,6 +43,7 @@ log_activate_tasks = [] log_connect_tasks = [] report_expensive = [] print_scope_updates = [] +print_task_invalidation = [] [[bench]] name = "mod" diff --git a/crates/turbo-tasks-memory/src/memory_backend.rs b/crates/turbo-tasks-memory/src/memory_backend.rs index 834e0a86fc142..c4c01192ee085 100644 --- a/crates/turbo-tasks-memory/src/memory_backend.rs +++ b/crates/turbo-tasks-memory/src/memory_backend.rs @@ -316,7 +316,7 @@ impl Backend for MemoryBackend { turbo_tasks: &dyn TurboTasksBackendApi, ) { self.with_task(task, |task| { - task.execution_result(result, turbo_tasks); + task.execution_result(result, self, turbo_tasks); }) } diff --git a/crates/turbo-tasks-memory/src/output.rs b/crates/turbo-tasks-memory/src/output.rs index 8dc70e1c71757..f5a9cef9c98eb 100644 --- a/crates/turbo-tasks-memory/src/output.rs +++ b/crates/turbo-tasks-memory/src/output.rs @@ -60,32 +60,8 @@ impl Output { } pub fn link(&mut self, target: RawVc, turbo_tasks: &dyn TurboTasksBackendApi) { - let change; - let mut _type_change = false; - match &self.content { - OutputContent::Link(old_target) => { - if match (old_target, &target) { - (RawVc::TaskOutput(old_task), RawVc::TaskOutput(new_task)) => { - old_task == new_task - } - ( - RawVc::TaskCell(old_task, old_index), - RawVc::TaskCell(new_task, new_index), - ) => old_task == new_task && *old_index == *new_index, - _ => false, - } { - change = None; - } else { - change = Some(target); - } - } - OutputContent::Empty | OutputContent::Error(_) | OutputContent::Panic(_) => { - change = Some(target); - } - }; - if let Some(target) = change { - self.assign(OutputContent::Link(target), turbo_tasks) - } + debug_assert!(*self != target); + self.assign(OutputContent::Link(target), turbo_tasks) } pub fn error(&mut self, error: Error, turbo_tasks: &dyn TurboTasksBackendApi) { @@ -130,3 +106,12 @@ impl Output { } } } + +impl PartialEq for Output { + fn eq(&self, rhs: &RawVc) -> bool { + match &self.content { + OutputContent::Link(old_target) => old_target == rhs, + OutputContent::Empty | OutputContent::Error(_) | OutputContent::Panic(_) => false, + } + } +} diff --git a/crates/turbo-tasks-memory/src/task.rs b/crates/turbo-tasks-memory/src/task.rs index 86b23a3a3ece1..418ce6f53394b 100644 --- a/crates/turbo-tasks-memory/src/task.rs +++ b/crates/turbo-tasks-memory/src/task.rs @@ -33,7 +33,7 @@ use crate::{ count_hash_set::CountHashSet, gc::{to_exp_u8, GcPriority, GcStats, GcTaskState}, memory_backend::Job, - output::Output, + output::{Output, OutputContent}, scope::{ScopeChildChangeEffect, TaskScopeId, TaskScopes}, stats::{ReferenceType, StatsReferences, StatsTaskType}, MemoryBackend, @@ -89,7 +89,17 @@ impl Debug for TaskType { match self { Self::Root(..) => f.debug_tuple("Root").finish(), Self::Once(..) => f.debug_tuple("Once").finish(), - Self::Persistent(ty) => ty.fmt(f), + Self::Persistent(ty) => Debug::fmt(ty, f), + } + } +} + +impl Display for TaskType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Root(..) => f.debug_tuple("Root").finish(), + Self::Once(..) => f.debug_tuple("Once").finish(), + Self::Persistent(ty) => Display::fmt(ty, f), } } } @@ -116,7 +126,8 @@ pub struct Task { impl Debug for Task { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut result = f.debug_struct("Task"); - result.field("type", &self.ty); + result.field("id", &self.id); + result.field("ty", &self.ty); if let Some(state) = self.try_state() { match state { TaskMetaStateReadGuard::Full(state) => { @@ -707,12 +718,30 @@ impl Task { pub(crate) fn execution_result( &self, result: Result, Option>>, + backend: &MemoryBackend, turbo_tasks: &dyn TurboTasksBackendApi, ) { let mut state = self.full_state_mut(); match state.state_type { InProgress { .. } => match result { - Ok(Ok(result)) => state.output.link(result, turbo_tasks), + Ok(Ok(result)) => { + if state.output != result { + if cfg!(feature = "print_task_invalidation") + && !matches!(state.output.content, OutputContent::Empty) + { + println!( + "Task {{ id: {}, name: {} }} invalidates:", + *self.id, self.ty + ); + for dep in state.output.dependent_tasks.iter() { + backend.with_task(*dep, |task| { + println!("\tTask {{ id: {}, name: {} }}", *task.id, task.ty); + }); + } + } + state.output.link(result, turbo_tasks) + } + } Ok(Err(mut err)) => { if let Some(name) = self.get_function_name() { err = err.context(format!("Execution of {} failed", name)); @@ -894,6 +923,10 @@ impl Task { }), }; drop(state); + + if cfg!(feature = "print_task_invalidation") { + println!("invalidated Task {{ id: {}, name: {} }}", *self.id, self.ty); + } turbo_tasks.schedule(self.id); } else { state.state_type = Dirty { diff --git a/crates/turbo-tasks/src/backend.rs b/crates/turbo-tasks/src/backend.rs index 5ed548165c9a4..2f4ab3b4dd056 100644 --- a/crates/turbo-tasks/src/backend.rs +++ b/crates/turbo-tasks/src/backend.rs @@ -1,6 +1,7 @@ use std::{ any::Any, borrow::Cow, + fmt, fmt::{Debug, Display}, future::Future, pin::Pin, @@ -75,6 +76,19 @@ pub enum PersistentTaskType { ResolveTrait(TraitTypeId, Cow<'static, str>, Vec), } +impl Display for PersistentTaskType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Native(fid, _) | Self::ResolveNative(fid, _) => { + Display::fmt(®istry::get_function(*fid).name, f) + } + Self::ResolveTrait(tid, n, _) => { + write!(f, "{}::{n}", registry::get_trait(*tid).name) + } + } + } +} + impl PersistentTaskType { pub fn shrink_to_fit(&mut self) { match self { From 028d6f91095336ef48d0735f75e4f92feb46035a Mon Sep 17 00:00:00 2001 From: Thomas Knickman Date: Thu, 2 Feb 2023 15:06:42 -0500 Subject: [PATCH 4/4] fix(cli): ci detection (#3605) --- cli/internal/ci/ci.go | 24 +++++- cli/internal/ci/ci_test.go | 105 ++++++++++++++++++++++++ cli/internal/ci/vendors.go | 164 +++++++++++++++++++++++-------------- 3 files changed, 230 insertions(+), 63 deletions(-) create mode 100644 cli/internal/ci/ci_test.go diff --git a/cli/internal/ci/ci.go b/cli/internal/ci/ci.go index 54851c00020db..bcc56f95ed2e3 100644 --- a/cli/internal/ci/ci.go +++ b/cli/internal/ci/ci.go @@ -4,7 +4,7 @@ package ci import "os" -var isCI = os.Getenv("CI") != "" || os.Getenv("CONTINUOUS_INTEGRATION") != "" || os.Getenv("BUILD_NUMBER") != "" || os.Getenv("RUN_ID") != "" || os.Getenv("TEAMCITY_VERSION") != "" || false +var isCI = os.Getenv("BUILD_ID") != "" || os.Getenv("BUILD_NUMBER") != "" || os.Getenv("CI") != "" || os.Getenv("CI_APP_ID") != "" || os.Getenv("CI_BUILD_ID") != "" || os.Getenv("CI_BUILD_NUMBER") != "" || os.Getenv("CI_NAME") != "" || os.Getenv("CONTINUOUS_INTEGRATION") != "" || os.Getenv("RUN_ID") != "" || os.Getenv("TEAMCITY_VERSION") != "" || false // IsCi returns true if the program is executing in a CI/CD environment func IsCi() bool { @@ -18,6 +18,7 @@ func Name() string { // Info returns information about a CI vendor func Info() Vendor { + // check both the env var key and value for _, env := range Vendors { if env.EvalEnv != nil { for name, value := range env.EvalEnv { @@ -26,8 +27,25 @@ func Info() Vendor { } } } else { - if os.Getenv(env.Env) != "" { - return env + // check for any of the listed env var keys, with any value + if env.Env.Any != nil && len(env.Env.Any) > 0 { + for _, envVar := range env.Env.Any { + if os.Getenv(envVar) != "" { + return env + } + } + // check for all of the listed env var keys, with any value + } else if env.Env.All != nil && len(env.Env.All) > 0 { + all := true + for _, envVar := range env.Env.All { + if os.Getenv(envVar) == "" { + all = false + break + } + } + if all { + return env + } } } } diff --git a/cli/internal/ci/ci_test.go b/cli/internal/ci/ci_test.go new file mode 100644 index 0000000000000..333ff610c528d --- /dev/null +++ b/cli/internal/ci/ci_test.go @@ -0,0 +1,105 @@ +package ci + +import ( + "os" + "reflect" + "strings" + "testing" +) + +func getVendor(name string) Vendor { + for _, v := range Vendors { + if v.Name == name { + return v + } + } + return Vendor{} +} + +func TestInfo(t *testing.T) { + tests := []struct { + name string + setEnv []string + want Vendor + }{ + { + name: "AppVeyor", + setEnv: []string{"APPVEYOR"}, + want: getVendor("AppVeyor"), + }, + { + name: "Vercel", + setEnv: []string{"VERCEL", "NOW_BUILDER"}, + want: getVendor("Vercel"), + }, + { + name: "Render", + setEnv: []string{"RENDER"}, + want: getVendor("Render"), + }, + { + name: "Netlify", + setEnv: []string{"NETLIFY"}, + want: getVendor("Netlify CI"), + }, + { + name: "Jenkins", + setEnv: []string{"BUILD_ID", "JENKINS_URL"}, + want: getVendor("Jenkins"), + }, + { + name: "Jenkins - failing", + setEnv: []string{"BUILD_ID"}, + want: getVendor(""), + }, + { + name: "GitHub Actions", + setEnv: []string{"GITHUB_ACTIONS"}, + want: getVendor("GitHub Actions"), + }, + { + name: "Codeship", + setEnv: []string{"CI_NAME=codeship"}, + want: getVendor("Codeship"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // unset existing envs + liveCi := "" + if Name() == "GitHub Actions" { + liveCi = os.Getenv("GITHUB_ACTIONS") + err := os.Unsetenv("GITHUB_ACTIONS") + if err != nil { + t.Errorf("Error un-setting GITHUB_ACTIONS env: %s", err) + } + } + // set envs + for _, env := range tt.setEnv { + envParts := strings.Split(env, "=") + val := "some value" + if len(envParts) > 1 { + val = envParts[1] + } + err := os.Setenv(envParts[0], val) + if err != nil { + t.Errorf("Error setting %s for %s test", envParts[0], tt.name) + } + defer os.Unsetenv(envParts[0]) //nolint errcheck + + } + // run test + if got := Info(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Info() = %v, want %v", got, tt.want) + } + + // reset env + if Name() == "GitHub Actions" { + err := os.Setenv("GITHUB_ACTIONS", liveCi) + if err != nil { + t.Errorf("Error re-setting GITHUB_ACTIONS env: %s", err) + } + } + }) + } +} diff --git a/cli/internal/ci/vendors.go b/cli/internal/ci/vendors.go index 60698d25ccd51..13bce77dc1805 100644 --- a/cli/internal/ci/vendors.go +++ b/cli/internal/ci/vendors.go @@ -1,78 +1,88 @@ package ci +type vendorEnvs struct { + Any []string + All []string +} + // Vendor describes a CI/CD vendor execution environment type Vendor struct { // Name is the name of the vendor Name string // Constant is the environment variable prefix used by the vendor Constant string - // Env is an environment variable that can be used to quickly determine the vendor (using simple os.Getenv(env) check) - Env string + // Env is one or many environment variables that can be used to quickly determine the vendor (using simple os.Getenv(env) check) + Env vendorEnvs // EvalEnv is key/value map of environment variables that can be used to quickly determine the vendor EvalEnv map[string]string } -// Vendors is a list of common CI/CD vendors +// Vendors is a list of common CI/CD vendors (from https://github.com/watson/ci-info/blob/master/vendors.json) var Vendors = []Vendor{ + { + Name: "Appcircle", + Constant: "APPCIRCLE", + Env: vendorEnvs{Any: []string{"AC_APPCIRCLE"}}, + }, { Name: "AppVeyor", Constant: "APPVEYOR", - Env: "APPVEYOR", + Env: vendorEnvs{Any: []string{"APPVEYOR"}}, }, { - Name: "Azure Pipelines", - Constant: "AZURE_PIPELINES", - Env: "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", + Name: "AWS CodeBuild", + Constant: "CODEBUILD", + Env: vendorEnvs{Any: []string{"CODEBUILD_BUILD_ARN"}}, }, { - Name: "Appcircle", - Constant: "APPCIRCLE", - Env: "AC_APPCIRCLE", + Name: "Azure Pipelines", + Constant: "AZURE_PIPELINES", + Env: vendorEnvs{Any: []string{"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"}}, }, { Name: "Bamboo", Constant: "BAMBOO", - Env: "bamboo_planKey", + Env: vendorEnvs{Any: []string{"bamboo_planKey"}}, }, { Name: "Bitbucket Pipelines", Constant: "BITBUCKET", - Env: "BITBUCKET_COMMIT", + Env: vendorEnvs{Any: []string{"BITBUCKET_COMMIT"}}, }, { Name: "Bitrise", Constant: "BITRISE", - Env: "BITRISE_IO", + Env: vendorEnvs{Any: []string{"BITRISE_IO"}}, }, { Name: "Buddy", Constant: "BUDDY", - Env: "BUDDY_WORKSPACE_ID", + Env: vendorEnvs{Any: []string{"BUDDY_WORKSPACE_ID"}}, }, { Name: "Buildkite", Constant: "BUILDKITE", - Env: "BUILDKITE", + Env: vendorEnvs{Any: []string{"BUILDKITE"}}, }, { Name: "CircleCI", Constant: "CIRCLE", - Env: "CIRCLECI", + Env: vendorEnvs{Any: []string{"CIRCLECI"}}, }, { Name: "Cirrus CI", Constant: "CIRRUS", - Env: "CIRRUS_CI", - }, - { - Name: "AWS CodeBuild", - Constant: "CODEBUILD", - Env: "CODEBUILD_BUILD_ARN", + Env: vendorEnvs{Any: []string{"CIRRUS_CI"}}, }, { Name: "Codefresh", Constant: "CODEFRESH", - Env: "CF_BUILD_ID", + Env: vendorEnvs{Any: []string{"CF_BUILD_ID"}}, + }, + { + Name: "Codemagic", + Constant: "CODEMAGIC", + Env: vendorEnvs{Any: []string{"CM_BUILD_ID"}}, }, { Name: "Codeship", @@ -84,126 +94,160 @@ var Vendors = []Vendor{ { Name: "Drone", Constant: "DRONE", - Env: "DRONE", + Env: vendorEnvs{Any: []string{"DRONE"}}, }, { Name: "dsari", Constant: "DSARI", - Env: "DSARI", + Env: vendorEnvs{Any: []string{"DSARI"}}, + }, + { + Name: "Expo Application Services", + Constant: "EAS", + Env: vendorEnvs{Any: []string{"EAS_BUILD"}}, }, { Name: "GitHub Actions", Constant: "GITHUB_ACTIONS", - Env: "GITHUB_ACTIONS", + Env: vendorEnvs{Any: []string{"GITHUB_ACTIONS"}}, }, { Name: "GitLab CI", Constant: "GITLAB", - Env: "GITLAB_CI", + Env: vendorEnvs{Any: []string{"GITLAB_CI"}}, }, { Name: "GoCD", Constant: "GOCD", - Env: "GO_PIPELINE_LABEL", + Env: vendorEnvs{Any: []string{"GO_PIPELINE_LABEL"}}, + }, + { + Name: "Google Cloud Build", + Constant: "GOOGLE_CLOUD_BUILD", + Env: vendorEnvs{Any: []string{"BUILDER_OUTPUT"}}, }, { Name: "LayerCI", Constant: "LAYERCI", - Env: "LAYERCI", + Env: vendorEnvs{Any: []string{"LAYERCI"}}, }, { - Name: "Hudson", - Constant: "HUDSON", - Env: "HUDSON_URL", + Name: "Gerrit", + Constant: "GERRIT", + Env: vendorEnvs{Any: []string{"GERRIT_PROJECT"}}, }, { - Name: "Jenkins", - Constant: "JENKINS", - Env: "JENKINS_URL", + Name: "Hudson", + Constant: "HUDSON", + Env: vendorEnvs{Any: []string{"HUDSON"}}, }, { Name: "Jenkins", Constant: "JENKINS", - Env: "BUILD_ID", + Env: vendorEnvs{All: []string{"JENKINS_URL", "BUILD_ID"}}, }, { Name: "Magnum CI", Constant: "MAGNUM", - Env: "MAGNUM", + Env: vendorEnvs{Any: []string{"MAGNUM"}}, }, { Name: "Netlify CI", Constant: "NETLIFY", - Env: "NETLIFY", + Env: vendorEnvs{Any: []string{"NETLIFY"}}, }, { Name: "Nevercode", Constant: "NEVERCODE", - Env: "NEVERCODE", + Env: vendorEnvs{Any: []string{"NEVERCODE"}}, + }, + { + Name: "ReleaseHub", + Constant: "RELEASEHUB", + Env: vendorEnvs{Any: []string{"RELEASE_BUILD_ID"}}, }, { Name: "Render", Constant: "RENDER", - Env: "RENDER", + Env: vendorEnvs{Any: []string{"RENDER"}}, }, { Name: "Sail CI", Constant: "SAIL", - Env: "SAILCI", - }, - { - Name: "Semaphore", - Constant: "SEMAPHORE", - Env: "SEMAPHORE", + Env: vendorEnvs{Any: []string{"SAILCI"}}, }, { Name: "Screwdriver", Constant: "SCREWDRIVER", - Env: "SCREWDRIVER", + Env: vendorEnvs{Any: []string{"SCREWDRIVER"}}, + }, + { + Name: "Semaphore", + Constant: "SEMAPHORE", + Env: vendorEnvs{Any: []string{"SEMAPHORE"}}, }, { Name: "Shippable", Constant: "SHIPPABLE", - Env: "SHIPPABLE", + Env: vendorEnvs{Any: []string{"SHIPPABLE"}}, }, { Name: "Solano CI", Constant: "SOLANO", - Env: "TDDIUM", + Env: vendorEnvs{Any: []string{"TDDIUM"}}, }, { - Name: "Strider CD", - Constant: "STRIDER", - Env: "STRIDER", + Name: "Sourcehut", + Constant: "SOURCEHUT", + EvalEnv: map[string]string{ + "CI_NAME": "sourcehut", + }, }, { - Name: "TaskCluster", - Constant: "TASKCLUSTER", - Env: "TASK_ID", + Name: "Strider CD", + Constant: "STRIDER", + Env: vendorEnvs{Any: []string{"STRIDER"}}, }, { Name: "TaskCluster", Constant: "TASKCLUSTER", - Env: "RUN_ID", + Env: vendorEnvs{All: []string{"TASK_ID", "RUN_ID"}}, }, { Name: "TeamCity", Constant: "TEAMCITY", - Env: "TEAMCITY_VERSION", + Env: vendorEnvs{Any: []string{"TEAMCITY_VERSION"}}, }, { Name: "Travis CI", Constant: "TRAVIS", - Env: "TRAVIS", + Env: vendorEnvs{Any: []string{"TRAVIS"}}, }, { Name: "Vercel", Constant: "VERCEL", - Env: "NOW_BUILDER", + Env: vendorEnvs{Any: []string{"NOW_BUILDER", "VERCEL"}}, }, { Name: "Visual Studio App Center", Constant: "APPCENTER", - Env: "APPCENTER_BUILD_ID", + Env: vendorEnvs{Any: []string{"APPCENTER"}}, + }, + { + Name: "Woodpecker", + Constant: "WOODPECKER", + EvalEnv: map[string]string{ + "CI": "woodpecker", + }, + }, + { + Name: "Xcode Cloud", + Constant: "XCODE_CLOUD", + Env: vendorEnvs{Any: []string{"CI_XCODE_PROJECT"}}, + }, + { + Name: "Xcode Server", + Constant: "XCODE_SERVER", + Env: vendorEnvs{Any: []string{"XCS"}}, }, }