#!/usr/bin/env -S deno run --check --allow-write=. --allow-read=. --lock=./tools/deno.lock.json
// Copyright 2018-2026 the Deno authors. MIT license.
import { parse as parseToml } from "jsr:@std/toml@1";
import {
  Condition,
  conditions,
  type ConfigValue,
  createWorkflow,
  defineArtifact,
  defineExprObj,
  defineMatrix,
  type ExpressionValue,
  job,
  literal,
  step,
} from "jsr:@david/gagen@0.3.1";

// Bump this number when you want to purge the cache.
// Note: the tools/release/01_bump_crate_versions.ts script will update this version
// automatically via regex, so ensure that this line maintains this format.
const cacheVersion = 123;

const ubuntuX86Runner = "ubuntu-24.04";
const ubuntuARMRunner = "ubuntu-24.04-arm";
const ubuntuARMXlRunner = "ubuntu-24.04-arm64-xl";
const windowsX86Runner = "windows-2022";
const windowsX86XlRunner = "windows-2022-xl";
const windowsArmRunner = "windows-11-arm";
const macosX86Runner = "macos-15-intel";
const macosArmRunner = "macos-14";

// shared conditions
const isDenoland = conditions.isRepository("denoland/deno");
const isMainBranch = conditions.isBranch("main");
const isTag = conditions.isTag();
const isNotTag = isTag.not();
const isMainOrTag = isMainBranch.or(isTag);
const isPr = conditions.isPr();
const hasCiFullLabel = conditions.hasPrLabel("ci-full");
const hasCiBenchLabel = conditions.hasPrLabel("ci-bench");

const Runners = {
  linuxX86: {
    os: "linux",
    arch: "x86_64",
    runner: ubuntuX86Runner,
  },
  linuxX86Xl: {
    os: "linux",
    arch: "x86_64",
    runner: ubuntuX86Runner,
  },
  linuxArm: {
    os: "linux",
    arch: "aarch64",
    runner: ubuntuARMRunner,
  },
  linuxArmXl: {
    os: "linux",
    arch: "aarch64",
    runner: isDenoland.and(isMainOrTag.or(hasCiFullLabel)).then(
      ubuntuARMXlRunner,
    ).else(ubuntuARMRunner),
    testRunner: ubuntuARMRunner,
  },
  macosX86: {
    os: "macos",
    arch: "x86_64",
    runner: macosX86Runner,
  },
  macosArm: {
    os: "macos",
    arch: "aarch64",
    runner: macosArmRunner,
  },
  macosArmSelfHosted: {
    os: "macos",
    arch: "aarch64",
    runner: macosArmRunner,
  },
  windowsX86: {
    os: "windows",
    arch: "x86_64",
    runner: windowsX86Runner,
  },
  windowsX86Xl: {
    os: "windows",
    arch: "x86_64",
    runner: isDenoland.then(windowsX86XlRunner).else(windowsX86Runner),
    testRunner: windowsX86Runner,
  },
  windowsArm: {
    os: "windows",
    arch: "aarch64",
    runner: windowsArmRunner,
  },
} as const;

const denoCorePackageDirs = [
  "libs/core_testing",
  "libs/core",
  "libs/core/examples/snapshot",
  "libs/dcore",
  "libs/ops",
  "libs/ops/compile_test_runner",
  "libs/serde_v8",
];

// discover test crates first so we know which workspace members are test packages
const { testCrates, testPackageMembers } = resolveTestCrateTests();
// discover workspace members for the libs test job, split by type
const { binCrates, libCrates } = resolveWorkspaceCrates(
  testPackageMembers,
);

// Note that you may need to add more version to the `apt-get remove` line below if you change this
const llvmVersion = 22;
const installPkgsCommand =
  `sudo apt-get install -y --no-install-recommends clang-${llvmVersion} lld-${llvmVersion} clang-tools-${llvmVersion} clang-format-${llvmVersion} clang-tidy-${llvmVersion}`;
const sysRootConfig = {
  name: "Set up incremental LTO and sysroot build",
  run: `# Setting up sysroot
export DEBIAN_FRONTEND=noninteractive
# Avoid running man-db triggers, which sometimes takes several minutes
# to complete.
sudo apt-get -qq remove --purge -y man-db > /dev/null 2> /dev/null
# Remove older clang before we install
sudo apt-get -qq remove \
  'clang-12*' 'clang-13*' 'clang-14*' 'clang-15*' 'clang-16*' 'clang-17*' 'clang-18*' 'clang-19*' 'clang-20*' 'clang-21*' 'llvm-12*' 'llvm-13*' 'llvm-14*' 'llvm-15*' 'llvm-16*' 'llvm-17*' 'llvm-18*' 'llvm-19*' 'llvm-20*' 'llvm-21*' 'lld-12*' 'lld-13*' 'lld-14*' 'lld-15*' 'lld-16*' 'lld-17*' 'lld-18*' 'lld-19*' 'lld-20*' 'lld-21*' > /dev/null 2> /dev/null

# Install clang-XXX, lld-XXX, and debootstrap.
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-${llvmVersion} main" |
  sudo dd of=/etc/apt/sources.list.d/llvm-toolchain-noble-${llvmVersion}.list
curl https://apt.llvm.org/llvm-snapshot.gpg.key |
  gpg --dearmor                                 |
sudo dd of=/etc/apt/trusted.gpg.d/llvm-snapshot.gpg
sudo apt-get update
# this was unreliable sometimes, so try again if it fails
${installPkgsCommand} || (echo 'Failed. Trying again.' && sudo apt-get clean && sudo apt-get update && ${installPkgsCommand})
# Fix alternatives
(yes '' | sudo update-alternatives --force --all) > /dev/null 2> /dev/null || true

clang-${llvmVersion} -c -o /tmp/memfd_create_shim.o tools/memfd_create_shim.c -fPIC
clang-${llvmVersion} -c -o /tmp/glibc_math_shim.o tools/glibc_math_shim.c -fPIC

echo "Decompressing sysroot..."
wget -q https://github.com/denoland/deno_sysroot_build/releases/download/sysroot-20250207/sysroot-\`uname -m\`.tar.xz -O /tmp/sysroot.tar.xz
cd /
xzcat /tmp/sysroot.tar.xz | sudo tar -x
sudo mount --rbind /dev /sysroot/dev
sudo mount --rbind /sys /sysroot/sys
sudo mount --rbind /home /sysroot/home
sudo mount -t proc /proc /sysroot/proc
cd

echo "Done."

# Configure the build environment. Both Rust and Clang will produce
# llvm bitcode only, so we can use lld's incremental LTO support.

# Load the sysroot's env vars
echo "sysroot env:"
cat /sysroot/.env
. /sysroot/.env

# Important notes:
#   1. -ldl seems to be required to avoid a failure in FFI tests. This flag seems
#      to be in the Rust default flags in the smoketest, so uncertain why we need
#      to be explicit here.
#   2. RUSTFLAGS and RUSTDOCFLAGS must be specified, otherwise the doctests fail
#      to build because the object formats are not compatible.
echo "
CARGO_PROFILE_BENCH_INCREMENTAL=false
CARGO_PROFILE_RELEASE_INCREMENTAL=false
RUSTFLAGS<<__1
  -C linker-plugin-lto=true
  -C linker=clang-${llvmVersion}
  -C link-arg=-fuse-ld=lld-${llvmVersion}
  -C link-arg=-Wl,--icf=safe
  -C link-arg=-ldl
  -C link-arg=-Wl,--allow-shlib-undefined
  -C link-arg=-Wl,--thinlto-cache-dir=$(pwd)/target/release/lto-cache
  -C link-arg=-Wl,--thinlto-cache-policy,cache_size_bytes=700m
  -C link-arg=/tmp/memfd_create_shim.o
  -C link-arg=/tmp/glibc_math_shim.o
  -C link-arg=-Wl,--wrap=expf
  -C link-arg=-Wl,--wrap=powf
  -C link-arg=-Wl,--wrap=exp2f
  -C link-arg=-Wl,--wrap=log2f
  -C link-arg=-Wl,--wrap=logf
  --cfg tokio_unstable
  $RUSTFLAGS
__1
RUSTDOCFLAGS<<__1
  -C linker-plugin-lto=true
  -C linker=clang-${llvmVersion}
  -C link-arg=-fuse-ld=lld-${llvmVersion}
  -C link-arg=-Wl,--icf=safe
  -C link-arg=-ldl
  -C link-arg=-Wl,--allow-shlib-undefined
  -C link-arg=-Wl,--thinlto-cache-dir=$(pwd)/target/release/lto-cache
  -C link-arg=-Wl,--thinlto-cache-policy,cache_size_bytes=700m
  -C link-arg=/tmp/memfd_create_shim.o
  -C link-arg=/tmp/glibc_math_shim.o
  -C link-arg=-Wl,--wrap=expf
  -C link-arg=-Wl,--wrap=powf
  -C link-arg=-Wl,--wrap=exp2f
  -C link-arg=-Wl,--wrap=log2f
  -C link-arg=-Wl,--wrap=logf
  --cfg tokio_unstable
  $RUSTFLAGS
__1
CC=/usr/bin/clang-${llvmVersion}
CFLAGS=$CFLAGS
" > $GITHUB_ENV`,
};

const S3Envs: Readonly<Record<string, ConfigValue>> = {
  AWS_ACCESS_KEY_ID: "${{ vars.S3_ACCESS_KEY_ID }}",
  AWS_SECRET_ACCESS_KEY: "${{ secrets.S3_SECRET_ACCESS_KEY }}",
  AWS_ENDPOINT_URL_S3: "${{ vars.S3_ENDPOINT }}",
  AWS_DEFAULT_REGION: "${{ vars.S3_REGION }}",
};

function handleBuildItems(items: {
  skip_pr?: Condition | true;
  skip?: Condition | boolean;
  os: "linux" | "macos" | "windows";
  arch: "x86_64" | "aarch64";
  runner: string | ExpressionValue;
  profile: string;
  use_sysroot?: boolean;
  testRunner?: string | ExpressionValue;
  wpt?: Condition | boolean;
}[]) {
  return items.map(({ skip_pr, ...rest }) => {
    const defaultValues = {
      skip: false,
      "use_sysroot": false,
      wpt: false,
    };
    if (skip_pr == null) {
      return {
        ...defaultValues,
        ...rest,
        save_cache: true,
      };
    } else {
      // on PRs without the ci-full label, use a free runner and skip the job
      const shouldSkip = hasCiFullLabel.not().and(isPr).and(skip_pr);
      return {
        ...defaultValues,
        ...rest,
        testRunner: shouldSkip.then(ubuntuX86Runner).else(
          rest.testRunner ?? rest.runner,
        ),
        runner: shouldSkip.then(ubuntuX86Runner).else(rest.runner),
        skip: shouldSkip,
        // do not save the cache on main if it won't be used by prs most of the time
        save_cache: skip_pr !== true,
      };
    }
  });
}

