Repository: denoland/deno
Pull request: ci: add workflow to run package managers under deno

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->


Changed files:
filename: .github/workflows/ecosystem_compat_test.yml
patch: @@ -0,0 +1,39 @@
+name: ecosystem_compat_test
+
+on:
+  schedule:
+    - cron: '0 10 * * *'
+  workflow_dispatch:
+
+jobs:
+  test:
+    runs-on: '${{ matrix.runner }}'
+    strategy:
+      matrix:
+        include:
+          - os: linux
+            runner: ubuntu-latest
+          - os: windows
+            runner: windows-latest
+          - os: darwin
+            runner: macos-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          submodules: true
+      - name: Setup Deno
+        uses: denoland/setup-deno@v2
+        with:
+          deno-version: canary
+      - name: Setup gcloud
+        uses: google-github-actions/setup-gcloud@v2
+        with:
+          project_id: denoland
+      - name: Run tests
+        run: deno -A tools/ecosystem_compat_tests.ts
+      - name: Post message to slack channel
+        run: deno -A tools/ecosystem_compat_slack.ts
+        env:
+          SLACK_TOKEN: ${{ secrets.NODE_COMPAT_SLACK_TOKEN }} # NodeCompat bot
+          SLACK_CHANNEL: ${{ secrets.NODE_COMPAT_SLACK_CHANNEL }}

filename: tools/ecosystem_compat_slack.ts
patch: @@ -0,0 +1,80 @@
+// Copyright 2018-2025 the Deno authors. MIT license.
+// deno-lint-ignore-file no-console
+
+import { LogLevel, WebClient } from "npm:@slack/web-api@7.8.0";
+
+const token = Deno.env.get("SLACK_TOKEN");
+const channel = Deno.env.get("SLACK_CHANNEL");
+
+if (!token) {
+  console.error("SLACK_TOKEN is required");
+}
+if (!channel) {
+  console.error("SLACK_CHANNEL is required");
+}
+
+interface PmResult {
+  exitCode: number;
+  duration: number;
+}
+
+interface EcosystemReport {
+  npm: PmResult;
+  yarn: PmResult;
+  pnpm: PmResult;
+}
+
+const client = new WebClient(token, {
+  logLevel: LogLevel.DEBUG,
+});
+
+function formatDuration(duration: number) {
+  return (duration / 1000).toFixed(2) + "s";
+}
+
+function createMessage(ecosystemReport: EcosystemReport) {
+  let mrkdwn = "Package manager report\n";
+
+  mrkdwn += `*npm*: exit code: ${ecosystemReport.npm.exitCode}, duration: ${
+    formatDuration(ecosystemReport.npm.duration)
+  }\n`;
+  mrkdwn += `*yarn*: exit code: ${ecosystemReport.yarn.exitCode}, duration: ${
+    formatDuration(ecosystemReport.yarn.duration)
+  }\n`;
+  mrkdwn += `*pnpm*: exit code: ${ecosystemReport.pnpm.exitCode}, duration: ${
+    formatDuration(ecosystemReport.pnpm.duration)
+  }\n`;
+
+  return [
+    {
+      type: "section",
+      text: {
+        type: "mrkdwn",
+        text: mrkdwn,
+      },
+    },
+  ];
+}
+
+async function main() {
+  const ecosystemReport = await Deno.readTextFile(
+    import.meta.resolve("./ecosystem_report.json"),
+  )
+    .then(JSON.parse) as EcosystemReport;
+
+  try {
+    const result = await client.chat.postMessage({
+      token,
+      channel,
+      blocks: createMessage(ecosystemReport),
+      unfurl_links: false,
+      unfurl_media: false,
+    });
+
+    console.log(result);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+await main();

filename: tools/ecosystem_compat_tests.ts
patch: @@ -0,0 +1,92 @@
+#!/usr/bin/env -S deno run --allow-all --config=tests/config/deno.json
+// Copyright 2018-2025 the Deno authors. MIT license.
+import $ from "jsr:@david/dax@^0.42.0";
+
+async function setupTestRepo() {
+  await teardownTestRepo();
+  await $`git clone https://github.com/lucacasonato/nextjs-demo`;
+}
+
+async function teardownTestRepo() {
+  await $`rm -rf nextjs-demo`;
+}
+
+async function runPackageManager(pm, cmd) {
+  const state = Date.now();
+  const result =
+    await $`cd nextjs-demo ; rm -rf node_modules ; ${Deno.execPath()} run -A --no-config npm:${pm} ${cmd}`
+      .stdout("inheritPiped").stderr("inheritPiped").noThrow().timeout(
+        "60s",
+      );
+  const duration = Date.now() - state;
+  return {
+    exitCode: result.code,
+    stdout: result.stdout,
+    stderr: result.stderr,
+    duration,
+  };
+}
+
+function testNpm() {
+  return runPackageManager("npm", "install");
+}
+
+function testYarn() {
+  return runPackageManager("yarn", "install");
+}
+
+function testPnpm() {
+  return runPackageManager("pnpm", "install");
+}
+
+async function main() {
+  let passed = true;
+  $.logStep("Setting up test repo...");
+  await setupTestRepo();
+  $.logStep("Testing npm...");
+  const npmResult = await testNpm();
+  if (npmResult.exitCode !== 0) {
+    passed = false;
+    $.logWarn(`npm install failed: ${npmResult.stderr}`);
+  }
+  $.logStep("Testing yarn...");
+  const yarnResult = await testYarn();
+  if (yarnResult.exitCode !== 0) {
+    passed = false;
+    $.logWarn(`yarn install failed: ${yarnResult.stderr}`);
+  }
+  $.logStep("Testing pnpm...");
+  const pnpmResult = await testPnpm();
+  if (pnpmResult.exitCode !== 0) {
+    passed = false;
+    $.logWarn(`pnpm install failed: ${pnpmResult.stderr}`);
+  }
+  if (passed) {
+    $.logStep("All tests passed!");
+  } else {
+    $.logError("Some tests failed");
+  }
+  await Deno.writeTextFile(
+    import.meta.resolve("./ecosystem_report.json"),
+    JSON.stringify({
+      npm: {
+        exitCode: npmResult.exitCode,
+        duration: npmResult.duration,
+      },
+      yarn: {
+        exitCode: yarnResult.exitCode,
+        duration: yarnResult.duration,
+      },
+      pnpm: {
+        exitCode: pnpmResult.exitCode,
+        duration: pnpmResult.duration,
+      },
+    }),
+  );
+}
+
+try {
+  await main();
+} finally {
+  await teardownTestRepo();
+}