@qawolf/ci-sdk 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ import type { DeploymentVerdict, MatchedRun, WaitContext } from "./types.js";
2
+ export type RunsFound = {
3
+ kind: "runs";
4
+ runs: MatchedRun[];
5
+ };
6
+ type Phase = DeploymentVerdict | RunsFound;
7
+ /**
8
+ * Polls the deployment's trigger evaluations until they name the runs that
9
+ * answer for it, or until it is clear nothing will run. Bounded by
10
+ * `runAppearanceTimeout`: a trigger that never resolves does not hold up the
11
+ * runs that did start.
12
+ */
13
+ export declare function awaitRuns(ctx: WaitContext): Promise<Phase>;
14
+ export {};
@@ -0,0 +1,19 @@
1
+ import type { DeploymentVerdict, MatchedRun, VerdictRun, WaitContext } from "./types.js";
2
+ /**
3
+ * The whole-deployment answer: one failing run fails the deployment, and a
4
+ * cancellation means QA Wolf reached no verdict rather than that it passed.
5
+ *
6
+ * A superseded run has no verdict either: a newer deployment replaced it, and
7
+ * the wait stops there rather than follow `supersededBy`, because the
8
+ * replacement may be testing a later commit. Its status is `superseded` even
9
+ * when a flow found a blocking bug before the run was abandoned, and that bug
10
+ * is still a bug in this commit, so the blocking bug count fails the
11
+ * deployment ahead of the status.
12
+ *
13
+ * A deployment is expected to resolve to exactly one run; the fold over several
14
+ * is a guard, so that a violation of that expectation cannot be reported as
15
+ * passing. When more than one run was superseded, the top-level replacement is
16
+ * the first one's, and each run carries its own.
17
+ */
18
+ export declare function aggregateRuns(deploymentId: string, runs: VerdictRun[]): DeploymentVerdict;
19
+ export declare function awaitVerdict(ctx: WaitContext, matched: MatchedRun[]): Promise<DeploymentVerdict>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * If you change these, update the JSDoc on `WaitForVerdictConfig` with them.
3
+ * The whole-operation budget matches the legacy `pollCiGreenlightStatus`
4
+ * `pollTimeout`, so moving off that function does not also move the default.
5
+ */
6
+ export declare const waitForVerdictDefaults: {
7
+ maxRetries: number;
8
+ retryInterval: number;
9
+ runAppearanceTimeout: number;
10
+ runPollInterval: number;
11
+ timeout: number;
12
+ triggerPollInterval: number;
13
+ };
@@ -0,0 +1,14 @@
1
+ import type { PublicApiCallFailure } from "../../publicApi/callContract.js";
2
+ import type { DeploymentVerdict } from "./types.js";
3
+ /**
4
+ * How the wait should answer a failed call: retry it until the budget runs
5
+ * out, or stop on a refusal that retrying cannot change.
6
+ */
7
+ export type FailureHandling = {
8
+ exhaustedVerdict: DeploymentVerdict;
9
+ kind: "retry";
10
+ } | {
11
+ kind: "stop";
12
+ verdict: DeploymentVerdict;
13
+ };
14
+ export declare function handleCallFailure(failure: PublicApiCallFailure, deploymentId: string): FailureHandling;
@@ -0,0 +1,12 @@
1
+ import type { ApiConfig } from "../../../api-types.js";
2
+ import type { SdkDependencies } from "../../dependencies.js";
3
+ import type { DeploymentVerdict, WaitForVerdictParams } from "./types.js";
4
+ /**
5
+ * Blocks until QA Wolf can say whether a deployment's tests passed.
6
+ *
7
+ * It takes only the deployment id the report gave back: resolving that to the
8
+ * runs its triggers created, and those runs to a status, stays inside. Nothing
9
+ * throws — a refusal, a dead network, a cancellation and an exhausted budget
10
+ * are all arms of the returned verdict.
11
+ */
12
+ export declare function waitForVerdict(deps: SdkDependencies, apiConfig: ApiConfig, { deploymentId, maxRetries, onProgress, retryInterval, runAppearanceTimeout, runPollInterval, signal, timeout, triggerPollInterval, }: WaitForVerdictParams): Promise<DeploymentVerdict>;
@@ -0,0 +1,3 @@
1
+ import type { LogDriver } from "@qawolf/ci-utils";
2
+ import type { DeploymentVerdict } from "./types.js";
3
+ export declare function logVerdict(log: LogDriver, verdict: DeploymentVerdict): void;
@@ -0,0 +1,20 @@
1
+ import type { PublicApiCallResult } from "../../publicApi/callContract.js";
2
+ import type { DeploymentVerdict, WaitContext } from "./types.js";
3
+ export type PollStep<Result> = {
4
+ intervalMs: number;
5
+ kind: "continue";
6
+ } | {
7
+ kind: "done";
8
+ result: Result;
9
+ };
10
+ /**
11
+ * Repeats one public API call until it answers something terminal, the caller
12
+ * hangs up, or the deadline passes. Nothing here throws: a refusal, a dead
13
+ * network and an exhausted budget are all verdicts the caller receives.
14
+ */
15
+ export declare function pollUntil<Data, Result>(ctx: WaitContext, { deadlineAt, onData, onDeadline, request, }: {
16
+ deadlineAt: number;
17
+ onData: (data: Data) => Promise<PollStep<Result>>;
18
+ onDeadline: () => DeploymentVerdict | Result;
19
+ request: () => Promise<PublicApiCallResult<Data>>;
20
+ }): Promise<DeploymentVerdict | Result>;
@@ -0,0 +1,7 @@
1
+ import type { VerdictStage, WaitContext } from "./types.js";
2
+ /**
3
+ * Reports a stage once, when it differs from the one before it. The log line
4
+ * is the identity of a stage, so a run that keeps executing does not repeat
5
+ * itself in the build log.
6
+ */
7
+ export declare function reportStage(ctx: WaitContext, previousDescription: string | undefined, stage: VerdictStage): Promise<string>;
@@ -0,0 +1,20 @@
1
+ import type { PublicApiOutput, publicContractsV1 } from "@qawolf/api-contracts";
2
+ import type { MatchedRun, NotTestedReason, TriggerNote } from "./types.js";
3
+ export type TriggerEvaluations = PublicApiOutput<typeof publicContractsV1.deployment.listTriggerEvaluations>;
4
+ export type EvaluationReading = {
5
+ kind: "not-evaluated";
6
+ }
7
+ /** Some trigger is still creating a run; `runs` are the ones already known. */
8
+ | {
9
+ kind: "awaiting-run";
10
+ matchedTriggerCount: number;
11
+ runs: MatchedRun[];
12
+ } | {
13
+ kind: "runs";
14
+ runs: MatchedRun[];
15
+ } | {
16
+ kind: "not-tested";
17
+ reason: NotTestedReason;
18
+ triggers: TriggerNote[];
19
+ };
20
+ export declare function readEvaluations(evaluations: TriggerEvaluations): EvaluationReading;
@@ -0,0 +1,53 @@
1
+ import type { ApiConfig } from "../../../api-types.js";
2
+ import type { SdkDependencies } from "../../dependencies.js";
3
+ export type Reply = {
4
+ body: unknown;
5
+ status: number;
6
+ } | {
7
+ kind: "network-error";
8
+ };
9
+ export declare function ok(payload: unknown): Reply;
10
+ export declare function httpError(status: number, message: string): Reply;
11
+ export declare const networkError: Reply;
12
+ export declare function evaluated(evaluations: unknown[]): Reply;
13
+ export declare const notEvaluated: Reply;
14
+ export declare function matchedTrigger(run: unknown, triggerName?: string): unknown;
15
+ export declare function unmatchedTrigger(reason: string, triggerName?: string): {
16
+ triggerName: string;
17
+ verdict: {
18
+ conditionSets: {
19
+ conditions: {
20
+ kind: string;
21
+ outcome: string;
22
+ reason: string;
23
+ }[];
24
+ outcome: string;
25
+ }[];
26
+ outcome: string;
27
+ };
28
+ };
29
+ export declare function runPayload({ blockingBugCount, runId, status, }: {
30
+ blockingBugCount?: number;
31
+ runId?: string;
32
+ status: string;
33
+ }): Record<string, unknown>;
34
+ /**
35
+ * A fetch that answers each contract from its own queue, holding on the last
36
+ * reply once a queue runs out, plus a clock that only moves when the SDK
37
+ * sleeps. Together they let a test walk a two-hour poll in milliseconds and
38
+ * count exactly how many times the SDK asked. `runs` is one queue shared by
39
+ * every run, or a queue per requested run id when a test needs to tell two
40
+ * runs apart.
41
+ */
42
+ export declare function makeHarness({ evaluations, runs, }: {
43
+ evaluations?: Reply[];
44
+ runs?: Record<string, Reply[]> | Reply[];
45
+ }): {
46
+ apiConfig: ApiConfig;
47
+ calls: {
48
+ evaluations: number;
49
+ runs: number;
50
+ };
51
+ deps: SdkDependencies;
52
+ fetch: import("jest-mock").Mock<(url: unknown, init: unknown) => Promise<Response>>;
53
+ };
@@ -0,0 +1,48 @@
1
+ import type { DeploymentVerdict, NotTestedReason, TriggerNote, VerdictRun, VerdictStage } from "@qawolf/api-contracts";
2
+ import type { ApiConfig } from "../../../api-types.js";
3
+ import type { SdkDependencies } from "../../dependencies.js";
4
+ export type { DeploymentVerdict, NotTestedReason, TriggerNote, VerdictRun, VerdictStage, };
5
+ export type WaitForVerdictConfig = {
6
+ /** Network and server-error retries before giving up. @default 10 */
7
+ maxRetries: number;
8
+ /** Fired on every stage change, for CI logs. */
9
+ onProgress: (stage: VerdictStage) => unknown | Promise<unknown>;
10
+ /** Between retries of a failed call. @default 10s */
11
+ retryInterval: number;
12
+ /**
13
+ * Give up if no trigger has produced a run by then. Runs that did appear are
14
+ * still waited on. @default 5min
15
+ */
16
+ runAppearanceTimeout: number;
17
+ /** Between polls while a run executes. @default 30s */
18
+ runPollInterval: number;
19
+ /** Cancels the wait; yields `outcome: "canceled-by-caller"`. */
20
+ signal: AbortSignal;
21
+ /**
22
+ * Whole-operation budget. Set it below the CI job's own ceiling, so the wait
23
+ * ends with a run URL and a stage instead of the job being killed.
24
+ * @default 2h
25
+ */
26
+ timeout: number;
27
+ /** Between polls while a trigger is still producing a run. @default 3s */
28
+ triggerPollInterval: number;
29
+ };
30
+ export type WaitForVerdictParams = {
31
+ deploymentId: string;
32
+ } & Partial<WaitForVerdictConfig>;
33
+ export type ResolvedWaitConfig = Omit<WaitForVerdictConfig, "onProgress" | "signal"> & {
34
+ onProgress: ((stage: VerdictStage) => unknown | Promise<unknown>) | undefined;
35
+ signal: AbortSignal | undefined;
36
+ };
37
+ export type WaitContext = {
38
+ apiConfig: ApiConfig;
39
+ config: ResolvedWaitConfig;
40
+ deploymentId: string;
41
+ deps: SdkDependencies;
42
+ startedAt: number;
43
+ };
44
+ /** A run one trigger created, before its status is known. */
45
+ export type MatchedRun = {
46
+ runId: string;
47
+ triggerName: string;
48
+ };
@@ -3,7 +3,7 @@ export declare function makeQaWolfSdk({ apiKey, serviceBase, userAgent, }: {
3
3
  apiKey: string;
4
4
  serviceBase?: string;
5
5
  userAgent?: string;
6
- }, { fetch, log, }?: Partial<SdkDependencies>): {
6
+ }, { clock, fetch, log, }?: Partial<SdkDependencies>): {
7
7
  /**
8
8
  * @deprecated Reports through the legacy `deploy_success` webhook, which
9
9
  * triggers do not see. Use `deployment.reportStatus` instead.
@@ -34,6 +34,137 @@ export declare function makeQaWolfSdk({ apiKey, serviceBase, userAgent, }: {
34
34
  } | undefined;
35
35
  service?: string | undefined;
36
36
  }) => Promise<import("./domain/reportDeployment/types.js").ReportDeploymentResult>;
37
+ waitForVerdict: (args_0: import("./domain/waitForVerdict/types.js").WaitForVerdictParams) => Promise<{
38
+ deploymentId: string;
39
+ outcome: "passed";
40
+ runs: {
41
+ blockingBugCount: number;
42
+ runId: string;
43
+ runUrl: string;
44
+ status: "canceled" | "failed" | "passed" | "superseded" | "queued" | "running";
45
+ triggerName: string;
46
+ supersededBy?: {
47
+ runId: string;
48
+ url: string;
49
+ } | undefined;
50
+ }[];
51
+ } | {
52
+ blockingBugCount: number;
53
+ deploymentId: string;
54
+ outcome: "failed";
55
+ runs: {
56
+ blockingBugCount: number;
57
+ runId: string;
58
+ runUrl: string;
59
+ status: "canceled" | "failed" | "passed" | "superseded" | "queued" | "running";
60
+ triggerName: string;
61
+ supersededBy?: {
62
+ runId: string;
63
+ url: string;
64
+ } | undefined;
65
+ }[];
66
+ } | {
67
+ deploymentId: string;
68
+ outcome: "not-tested";
69
+ reason: "no-trigger-matched" | "no-run-created";
70
+ triggers: {
71
+ message: string;
72
+ triggerName: string;
73
+ reason?: "skipped" | "billing-prevented" | "branch-still-syncing" | "deployment-no-longer-live" | "duplicate-run" | "environment-not-linked-to-branch" | "environment-not-ready" | "environment-terminated" | "internal-error" | "low-risk-change" | "no-flows-to-run" | "no-matching-trigger" | "no-pull-request" | "no-relevant-flows" | "rate-limited" | "run-not-created" | "test-configuration-error" | "trigger-not-found" | "workspace-inactive" | "workspace-not-ready" | undefined;
74
+ }[];
75
+ } | {
76
+ deploymentId: string;
77
+ outcome: "run-canceled";
78
+ runs: {
79
+ blockingBugCount: number;
80
+ runId: string;
81
+ runUrl: string;
82
+ status: "canceled" | "failed" | "passed" | "superseded" | "queued" | "running";
83
+ triggerName: string;
84
+ supersededBy?: {
85
+ runId: string;
86
+ url: string;
87
+ } | undefined;
88
+ }[];
89
+ } | {
90
+ deploymentId: string;
91
+ outcome: "superseded";
92
+ runs: {
93
+ blockingBugCount: number;
94
+ runId: string;
95
+ runUrl: string;
96
+ status: "canceled" | "failed" | "passed" | "superseded" | "queued" | "running";
97
+ triggerName: string;
98
+ supersededBy?: {
99
+ runId: string;
100
+ url: string;
101
+ } | undefined;
102
+ }[];
103
+ supersededBy: {
104
+ runId: string;
105
+ url: string;
106
+ };
107
+ } | {
108
+ deploymentId: string;
109
+ elapsedMs: number;
110
+ lastStage: {
111
+ stage: "waiting-for-evaluation";
112
+ } | {
113
+ matchedTriggerCount: number;
114
+ stage: "waiting-for-run";
115
+ } | {
116
+ runs: {
117
+ blockingBugCount: number;
118
+ runId: string;
119
+ runUrl: string;
120
+ status: "canceled" | "failed" | "passed" | "superseded" | "queued" | "running";
121
+ triggerName: string;
122
+ supersededBy?: {
123
+ runId: string;
124
+ url: string;
125
+ } | undefined;
126
+ }[];
127
+ stage: "waiting-for-verdict";
128
+ };
129
+ outcome: "timed-out";
130
+ runs: {
131
+ blockingBugCount: number;
132
+ runId: string;
133
+ runUrl: string;
134
+ status: "canceled" | "failed" | "passed" | "superseded" | "queued" | "running";
135
+ triggerName: string;
136
+ supersededBy?: {
137
+ runId: string;
138
+ url: string;
139
+ } | undefined;
140
+ }[];
141
+ } | {
142
+ deploymentId: string;
143
+ outcome: "canceled-by-caller";
144
+ } | {
145
+ deploymentId: string;
146
+ outcome: "unauthorized";
147
+ message?: string | undefined;
148
+ } | {
149
+ deploymentId: string;
150
+ outcome: "not-found";
151
+ message?: string | undefined;
152
+ } | {
153
+ deploymentId: string;
154
+ httpStatus: number;
155
+ outcome: "server-error";
156
+ } | {
157
+ deploymentId: string;
158
+ httpStatus: number;
159
+ outcome: "unexpected-status";
160
+ message?: string | undefined;
161
+ } | {
162
+ deploymentId: string;
163
+ outcome: "network-error";
164
+ } | {
165
+ deploymentId: string;
166
+ outcome: "invalid-response-body";
167
+ }>;
37
168
  };
38
169
  generateSignedUrlForRunInputsExecutablesStorage: (config: import("./domain/generateSignedUrls/types.js").GenerateSignedUrlConfig) => Promise<import("./domain/generateSignedUrls/types.js").GenerateSignedUrlStatus>;
39
170
  generateSignedUrlForTempTeamStorage: (config: import("./domain/generateSignedUrls/types.js").GenerateSignedUrlConfig) => Promise<{
@@ -60,3 +191,4 @@ export type * from "./domain/generateSignedUrls/types.js";
60
191
  export type * from "./domain/notifyTerminatedEphemeralEnvironment/types.js";
61
192
  export type * from "./domain/pollCiGreenlight/types.js";
62
193
  export type * from "./domain/reportDeployment/types.js";
194
+ export type * from "./domain/waitForVerdict/types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/ci-sdk",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "private": false,
5
5
  "description": "A simple SDK for interacting with QAWolf in CI scripts.",
6
6
  "keywords": [],
@@ -29,7 +29,7 @@
29
29
  "tsc:check": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@qawolf/api-contracts": ">=0.68.0",
32
+ "@qawolf/api-contracts": ">=0.73.0",
33
33
  "@qawolf/ci-utils": "^2.0.0",
34
34
  "@sinonjs/fake-timers": "^10.3.0",
35
35
  "tslib": "^2.6.2",