// shared steps
const cloneRepoStep = step({
  name: "Configure git",
  run: [
    "git config --global core.symlinks true",
    "git config --global fetch.parallel 32",
  ],
}, {
  name: "Clone repository",
  uses: "actions/checkout@v6",
  with: {
    // Use depth > 1, because sometimes we need to rebuild main and if
    // other commits have landed it will become impossible to rebuild if
    // the checkout is too shallow.
    "fetch-depth": 5,
    submodules: false,
  },
});
const cloneSubmodule = (path: string) =>
  step({
    name: `Clone submodule ${path}`,
    run: `git submodule update --init --recursive --depth=1 -- ${path}`,
  });
const cloneStdSubmoduleStep = cloneSubmodule("./tests/util/std");
const installDenoStep = step({
  name: "Install Deno",
  uses: "denoland/setup-deno@v2",
  with: { "deno-version": "v2.x" },
});
const installNodeStep = step({
  name: "Install Node",
  uses: "actions/setup-node@v6",
  with: {
    "node-version": 22,
  },
});

function createRestoreAndSaveCacheSteps(m: {
  name: string;
  cacheKeyPrefix: string;
  path: string[];
}) {
  // this must match for save and restore (https://github.com/actions/cache/issues/1444)
  const path = m.path.join("\n");
  const restoreCacheStep = step({
    name: `Restore cache ${m.name}`,
    uses: "actions/cache/restore@v4",
    with: {
      path,
      key: "never_saved",
      "restore-keys": `${m.cacheKeyPrefix}-`,
    },
  });
  const saveCacheStep = step({
    name: `Cache ${m.name}`,
    uses: "actions/cache/save@v4",
    with: {
      path,
      // We force saving a new cache on every main run so that PRs can
      // always be up to date with the freshest information. We do this
      // unconditionally because we don't want caches that only need updating
      // occassionally (like the cargo home cache) to be lost over time as
      // other caches that need to be updated frequently (like the cargo build
      // cache) get populated and purge old caches.
      key: `${m.cacheKeyPrefix}-\${{ github.sha }}`,
    },
  });
  return { restoreCacheStep, saveCacheStep };
}

function createCargoCacheHomeStep(m: {
  os: ExpressionValue;
  arch: ExpressionValue;
  cachePrefix: string;
}) {
  const steps = createRestoreAndSaveCacheSteps({
    name: "cargo home",
    path: [
      "~/.cargo/.crates.toml",
      "~/.cargo/.crates2.json",
      "~/.cargo/bin",
      "~/.cargo/registry/index",
      "~/.cargo/registry/cache",
      "~/.cargo/git/db",
    ],
    cacheKeyPrefix:
      `${cacheVersion}-cargo-home-${m.os}-${m.arch}-${m.cachePrefix}`,
  });

  return {
    restoreCacheStep: steps.restoreCacheStep.if(isNotTag),
    saveCacheStep: steps.saveCacheStep.if(isMainBranch.and(isNotTag)),
  };
}

// factory for cache steps parameterized by os/arch/profile/job
// works with both defineExprObj (inline values) and defineMatrix (matrix expressions)
function createCacheSteps(m: {
  os: ExpressionValue;
  arch: ExpressionValue;
  profile: ExpressionValue;
  cachePrefix: string;
}) {
  const cargoHomeCacheSteps = createCargoCacheHomeStep(m);
  const buildCacheSteps = createRestoreAndSaveCacheSteps({
    name: "build output",
    path: [
      "./target",
      "!./target/*/gn_out",
      "!./target/*/gn_root",
      "!./target/*/*.zip",
      "!./target/*/*.tar.gz",
    ],
    cacheKeyPrefix:
      `${cacheVersion}-cargo-target-${m.os}-${m.arch}-${m.profile}-${m.cachePrefix}`,
  });
  const mtimeCacheAndRestoreStep = step({
    name: "Apply and update mtime cache",
    uses: "./.github/mtime_cache",
    with: {
      "cache-path": "./target",
    },
  });
  return {
    restoreCacheStep: step(
      cargoHomeCacheSteps.restoreCacheStep,
      buildCacheSteps.restoreCacheStep.if(isMainBranch.not().and(isNotTag)),
      // this should always be done when saving OR restoring
      mtimeCacheAndRestoreStep,
    ),
    saveCacheStep: step(
      cargoHomeCacheSteps.saveCacheStep,
      buildCacheSteps.saveCacheStep.if(isMainBranch.and(isNotTag)),
    ),
  };
}
// Pin rustup-init to 1.28.2: sh.rustup.rs currently serves 1.29.0, which has
// a broken proxy multi-call dispatch (cargo/rustc identify as rustup-init).
// Pre-installing rustup makes `dsherret/rust-toolchain-file@v1`'s internal
// `command -v rustup` check short-circuit and skip the broken curl install.
const installRustStep = step(
  step({
    name: "Pre-install rustup 1.28.2 (workaround broken 1.29.0)",
    shell: "bash",
    run: [
      "if command -v rustup >/dev/null 2>&1; then",
      "  if ! rustup --version 2>&1 | grep -q '1\\.29\\.0'; then exit 0; fi",
      '  echo "Detected broken rustup 1.29.0, replacing with 1.28.2"',
      "fi",
      'case "${RUNNER_OS}-${RUNNER_ARCH}" in',
      "  Linux-X64)     target=x86_64-unknown-linux-gnu; ext= ;;",
      "  Linux-ARM64)   target=aarch64-unknown-linux-gnu; ext= ;;",
      "  macOS-X64)     target=x86_64-apple-darwin; ext= ;;",
      "  macOS-ARM64)   target=aarch64-apple-darwin; ext= ;;",
      "  Windows-X64)   target=x86_64-pc-windows-msvc; ext=.exe ;;",
      "  Windows-ARM64) target=aarch64-pc-windows-msvc; ext=.exe ;;",
      '  *) echo "Unsupported: ${RUNNER_OS}-${RUNNER_ARCH}"; exit 1 ;;',
      "esac",
      "curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused -fsSL \\",
      '  "https://static.rust-lang.org/rustup/archive/1.28.2/${target}/rustup-init${ext}" \\',
      '  -o "rustup-init${ext}"',
      'chmod +x "rustup-init${ext}"',
      '"./rustup-init${ext}" -y --default-toolchain none --no-modify-path',
      'rm "rustup-init${ext}"',
      'echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> "$GITHUB_PATH"',
    ].join("\n"),
  }),
  step({
    uses: "dsherret/rust-toolchain-file@v1",
  }),
);
const installWasmStep = step({
  name: "Install wasm target",
  run: "rustup target add wasm32-unknown-unknown",
});

function getOsSpecificSteps({
  isWindows,
  isMacos,
  isAarch64,
}: {
  isWindows: Condition;
  isMacos: Condition;
  isAarch64: Condition;
}) {
  const installPythonStep = step({
    name: "Install Python",
    uses: "actions/setup-python@v6",
    with: {
      "python-version": 3.11,
    },
  }, {
    name: "Remove unused versions of Python",
    if: isWindows,
    shell: "pwsh",
    run: [
      '$env:PATH -split ";" |',
      '  Where-Object { Test-Path "$_\\python.exe" } |',
      "  Select-Object -Skip 1 |",
      '  ForEach-Object { Move-Item "$_" "$_.disabled" }',
    ],
  });
  const setupPrebuiltMacStep = step({
    if: isMacos,
    env: {
      GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}",
    },
    run: "echo $GITHUB_WORKSPACE/third_party/prebuilt/mac >> $GITHUB_PATH",
  });
  const installLldStep = step
    .dependsOn(
      cloneStdSubmoduleStep,
      installDenoStep,
      setupPrebuiltMacStep,
    )({
      name: "Install macOS aarch64 lld",
      if: isMacos.and(isAarch64),
      env: {
        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}",
      },
      run: "./tools/install_prebuilt.js ld64.lld",
    });
  return {
    installPythonStep,
    setupPrebuiltMacStep,
    installLldStep,
  };
}

// === pre_build job ===
// The pre_build step is used to skip running the CI on draft PRs and to not even
// start the build job. This can be overridden by adding [ci] to the commit title

const preBuildCheckStep = step({
  id: "check",
  if: conditions.hasPrLabel("ci-draft").not(),
  run: [
    "GIT_MESSAGE=$(git log --format=%s -n 1 ${{github.event.after}})",
    "echo Commit message: $GIT_MESSAGE",
    "echo $GIT_MESSAGE | grep '\\[ci\\]' || (echo 'Exiting due to draft PR. Commit with [ci] to bypass or add the ci-draft label.' ; echo 'skip_build=true' >> $GITHUB_OUTPUT)",
  ],
  outputs: ["skip_build"] as const,
});

