async function runCoverage(options) {
  const webglStub = options.webglStub ?? false;
  const suppressPassed = options.suppressPassed ?? false;
  const failTaskOnError = options.failTaskOnError ?? false;
  const workspace = options.workspace;

  const folders = [];
  let browsers = ["Chrome"];
  if (argv.browsers) {
    browsers = argv.browsers.split(",");
  }

  const instrumenter = createInstrumenter({
    esModules: true,
  });

  // Setup plugin to use instrumenter on source files.

  const instrumentPlugin = {
    name: "instrument",
    setup: (build) => {
      build.onLoad(
        {
          filter: options.filter,
        },
        async (args) => {
          const source = await readFile(args.path, { encoding: "utf8" });
          try {
            const generatedCode = instrumenter.instrumentSync(
              source,
              args.path,
            );

            return { contents: generatedCode };
          } catch (e) {
            return {
              errors: {
                text: e.message,
              },
            };
          }
        },
      );
    },
  };

  const karmaBundle = join(options.outputDirectory, "karma-main.js");
  await esbuild({
    entryPoints: ["Specs/karma-main.js"],
    bundle: true,
    sourcemap: true,
    format: "esm",
    target: "es2020",
    outfile: karmaBundle,
    logLevel: "error", // print errors immediately, and collect warnings so we can filter out known ones
  });

  // Generate instrumented bundle for Specs.

  const specListBundle = join(options.outputDirectory, "SpecList.js");
  await esbuild({
    entryPoints: [options.specList],
    bundle: true,
    sourcemap: true,
    format: "esm",
    target: "es2020",
    outfile: specListBundle,
    plugins: [instrumentPlugin],
    logLevel: "error", // print errors immediately, and collect warnings so we can filter out known ones
  });

  let files = [
    {
      pattern: karmaBundle,
      included: true,
      type: "module",
    },
    {
      pattern: specListBundle,
      included: true,
      type: "module",
    },
    // Static assets are always served from the shared/combined folders.
    { pattern: "Specs/Data/**", included: false },
    { pattern: "Specs/TestWorkers/**/*.wasm", included: false },
    { pattern: "Build/CesiumUnminified/**", included: false },
    { pattern: "Build/Specs/TestWorkers/**.js", included: false },
  ];

  let proxies;
  if (workspace) {
    // Setup files and proxies for the engine package first, since it is the lowest level dependency.
    files = [
      {
        pattern: karmaBundle,
        included: true,
        type: "module",
      },
      {
        pattern: specListBundle,
        included: true,
        type: "module",
      },
      { pattern: "Specs/Data/**", included: false },
      { pattern: "Specs/TestWorkers/**/*.wasm", included: false },
      { pattern: "packages/engine/Build/Workers/**", included: false },
      { pattern: "packages/engine/Source/Assets/**", included: false },
      { pattern: "packages/engine/Source/ThirdParty/**", included: false },
      { pattern: "packages/engine/Source/Widget/*.css", included: false },
      { pattern: "Build/Specs/TestWorkers/**.js", included: false },
    ];

    proxies = {
      "/base/Build/CesiumUnminified/Assets/":
        "/base/packages/engine/Source/Assets/",
      "/base/Build/CesiumUnminified/ThirdParty/":
        "/base/packages/engine/Source/ThirdParty/",
      "/base/Build/CesiumUnminified/Widgets/CesiumWidget/":
        "/base/packages/engine/Source/Widget/",
      "/base/Build/CesiumUnminified/Workers/":
        "/base/packages/engine/Build/Workers/",
    };
  }

  // Setup Karma config.

  const config = await karma.config.parseConfig(
    karmaConfigFile,
    {
      configFile: karmaConfigFile,
      browsers: browsers,
      specReporter: {
        suppressErrorSummary: false,
        suppressFailed: false,
        suppressPassed: suppressPassed,
        suppressSkipped: true,
      },
      files: files,
      proxies: proxies,
      reporters: ["spec", "coverage"],
      coverageReporter: {
        dir: options.coverageDirectory,
        subdir: function (browserName) {
          folders.push(browserName);
          return browserName;
        },
        includeAllSources: true,
      },
      client: {
        captureConsole: false,
        args: [
          undefined,
          undefined,
          undefined,
          undefined,
          undefined,
          webglStub,
          undefined,
        ],
      },
    },
    { promiseConfig: true, throwErrors: true },
  );

  return new Promise((resolve, reject) => {
    const server = new karma.Server(config, function doneCallback(e) {
      let html = "<!doctype html><html><body><ul>";
      folders.forEach(function (folder) {
        html += `<li><a href="${encodeURIComponent(
          folder,
        )}/index.html">${folder}</a></li>`;
      });
      html += "</ul></body></html>";
      writeFileSync(join(options.coverageDirectory, "index.html"), html);

      if (!process.env.CI) {
        folders.forEach(function (dir) {
          open(join(options.coverageDirectory, `${dir}/index.html`));
        });
      }

      if (failTaskOnError && e) {
        reject(e);
        return;
      }

      resolve();
    });
    server.start();
  });
}
",
          "!Apps/Sandcastle/gallery/development/**",
        ],
        {
          encoding: false,
          base: ".",
        },
      ),
    )
    .pipe(
      gulpTap(function (file) {
        // Work around an issue with gulp-zip where archives generated on Windows do
        // not properly have their directory executable mode set.
        // see https://github.com/sindresorhus/gulp-zip/issues/64#issuecomment-205324031
        if (file.isDirectory()) {
          file.stat.mode = parseInt("40777", 8);
        }
      }),
    )
    .pipe(gulpZip(`Cesium-${version}.zip`))
    .pipe(gulp.dest("."));

  await finished(src);

  rimraf.sync("./package.noprepare.json");
  rimraf.sync("./packages/engine/package.noprepare.json");
  rimraf.sync("./packages/widgets/package.noprepare.json");

  return src;
});

