npm package: @qawolf/ci-sdk
Description: A simple SDK for interacting with QAWolf in CI scripts.
Author: Jules Sam. Randolph
Latest version: 1.3.1

# QAWolf CI SDK

This package provides a TypeScript (CSM and ESM compatible) SDK
to interact with the QA Wolf Customer-facing API.

It exposes several functions associated with different endpoints, which are detailed in the table of contents below.

Note that these functions do not throw. They yield a result object that
contains the outcome of the operation. This outcome should be scrutinized
to determine the status of your CI/CD step/job/action.

## Table of Contents

- [Notify Deployment](#notify-deployment)
- [Notify Preview Deployment (Pull Request / Merge Request Testing)](#notify-preview)
- [Poll for CI Greenlight Status](#ci-greenlight)
- [Advanced: Poll with Custom Control (Iterator)](#ci-greenlight-iterator)
- [Notify a Terminated Ephemeral Environment](#notify-terminated-ephemeral-environment)
- [Upload Run Input Artifacts](#upload-artifacts)
- [Requirements](#requirements)
- [Versioning](#versioning)
- [Changelog](#changelog)

<a id="notify-deployment"></a>

## Notify Deployment

Notify us of a successful deployment to trigger a run.

> ℹ️ [See the API documentation page for this endpoint](https://qawolf.notion.site/dd72e46ceb7f451dae4e9ef06f64a2cc).

> 💡 **For GitHub Actions Users**: If you're using GitHub Actions, we strongly recommend using our [GitHub Action](https://github.com/marketplace/actions/notify-qa-wolf-on-deploy). The GitHub Action provides simpler configuration and automatic information extraction.

> ⚠️ Important: A run is only created if there is a matching trigger in your QA Wolf configuration. The deployment notification alone doesn't guarantee a run will be created.

### Example

```ts
import { type DeployConfig, makeQaWolfSdk } from "@qawolf/ci-sdk";

// Example for GitHub repositories
const deployConfig: GitHubDeployConfig = {
  branch: undefined,
  commitUrl: undefined,
  deduplicationKey: undefined,
  // Required only if the target trigger requires matching a deployment type
  deploymentType: "staging", // e.g., "production", "staging", "qa"
  deploymentUrl: undefined,
  // Specify GitHub as hosting service
  hostingService: "GitHub",
  // Optional: Include pull request number for PR testing
  // See Notify Preview Deployment (Pull Request / Merge Request Testing) section
  pullRequestNumber: 123,
  // Recommended: Include repository information
  repository: {
    name: "your-repo",
    owner: "your-org",
  },
  sha: undefined,
  variables: undefined,
};

// Example for GitLab repositories
const deployConfig: GitLabDeployConfig = {
  branch: undefined,
  commitUrl: undefined,
  deduplicationKey: undefined,
  // Required only if the target trigger requires matching a deployment type
  deploymentType: "staging", // e.g., "production", "staging", "qa"
  deploymentUrl: undefined,
  // Specify GitLab as hosting service
  hostingService: "GitLab",
  // Optional: Include merge request number for MR testing
  // See Notify Preview Deployment (Pull Request / Merge Request Testing) section
  mergeRequestNumber: 123,
  // Recommended: Include repository information
  repository: {
    name: "your-repo",
    namespace: "your-group",
  },
};

// Example for Ephemeral deployments
const deployConfig: EphemeralDeployConfig = {
  branch: undefined,
  commitUrl: undefined,
  deduplicationKey: undefined,
  // Required only if the target trigger requires matching a deployment type
  deploymentType: "staging", // e.g., "production", "staging", "qa"
  deploymentUrl: "https://preview.environment.com",
  ephemeralEnvironment: true,
  sha: undefined,
  variables: undefined,
};

// General Example
// Edit this to your needs.
const deployConfig: DeployConfig = {
  branch: undefined,
  commitUrl: undefined,
  deduplicationKey: undefined,
  // Required only if the target trigger requires matching a deployment type
  deploymentType: "staging", // e.g., "production", "staging", "qa"
  deploymentUrl: undefined,
  hostingService: undefined,
  sha: undefined,
  variables: undefined,
};

const { attemptNotifyDeploy } = makeQaWolfSdk({
  apiKey: "qawolf_xxxxx",
});

(async () => {
  const result = await attemptNotifyDeploy(deployConfig);
  if (result.outcome !== "success") {
    // Fail the job.
    process.exit(1);
  }
  const runId = result.runId;
  // Store the runId as an output of the job to be used in a CI-greenlight job.
  // This will depend on the CI platform you are using.
})();
```

<a id="notify-preview"></a>

## Notify Preview Deployment (Pull Request / Merge Request Testing)

> ⚠️ **Important**: PR/MR testing functionality must be activated by QA Wolf. Please reach out to your QA Wolf representative to enable this feature and help with the setup.

Once enabled, to use PR/MR testing functionality:

1. For `GitHub` repositories:

   - Preferably, use our [GitHub Action](https://github.com/marketplace/actions/notify-qa-wolf-on-deploy)
   - Pass `hostingService: "GitHub"`, `repository` information, and `pullRequestNumber` while notifying a deployment as described in the [Notify Deployment](#notify-deployment) section

2. For `GitLab` repositories:

   - Pass `hostingService: "GitLab"`, `repository` information, and `mergeRequestNumber` while notifying a deployment as described in the [Notify Deployment](#notify-deployment) section

3. For `Ephemeral` deployments (no code hosting integration):

   - Pass `ephemeralEnvironment: true` and `deploymentUrl` while notifying a deployment as described in the [Notify Deployment](#notify-deployment) section

<a id="ci-greenlight"></a>

## Poll for CI Greenlight Status

> ℹ️ [See the API documentation page for this endpoint](https://qawolf.notion.site/1b170576efea411fa785842a71e7c99e).

```ts
import { makeQaWolfSdk } from "@qawolf/ci-sdk";

const { pollCiGreenlightStatus } = makeQaWolfSdk({
  apiKey: "qawolf_xxxxx",
});

(async () => {
  // Retrieve runId from the previous job.
  const { outcome } = await pollCiGreenlightStatus({
    runId,
    // Optional: Callback to be called when the run stage changes.
    // See https://qawolf.notion.site/1b170576efea411fa785842a71e7c99e for
    // documentation on these run stages.
    onRunStageChanged: (current, previous) => {
      console.log(current, previous);
    },
    // Optional: Defaults to false. When set to true, the polling operation
    // will abort when the run is superseded.
    abortOnSuperseded: false,
  });
  if (outcome !== "success") {
    // Fail the job.
    // This will depend on the CI platform you are using.
    // You can also distinguish between "failed" and "aborted" outcomes.
    // Only "failed" outcome indicates bugs were found.
    process.exit(1);
  }
  // Continue CI.
})();
```

<a id="ci-greenlight-iterator"></a>

## Advanced: Poll with Custom Control (Iterator)

For advanced use cases where you need fine-grained control over the polling lifecycle, use `makePollCiGreenlightStatusIterator`. This async generator gives you full control to implement custom logic such as:

- Stop polling early based on time limits (e.g., "wait max 10 minutes in review then proceed")
- Stop polling based on bug counts (e.g., "proceed if only 3 or fewer blocking bugs")
- Implement custom logging or monitoring at each poll iteration
- Access detailed bug data during the "under review" stage

> ℹ️ [See the API documentation page for this endpoint](https://qawolf.notion.site/1b170576efea411fa785842a71e7c99e).

### Example: Early Exit with Custom Logic

> ⚠️ **Important**: When using the iterator, ensure you handle all run stages in your switch statement. The `default` case with `satisfies never` provides compile-time safety - if a new stage is added, TypeScript will error. This prevents silently passing your CI job while ignoring error conditions.

```ts
import { makeQaWolfSdk } from "@qawolf/ci-sdk";

const { makePollCiGreenlightStatusIterator } = makeQaWolfSdk({
  apiKey: "qawolf_xxxxx",
});

(async () => {
  let underReviewStartTime: number | null = null;

  const iterator = makePollCiGreenlightStatusIterator({
    runId: "your-run-id",
  });

  for await (const iteration of iterator) {
    // Check if the iterator yielded an abort result
    if (iteration.isAborted) {
      console.error(`Poll aborted: ${iteration.abortReason}`);
      process.exit(1);
    }

    // Handle status iterations
    const { status, stageChanged } = iteration;

    switch (status.runStage) {
      case "initializing":
        console.log(`Run stage: ${status.runStage}`);
        break;

      case "underReview": {
        // Track when we first enter underReview
        if (stageChanged) {
          underReviewStartTime = Date.now();
          console.log(`Run stage: ${status.runStage}`);
        }

        const timeInUnderReview = underReviewStartTime
          ? Date.now() - underReviewStartTime
          : 0;

        // Option 1: Stop after 10 minutes in review
        if (timeInUnderReview > 10 * 60 * 1000) {
          console.log("10 minutes in review, proceeding with deployment");
          // Decide whether to fail CI based on your criteria
          if (status.blockingBugsCount > 5) {
            console.log("Too many blocking bugs, failing CI");
            process.exit(1);
          }
          break;
        }

        // Option 2: Stop if bug count is acceptable
        if (status.blockingBugsCount <= 3) {
          console.log(
            `Only ${status.blockingBugsCount} blocking bugs, acceptable to proceed`,
          );
          break;
        }
        break;
      }

      case "completed":
        console.log(`Completed with greenlight: ${status.greenlight}`);
        if (!status.greenlight) {
          console.log("Run failed, blocking bugs found");
          process.exit(1);
        }
        console.log("Run passed successfully");
        return; // Exit the loop

      case "canceled":
        console.log("Run was canceled");
        process.exit(1);

      default:
        // Exhaustive check - TypeScript will error if a case is missing
        status.runStage satisfies never;
        throw new Error(`Unexpected run stage: ${status.runStage}`);
    }
  }
})();
```

### Yielded Iteration Object

The iterator yields a discriminated union that can be either a status update or an abort notification:

**Status Iteration** (`isAborted: false`):

- `isAborted`: `false` - Indicates a normal status update
- `status`: The current `CiGreenlightStatus` from the API
- `previousStatus`: The status from the previous iteration (undefined on first iteration)
- `stageChanged`: Boolean indicating if the run stage changed from the previous iteration
- `elapsedMs`: Milliseconds elapsed since polling started

**Aborted Iteration** (`isAborted: true`):

- `isAborted`: `true` - Indicates the polling was aborted
- `abortReason`: String indicating why polling was aborted (e.g., `"poll-timed-out"`, `"run-canceled"`, `"network-error"`, `"4XX-client-error"`, `"5XX-server-error"`)
- `httpStatus`: HTTP status code if applicable, otherwise `undefined`
- `elapsedMs`: Milliseconds elapsed since polling started

**Important**: Always check `iteration.isAborted` to determine which type of result you received. When `isAborted === true`, you should handle the abort reason and exit appropriately.

<a id="notify-terminated-ephemeral-environment"></a>

## Notify a Terminated Ephemeral Environment

> ℹ️ [See the API documentation page for this endpoint](https://qawolf.notion.site/2c45b2a994fb8096a42af8d41c811ae2).

After you terminate an ephemeral environment or release it for something else to be installed in it, you should notify QA Wolf of that fact. When you notify us, we will stop all runs targeting that environment and check whether there are any changes to flows that need to be promoted to the static base environment. This eventually results in the environment showing as "closed" in QA Wolf.

> ⚠️ **Note**: If you have the QA Wolf GitHub integration enabled for your preview testing, GitHub will notify us of PRs being merged or closed, and we use that notification to stop runs and trigger flow promotion. In this case, it is typically redundant and unnecessary to call this..

> ⚠️ **Important**: PR/MR testing functionality must be activated by QA Wolf. Please reach out to your QA Wolf representative to enable this feature and help with the setup.

```ts
import {
  type NotifyTerminatedEphemeralEnvironmentInput,
  makeQaWolfSdk,
} from "@qawolf/ci-sdk";

// Example for environmentId
const terminateConfig: NotifyTerminatedEphemeralEnvironmentInput = {
  environmentId: "test-environment-id",
};

// Example for environmentAlias
const terminateConfig: NotifyTerminatedEphemeralEnvironmentInput = {
  environmentAlias: "test-environment",
};

// Example for deploymentUrl
const terminateConfig: NotifyTerminatedEphemeralEnvironmentInput = {
  deploymentUrl: "https://test-environment",
};

const { notifyTerminatedEphemeralEnvironment } = makeQaWolfSdk({
  apiKey: "qawolf_xxxxx",
});

(async () => {
  const result = await notifyTerminatedEphemeralEnvironment(terminateConfig);
  if (result.outcome !== "success") {
    // Fail the job.
    process.exit(1);
  }
  const environmentId = result.environmentId;
})();
```

<a id="upload-artifacts"></a>

## Upload Run Input Artifacts

```ts
import { type DeployConfig, makeQaWolfSdk } from "@qawolf/ci-sdk";
import fs from "fs/promises";
import path from "path";

const { generateSignedUrlForTempTeamStorage, attemptNotifyDeploy } =
  makeQaWolfSdk({
    apiKey: "qawolf_xxxxx",
  });

(async () => {
  const playgroundFileLocation = await uploadRunArtifact("");

  if (playgroundFileLocation) {
    const deployConfig: DeployConfig = {
      branch: undefined,
      commitUrl: undefined,
      deduplicationKey: undefined,
      deploymentType: undefined,
      deploymentUrl: undefined,
      hostingService: undefined,
      sha: undefined,
      variables: {
        RUN_INPUT_PATH: playgroundFileLocation,
      },
    };

    const result = await attemptNotifyDeploy(deployConfig);
    if (result.outcome !== "success") {
      // Fail the job.
      process.exit(1);
    }
    const runId = result.runId;
  }
})();

async function uploadRunArtifact(filePath: string): Promise<string> {
  const fileName = path.basename(filePath);

  const signedUrlResponse = await generateSignedUrlForTempTeamStorage({
    destinationFilePath: fileName,
  });

  if (
    signedUrlResponse?.success &&
    signedUrlResponse.playgroundFileLocation &&
    signedUrlResponse.uploadUrl
  ) {
    const fileBuffer = await fs.readFile(filePath);
    const url = signedUrlResponse.uploadUrl;

    try {
      const response = await fetch(url, {
        method: "PUT",
        body: fileBuffer,
        headers: {
          "Content-Type": "application/octet-stream",
        },
      });

      if (!response.ok) {
        return "";
      }
    } catch (error) {
      return "";
    }
    return signedUrlResponse.playgroundFileLocation;
  }
  return "";
}
```

<a name="requirements"></a>

## Requirements

This packages will work out of the box with NodeJS ≥ 18. If you are using an older NodeJS version, you will need to pass a `fetch` polyfill function to `makeQaWolfSdk`. We recommend
[undici](https://undici.nodejs.org/) for this purpose, see below snippet:

```ts
import { fetch } from "undici";

const sdk = makeQaWolfSdk(
  {
    apiKey: "qawolf_xxxxx",
  },
  { fetch },
);
```

<a name="versioning"></a>

## Versioning

This package follows the [SemVer](https://semver.org/) versioning scheme. Additional notes:

- We recommend depending on the `^` range operator for this package, as it will not introduce breaking changes and guarantee an up-to-date API usage version.
<!-- There is a limitation in NPM preventing us from using relative links and hosting other MD files when the repository hosting code is not
  public, hence we inline the changelog in the README. See https://github.com/npm/feedback/discussions/666 -->
- We will provide a changelog for each release, which will be available in the Changelog section below.
- This package major version will be bumped when an API breaking change is introduced. This won't
  happen too often, and we will reach out to you and give advance notice when it does.
- Only top-level exports are considered part of the public API and covered by SemVer.
- Addition of new fields in the API response types are not considered breaking changes.
- Logs and debug messages are not considered part of the public API and can change at any time.

<a name="changelog"></a>

# Changelog

## v1.3.1

- Address `attemptNotifyDeploy` timing-out for some teams

## v1.3.0

- New `notifyTerminatedEphemeralEnvironment` method

## v1.2.0

- New `makePollCiGreenlightStatusIterator` async generator function for advanced polling control with custom early-exit logic

## v1.1.0

- New `ephemeralEnvironment` parameter for `attemptNotifyDeploy`

## v1.0.1

- Internal ESM-related refactoring

## v1.0.0

- Breaking Change: Remove `experimental_vcsBranchTesting` and `experimental_testPreview`. See how to migrate in [Notify Preview Deployment (Pull Request / Merge Request Testing)](#notify-preview) section.

## v0.23.1

- Fix ESM build

## v0.23.0

- Expose an `eventId` on errors
- Output the `environmentId` on `attemptNotifyDeploy`

## v0.22.0

- Support passing code hosting service repository information and PR/MR number to `deployConfig` in `attemptNotifyDeploy`.
- Handle `duplicate_suite_id` in `attemptNotifyDeploy` response.

## v0.21.0

- Make `baseEnvironmentsMapping` optional in `notifyVCSBranchBuildDeployed` and `notifyVCSBranchMergeCompleted`.
- Send `baseVcsBranch` when requesting VCS branch testing and VCS branch merge completion.

## v0.20.0

- `pollCiGreenlightStatus`: bug data fields are now available in the `"underReview"` run stage.
- `pollCiGreenlightStatus`: new `otherBlockingBugsInEnvironment` response
  field. Some runs with a subset of workflows in environment will not reproduce
  blocking bugs in this environment. You can now see these other blocking bugs
  with this field.

## v0.19.0

- Add `abortOnSuperseded` option to `pollCiGreenlightStatus` to allow for aborting the polling operation when the run is superseded.

## v0.18.0

- Add `generateSignedUrlForRunInputsExecutablesStorage` function to generate a signed url used to upload files to the Run Inputs Executables bucket.

## v0.17.1

- Add detailed feedback when `generateSignedUrlForTempTeamStorage` fails.

## v0.17.0

- Add `generateSignedUrlForTempTeamStorage` function to be used in customer's CI pipeline.

## v0.16.0

- Document experimental VCS Branch/PR Testing features.
- Deprecate `experimental_testPreview` and `experimental_removeEnvironment`. These methods will be removed in an upcoming release.

## v0.15.8

- Fix vcsBranchTesting error handling.

## v0.15.7

- Set the default timeout for `fetch` calls to 60 seconds.

## v0.15.6

- Add a new dependency, `@qawolf/ci-utils`, and move the `LogDriver` interface to it.
- Add new experimental features to the SDK in `experimental_vcsBranchTesting`. These
  features are not available to customers nor documented yet, but will be advertised
  in a near future.

## v0.15.5

- Throw a runtime error when `fetch` is not defined. This would be the case for
  setups with a NodeJS version < 18.

## v0.15.4

- Fix broken ESM build due to lacking `.js` extension in some built files.

## v0.15.0

- New `experimental_testPreview` and `experimental_deleteEnvironment` methods.

## v0.14.0

- New `failReason`, `abortReason` and `httpStatus` fields added to the `attemptNotifyDeploy`
  function result object to provide more context on why the operation failed or was aborted.
- Exported missing TypeScript typings describing result objects and fields for both functions.

## v0.13.0

- New `pollTimeout` parameter for `pollCiGreenlightStatus` to allow for a custom
  timeout in milliseconds for the polling operation. It now defaults to two hours.
- New `abortReason` and `httpStatus` fields added to the `PollCiGreenlightStatus` type
  with `"aborted"` outcome to provide more context on why the poll was aborted.

## v0.12.1

- Export dependencies types from root module.
- Refine `DeployConfig.hostingService` type to match API requirements.
- Fix inaccessible changelog file from NPM.

## v0.12.0

- Define a more restrictive `LogDriver` interface for easy integration with GHA
  `core` interface.

## v0.11.0

- Support `reproducedBugs` field from CI-greenlight API.

> :warning: These are the last breaking changes brought to the 0.x major version.
> These are being introduced while the SDK hasn't been advertised to the public yet.
> Future changes will follow the SemVer versioning scheme.

BREAKING CHANGES:

- `pollCiGreenlightStatus` and `attemptNotifyDeploy` now return a "result" object
  with an `outcome` field that can be either `"success"`, `"failed"` or `"aborted"`,
  instead of exiting the process with a non-zero code. This will provide more
  flexibility to the user to decide how to handle the outcome.

## v0.10.3

BREAKING CHANGES:

- Renamed `attemptDeploy` to `attemptNotifyDeploy` .

## v0.10.2

- Avoid logging dots after URL names in the logs. Dots can confuse terminal
  URL detection.

## v0.10.1

- Fix TypeScript types visibility.

## v0.10.0

Initial release.

### Supported Endpoints

- `/api/deploy_success`
- `/api/v0/ci-greenlight/[root-run-id]`