const denoCoreChangesCheckStep = step({
  id: "deno_core_changes",
  run: [
    // 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,
});

// Detects PRs that only touch the `doc/` directory. Such PRs run the `lint`
// job alone (markdown is still formatted/linted) and skip the build, test,
// bench and deno_core jobs. The base SHA is already fetched by the deno_core
// changes step above.
const docsOnlyChangesCheckStep = step({
  id: "docs_only_changes",
  run: [
    `deno run -A tools/check_docs_only_changes.js \${{ github.event.pull_request.base.sha }}`,
  ],
  outputs: ["docs_only"] as const,
});

const preBuildJob = job("pre_build", {
  name: "pre-build",
  runsOn: "ubuntu-latest",
  steps: step.if(isPr)(
    cloneRepoStep,
    installDenoStep,
    step.if(conditions.isDraftPr())(preBuildCheckStep),
    denoCoreChangesCheckStep,
    docsOnlyChangesCheckStep,
  ),
  outputs: {
    skip_build: preBuildCheckStep.outputs.skip_build,
    skip_deno_core_test: denoCoreChangesCheckStep.outputs.skip_deno_core_test,
    docs_only: docsOnlyChangesCheckStep.outputs.docs_only,
  },
});

// Jobs that compile or test code should not run when a PR only edits docs.
const notDocsOnly = preBuildJob.outputs.docs_only.notEquals("true");

// === build job ===

const buildItems = handleBuildItems([{
  ...Runners.macosX86,
  profile: "debug",
}, {
  ...Runners.macosX86,
  profile: "release",
  skip_pr: true,
}, {
  ...Runners.macosArm,
  profile: "debug",
}, {
  ...Runners.macosArmSelfHosted,
  profile: "release",
  skip_pr: true,
}, {
  ...Runners.windowsX86,
  profile: "debug",
}, {
  ...Runners.windowsX86Xl,
  profile: "release",
  skip_pr: true,
}, {
  ...Runners.windowsArm,
  profile: "debug",
}, {
  ...Runners.windowsArm,
  profile: "release",
  skip_pr: true,
}, {
  ...Runners.linuxX86Xl,
  profile: "release",
  use_sysroot: true,
  // Because CI is so slow on for OSX and Windows, we
  // currently run the Web Platform tests only on Linux.
  wpt: isNotTag,
}, {
  ...Runners.linuxX86,
  profile: "debug",
  use_sysroot: true,
}, {
  ...Runners.linuxArm,
  profile: "debug",
}, {
  ...Runners.linuxArmXl,
  profile: "release",
  use_sysroot: true,
  skip_pr: true,
}]);

const buildJobs = buildItems.map((rawBuildItem) => {
  const buildItem = defineExprObj(rawBuildItem);
  // Linux release artifacts use frame pointers for stack walking, so their
  // DWARF unwind tables can be removed during packaging. Rebuild std to keep
  // the frame-pointer chain intact through Rust code.
  const usesFramePointerPanicTrace = rawBuildItem.profile === "release" &&
    rawBuildItem.os === "linux";
  const buildStdArgs = usesFramePointerPanicTrace
    ? " -Zbuild-std=core,alloc,std,proc_macro,panic_abort"
    : "";
  const panicTraceFeatures = usesFramePointerPanicTrace
    ? "deno/panic-trace-frame-pointer"
    : "deno/panic-trace";
  const usesStartupOrder = rawBuildItem.profile === "release" &&
    ((rawBuildItem.os === "linux" &&
      (rawBuildItem.arch === "x86_64" || rawBuildItem.arch === "aarch64")) ||
      (rawBuildItem.os === "macos" && rawBuildItem.arch === "aarch64"));
  const startupOrderTarget = rawBuildItem.os === "macos"
    ? "aarch64-apple-darwin"
    : `${rawBuildItem.arch}-unknown-linux-gnu`;
  const startupOrderPath =
    `target/release/startup-order-${startupOrderTarget}.order`;
  const isLinux = buildItem.os.equals("linux");
  const isWindows = buildItem.os.equals("windows");
  const isMacos = buildItem.os.equals("macos");
  const profileName = `${buildItem.profile}-${buildItem.os}-${buildItem.arch}`;
  const jobIdForJob = (name: string) => `${name}-${profileName}`;
  const jobNameForJob = (name: string) =>
    `${name} ${buildItem.profile} ${buildItem.os}-${buildItem.arch}`;
  const createBinaryArtifact = (name: string) => {
    const directory = `target/${buildItem.profile}`;
    const exeExt = rawBuildItem.os === "windows" ? ".exe" : "";
    const fileName = `${name}${exeExt}`;
    const artifact = defineArtifact(
      `${profileName}-${name.replaceAll("_", "-")}`,
      {
        retentionDays: 3,
      },
    );
    const filePath = `${directory}/${fileName}`;
    return {
      upload() {
        return artifact.upload({
          path: filePath,
        });
      },
      download() {
        return step(
          artifact.download({
            dirPath: directory,
          }),
          step({
            name: `Set ${filePath} permissions`,
            if: isWindows.not(),
            run: `chmod +x ${filePath}`,
          }),
        );
      },
    };
  };

  const denoArtifact = createBinaryArtifact("deno");
  const denortArtifact = createBinaryArtifact("denort");
  const testServerArtifact = createBinaryArtifact("test_server");
  const env = {
    CARGO_TERM_COLOR: "always",
    RUST_BACKTRACE: "full",
    // disable anyhow's library backtrace
    RUST_LIB_BACKTRACE: 0,
  };
  const defaults = {
    run: {
      // GH actions does not fail fast by default on
      // Windows, so we set bash as the default shell
      shell: "bash",
    },
  };

  const {
    installPythonStep,
    setupPrebuiltMacStep,
    installLldStep,
  } = getOsSpecificSteps({
    isWindows,
    isMacos,
    isAarch64: buildItem.arch.equals("aarch64"),
  });
  const isRelease = buildItem.profile.equals("release");
  const isDebug = buildItem.profile.equals("debug");
  const sysRootStep = step({
    if: buildItem.use_sysroot,
    ...sysRootConfig,
  });
  const buildJob = job(
    jobIdForJob("build"),
    {
      name: jobNameForJob("build"),
      needs: [preBuildJob],
      if: preBuildJob.outputs.skip_build.notEquals("true").and(notDocsOnly),
      runsOn: buildItem.runner,
      // This is required to successfully authenticate with Azure using OIDC for
      // code signing.
      environment: {
        name: isMainOrTag.then("build").else(""),
      },
      timeoutMinutes: 240,
      defaults,
      env,
      steps: (() => {
        const {
          restoreCacheStep,
          saveCacheStep,
        } = createCacheSteps({
          ...buildItem,
          cachePrefix: "build-main",
        });
        const tarSourcePublishStep = step({
          name: "Create source tarballs (release, linux)",
          if: buildItem.os.equals("linux")
            .and(buildItem.arch.equals("x86_64")),
          run: [
            "mkdir -p target/release",
            'tar --exclude=".git*" --exclude=target --exclude=third_party/prebuilt \\',
            "    -czvf target/release/deno_src.tar.gz -C .. deno",
          ],
        });

        const preRelease = step(
          {
            name: "Pre-release (linux)",
            if: isLinux.and(isDenoland),
            run: [
              "cd target/release",
              `./deno -A ../../tools/release/create_symcache.ts deno-${buildItem.arch}-unknown-linux-gnu.symcache`,
              "strip --remove-section=.eh_frame --remove-section=.eh_frame_hdr ./deno",
              "if readelf -SW ./deno | grep -Eq '\\.eh_frame(_hdr)?'; then echo 'unwind sections remain in deno'; exit 1; fi",
              "if readelf -lW ./deno | grep -q GNU_EH_FRAME; then echo 'PT_GNU_EH_FRAME remains in deno'; exit 1; fi",
              `shasum -a 256 deno > deno-${buildItem.arch}-unknown-linux-gnu.sha256sum`,
              `zip -r deno-${buildItem.arch}-unknown-linux-gnu.zip deno`,
              `shasum -a 256 deno-${buildItem.arch}-unknown-linux-gnu.zip > deno-${buildItem.arch}-unknown-linux-gnu.zip.sha256sum`,
              // denort is the `deno compile` base binary: libsui rewrites its
              // ELF to embed user code, and that rewrite drops `.relr.dyn`
              // relative relocations when the symbol table is gone, producing a
              // compiled binary whose C++ static-init guards deadlock
              // (`__cxa_guard_acquire failed to acquire mutex`). Keep .symtab
              // (strip only debug info) so the relocations survive.
              "strip --strip-debug --remove-section=.eh_frame --remove-section=.eh_frame_hdr ./denort",
              "if readelf -SW ./denort | grep -Eq '\\.eh_frame(_hdr)?'; then echo 'unwind sections remain in denort'; exit 1; fi",
              "if readelf -lW ./denort | grep -q GNU_EH_FRAME; then echo 'PT_GNU_EH_FRAME remains in denort'; exit 1; fi",
              `zip -r denort-${buildItem.arch}-unknown-linux-gnu.zip denort`,
              `shasum -a 256 denort-${buildItem.arch}-unknown-linux-gnu.zip > denort-${buildItem.arch}-unknown-linux-gnu.zip.sha256sum`,
              "strip --remove-section=.eh_frame --remove-section=.eh_frame_hdr ./libdenort.so",
              "if readelf -SW ./libdenort.so | grep -Eq '\\.eh_frame(_hdr)?'; then echo 'unwind sections remain in libdenort.so'; exit 1; fi",
              "if readelf -lW ./libdenort.so | grep -q GNU_EH_FRAME; then echo 'PT_GNU_EH_FRAME remains in libdenort.so'; exit 1; fi",
              `zip -r libdenort-${buildItem.arch}-unknown-linux-gnu.zip libdenort.so`,
              `shasum -a 256 libdenort-${buildItem.arch}-unknown-linux-gnu.zip > libdenort-${buildItem.arch}-unknown-linux-gnu.zip.sha256sum`,
              // QuickJS denort/libdenort for `deno compile --engine quickjs`.
              // Reuse the same target dir (a second one would exceed runner
              // disk). Back up the v8 binaries and restore them even if the
              // QuickJS build fails so later release steps never see a partial
              // or wrong-engine runtime.
              "restore_v8_runtimes() {",
              "  test ! -e denort.v8 || mv -f denort.v8 denort",
              "  test ! -e libdenort.v8.so || mv -f libdenort.v8.so libdenort.so",
              "}",
              "trap restore_v8_runtimes EXIT",
              "mv denort denort.v8",
              "mv libdenort.so libdenort.v8.so",
              `(cd ../.. && cargo build --release --locked -p denort -p denort_desktop --no-default-features --features quickjs)`,
              "strip --strip-debug ./denort",
              `zip -r denort-quickjs-${buildItem.arch}-unknown-linux-gnu.zip denort`,
              `shasum -a 256 denort-quickjs-${buildItem.arch}-unknown-linux-gnu.zip > denort-quickjs-${buildItem.arch}-unknown-linux-gnu.zip.sha256sum`,
              "strip ./libdenort.so",
              `zip -r libdenort-quickjs-${buildItem.arch}-unknown-linux-gnu.zip libdenort.so`,
              `shasum -a 256 libdenort-quickjs-${buildItem.arch}-unknown-linux-gnu.zip > libdenort-quickjs-${buildItem.arch}-unknown-linux-gnu.zip.sha256sum`,
              "restore_v8_runtimes",
              "trap - EXIT",
              "./deno types > lib.deno.d.ts",
            ],
          },
          step.dependsOn(setupPrebuiltMacStep, installDenoStep)({
            name: "Install rust-codesign",
            if: buildItem.os.equals("macos").and(isDenoland),
            env: {
              GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}",
            },
            run: "./tools/install_prebuilt.js rcodesign",
          }),
          {
            name: "Pre-release (mac)",
            if: isMacos.and(isDenoland),
            env: {
              "APPLE_CODESIGN_KEY": "${{ secrets.APPLE_CODESIGN_KEY }}",
              "APPLE_CODESIGN_PASSWORD":
                "${{ secrets.APPLE_CODESIGN_PASSWORD }}",
            },
            run: [
              `target/release/deno -A tools/release/create_symcache.ts target/release/deno-${buildItem.arch}-apple-darwin.symcache`,
              "strip -x -S target/release/deno",
              'if [[ "$GITHUB_REF" == "refs/heads/main" || "$GITHUB_REF" == refs/tags/* ]]; then',
              '  echo "Key is $(echo $APPLE_CODESIGN_KEY | base64 -d | wc -c) bytes"',
              "  rcodesign sign target/release/deno " +
              "--code-signature-flags=runtime " +
              '--p12-password="$APPLE_CODESIGN_PASSWORD" ' +
              "--p12-file=<(echo $APPLE_CODESIGN_KEY | base64 -d) " +
              "--entitlements-xml-file=cli/entitlements.plist",
              "fi",
              "cd target/release",
              `shasum -a 256 deno > deno-${buildItem.arch}-apple-darwin.sha256sum`,
              `zip -r deno-${buildItem.arch}-apple-darwin.zip deno`,
              `shasum -a 256 deno-${buildItem.arch}-apple-darwin.zip > deno-${buildItem.arch}-apple-darwin.zip.sha256sum`,
              "strip -x -S ./denort",
              `zip -r denort-${buildItem.arch}-apple-darwin.zip denort`,
              `shasum -a 256 denort-${buildItem.arch}-apple-darwin.zip > denort-${buildItem.arch}-apple-darwin.zip.sha256sum`,
              "strip -x -S ./libdenort.dylib",
              `zip -r libdenort-${buildItem.arch}-apple-darwin.zip libdenort.dylib`,
              `shasum -a 256 libdenort-${buildItem.arch}-apple-darwin.zip > libdenort-${buildItem.arch}-apple-darwin.zip.sha256sum`,
              // QuickJS denort/libdenort for `deno compile --engine quickjs`
              // (see the linux note).
              "restore_v8_runtimes() {",
              "  test ! -e denort.v8 || mv -f denort.v8 denort",
              "  test ! -e libdenort.v8.dylib || mv -f libdenort.v8.dylib libdenort.dylib",
              "}",
              "trap restore_v8_runtimes EXIT",
              "mv denort denort.v8",
              "mv libdenort.dylib libdenort.v8.dylib",
              `(cd ../.. && cargo build --release --locked -p denort -p denort_desktop --no-default-features --features quickjs)`,
              "strip -x -S ./denort",
              `zip -r denort-quickjs-${buildItem.arch}-apple-darwin.zip denort`,
              `shasum -a 256 denort-quickjs-${buildItem.arch}-apple-darwin.zip > denort-quickjs-${buildItem.arch}-apple-darwin.zip.sha256sum`,
              "strip -x -S ./libdenort.dylib",
              `zip -r libdenort-quickjs-${buildItem.arch}-apple-darwin.zip libdenort.dylib`,
              `shasum -a 256 libdenort-quickjs-${buildItem.arch}-apple-darwin.zip > libdenort-quickjs-${buildItem.arch}-apple-darwin.zip.sha256sum`,
              "restore_v8_runtimes",
              "trap - EXIT",
            ],
          },
          {
            // Note: Azure OIDC credentials are only valid for 5 minutes, so
            // authentication must be done right before signing.
            name: "Authenticate with Azure (windows)",
            if: isWindows.and(isDenoland).and(isMainOrTag),
            uses: "azure/login@v2",
            with: {
              "client-id": "${{ secrets.AZURE_CLIENT_ID }}",
              "tenant-id": "${{ secrets.AZURE_TENANT_ID }}",
              "subscription-id": "${{ secrets.AZURE_SUBSCRIPTION_ID }}",
              "enable-AzPSSession": true,
            },
          },
          {
            name: "Code sign deno.exe (windows)",
            if: isWindows.and(isDenoland).and(isMainOrTag),
            uses: "Azure/artifact-signing-action@v0",
            with: {
              "endpoint": "https://eus.codesigning.azure.net/",
              "trusted-signing-account-name": "deno-cli-code-signing",
              "certificate-profile-name": "deno-cli-code-signing-cert",
              "files-folder": "target/release",
              "files-folder-filter": "deno.exe",
              "file-digest": "SHA256",
              "timestamp-rfc3161": "http://timestamp.acs.microsoft.com",
              "timestamp-digest": "SHA256",
              "exclude-environment-credential": true,
              "exclude-workload-identity-credential": true,
              "exclude-managed-identity-credential": true,
              "exclude-shared-token-cache-credential": true,
              "exclude-visual-studio-credential": true,
              "exclude-visual-studio-code-credential": true,
              "exclude-azure-cli-credential": false,
            },
          },
          {
            name: "Verify signature (windows)",
            if: isWindows.and(isDenoland).and(isMainOrTag),
            shell: "pwsh",
            run: [
              '$SignTool = Get-ChildItem -Path "C:\\Program Files*\\Windows Kits\\*\\bin\\*\\x64\\signtool.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1',
              "$SignToolPath = $SignTool.FullName",
              "& $SignToolPath verify /pa /v target\\release\\deno.exe",
            ],
          },
          {
            name: "Pre-release (windows)",
            if: isWindows.and(isDenoland),
            shell: "pwsh",
            run: [
              `Get-FileHash target/release/deno.exe -Algorithm SHA256 | Format-List > target/release/deno-${buildItem.arch}-pc-windows-msvc.sha256sum`,
              `Compress-Archive -CompressionLevel Optimal -Force -Path target/release/deno.exe -DestinationPath target/release/deno-${buildItem.arch}-pc-windows-msvc.zip`,
              `Get-FileHash target/release/deno-${buildItem.arch}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/deno-${buildItem.arch}-pc-windows-msvc.zip.sha256sum`,
              `Compress-Archive -CompressionLevel Optimal -Force -Path target/release/denort.exe -DestinationPath target/release/denort-${buildItem.arch}-pc-windows-msvc.zip`,
              `Get-FileHash target/release/denort-${buildItem.arch}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/denort-${buildItem.arch}-pc-windows-msvc.zip.sha256sum`,
              `Compress-Archive -CompressionLevel Optimal -Force -Path target/release/denort.dll -DestinationPath target/release/libdenort-${buildItem.arch}-pc-windows-msvc.zip`,
              `Get-FileHash target/release/libdenort-${buildItem.arch}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/libdenort-${buildItem.arch}-pc-windows-msvc.zip.sha256sum`,
              // QuickJS denort/libdenort for `deno compile --engine quickjs`
              // (see the linux note).
              "try {",
              "  Move-Item target/release/denort.exe target/release/denort.v8.exe",
              "  Move-Item target/release/denort.dll target/release/denort.v8.dll",
              // Both packages produce denort.pdb on Windows. Building them in
              // one Cargo invocation lets their linkers race to write it.
              `  cargo build --release --locked -p denort --no-default-features --features quickjs`,
              '  if ($LASTEXITCODE -ne 0) { throw "QuickJS denort build failed" }',
              `  cargo build --release --locked -p denort_desktop --no-default-features --features quickjs`,
              '  if ($LASTEXITCODE -ne 0) { throw "QuickJS denort_desktop build failed" }',
              `  Compress-Archive -CompressionLevel Optimal -Force -Path target/release/denort.exe -DestinationPath target/release/denort-quickjs-${buildItem.arch}-pc-windows-msvc.zip`,
              `  Get-FileHash target/release/denort-quickjs-${buildItem.arch}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/denort-quickjs-${buildItem.arch}-pc-windows-msvc.zip.sha256sum`,
              `  Compress-Archive -CompressionLevel Optimal -Force -Path target/release/denort.dll -DestinationPath target/release/libdenort-quickjs-${buildItem.arch}-pc-windows-msvc.zip`,
              `  Get-FileHash target/release/libdenort-quickjs-${buildItem.arch}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/libdenort-quickjs-${buildItem.arch}-pc-windows-msvc.zip.sha256sum`,
              "} finally {",
              "  if (Test-Path target/release/denort.v8.exe) { Move-Item -Force target/release/denort.v8.exe target/release/denort.exe }",
              "  if (Test-Path target/release/denort.v8.dll) { Move-Item -Force target/release/denort.v8.dll target/release/denort.dll }",
              "}",
              `target/release/deno.exe -A tools/release/create_symcache.ts target/release/deno-${buildItem.arch}-pc-windows-msvc.symcache`,
            ],
          },
          {
            name: "Build bsdiff helper",
            if: isDenoland.and(isTag),
            run: [
              "cargo build --release -p bsdiff_helper",
            ],
          },
          {
            name: "Generate delta patch (linux)",
            if: isLinux.and(isDenoland).and(isTag),
            run: [
              `TARGET="${buildItem.arch}-unknown-linux-gnu"`,
              'PREV_VERSION=$(curl -sf https://dl.deno.land/release-latest.txt | tr -d "v\\n") || true',
              'if [ -z "$PREV_VERSION" ]; then echo "No previous version found, skipping delta"; exit 0; fi',
              'echo "Generating delta from $PREV_VERSION for $TARGET"',
              'curl -fSL -o prev.zip "https://github.com/denoland/deno/releases/download/v${PREV_VERSION}/deno-${TARGET}.zip" || { echo "Previous release not found, skipping delta"; exit 0; }',
              "unzip -o prev.zip -d prev/",
              './target/release/bsdiff_helper prev/deno target/release/deno "target/release/deno-${TARGET}.from-${PREV_VERSION}.bsdiff"',
              'cd target/release && shasum -a 256 "deno-${TARGET}.from-${PREV_VERSION}.bsdiff" > "deno-${TARGET}.from-${PREV_VERSION}.bsdiff.sha256sum"',
            ],
          },
          {
            name: "Generate delta patch (mac)",
            if: isMacos.and(isDenoland).and(isTag),
            run: [
              `TARGET="${buildItem.arch}-apple-darwin"`,
              'PREV_VERSION=$(curl -sf https://dl.deno.land/release-latest.txt | tr -d "v\\n") || true',
              'if [ -z "$PREV_VERSION" ]; then echo "No previous version found, skipping delta"; exit 0; fi',
              'echo "Generating delta from $PREV_VERSION for $TARGET"',
              'curl -fSL -o prev.zip "https://github.com/denoland/deno/releases/download/v${PREV_VERSION}/deno-${TARGET}.zip" || { echo "Previous release not found, skipping delta"; exit 0; }',
              "unzip -o prev.zip -d prev/",
              './target/release/bsdiff_helper prev/deno target/release/deno "target/release/deno-${TARGET}.from-${PREV_VERSION}.bsdiff"',
              'cd target/release && shasum -a 256 "deno-${TARGET}.from-${PREV_VERSION}.bsdiff" > "deno-${TARGET}.from-${PREV_VERSION}.bsdiff.sha256sum"',
            ],
          },
          {
            name: "Generate delta patch (windows)",
            if: isWindows.and(isDenoland).and(isTag),
            shell: "pwsh",
            run: [
              `$Target = "${buildItem.arch}-pc-windows-msvc"`,
              '$PrevVersion = (Invoke-RestMethod https://dl.deno.land/release-latest.txt).Trim() -replace "^v", ""',
              'if (-not $PrevVersion) { Write-Host "No previous version found, skipping delta"; exit 0 }',
              'Write-Host "Generating delta from $PrevVersion for $Target"',
              'try { Invoke-WebRequest -Uri "https://github.com/denoland/deno/releases/download/v$PrevVersion/deno-$Target.zip" -OutFile prev.zip } catch { Write-Host "Previous release not found, skipping delta"; exit 0 }',
              "Remove-Item -Recurse -Force prev -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path prev | Out-Null",
              "Expand-Archive -Force -Path prev.zip -DestinationPath prev",
              '& .\\target\\release\\bsdiff_helper.exe "prev\\deno.exe" "target\\release\\deno.exe" "target\\release\\deno-$Target.from-$PrevVersion.bsdiff"',
              'Get-FileHash "target\\release\\deno-$Target.from-$PrevVersion.bsdiff" -Algorithm SHA256 | Format-List > "target\\release\\deno-$Target.from-$PrevVersion.bsdiff.sha256sum"',
            ],
          },
          step({
            name: "Upload canary to dl.deno.land",
            if: isDenoland.and(isMainBranch),
            env: S3Envs,
            run: [
              'aws s3 sync ./target/release/ s3://dl-deno-land/canary/$(git rev-parse HEAD)/ --exclude "*" --include "*.zip"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/canary/$(git rev-parse HEAD)/ --exclude "*" --include "*.sha256sum"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/canary/$(git rev-parse HEAD)/ --exclude "*" --include "*.symcache"',
              "echo ${{ github.sha }} > canary-latest.txt",
              'aws s3 cp canary-latest.txt s3://dl-deno-land/canary-$(rustc -vV | sed -n "s|host: ||p")-latest.txt',
              "rm canary-latest.txt",
            ],
          }),
        );
        const packagesToBuild = ["deno", "denort", "test_server"]
          .map((name) => `-p ${name}`).join(" ");
        const binsToBuild = ["deno", "denort", "test_server"]
          .map((name) => `--bin ${name}`).join(" ");
        const cargoBuildReleaseCommand =
          `cargo build${buildStdArgs} --release --locked ${packagesToBuild} ${binsToBuild} --features=${panicTraceFeatures}`;
        const cargoBuildReleaseStep = step
          .if(
            isRelease.and(isDenoland.or(buildItem.use_sysroot)),
          )
          .dependsOn(
            installLldStep,
            installDenoStep,
            restoreCacheStep,
            installRustStep,
            sysRootStep,
          )(
            {
              // do this on PRs as well as main so that PRs can use the cargo build cache from main
              name: "Configure canary build",
              if: isNotTag,
              run: 'echo "DENO_CANARY=true" >> $GITHUB_ENV',
            },
            ...(usesFramePointerPanicTrace
              ? [{
                name: "Configure frame-pointer panic traces",
                run: [
                  // `build-std` is unstable, but keeping Deno's pinned stable
                  // compiler plus this narrow opt-in is preferable to moving
                  // release builds to a separate nightly toolchain.
                  'echo "RUSTC_BOOTSTRAP=1" >> "$GITHUB_ENV"',
                  // `RUSTFLAGS` is a multi-line value, but cargo splits it on
                  // spaces only (trimming each piece), so a newline is not a
                  // separator. Append on the same line as the tail of the
                  // existing value, otherwise the added flags fuse into one
                  // bogus token.
                  "{",
                  '  echo "RUSTFLAGS<<__DENO_RUSTFLAGS"',
                  '  echo "$RUSTFLAGS -C force-frame-pointers=yes -C force-unwind-tables=no -C link-arg=-Wl,--no-eh-frame-hdr"',
                  '  echo "__DENO_RUSTFLAGS"',
                  '} >> "$GITHUB_ENV"',
                ],
              }]
              : []),
            {
              name: "Build release",
              env: {
                DENO_SNAPSHOT_MINIFY_SOURCES: "1",
              },
              run: [
                // On macOS aarch64, link through lzld so system frameworks
                // (CoreFoundation/Foundation/Security/CoreServices/Metal/...) are
                // dlopen'd on first use instead of loaded at launch, cutting dyld
                // startup cost. lzld needs an absolute -fuse-ld path (Apple clang
                // rejects relative ones), so patch it in here rather than in the
                // committed .cargo/config.toml. See tools/lzld.
                'if [ "$(uname -s)" = Darwin ] && [ "$(uname -m)" = arm64 ]; then',
                "  git submodule update --init tools/lzld",
                "  make -C tools/lzld",
                "  sed -i '' \"s#-fuse-ld=lld #-fuse-ld=$GITHUB_WORKSPACE/tools/lzld/lzld -L$GITHUB_WORKSPACE/tools/lzld -llzld_arm64 #\" .cargo/config.toml",
                "  grep -n fuse-ld .cargo/config.toml",
                "fi",
                // output fs space before and after building
                "df -h",
                cargoBuildReleaseCommand,
                // Build the desktop runtime shared library (libdenort cdylib) for
                // laufey-based desktop apps. It is a separate invocation, but
                // still needs the frame-pointer standard library before its
                // unwind sections can be removed during packaging.
                `cargo build${buildStdArgs} --release --locked -p denort_desktop`,
                "df -h",
              ],
            },
            ...(usesStartupOrder
              ? [{
                name: "Trace startup order",
                run: rawBuildItem.os === "macos"
                  ? [
                    "cp -p target/release/deno target/release/deno-before-startup-order",
                    "target/release/deno run -A tools/startup_order/generate_macos_function_orderfile.ts \\",
                    "  --binary $GITHUB_WORKSPACE/target/release/deno-before-startup-order \\",
                    `  --output $GITHUB_WORKSPACE/${startupOrderPath} \\`,
                    "  --repeats 3 \\",
                    "  --workload-profile run-first",
                  ]
                  : [
                    "cp -p target/release/deno target/release/deno-before-startup-order",
                    "target/release/deno run -A tools/startup_order/generate_linux_function_orderfile.ts \\",
                    "  --binary $GITHUB_WORKSPACE/target/release/deno-before-startup-order \\",
                    `  --output $GITHUB_WORKSPACE/${startupOrderPath} \\`,
                    "  --repeats 3 \\",
                    "  --workload-profile run-first",
                  ],
                env: { NO_COLOR: 1 },
              }, {
                name: "Relink release deno with startup order",
                run: cargoBuildReleaseCommand,
                env: {
                  DENO_SNAPSHOT_MINIFY_SOURCES: "1",
                  DENO_USE_STARTUP_ORDER: "1",
                  DENO_STARTUP_ORDER_FILE:
                    `\${{ github.workspace }}/${startupOrderPath}`,
                },
              }, {
                name: "Verify startup order",
                run: [
                  "target/release/deno run -A tools/startup_order/verify_orderfile.ts \\",
                  "  --baseline-binary target/release/deno-before-startup-order \\",
                  "  --binary target/release/deno \\",
                  `  --order ${startupOrderPath} \\`,
                  `  --output ${startupOrderPath}.verify.json`,
                ],
                env: { NO_COLOR: 1 },
              }, {
                name: "Upload startup order",
                uses: "actions/upload-artifact@v6",
                if: conditions.status.always(),
                with: {
                  name: `startup-order-${profileName}`,
                  path: [
                    startupOrderPath,
                    `${startupOrderPath}.json`,
                    ...(rawBuildItem.os === "linux"
                      ? [`${startupOrderPath}.starts.json`]
                      : []),
                    `${startupOrderPath}.verify.json`,
                  ].join("\n"),
                  "retention-days": 7,
                  "if-no-files-found": "warn",
                },
              }]
              : []),
            {
              name: "Check release snapshot flags",
              if: isLinux,
              run: [
                "if strings target/release/deno | grep -F -- '--no-lazy --no-lazy-eval --no-lazy-streaming'; then",
                '  echo "release deno binary contains eager snapshot flags"',
                "  exit 1",
                "fi",
              ],
            },
            {
              // Eager bootstrap modules must lazy-load node:/heavy closures via
              // core.createLazyLoader, never a static `import`. A static import
              // pulls the module's whole transitive closure into the startup
              // snapshot (e.g. `import "node:buffer"` dragged ~22 node internal
              // modules / ~700 SFIs in). Keep them out.
              name: "Check eager bootstrap does not static-import node:",
              if: isLinux,
              run: [
                "if grep -rEn '^import .* from \"node:' runtime/js/; then",
                '  echo "eager bootstrap statically imports a node: module — use core.createLazyLoader so it stays out of the startup snapshot"',
                "  exit 1",
                "fi",
              ],
            },
            {
              name: "Generate symcache",
              run: [
                "target/release/deno -A tools/release/create_symcache.ts ./deno.symcache",
                "du -h deno.symcache",
                "du -h target/release/deno",
              ],
              env: { NO_COLOR: 1 },
            },
            preRelease,
            {
              name: "Build product size info",
              if: isMainOrTag,
              run: [
                `du -hd1 "./target/${buildItem.profile}"`,
                `du -ha  "./target/${buildItem.profile}/deno"`,
                `du -ha  "./target/${buildItem.profile}/denort"`,
              ],
            },
          );
        const cargoBuildStep = step
          .dependsOn(
            installLldStep,
            restoreCacheStep,
            installRustStep,
            sysRootStep,
          )
          .comesAfter(tarSourcePublishStep)(
            {
              name: "Build debug",
              if: isDebug,
              run:
                `cargo build --locked ${packagesToBuild} ${binsToBuild} --features=deno/panic-trace`,
              env: { CARGO_PROFILE_DEV_DEBUG: 0 },
            },
            {
              // The rest of CI only exercises the default v8 backend. Make sure the
              // experimental QuickJS backend (the deno_v8 facade over the v8x crate)
              // keeps compiling for the deno + denort binaries so `deno compile`
              // and the desktop runtime don't silently regress (notably the
              // mutually-exclusive v8/quickjs feature guard). A `check` is enough to
              // catch feature/build-script breakage and does not clobber the v8
              // binary this job produces.
              name: "Check QuickJS backend (deno + denort)",
              if: isDebug.and(
                isLinux.and(buildItem.arch.equals("x86_64")).or(
                  isMacos.and(buildItem.arch.equals("aarch64")),
                ),
              ),
              run:
                `cargo check --locked -p deno -p denort --no-default-features --features quickjs`,
              env: { CARGO_PROFILE_DEV_DEBUG: 0 },
            },
            cargoBuildReleaseStep,
            {
              // Run a minimal check to ensure that binary is not corrupted, regardless
              // of our build mode
              name: "Check deno binary",
              run:
                `target/${buildItem.profile}/deno eval "console.log(1+2)" | grep 3`,
              env: { NO_COLOR: 1 },
            },
            {
              // Verify that the binary actually works in the Ubuntu-16.04 sysroot.
              name: "Check deno binary (in sysroot)",
              if: buildItem.use_sysroot,
              run:
                `sudo chroot /sysroot "$(pwd)/target/${buildItem.profile}/deno" --version`,
            },
            denoArtifact.upload(),
            denortArtifact.upload(),
            testServerArtifact.upload(),
          );

        const shouldPublishCondition = isRelease.and(isDenoland)
          .and(isTag);
        const publishStep = step.if(shouldPublishCondition)(
          step({
            name: "Upload release to dl.deno.land (unix)",
            if: isWindows.not(),
            env: S3Envs,
            run: [
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.zip"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.sha256sum"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.symcache"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.bsdiff"',
            ],
          }, {
            name: "Upload release to dl.deno.land (windows)",
            if: isWindows,
            env: {
              ...S3Envs,
              CLOUDSDK_PYTHON: "${{env.pythonLocation}}\\python.exe",
            },
            run: [
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.zip"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.sha256sum"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.symcache"',
              'aws s3 sync ./target/release/ s3://dl-deno-land/release/${GITHUB_REF#refs/*/}/ --exclude "*" --include "*.bsdiff"',
            ],
          }),
          {
            name: "Create release notes",
            run: [
              "export PATH=$PATH:$(pwd)/target/release",
              "./tools/release/05_create_release_notes.ts",
            ],
          },
          {
            name: "Upload release to GitHub",
            uses: "softprops/action-gh-release@v2",
            env: {
              GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}",
            },
            with: {
              files: [
                "target/release/deno-x86_64-pc-windows-msvc.zip",
                "target/release/deno-x86_64-pc-windows-msvc.zip.sha256sum",
                "target/release/deno-x86_64-pc-windows-msvc.sha256sum",
                "target/release/denort-x86_64-pc-windows-msvc.zip",
                "target/release/denort-x86_64-pc-windows-msvc.zip.sha256sum",
                "target/release/libdenort-x86_64-pc-windows-msvc.zip",
                "target/release/libdenort-x86_64-pc-windows-msvc.zip.sha256sum",
                "target/release/deno-aarch64-pc-windows-msvc.zip",
                "target/release/deno-aarch64-pc-windows-msvc.zip.sha256sum",
                "target/release/deno-aarch64-pc-windows-msvc.sha256sum",
                "target/release/denort-aarch64-pc-windows-msvc.zip",
                "target/release/denort-aarch64-pc-windows-msvc.zip.sha256sum",
                "target/release/libdenort-aarch64-pc-windows-msvc.zip",
                "target/release/libdenort-aarch64-pc-windows-msvc.zip.sha256sum",
                "target/release/deno-x86_64-unknown-linux-gnu.zip",
                "target/release/deno-x86_64-unknown-linux-gnu.zip.sha256sum",
                "target/release/deno-x86_64-unknown-linux-gnu.sha256sum",
                "target/release/denort-x86_64-unknown-linux-gnu.zip",
                "target/release/denort-x86_64-unknown-linux-gnu.zip.sha256sum",
                "target/release/libdenort-x86_64-unknown-linux-gnu.zip",
                "target/release/libdenort-x86_64-unknown-linux-gnu.zip.sha256sum",
                "target/release/deno-x86_64-apple-darwin.zip",
                "target/release/deno-x86_64-apple-darwin.zip.sha256sum",
                "target/release/deno-x86_64-apple-darwin.sha256sum",
                "target/release/denort-x86_64-apple-darwin.zip",
                "target/release/denort-x86_64-apple-darwin.zip.sha256sum",
                "target/release/libdenort-x86_64-apple-darwin.zip",
                "target/release/libdenort-x86_64-apple-darwin.zip.sha256sum",
                "target/release/deno-aarch64-unknown-linux-gnu.zip",
                "target/release/deno-aarch64-unknown-linux-gnu.zip.sha256sum",
                "target/release/deno-aarch64-unknown-linux-gnu.sha256sum",
                "target/release/denort-aarch64-unknown-linux-gnu.zip",
                "target/release/denort-aarch64-unknown-linux-gnu.zip.sha256sum",
                "target/release/libdenort-aarch64-unknown-linux-gnu.zip",
                "target/release/libdenort-aarch64-unknown-linux-gnu.zip.sha256sum",
                "target/release/deno-aarch64-apple-darwin.zip",
                "target/release/deno-aarch64-apple-darwin.zip.sha256sum",
                "target/release/deno-aarch64-apple-darwin.sha256sum",
                "target/release/denort-aarch64-apple-darwin.zip",
                "target/release/denort-aarch64-apple-darwin.zip.sha256sum",
                "target/release/libdenort-aarch64-apple-darwin.zip",
                "target/release/libdenort-aarch64-apple-darwin.zip.sha256sum",
                "target/release/deno_src.tar.gz",
                "target/release/lib.deno.d.ts",
                "target/release/deno-*.bsdiff",
                "target/release/deno-*.bsdiff.sha256sum",
              ].join("\n"),
              body_path: "target/release/release-notes.md",
              draft: true,
              // Flag pre-release tags (e.g. -alpha./-beta./-rc.) as pre-releases
              // so they are not marked "Latest" when the draft is published.
              prerelease: "${{ contains(github.ref_name, '-') }}",
            },
          },
        );

        return step.if(buildItem.skip.not())(
          cloneRepoStep,
          cloneStdSubmoduleStep,
          // ensure this happens right after cloning
          tarSourcePublishStep.if(shouldPublishCondition),
          {
            name: "Remove macOS cURL --ipv4 flag",
            run: [
              // cURL's --ipv4 flag is busted for now
              "curl --version",
              "which curl",
              "cat /etc/hosts",
              "rm ~/.curlrc || true",
            ],
            if: buildItem.os.equals("macos"),
          },
          step({
            name: "Log versions",
            run: [
              "echo '*** Python'",
              "command -v python && python --version || echo 'No python found or bad executable'",
              "echo '*** Rust'",
              "command -v rustc && rustc --version || echo 'No rustc found or bad executable'",
              "echo '*** Cargo'",
              "command -v cargo && cargo --version || echo 'No cargo found or bad executable'",
              "echo '*** Deno'",
              "command -v deno && deno --version || echo 'No deno found or bad executable'",
              "echo '*** Node'",
              "command -v node && node --version || echo 'No node found or bad executable'",
              "echo '*** Installed packages'",
              "command -v dpkg && dpkg -l || echo 'No dpkg found or bad executable'",
            ],
          }).comesAfter(
            installDenoStep,
            installNodeStep,
            installPythonStep,
            installRustStep,
          ),
          cargoBuildStep,
          publishStep,
          saveCacheStep.if(buildItem.save_cache),
        );
      })(),
    },
  );

  const additionalJobs = [];

  {
    const shardedCrates = new Map([
      ["specs", 2],
      ["integration", 2],
      ["node_compat", 3],
    ]);
    const testMatrix = defineMatrix({
      include: testCrates.flatMap((tc) => {
        const total = shardedCrates.get(tc.name) ?? 1;
        return Array.from({ length: total }, (_, i) => ({
          test_crate: tc.name,
          test_package: tc.package,
          // make these strings so index isn't falsy when 0
          shard_index: i.toString(),
          shard_total: total.toString(),
          shard_label: total > 1 ? `(${i + 1}/${total}) ` : "",
        }));
      }),
    });
    const testCrateNameExpr = testMatrix.test_crate;
    const {
      restoreCacheStep,
      saveCacheStep,
    } = createCacheSteps({
      ...buildItem,
      cachePrefix: `test-${testCrateNameExpr}`,
    });
    // shard_index > 0 jobs only run on PRs (main runs unsharded)
    const isShardZero = testMatrix.shard_index.equals(0);
    const shouldRunShard = isShardZero.or(isPr);
    // Some test shards can finish close to the default 30m job timeout
    // and get cancelled during harness shutdown.
    const timeoutMinutes = ((rawBuildItem.profile === "debug" &&
        ((rawBuildItem.os === "windows" &&
          rawBuildItem.arch === "aarch64") ||
          (rawBuildItem.os === "macos" &&
            rawBuildItem.arch === "x86_64"))) ||
        (rawBuildItem.os === "linux" &&
          rawBuildItem.arch === "x86_64"))
      ? 60
      : 30;
    additionalJobs.push(job(
      jobIdForJob("test"),
      {
        name:
          `test ${testMatrix.test_crate} ${testMatrix.shard_label}${buildItem.profile} ${buildItem.os}-${buildItem.arch}`,
        needs: [buildJob],
        runsOn: buildItem.testRunner ?? buildItem.runner,
        timeoutMinutes,
        defaults,
        env,
        strategy: {
          matrix: testMatrix,
          failFast: false,
        },
        steps: step.if(isNotTag.and(buildItem.skip.not()).and(shouldRunShard))(
          cloneRepoStep,
          cloneSubmodule("./tests/node_compat/runner/suite")
            .if(testCrateNameExpr.equals("node_compat")),
          cloneStdSubmoduleStep,
          restoreCacheStep,
          installNodeStep,
          installRustStep,
          installLldStep,
          sysRootStep,
          denoArtifact.download(),
          denortArtifact.download().if(
            testCrateNameExpr.equals("integration")
              .or(testCrateNameExpr.equals("specs")),
          ),
          testServerArtifact.download().if(
            testCrateNameExpr.equals("integration")
              .or(testCrateNameExpr.equals("specs"))
              .or(testCrateNameExpr.equals("unit"))
              .or(testCrateNameExpr.equals("unit_node")),
          ),
          {
            name: "Set up playwright cache",
            uses: "actions/cache@v5",
            with: {
              path: "./.ms-playwright",
              key: "playwright-${{ runner.os }}-${{ runner.arch }}",
            },
          },
          {
            if: buildItem.os.equals("linux").and(
              buildItem.arch.equals("aarch64"),
            ),
            name: "Load 'vsock_loopback; kernel module",
            run: "sudo modprobe vsock_loopback",
          },
          {
            name: "Build ffi (debug)",
            if: isDebug.and(testCrateNameExpr.equals("specs")),
            run: "cargo build -p test_ffi",
          },
          {
            name: "Build ffi (release)",
            if: isRelease.and(testCrateNameExpr.equals("specs")),
            run: "cargo build --release -p test_ffi",
          },
          {
            name: "Test (debug)",
            if: isDebug,
            run:
              `cargo test -p ${testMatrix.test_package} --test ${testMatrix.test_crate}`,
            env: {
              CARGO_PROFILE_DEV_DEBUG: 0,
              CI_SHARD_INDEX: isPr.then(testMatrix.shard_index).else(""),
              CI_SHARD_TOTAL: isPr.then(testMatrix.shard_total).else(""),
            },
          },
          {
            name: "Test (release)",
            if: isRelease.and(
              isDenoland.or(buildItem.use_sysroot),
            ),
            run:
              `cargo test -p ${testMatrix.test_package} --test ${testMatrix.test_crate} --release`,
            env: {
              CI_SHARD_INDEX: isPr.then(testMatrix.shard_index).else(""),
              CI_SHARD_TOTAL: isPr.then(testMatrix.shard_total).else(""),
            },
          },
          {
            name: "Ensure no git changes",
            if: isPr,
            run: [
              'if [[ -n "$(git status --porcelain)" ]]; then',
              'echo "❌ Git working directory is dirty. Ensure `cargo test` is not modifying git tracked files."',
              'echo ""',
              'echo "📋 Status:"',
              "git status",
              'echo ""',
              "exit 1",
              "fi",
            ],
          },
          {
            name: "Upload test results",
            uses: "actions/upload-artifact@v6",
            if: conditions.status.always().and(isNotTag),
            with: {
              name:
                `test-results-${buildItem.os}-${buildItem.arch}-${buildItem.profile}-${testMatrix.test_crate}${
                  testMatrix.shard_total.greaterThan(1).then(
                    literal("-shard-").concat(testMatrix.shard_index),
                  ).else("")
                }.json`,
              path: `target/test_results_${testMatrix.test_crate}.json`,
            },
          },
          saveCacheStep.if(buildItem.save_cache),
        ),
      },
    ));
  }

  const libsCondition = isDebug.and(
    // aarc64 runner seems faster than x86
    isLinux.and(buildItem.arch.equals("aarch64"))
      .or(isMacos.and(buildItem.arch.equals("aarch64")))
      .or(isWindows.and(buildItem.arch.equals("x86_64"))),
  );
  if (libsCondition.isPossiblyTrue()) {
    const {
      restoreCacheStep,
      saveCacheStep,
    } = createCacheSteps({
      ...buildItem,
      cachePrefix: "test-libs",
    });
    additionalJobs.push(job(jobIdForJob("test-libs"), {
      name: jobNameForJob("test libs"),
      needs: [buildJob],
      runsOn: buildItem.testRunner ?? buildItem.runner,
      timeoutMinutes: 30,
      steps: step.if(isNotTag.and(buildItem.skip.not()))(
        cloneRepoStep,
        restoreCacheStep,
        installNodeStep,
        installRustStep,
        installLldStep,
        sysRootStep,
        denoArtifact.download(),
        testServerArtifact.download(),
        {
          name: "Test libs",
          run: `cargo test --locked --lib ${
            [...binCrates, ...libCrates].map((p) => `-p ${p}`).join(" ")
          }`,
          env: { CARGO_PROFILE_DEV_DEBUG: 0 },
        },
        saveCacheStep,
      ),
    }));
  }
  if (
    isDebug.and(isLinux).and(buildItem.arch.equals("x86_64")).isPossiblyTrue()
  ) {
    const {
      restoreCacheStep,
      saveCacheStep,
    } = createCacheSteps({
      ...buildItem,
      cachePrefix: "build-libs",
    });
    additionalJobs.push(job(jobIdForJob("build-libs"), {
      name: jobNameForJob("build libs"),
      needs: [preBuildJob],
      if: preBuildJob.outputs.skip_build.notEquals("true").and(notDocsOnly),
      runsOn: buildItem.runner,
      timeoutMinutes: 30,
      steps: step.if(isNotTag.and(buildItem.skip.not()))(
        cloneRepoStep,
        installRustStep,
        restoreCacheStep,
        installWasmStep,
        // we want these crates to be Wasm compatible
        {
          name: "Cargo check (deno_resolver)",
          run:
            "cargo check --target wasm32-unknown-unknown -p deno_resolver && cargo check --target wasm32-unknown-unknown -p deno_resolver --features graph && cargo check --target wasm32-unknown-unknown -p deno_resolver --features graph --features deno_ast",
        },
        {
          name: "Cargo check (deno_npm_installer)",
          run:
            "cargo check --target wasm32-unknown-unknown -p deno_npm_installer",
        },
        {
          name: "Cargo check (deno_config)",
          run: [
            "cargo check --no-default-features -p deno_config",
            "cargo check --no-default-features --features workspace -p deno_config",
            "cargo check --no-default-features --features package_json -p deno_config",
            "cargo check --no-default-features --features workspace --features sync -p deno_config",
            "cargo check --target wasm32-unknown-unknown --all-features -p deno_config",
            "cargo check -p deno --features=lsp-tracing",
          ],
        },
        saveCacheStep,
      ),
    }));
  }

  if (buildItem.wpt.isPossiblyTrue()) {
    const buildCacheSteps = createRestoreAndSaveCacheSteps({
      name: "wpt and autobahn test run hashes",
      path: [
        "./target/wpt_input_hash",
        "./target/autobahn_input_hash",
      ],
      cacheKeyPrefix:
        `${cacheVersion}-wpt-target-${buildItem.os}-${buildItem.arch}-${buildItem.profile}`,
    });
    additionalJobs.push(job(
      jobIdForJob("wpt"),
      {
        name: jobNameForJob("wpt"),
        needs: [buildJob],
        runsOn: buildItem.testRunner ?? buildItem.runner,
        timeoutMinutes: 30,
        defaults,
        env,
        steps: step.if(isNotTag.and(buildItem.skip.not()))(
          cloneRepoStep,
          cloneStdSubmoduleStep,
          cloneSubmodule("./tests/wpt/suite"),
          buildCacheSteps.restoreCacheStep,
          installDenoStep,
          installPythonStep,
          denoArtifact.download(),
          {
            name: "Configure hosts file for WPT",
            run: "./wpt make-hosts-file | sudo tee -a /etc/hosts",
            workingDirectory: "tests/wpt/suite/",
          },
          {
            name: "Run web platform tests (debug)",
            if: isDebug,
            env: { DENO_BIN: "./target/debug/deno" },
            run: [
              "deno run -RWNE --allow-run --lock=tools/deno.lock.json --config tests/config/deno.json \\",
              "    ./tests/wpt/wpt.ts setup",
              "deno run -RWNE --allow-run --lock=tools/deno.lock.json --config tests/config/deno.json --unsafely-ignore-certificate-errors \\",
              '    ./tests/wpt/wpt.ts run --all --quiet --binary="$DENO_BIN"',
            ],
          },
          {
            name: "Run web platform tests (release)",
            if: isRelease,
            env: {
              DENO_BIN: "./target/release/deno",
            },
            run: [
              "deno run -RWNE --allow-run --lock=tools/deno.lock.json --config tests/config/deno.json \\",
              "    ./tests/wpt/wpt.ts setup",
              "deno run -RWNE --allow-run --lock=tools/deno.lock.json --config tests/config/deno.json --unsafely-ignore-certificate-errors \\",
              '    ./tests/wpt/wpt.ts run --all --quiet --release --binary="$DENO_BIN" --json=wpt.json --wptreport=wptreport.json',
            ],
          },
          {
            name: "Autobahn testsuite",
            if: isRelease,
            run:
              "target/release/deno run -A --config tests/config/deno.json ext/websocket/autobahn/fuzzingclient.js",
          },
          step({
            name: "Upload wpt results to dl.deno.land",
            continueOnError: true,
            if: isRelease.and(isLinux).and(isDenoland).and(isMainBranch).and(
              isNotTag,
            ),
            env: S3Envs,
            run: [
              "gzip ./wptreport.json",
              "aws s3 cp ./wpt.json s3://dl-deno-land/wpt/$(git rev-parse HEAD).json",
              "aws s3 cp ./wptreport.json.gz s3://dl-deno-land/wpt/$(git rev-parse HEAD)-wptreport.json.gz",
              "echo $(git rev-parse HEAD) > wpt-latest.txt",
              "aws s3 cp wpt-latest.txt s3://dl-deno-land/wpt-latest.txt",
            ],
          }),
          {
            name: "Upload wpt results to wpt.fyi",
            continueOnError: true,
            if: isRelease.and(isLinux).and(isDenoland).and(isMainBranch).and(
              isNotTag,
            ),
            env: {
              WPT_FYI_USER: "deno",
              WPT_FYI_PW: "${{ secrets.WPT_FYI_PW }}",
              GITHUB_TOKEN: "${{ secrets.DENOBOT_PAT }}",
            },
            run: [
              "./target/release/deno run --allow-all --lock=tools/deno.lock.json \\",
              "    ./tools/upload_wptfyi.js $(git rev-parse HEAD) --ghstatus",
            ],
          },
          buildCacheSteps.saveCacheStep.if(isMainBranch.and(isNotTag)),
        ),
      },
    ));
  }

  return {
    buildJob,
    additionalJobs,
  };
});

