Repository: denoland/deno
Pull request: ci: fix deno_core changes check in shallow clones

## Summary
- The previous shell one-liner used `git diff` against the PR base SHA, which often isn't available in the shallow clone (`fetch-depth: 5`). When the base SHA is missing, `git diff` fails silently, `grep` gets no input, and deno_core tests are incorrectly skipped.
- Fix by explicitly fetching the base SHA and replacing the fragile shell pipeline with a Deno script (`tools/check_deno_core_changes.js`) using `Deno.spawnAndWait()`.
- Added `installDenoStep` to the pre_build job since the script needs Deno.

## Test plan
- [ ] Verify CI runs deno_core tests on a PR that changes files in `libs/core/`
- [ ] Verify CI skips deno_core tests on a PR that only changes files outside core dirs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: .github/workflows/ci.generate.ts
patch: @@ -480,11 +480,9 @@ const preBuildCheckStep = step({
 const denoCoreChangesCheckStep = step({
   id: "deno_core_changes",
   run: [
-    `DENO_CORE_CHANGED=$(git diff --name-only \${{ github.event.pull_request.base.sha }}..HEAD | grep -qE '^(${
-      denoCorePackageDirs.join("|")
-    })/|^Cargo\\.lock$|^Cargo\\.toml$' && echo true || echo false)`,
-    `echo "Deno core changed: $DENO_CORE_CHANGED"`,
-    `echo "skip_deno_core_test=$([ "$DENO_CORE_CHANGED" = "false" ] && echo true || echo false)" >> $GITHUB_OUTPUT`,
+    // Fetch the base SHA so it's available even in shallow clones
+    `git fetch --depth=1 origin \${{ github.event.pull_request.base.sha }}`,
+    `deno run -A tools/check_deno_core_changes.js \${{ github.event.pull_request.base.sha }}`,
   ],
   outputs: ["skip_deno_core_test"] as const,
 });
@@ -494,6 +492,7 @@ const preBuildJob = job("pre_build", {
   runsOn: "ubuntu-latest",
   steps: step.if(isPr)(
     cloneRepoStep,
+    installDenoStep,
     step.if(conditions.isDraftPr())(preBuildCheckStep),
     denoCoreChangesCheckStep,
   ),

filename: .github/workflows/ci.yml
patch: @@ -38,6 +38,11 @@ jobs:
         with:
           fetch-depth: 5
           submodules: false
+      - name: Install Deno
+        uses: denoland/setup-deno@v2
+        if: github.event_name == 'pull_request'
+        with:
+          deno-version: v2.x
       - id: check
         if: 'github.event_name == ''pull_request'' && github.event.pull_request.draft == true && !contains(github.event.pull_request.labels.*.name, ''ci-draft'')'
         run: |-
@@ -47,9 +52,8 @@ jobs:
       - id: deno_core_changes
         if: github.event_name == 'pull_request'
         run: |-
-          DENO_CORE_CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..HEAD | grep -qE '^(libs/core_testing|libs/core|libs/core/examples/snapshot|libs/dcore|libs/ops|libs/ops/compile_test_runner|libs/serde_v8)/|^Cargo\.lock$|^Cargo\.toml$' && echo true || echo false)
-          echo "Deno core changed: $DENO_CORE_CHANGED"
-          echo "skip_deno_core_test=$([ "$DENO_CORE_CHANGED" = "false" ] && echo true || echo false)" >> $GITHUB_OUTPUT
+          git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
+          deno run -A tools/check_deno_core_changes.js ${{ github.event.pull_request.base.sha }}
   bench:
     name: bench release linux-x86_64
     needs:

filename: tools/check_deno_core_changes.js
patch: @@ -0,0 +1,69 @@
+// Copyright 2018-2026 the Deno authors. MIT license.
+
+// deno-lint-ignore-file no-console
+
+// Checks if any files changed relative to the base branch belong to
+// deno_core-related package directories (or root Cargo files). Used in CI
+// to decide whether to run deno_core tests.
+//
+// Usage: deno run --allow-run=git check_deno_core_changes.js <base_sha>
+// Writes "skip_deno_core_test=true" or "skip_deno_core_test=false" to
+// $GITHUB_OUTPUT when running in GitHub Actions.
+
+const DENO_CORE_PACKAGE_DIRS = [
+  "libs/core_testing",
+  "libs/core",
+  "libs/core/examples/snapshot",
+  "libs/dcore",
+  "libs/ops",
+  "libs/ops/compile_test_runner",
+  "libs/serde_v8",
+];
+
+const baseSha = Deno.args[0];
+if (!baseSha) {
+  console.error("Usage: check_deno_core_changes.js <base_sha>");
+  Deno.exit(1);
+}
+
+// Get list of changed files between the base SHA and HEAD
+const { code, stdout, stderr } = await Deno.spawnAndWait("git", [
+  "diff",
+  "--name-only",
+  `${baseSha}..HEAD`,
+]);
+
+if (code !== 0) {
+  console.error("git diff failed:", stderr);
+  console.log("Defaulting to running deno_core tests (skip=false)");
+  await writeOutput(false);
+  Deno.exit(0);
+}
+
+const stdoutText = new TextDecoder().decode(stdout);
+const changedFiles = stdoutText.trim().split("\n").filter(Boolean);
+
+const denoCoreChanged = changedFiles.some((file) =>
+  DENO_CORE_PACKAGE_DIRS.some((dir) => file.startsWith(dir + "/")) ||
+  file === "Cargo.lock" ||
+  file === "Cargo.toml"
+);
+
+const skip = !denoCoreChanged;
+console.log(`Changed files (${changedFiles.length}):`);
+for (const f of changedFiles) {
+  console.log(`  ${f}`);
+}
+console.log(`Deno core changed: ${denoCoreChanged}`);
+console.log(`skip_deno_core_test: ${skip}`);
+
+await writeOutput(skip);
+
+async function writeOutput(skip) {
+  const outputFile = Deno.env.get("GITHUB_OUTPUT");
+  if (outputFile) {
+    await Deno.writeTextFile(outputFile, `skip_deno_core_test=${skip}\n`, {
+      append: true,
+    });
+  }
+}