export async function deploySetVersion() {
  const buildVersion = argv.buildVersion;
  if (buildVersion) {
    // NPM versions can only contain alphanumeric and hyphen characters
    packageJson.version += `-${buildVersion.replace(/[^[0-9A-Za-z-]/g, "")}`;
    return writeFile("package.json", JSON.stringify(packageJson, undefined, 2));
  }
}

export async function deployStatus() {
  const status = argv.status;
  const message = argv.message;

  const deployUrl = `${devDeployUrl + process.env.BRANCH}/`;
  const zipUrl = `${deployUrl}Cesium-${version}.zip`;
  const npmUrl = `${deployUrl}cesium-${version}.tgz`;
  const coverageUrl = `${
    devDeployUrl + process.env.BRANCH
  }/Build/Coverage/index.html`;

  return Promise.all([
    setStatus(status, deployUrl, message, "deployment"),
    setStatus(status, zipUrl, message, "zip file"),
    setStatus(status, npmUrl, message, "npm package"),
    setStatus(status, coverageUrl, message, "coverage results"),
  ]);
}

async function setStatus(state, targetUrl, description, context) {
  // skip if the environment does not have the token
  if (!process.env.GITHUB_TOKEN) {
    return;
  }

  const body = {
    state: state,
    target_url: targetUrl,
    description: description,
    context: context,
  };

  const response = await fetch(
    `https://api.github.com/repos/${process.env.GITHUB_REPO}/statuses/${process.env.GITHUB_SHA}`,
    {
      method: "post",
      body: JSON.stringify(body),
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${process.env.GITHUB_TOKEN}`,
        "User-Agent": "Cesium",
      },
    },
  );

  const result = await response.json();
  return result;
}

/**
Generates coverage report.

@param {object} options An object with the following properties:
@param {string} options.outputDirectory The output directory for the generated build artifacts.
@param {string} options.coverageDirectory The path where the coverage reports should be saved to.
@param {string} options.specList The path to the spec list for the package.
@param {RegExp} options.filter The filter for finding which files should be instrumented.
@param {boolean} [options.webglStub=false] True if WebGL stub should be used when running tests.
@param {boolean} [options.suppressPassed=false] True if output should be suppressed for tests that pass.
@param {boolean} [options.failTaskOnError=false] True if the gulp task should fail on errors in the tests.
@param {string} options.workspace The name of the workspace, if any.