// === bench job ===

const benchProfile = defineExprObj(Runners.linuxX86Xl);
const benchCacheSteps = createCargoCacheHomeStep({
  ...benchProfile,
  cachePrefix: "bench",
});
const benchJob = job(
  "bench",
  {
    name: `bench release ${benchProfile.os}-${benchProfile.arch}`,
    needs: [preBuildJob],
    if: preBuildJob.outputs.skip_build.notEquals("true").and(notDocsOnly),
    runsOn: benchProfile.runner,
    timeoutMinutes: 240,
    defaults: {
      run: {
        // GH actions does not fail fast by default on
        // Windows, so we set bash as the default shell
        shell: "bash",
      },
    },
    steps: step
      .if(
        (hasCiBenchLabel.or(isMainBranch)).and(isNotTag),
      )(
        cloneRepoStep,
        benchCacheSteps.restoreCacheStep,
        installNodeStep,
        installRustStep,
        cloneSubmodule("./tests/bench/testdata/lsp_benchdata"),
        cloneStdSubmoduleStep,
        step(sysRootConfig),
        installDenoStep,
        {
          name: "Install benchmark tools",
          env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" },
          run: "./tools/install_prebuilt.js wrk hyperfine",
        },
        // We currently do a full deno build instead of getting this from the build
        // job because the benchmarks inspect the target folder to see the sizes of
        // libraries like v8 and swc as well as the snapshot sizes. Maybe in the future
        // we could optimize this to not need this.
        {
          name: "Build deno",
          env: {
            DENO_SNAPSHOT_MINIFY_SOURCES: "1",
          },
          run: "cargo build --release -p deno",
        },
        {
          name: "Run benchmarks",
          run: "cargo bench -p bench_tests --bench deno_bench --locked",
        },
        {
          name: "Post benchmarks",
          if: isDenoland.and(isMainBranch),
          env: {
            DENOBOT_PAT: "${{ secrets.DENOBOT_PAT }}",
          },
          run: [
            "git clone --depth 1 --branch gh-pages                             \\",
            "    https://${DENOBOT_PAT}@github.com/denoland/benchmark_data.git \\",
            "    gh-pages",
            "./target/release/deno run --allow-all ./tools/build_benchmark_jsons.js --release",
            "cd gh-pages",
            'git config user.email "propelml@gmail.com"',
            'git config user.name "denobot"',
            "git add .",
            'git commit --message "Update benchmarks"',
            "git push origin gh-pages",
          ],
        },
        {
          name: "Worker info",
          run: ["cat /proc/cpuinfo", "cat /proc/meminfo"],
        },
        benchCacheSteps.saveCacheStep,
      ),
  },
);

// === lint job ===

const lintMatrix = defineMatrix({
  include: [{
    ...Runners.linuxX86,
    profile: "debug",
    job: "lint",
  }, {
    ...Runners.macosX86,
    profile: "debug",
    job: "lint",
  }, {
    ...Runners.windowsX86,
    profile: "debug",
    job: "lint",
  }],
});

const lintJob = job("lint", {
  name: `lint ${lintMatrix.profile} ${lintMatrix.os}-${lintMatrix.arch}`,
  needs: [preBuildJob],
  if: preBuildJob.outputs.skip_build.notEquals("true"),
  runsOn: lintMatrix.runner,
  timeoutMinutes: 30,
  defaults: {
    run: {
      shell: "bash",
    },
  },
  strategy: {
    matrix: lintMatrix,
  },
  steps: (() => {
    const {
      restoreCacheStep,
      saveCacheStep,
    } = createCacheSteps({
      ...lintMatrix,
      cachePrefix: "lint",
    });
    return step(
      cloneRepoStep,
      cloneStdSubmoduleStep,
      restoreCacheStep,
      installRustStep,
      installDenoStep,
      step.if(lintMatrix.os.equals("linux"))(
        {
          name: "test_format.js",
          run:
            "deno run --allow-write --allow-read --allow-run --allow-net ./tools/format.js --check",
        },
        {
          name: "jsdoc_checker.js",
          run:
            "deno run --allow-read --allow-env --allow-sys ./tools/jsdoc_checker.js",
        },
      ),
      {
        name: "lint.js",
        env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" },
        run:
          "deno run --allow-write --allow-read --allow-run --allow-net --allow-env ./tools/lint.js",
      },
      saveCacheStep,
    );
  })(),
});

// === publish-canary job ===

const publishCanaryJob = job("publish-canary", {
  name: "publish canary",
  runsOn: ubuntuX86Runner,
  needs: [...buildJobs.map((b) => b.buildJob)],
  if: isDenoland.and(isMainBranch),
  steps: (() => {
    return step(
      {
        name: "Upload canary version file to dl.deno.land",
        env: S3Envs,
        run: [
          "echo ${{ github.sha }} > canary-latest.txt",
          "aws s3 cp canary-latest.txt s3://dl-deno-land/canary-latest.txt",
        ],
      },
    );
  })(),
});

// === deno_core test job ===
// Ported from denoland/deno_core .github/workflows/ci-test/action.yml
// Tests the merged deno_core crates (libs/*) using cargo nextest.

// Cargo package names for the libs/* workspace members (merged from deno_core).
const denoCorePackageNames = [
  "deno_core",
  "deno_v8",
  "build-your-own-js-snapshot",
  "dcore",
  "deno_ops",
  "deno_ops_compile_test_runner",
  "serde_v8",
  "deno_core_testing",
];
const denoCoreTestProfile = defineExprObj({
  ...Runners.linuxX86Xl,
  profile: "release",
});
const denoCoreTestCacheSteps = createCacheSteps({
  ...denoCoreTestProfile,
  cachePrefix: "deno-core-test",
});
const denoCoreTestJob = job("deno-core-test", {
  name: `deno_core test linux-x86_64`,
  needs: [preBuildJob],
  if: preBuildJob.outputs.skip_build.notEquals("true")
    .and(preBuildJob.outputs.skip_deno_core_test.notEquals("true"))
    .and(notDocsOnly),
  runsOn: denoCoreTestProfile.runner,
  timeoutMinutes: 60,
  defaults: {
    run: {
      shell: "bash",
    },
  },
  env: {
    CARGO_TERM_COLOR: "always",
    RUST_BACKTRACE: "full",
    RUST_LIB_BACKTRACE: 0,
  },
  steps: step.if(isNotTag)(
    {
      // Frees several GB of preinstalled toolchains this job never uses
      // (.NET, Android SDK, GHC, Boost), fixing recurring "No space left
      // on device" failures while compiling deno_core's doctests.
      name: "Free disk space",
      run: [
        'sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost "$AGENT_TOOLSDIRECTORY" || true',
        "df -h",
      ],
    },
    cloneRepoStep,
    denoCoreTestCacheSteps.restoreCacheStep,
    installRustStep,
    installDenoStep,
    step(sysRootConfig),
    {
      name: "Install cargo-binstall",
      uses: "cargo-bins/cargo-binstall@main",
    },
    {
      name: "Install nextest",
      run: "cargo binstall cargo-nextest --secure --locked",
    },
    {
      name: "Cargo nextest (release)",
      run: [
        `cargo nextest run --release`,
        `  --features "deno_core/default deno_core/unsafe_use_unprotected_platform deno_core/v8"`,
        `  --tests --examples`,
        `  ${denoCorePackageNames.map((p) => `-p ${p}`).join(" ")}`,
      ].join(" \\\n    "),
    },
    {
      // Ported from denoland/deno_core .github/workflows/ci-test-ops/action.yml
      name: "Cargo nextest ops compile test runner (release)",
      run:
        "cargo nextest run --release -p deno_ops_compile_test_runner -p deno_v8",
    },
    {
      name: "Cargo doc test",
      run: `cargo test --doc --release ${
        denoCorePackageNames.filter((p) =>
          p !== "deno_ops_compile_test_runner" && p !== "dcore"
        ).map((p) => `-p ${p}`).join(" ")
      }`,
    },
    {
      // Regression test for https://github.com/denoland/deno/pull/19615.
      name: "Run examples (regression tests)",
      run: [
        "cargo run -p deno_core --example op2 --features v8",
      ],
    },
    denoCoreTestCacheSteps.saveCacheStep,
  ),
});

// === deno_core miri test job ===
// Ported from denoland/deno_core .github/workflows/ci-test-miri/action.yml
// Runs miri tests for deno_core using a nightly Rust toolchain.

const miriNightlyToolchain = "nightly-2025-11-12";
const denoCoreMiriJob = job("deno-core-miri", {
  name: "deno_core miri linux-x86_64",
  needs: [preBuildJob],
  if: preBuildJob.outputs.skip_build.notEquals("true").and(notDocsOnly),
  runsOn: Runners.linuxX86Xl.runner,
  timeoutMinutes: 60,
  defaults: {
    run: {
      shell: "bash",
    },
  },
  env: {
    CARGO_TERM_COLOR: "always",
    RUST_BACKTRACE: "full",
    RUST_LIB_BACKTRACE: 0,
  },
  steps: step.if(isNotTag)(
    cloneRepoStep,
    {
      name: "Install Rust (nightly)",
      uses: "dtolnay/rust-toolchain@master",
      with: {
        toolchain: miriNightlyToolchain,
      },
    },
    {
      name: "Cargo test (miri)",
      run: [
        "cargo clean",
        `rustup component add --toolchain ${miriNightlyToolchain} miri`,
        "# This somehow prints errors in CI that don't show up locally",
        `RUSTFLAGS=-Awarnings cargo +${miriNightlyToolchain} miri test -p deno_core --features v8`,
      ],
    },
  ),
});

// === ci status job (status check gate) ===

const ciStatusJob = job("ci-status", {
  name: "ci status",
  // We use this job in the main branch rule status checks for PRs.
  // All jobs that are required to pass on a PR should be listed here.
  needs: [
    benchJob,
    ...buildJobs.map((j) => [j.buildJob, ...j.additionalJobs]).flat(),
    lintJob,
    denoCoreTestJob,
    denoCoreMiriJob,
  ],
  if: preBuildJob.outputs.skip_build.notEquals("true")
    .and(conditions.status.always()),
  runsOn: "ubuntu-latest",
  steps: step({
    name: "Ensure CI success",
    run: [
      "if [[ \"${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}\" == \"true\" ]]; then",
      "  echo 'CI failed'",
      "  exit 1",
      "fi",
    ],
  }),
});

// === generate workflow ===

const workflow = createWorkflow({
  name: "ci",
  permissions: {
    contents: "write",
    "id-token": "write", // Required for GitHub OIDC with Azure for code signing
  },
  on: {
    push: {
      branches: ["main"],
      tags: ["*"],
    },
    pull_request: {
      types: [
        "opened",
        "reopened",
        "synchronize",
        // need to re-run the action when converting from draft because
        // draft PRs will not necessarily run all the steps
        "ready_for_review",
      ],
    },
  },
  concurrency: {
    group:
      "${{ github.workflow }}-${{ !contains(github.event.pull_request.labels.*.name, 'ci-test-flaky') && github.head_ref || github.run_id }}",
    cancelInProgress: true,
  },
  jobs: [
    preBuildJob,
    benchJob,
    ...buildJobs.map((j) => [j.buildJob, ...j.additionalJobs]).flat(),
    lintJob,
    denoCoreTestJob,
    denoCoreMiriJob,
    ciStatusJob,
    publishCanaryJob,
  ],
});

export function generate() {
  return workflow.toYamlString({
    header: "# GENERATED BY ./ci.ts -- DO NOT DIRECTLY EDIT",
  });
}

export const CI_YML_URL = new URL("./ci.generated.yml", import.meta.url);

if (import.meta.main) {
  workflow.writeOrLint({
    filePath: CI_YML_URL,
    header: "# GENERATED BY ./ci.ts -- DO NOT DIRECTLY EDIT",
  });
}

function resolveTestCrateTests() {
  const rootCargoToml = parseToml(
    Deno.readTextFileSync(new URL("../../Cargo.toml", import.meta.url)),
  ) as { workspace: { members: string[] } };

  const testCrates: { name: string; package: string }[] = [];
  const testPackageMembers = new Set<string>();

  for (const member of rootCargoToml.workspace.members) {
    if (!member.startsWith("tests")) continue;
    const cargoToml = parseToml(
      Deno.readTextFileSync(
        new URL(`../../${member}/Cargo.toml`, import.meta.url),
      ),
    ) as {
      package: { name: string; autotests?: boolean };
      test?: { name: string; path: string }[];
    };
    // only include crates that explicitly disable auto-test discovery,
    // indicating they are intentional test packages (not helper libraries
    // like tests/ffi or tests/util/server)
    if (cargoToml.package.autotests !== false) continue;
    const tests = cargoToml.test ?? [];
    if (tests.length > 0) {
      testPackageMembers.add(member);
      for (const test of tests) {
        testCrates.push({ name: test.name, package: cargoToml.package.name });
      }
    }
  }

  return { testCrates, testPackageMembers };
}

function resolveWorkspaceCrates(testPackageMembers: Set<string>) {
  // discover workspace members for the libs test job, split by type
  const rootCargoToml = parseToml(
    Deno.readTextFileSync(new URL("../../Cargo.toml", import.meta.url)),
  ) as { workspace: { members: string[] } };

  const libCrates: string[] = [];
  const binCrates: string[] = [];
  for (const member of rootCargoToml.workspace.members) {
    const cargoToml = parseToml(
      Deno.readTextFileSync(
        new URL(`../../${member}/Cargo.toml`, import.meta.url),
      ),
    ) as {
      package: { name: string };
      bin?: unknown[];
      test?: { path?: string }[];
    };

    if (member.startsWith("tests")) {
      if (!testPackageMembers.has(member)) {
        ensureNoIntegrationTests(member, cargoToml);
      }
    } else if (denoCorePackageDirs.includes(member)) {
      // libs/* crates (merged from deno_core) have their own dedicated
      // deno-core-test CI job, so skip them here.
      continue;
    } else if (cargoToml.bin) {
      ensureNoIntegrationTests(member, cargoToml);
      binCrates.push(cargoToml.package.name);
    } else {
      libCrates.push(cargoToml.package.name);
    }
  }
  return { libCrates, binCrates };
}

function ensureNoIntegrationTests(
  member: string,
  cargoToml: {
    package: { name: string };
    test?: { path?: string }[];
  },
) {
  const errors: string[] = [];
  if (existsSync(new URL(`../../${member}/tests/`, import.meta.url))) {
    errors.push("has a tests/ folder");
  }
  const hasNonRunnerTests = cargoToml.test?.some(
    // this path is allowed because it's only used by deno and denort
    // to cause the deno and denort binaries to be built when running
    // tests, but it doesn't actually run any tests itself
    (t) => t.path !== "integration_tests_runner.rs",
  );
  if (hasNonRunnerTests) {
    errors.push("has a [[test]] section in Cargo.toml");
  }
  if (errors.length > 0) {
    throw new Error(
      `crate "${cargoToml.package.name}" (${member}) ${
        errors.join(" and ")
      }. ` +
        `Integration tests in these crates won't run on CI because we build ` +
        `binaries on one runner then test on another. ` +
        `Move them to spec tests, the test crates in tests/, or use #[cfg(test)] lib tests instead.`,
    );
  }
}

function existsSync(path: string | URL) {
  try {
    Deno.statSync(path);
    return true;
  } catch (e) {
    if (!(e instanceof Deno.errors.NotFound)) throw e;
    return false;
  }
}
