@qawolf/ci-sdk 3.0.0 → 3.1.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.
- package/README.md +271 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +221 -4
- package/dist/index.js.map +4 -4
- package/dist/lib/sdk/domain/attemptDeploy/index.d.ts +4 -0
- package/dist/lib/sdk/domain/detectProviderDeploymentId/index.d.ts +49 -0
- package/dist/lib/sdk/domain/pollCiGreenlight/index.d.ts +4 -0
- package/dist/lib/sdk/domain/reportDeployment/index.d.ts +4 -0
- package/dist/lib/sdk/domain/reportDeployment/types.d.ts +50 -0
- package/dist/lib/sdk/index.d.ts +35 -0
- package/dist/lib/sdk/publicApi/callContract.d.ts +39 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -11,9 +11,11 @@ to determine the status of your CI/CD step/job/action.
|
|
|
11
11
|
|
|
12
12
|
## Table of Contents
|
|
13
13
|
|
|
14
|
-
- [
|
|
14
|
+
- [API Keys and Workspaces](#api-keys)
|
|
15
|
+
- [Report a Deployment](#report-deployment)
|
|
16
|
+
- [Notify Deployment (deprecated)](#notify-deployment)
|
|
15
17
|
- [Notify Preview Deployment (Pull Request / Merge Request Testing)](#notify-preview)
|
|
16
|
-
- [Poll for CI Greenlight Status](#ci-greenlight)
|
|
18
|
+
- [Poll for CI Greenlight Status (deprecated)](#ci-greenlight)
|
|
17
19
|
- [Advanced: Poll with Custom Control (Iterator)](#ci-greenlight-iterator)
|
|
18
20
|
- [Notify a Terminated Ephemeral Environment](#notify-terminated-ephemeral-environment)
|
|
19
21
|
- [Upload Run Input Artifacts](#upload-artifacts)
|
|
@@ -21,10 +23,240 @@ to determine the status of your CI/CD step/job/action.
|
|
|
21
23
|
- [Versioning](#versioning)
|
|
22
24
|
- [Changelog](#changelog)
|
|
23
25
|
|
|
26
|
+
<a id="api-keys"></a>
|
|
27
|
+
|
|
28
|
+
## API Keys and Workspaces
|
|
29
|
+
|
|
30
|
+
Every call you make is authenticated by the `apiKey` you pass to `makeQaWolfSdk`, and every call
|
|
31
|
+
acts on exactly one workspace. How that workspace is determined depends on which kind of API key
|
|
32
|
+
you hold:
|
|
33
|
+
|
|
34
|
+
- A **workspace API key** (also called a team API key) already belongs to a single workspace. It
|
|
35
|
+
implies the workspace, so you never need to name one.
|
|
36
|
+
- An **organization API key** or a **user API key** can reach several workspaces, so it does not
|
|
37
|
+
imply one. You name the workspace by passing `workspaceId` on the call.
|
|
38
|
+
|
|
39
|
+
`workspaceId` is the ID of the workspace (team) you want the call to act on. If you are not sure
|
|
40
|
+
which value to use, ask your QA Wolf representative; `attemptNotifyDeploy` also reports the
|
|
41
|
+
workspace it selected, with its ID, in the warning described below.
|
|
42
|
+
|
|
43
|
+
### Passing `workspaceId`
|
|
44
|
+
|
|
45
|
+
The field is accepted on every function that acts on a workspace:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const { attemptNotifyDeploy, generateSignedUrlForRunInputsExecutablesStorage } =
|
|
49
|
+
makeQaWolfSdk({
|
|
50
|
+
apiKey: "qawolf_xxxxx",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Organization and user API keys must name the workspace on each call.
|
|
54
|
+
await generateSignedUrlForRunInputsExecutablesStorage({
|
|
55
|
+
destinationFilePath: "unityexpo.ipa",
|
|
56
|
+
workspaceId: "your-workspace-id",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await attemptNotifyDeploy({
|
|
60
|
+
...deployConfig,
|
|
61
|
+
workspaceId: "your-workspace-id",
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`notifyTerminatedEphemeralEnvironment` accepts `workspaceId` the same way.
|
|
66
|
+
|
|
67
|
+
### What happens when it is missing
|
|
68
|
+
|
|
69
|
+
Omitting `workspaceId` with an organization or user API key is handled differently depending on the
|
|
70
|
+
call, so it is worth knowing which behavior to expect:
|
|
71
|
+
|
|
72
|
+
| Function | Behavior when `workspaceId` is omitted |
|
|
73
|
+
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `generateSignedUrlForTempTeamStorage` | Fails. The backend answers HTTP `400` with the message `workspaceId is a required query parameter for organization and user API keys`. |
|
|
75
|
+
| `generateSignedUrlForRunInputsExecutablesStorage` | Same as above — the backend answers HTTP `400`. |
|
|
76
|
+
| `attemptNotifyDeploy` | Succeeds against your organization's first-created workspace and returns a `warning` naming it. |
|
|
77
|
+
| `notifyTerminatedEphemeralEnvironment` | Succeeds against your organization's first-created workspace and returns a `warning` naming it. |
|
|
78
|
+
|
|
79
|
+
> ⚠️ The signed-URL functions **require** `workspaceId` for organization and user API keys — they do
|
|
80
|
+
> not fall back to a default workspace. A `400` from either function, especially when the API key and
|
|
81
|
+
> artifact name are otherwise correct, most often means `workspaceId` was not sent.
|
|
82
|
+
|
|
83
|
+
When this happens, the SDK logs the real status before it reports the outcome, so the log line is
|
|
84
|
+
the reliable signal:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
❌ [callGenerateSignedUrlForRunInputsExecutablesStorage] 400 Bad Request undefined
|
|
88
|
+
🚫 Unrecoverable error (status 0) when generating signed upload url: Network error, aborting request to generate signed URL. aborting. undefined
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `400 Bad Request` on the first line is the actual response. The `status 0` and "Network error"
|
|
92
|
+
on the second line are misreported and do not indicate a transport problem — read the first line
|
|
93
|
+
and add `workspaceId`.
|
|
94
|
+
|
|
95
|
+
For the same reason, do not branch on `httpStatus` or `abortReason` to detect this case: a failed
|
|
96
|
+
signed-URL request currently reports `httpStatus: 0` and `abortReason: "client-network-error"`
|
|
97
|
+
whatever the real status was. Treat any `success: false` from these two functions as a request that
|
|
98
|
+
did not reach a signed URL, and check the logged status to find out why.
|
|
99
|
+
|
|
100
|
+
Passing `workspaceId` with a workspace API key is unnecessary but harmless, as long as the ID names
|
|
101
|
+
the key's own workspace. Naming a workspace the key cannot reach fails with HTTP `403`.
|
|
102
|
+
|
|
103
|
+
`workspaceId` support was added in `@qawolf/ci-sdk` 2.2.0. On earlier versions, use a
|
|
104
|
+
workspace-scoped API key.
|
|
105
|
+
|
|
106
|
+
<a id="report-deployment"></a>
|
|
107
|
+
|
|
108
|
+
## Report a Deployment
|
|
109
|
+
|
|
110
|
+
Report a deployment through the public `deployment.reportStatus` API. A deployment's first `success` report is what evaluates your triggers, so this is the call that starts a run.
|
|
111
|
+
|
|
112
|
+
> ⚠️ Triggers only see deployments reported this way. `attemptNotifyDeploy` reports through the legacy `deploy_success` webhook and is deprecated.
|
|
113
|
+
|
|
114
|
+
### Example
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { detectProviderDeploymentId, makeQaWolfSdk } from "@qawolf/ci-sdk";
|
|
118
|
+
|
|
119
|
+
const { deployment } = makeQaWolfSdk({ apiKey: "qawolf_xxxxx" });
|
|
120
|
+
|
|
121
|
+
(async () => {
|
|
122
|
+
// `{ outcome: "detected", ci: "github", id: "18273645-2-deploy" }` on GitHub
|
|
123
|
+
// Actions, Jenkins, GitLab CI, CircleCI, Bitbucket Pipelines and Buildkite;
|
|
124
|
+
// `{ outcome: "unsupported" }` elsewhere, in which case compose the id from
|
|
125
|
+
// your own CI's variables.
|
|
126
|
+
const detected = detectProviderDeploymentId();
|
|
127
|
+
if (detected.outcome !== "detected") process.exit(1);
|
|
128
|
+
|
|
129
|
+
const result = await deployment.reportStatus({
|
|
130
|
+
environment: { name: "staging" },
|
|
131
|
+
metadata: {
|
|
132
|
+
commitSha: process.env.GITHUB_SHA,
|
|
133
|
+
ref: process.env.GITHUB_REF_NAME,
|
|
134
|
+
repository: process.env.GITHUB_REPOSITORY,
|
|
135
|
+
},
|
|
136
|
+
providerDeploymentId: detected.id,
|
|
137
|
+
status: "success",
|
|
138
|
+
workspaceId: "your-workspace-id",
|
|
139
|
+
});
|
|
140
|
+
if (result.outcome !== "reported") {
|
|
141
|
+
// `result.outcome` says why, and the refusal arms carry the server's
|
|
142
|
+
// explanation in `result.message` when it sent one.
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
console.log(
|
|
146
|
+
`Reported deployment ${result.deployment.id}: ${result.deployment.url}`,
|
|
147
|
+
);
|
|
148
|
+
})();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The parameters are exactly the input of `deployment.reportStatus`; `ReportDeploymentParams` is derived from the published `@qawolf/api-contracts` package, so the SDK cannot drift from the API. Every field the legacy webhook accepted has an equivalent: `environmentVariables`, `deployTarget`, ephemeral environments through `environment.ephemeral` and `environment.baseEphemeralEnvironment`, and `metadata.repository` / `pullRequestNumber` / `commitSha` / `ref`.
|
|
152
|
+
|
|
153
|
+
### `status`
|
|
154
|
+
|
|
155
|
+
You choose the status: `pending`, `success`, `failure` or `inactive`. Only a deployment's **first `success`** report evaluates triggers, and it is the only report that does. A `pending` report records the deployment and starts nothing, so report `pending` when the deploy begins if you want it visible, and report `success` when the deploy is actually live — that second call is what starts the run. A trigger added or unpaused after that first `success` does not make the deployment evaluate again; report the deployment under a new `providerDeploymentId` to evaluate it against the current triggers.
|
|
156
|
+
|
|
157
|
+
### `providerDeploymentId`
|
|
158
|
+
|
|
159
|
+
This is the deployment's identity. Reports that share one update the same deployment, and the deployment stays bound to the environment its first report resolved.
|
|
160
|
+
|
|
161
|
+
It is required, and the SDK never guesses it. Beware that `GITHUB_RUN_ID` alone is **stable across re-runs** and **shared by every job in the workflow**: a re-run of a failed job, or a second job deploying alongside the first, would report into the deployment that already evaluated, create no run, and appear to hang. `detectProviderDeploymentId()` composes an id that is distinct for every job definition and every re-run:
|
|
162
|
+
|
|
163
|
+
| CI system | Detected on | Id |
|
|
164
|
+
| ------------------- | ------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
165
|
+
| GitHub Actions | `GITHUB_ACTIONS` | `GITHUB_RUN_ID`-`GITHUB_RUN_ATTEMPT`-`GITHUB_JOB` |
|
|
166
|
+
| GitLab CI | `GITLAB_CI` | `CI_PIPELINE_ID`-`CI_JOB_ID` |
|
|
167
|
+
| CircleCI | `CIRCLECI` | `CIRCLE_WORKFLOW_ID`-`CIRCLE_BUILD_NUM` |
|
|
168
|
+
| Buildkite | `BUILDKITE` | `BUILDKITE_BUILD_ID`-`BUILDKITE_JOB_ID`-`BUILDKITE_RETRY_COUNT` |
|
|
169
|
+
| Jenkins | `JENKINS_URL` or `JENKINS_HOME` | `BUILD_TAG` |
|
|
170
|
+
| Bitbucket Pipelines | `BITBUCKET_WORKSPACE` or `BITBUCKET_BUILD_NUMBER` | `BITBUCKET_BUILD_NUMBER`-`BITBUCKET_STEP_UUID`-`BITBUCKET_STEP_RUN_NUMBER` |
|
|
171
|
+
|
|
172
|
+
Jenkins needs no job or attempt part: `BUILD_TAG` is `jenkins-<job name>-<build number>`, and Jenkins allocates a fresh build number for every re-run you can start from outside the build. Bitbucket needs all three, because rerunning only the failed steps keeps the build number while parallel steps share both it and the step run number.
|
|
173
|
+
|
|
174
|
+
The helper reports `{ outcome: "missing-variables", missingVariables }` when the CI system is recognized but a variable is absent, and `{ outcome: "unsupported" }` on any other CI, rather than guess an id that would not change on a re-run.
|
|
175
|
+
|
|
176
|
+
#### Concurrent deployments from one job definition
|
|
177
|
+
|
|
178
|
+
The guarantee stops at the job definition. Where one definition deploys more than once at a time, nothing in the environment tells the deployments apart, and the helper alone would hand them all the same id:
|
|
179
|
+
|
|
180
|
+
| CI system | What collides |
|
|
181
|
+
| -------------- | --------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| GitHub Actions | Every leg of a `strategy.matrix` shares `GITHUB_JOB`, `GITHUB_RUN_ID` and `GITHUB_RUN_ATTEMPT`. |
|
|
183
|
+
| Jenkins | A declarative `matrix` or `parallel` block runs its branches inside one build, sharing `BUILD_TAG`. |
|
|
184
|
+
| CircleCI | A job run with `parallelism` shares `CIRCLE_BUILD_NUM` across its containers. |
|
|
185
|
+
|
|
186
|
+
GitLab CI, Buildkite and Bitbucket Pipelines are not affected: `CI_JOB_ID`, `BUILDKITE_JOB_ID` and `BITBUCKET_STEP_UUID` are already distinct for each parallel leg.
|
|
187
|
+
|
|
188
|
+
Pass a `discriminator` for those, and for any job that loops over several environments. It is appended to the composed id:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const detected = detectProviderDeploymentId({
|
|
192
|
+
// In GitHub Actions: `discriminator: ${{ toJSON(matrix) }}` through an env
|
|
193
|
+
// variable, or just the one matrix value that names the deployment.
|
|
194
|
+
discriminator: process.env.DEPLOY_ENVIRONMENT,
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`detectProviderDeploymentId` also takes `env`, which defaults to `process.env`. Composing the `providerDeploymentId` yourself is always an option; the helper is a convenience, not a requirement.
|
|
199
|
+
|
|
200
|
+
### `workspaceId`
|
|
201
|
+
|
|
202
|
+
`deployment.reportStatus` requires `workspaceId` for every key kind; a workspace API key must name its own workspace. See [API Keys and Workspaces](#api-keys).
|
|
203
|
+
|
|
204
|
+
### Result
|
|
205
|
+
|
|
206
|
+
`deployment.reportStatus` returns a discriminated union and never throws for an outcome you are expected to branch on. Branch on `outcome` alone:
|
|
207
|
+
|
|
208
|
+
| `outcome` | Meaning |
|
|
209
|
+
| ----------------------- | ------------------------------------------------------------------------------------------ |
|
|
210
|
+
| `reported` | Carries `deployment`, the whole resource: `id`, `status` and `url`. |
|
|
211
|
+
| `invalid-request` | The report did not satisfy the contract. Carries `message`. |
|
|
212
|
+
| `unauthorized` | The API key is unknown, or may not act on this workspace. Carries `message`. |
|
|
213
|
+
| `not-found` | The workspace or environment named in the report does not exist. Carries `message`. |
|
|
214
|
+
| `conflict` | The `providerDeploymentId` is already bound to a different environment. Carries `message`. |
|
|
215
|
+
| `server-error` | QA Wolf failed to record the report; retrying is reasonable. Carries `httpStatus`. |
|
|
216
|
+
| `unexpected-status` | A status this SDK version does not map. Carries `httpStatus` and `message`. |
|
|
217
|
+
| `network-error` | The request never reached QA Wolf, or the connection dropped. |
|
|
218
|
+
| `invalid-response-body` | QA Wolf answered with a body the contract rejects. |
|
|
219
|
+
|
|
220
|
+
So `if (result.outcome !== "reported") process.exit(1)` is the whole check a gate needs.
|
|
221
|
+
|
|
222
|
+
The response carries the deployment only, not the resulting runs. Waiting for a verdict is not part of this call.
|
|
223
|
+
|
|
224
|
+
### Migrating from `attemptNotifyDeploy`
|
|
225
|
+
|
|
226
|
+
```diff
|
|
227
|
+
-const { attemptNotifyDeploy } = makeQaWolfSdk({ apiKey });
|
|
228
|
+
+const { deployment } = makeQaWolfSdk({ apiKey });
|
|
229
|
+
|
|
230
|
+
-const deploy = await attemptNotifyDeploy({
|
|
231
|
+
- branch: process.env.GITHUB_REF_NAME,
|
|
232
|
+
- deploymentType: "staging",
|
|
233
|
+
- hostingService: "GitHub",
|
|
234
|
+
- sha: process.env.GITHUB_SHA,
|
|
235
|
+
-});
|
|
236
|
+
-if (deploy.outcome === "skipped") process.exit(0);
|
|
237
|
+
-if (deploy.outcome !== "success") process.exit(1);
|
|
238
|
+
+const detected = detectProviderDeploymentId();
|
|
239
|
+
+if (detected.outcome !== "detected") process.exit(1);
|
|
240
|
+
+const deploy = await deployment.reportStatus({
|
|
241
|
+
+ environment: { name: "staging" },
|
|
242
|
+
+ metadata: { commitSha: process.env.GITHUB_SHA, ref: process.env.GITHUB_REF_NAME },
|
|
243
|
+
+ providerDeploymentId: detected.id,
|
|
244
|
+
+ status: "success",
|
|
245
|
+
+ workspaceId: "your-workspace-id",
|
|
246
|
+
+});
|
|
247
|
+
+if (deploy.outcome !== "reported") process.exit(1);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`detectProviderDeploymentId()` composes the run, attempt and job, so two jobs in one workflow do not report into the same deployment. Compose the id yourself only if you would rather not call it, and include the job.
|
|
251
|
+
|
|
252
|
+
Four things move: `deploymentType` becomes `environment.name`; `branch` / `sha` become `metadata`; `providerDeploymentId` is new and required (`deduplicationKey` is subsumed by it); `hostingService` disappears, since the code host comes from your integration. There is no `skipped` outcome: whether a trigger matched is not known at report time. `pollCiGreenlightStatus` takes a run id that only `attemptNotifyDeploy` returns, so a pipeline that waits for a verdict stays on the legacy pair until the successor to polling ships.
|
|
253
|
+
|
|
24
254
|
<a id="notify-deployment"></a>
|
|
25
255
|
|
|
26
256
|
## Notify Deployment
|
|
27
257
|
|
|
258
|
+
> ⚠️ **Deprecated.** This reports through the legacy `deploy_success` webhook, which triggers do not see. Use [Report a Deployment](#report-deployment) instead. It keeps working and there is no removal date.
|
|
259
|
+
|
|
28
260
|
Notify us of a successful deployment to trigger a run.
|
|
29
261
|
|
|
30
262
|
> ℹ️ [See the API documentation page for this endpoint](https://qawolf.notion.site/dd72e46ceb7f451dae4e9ef06f64a2cc).
|
|
@@ -135,22 +367,34 @@ const { attemptNotifyDeploy } = makeQaWolfSdk({
|
|
|
135
367
|
|
|
136
368
|
> ⚠️ **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.
|
|
137
369
|
|
|
138
|
-
Once enabled,
|
|
370
|
+
Once enabled, report the preview deployment with [`deployment.reportStatus`](#report-deployment) and name the pull or merge request in `metadata`. The code host comes from your QA Wolf integration, so there is no `hostingService` to pass:
|
|
139
371
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
372
|
+
```ts
|
|
373
|
+
await deployment.reportStatus({
|
|
374
|
+
deployTarget: previewUrl,
|
|
375
|
+
environment: { ephemeral: true, name: `pr-${pullRequestNumber}` },
|
|
376
|
+
metadata: {
|
|
377
|
+
commitSha: process.env.GITHUB_SHA,
|
|
378
|
+
pullRequestNumber,
|
|
379
|
+
ref: process.env.GITHUB_HEAD_REF,
|
|
380
|
+
repository: process.env.GITHUB_REPOSITORY,
|
|
381
|
+
},
|
|
382
|
+
providerDeploymentId: detected.id,
|
|
383
|
+
status: "success",
|
|
384
|
+
workspaceId: "your-workspace-id",
|
|
385
|
+
});
|
|
386
|
+
```
|
|
143
387
|
|
|
144
|
-
|
|
145
|
-
- Pass `hostingService: "GitLab"`, `repository` information, and `mergeRequestNumber` while notifying a deployment as described in the [Notify Deployment](#notify-deployment) section
|
|
388
|
+
`metadata.pullRequestNumber` names a GitLab merge request too. For a preview with no code hosting integration, pass `deployTarget` and an ephemeral `environment` and leave the repository fields out. `environment.baseEphemeralEnvironment` names the environment an ephemeral one inherits its configuration from.
|
|
146
389
|
|
|
147
|
-
|
|
148
|
-
- Pass `ephemeralEnvironment: true` and `deploymentUrl` while notifying a deployment as described in the [Notify Deployment](#notify-deployment) section
|
|
390
|
+
The [GitHub Action](https://github.com/marketplace/actions/notify-qa-wolf-on-deploy) and `attemptNotifyDeploy`'s `hostingService` / `mergeRequestNumber` / `ephemeralEnvironment` / `deploymentUrl` fields still work, but they report through the legacy `deploy_success` webhook that triggers do not see.
|
|
149
391
|
|
|
150
392
|
<a id="ci-greenlight"></a>
|
|
151
393
|
|
|
152
394
|
## Poll for CI Greenlight Status
|
|
153
395
|
|
|
396
|
+
> ⚠️ **Deprecated.** This polls a run id that only `attemptNotifyDeploy` returns, so it keeps your pipeline on the legacy `deploy_success` webhook. It keeps working and there is no removal date.
|
|
397
|
+
|
|
154
398
|
> ℹ️ [See the API documentation page for this endpoint](https://qawolf.notion.site/1b170576efea411fa785842a71e7c99e).
|
|
155
399
|
|
|
156
400
|
```ts
|
|
@@ -407,6 +651,8 @@ const { generateSignedUrlForTempTeamStorage, attemptNotifyDeploy } =
|
|
|
407
651
|
async function uploadRunArtifact(filePath: string): Promise<string> {
|
|
408
652
|
const fileName = path.basename(filePath);
|
|
409
653
|
|
|
654
|
+
// Organization and user API keys must also pass `workspaceId` here, or the
|
|
655
|
+
// request fails with a 400. See "API Keys and Workspaces" above.
|
|
410
656
|
const signedUrlResponse = await generateSignedUrlForTempTeamStorage({
|
|
411
657
|
destinationFilePath: fileName,
|
|
412
658
|
});
|
|
@@ -444,6 +690,10 @@ async function uploadRunArtifact(filePath: string): Promise<string> {
|
|
|
444
690
|
`playgroundFileLocation` field with the same value as `interactiveRunFileLocation`. It is deprecated and
|
|
445
691
|
will be removed in a future major version — use `interactiveRunFileLocation` instead.
|
|
446
692
|
|
|
693
|
+
Both functions require `workspaceId` when you authenticate with an organization or user API key.
|
|
694
|
+
See [API Keys and Workspaces](#api-keys) for details and for what a missing `workspaceId` looks
|
|
695
|
+
like.
|
|
696
|
+
|
|
447
697
|
<a name="requirements"></a>
|
|
448
698
|
|
|
449
699
|
## Requirements
|
|
@@ -485,6 +735,13 @@ This package follows the [SemVer](https://semver.org/) versioning scheme. Additi
|
|
|
485
735
|
|
|
486
736
|
# Changelog
|
|
487
737
|
|
|
738
|
+
## v3.1.0
|
|
739
|
+
|
|
740
|
+
- New `deployment.reportStatus` function, which reports a deployment through the public `deployment.reportStatus` API — the one your triggers watch. A deployment's first `success` report is what starts a run. Its parameters are the API's own input, taken from the `@qawolf/api-contracts` package, so the SDK cannot drift from the API, and it returns `{ outcome: "reported", deployment }` carrying the deployment's `id`, `status` and `url`. A refusal comes back as one named outcome you can branch on — `invalid-request`, `unauthorized`, `not-found`, `conflict`, `server-error`, `unexpected-status`, `network-error` or `invalid-response-body` — so `if (result.outcome !== "reported")` is the whole check a CI gate needs. See [Report a Deployment](#report-deployment).
|
|
741
|
+
- New `detectProviderDeploymentId` function, which works out the `providerDeploymentId` for you from the variables your CI already sets on GitHub Actions, GitLab CI, CircleCI, Buildkite, Jenkins and Bitbucket Pipelines. The id it builds is different for every job definition and every re-run, so a retried job and two jobs deploying side by side each get their own deployment instead of reporting into one that has already started its run. Where one job definition deploys more than once at a time — a GitHub Actions or Jenkins matrix, a CircleCI job with `parallelism`, a loop over environments — no variable tells the legs apart, so pass a `discriminator` and it is appended to the id. On any other CI it returns `{ outcome: "unsupported" }` rather than guess. Passing `providerDeploymentId` yourself is still required; this helper is optional.
|
|
742
|
+
- Pull request and merge request testing is now documented on `deployment.reportStatus`: pass `metadata.repository` and `metadata.pullRequestNumber` instead of `hostingService`. See [Notify Preview Deployment](#notify-preview).
|
|
743
|
+
- `attemptNotifyDeploy` and `pollCiGreenlightStatus` are deprecated in favour of `deployment.reportStatus`. They report through the legacy `deploy_success` webhook, which triggers do not see. Both keep working unchanged, print no warning, and have no removal date.
|
|
744
|
+
|
|
488
745
|
## v3.0.0
|
|
489
746
|
|
|
490
747
|
- **Breaking**: this package is now published as ESM only. The CommonJS build (`dist/index.cjs`) and the `require` entry in `exports` are gone, so `require("@qawolf/ci-sdk")` no longer resolves. Import the package with `import` from an ESM module, or load it from CommonJS with a dynamic `await import("@qawolf/ci-sdk")`.
|
|
@@ -519,6 +776,10 @@ This package follows the [SemVer](https://semver.org/) versioning scheme. Additi
|
|
|
519
776
|
- `attemptNotifyDeploy`: a failed suite result now carries `failureCode` and `failureMessage` alongside the existing `failureReason`. `failureCode` is a stable code to branch on, such as `billing-prevented`, `environment-terminated`, `environment-not-ready`, `test-configuration-error`, or `internal-error`; treat it as an open set, since new codes are added over time. Note that `environment-terminated` is permanent while `environment-not-ready` clears on its own, so retrying is worthwhile only for the second. `failureMessage` is human-readable copy for logs, and the SDK's own failure log line now prints it instead of the internal reason.
|
|
520
777
|
- `attemptNotifyDeploy`: `failureReason` is deprecated. It still ships unchanged and is safe to keep reading, but it carries an internal diagnostic whose values change without notice. Branch on `failureCode` and display `failureMessage` instead. Both are `undefined` when the SDK talks to a backend that predates them.
|
|
521
778
|
|
|
779
|
+
## v2.2.0
|
|
780
|
+
|
|
781
|
+
- `generateSignedUrlForTempTeamStorage` and `generateSignedUrlForRunInputsExecutablesStorage`: both now accept an optional `workspaceId`, which names the workspace the signed URL is generated for. It is required when authenticating with an organization or user API key, since neither is tied to a single workspace, and without it the request fails with HTTP `400`. Omit it when authenticating with a workspace API key, which already implies one. This entry was missing from the changelog when 2.2.0 was released and is documented here retroactively; see [API Keys and Workspaces](#api-keys).
|
|
782
|
+
|
|
522
783
|
## v2.1.0
|
|
523
784
|
|
|
524
785
|
- `attemptNotifyDeploy`: the `deploy_success` response is now validated at runtime instead of being trusted. A response with a non-JSON body, or a body that does not match the expected shape, returns `outcome: "aborted"` with the new `abortReason: "invalid-response-body"`. This is distinct from `"network-error"`, which now covers only transport and body-stream failures rather than also masking unparseable responses. A result the SDK does not recognize (for example a new result kind sent by a newer backend) returns `outcome: "failed"` with the new `failReason: "unknown-result"` instead of being reported as a success. Recognized results are unchanged, and older backends that omit newer fields remain fully supported.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { CiGreenlightStatus, CiGreenlightStatusBase, } from "./lib/api/index.js";
|
|
2
2
|
export * from "./lib/sdk/dependencies.js";
|
|
3
3
|
export * from "./lib/sdk/domain/attemptDeploy/types.js";
|
|
4
|
+
export * from "./lib/sdk/domain/detectProviderDeploymentId/index.js";
|
|
4
5
|
export * from "./lib/sdk/index.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
4
|
|
|
5
|
+
// src/lib/sdk/domain/detectProviderDeploymentId/index.ts
|
|
6
|
+
var ciSystems = [
|
|
7
|
+
{
|
|
8
|
+
ci: "github",
|
|
9
|
+
isActive: /* @__PURE__ */ __name((env) => env.GITHUB_ACTIONS === "true", "isActive"),
|
|
10
|
+
// GITHUB_RUN_ID is stable across re-runs and GITHUB_RUN_ATTEMPT increments,
|
|
11
|
+
// and both are shared by every job in the workflow, so GITHUB_JOB separates
|
|
12
|
+
// jobs that deploy in parallel. GITHUB_JOB names the job definition, which
|
|
13
|
+
// every leg of a matrix shares, so those legs need a discriminator.
|
|
14
|
+
variables: ["GITHUB_RUN_ID", "GITHUB_RUN_ATTEMPT", "GITHUB_JOB"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
ci: "gitlab",
|
|
18
|
+
isActive: /* @__PURE__ */ __name((env) => env.GITLAB_CI === "true", "isActive"),
|
|
19
|
+
// Retrying a job keeps the pipeline id and mints a new job id.
|
|
20
|
+
variables: ["CI_PIPELINE_ID", "CI_JOB_ID"]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
ci: "circleci",
|
|
24
|
+
isActive: /* @__PURE__ */ __name((env) => env.CIRCLECI === "true", "isActive"),
|
|
25
|
+
// A rerun keeps the workflow id and allocates a new build number. A job
|
|
26
|
+
// run with `parallelism` shares both across its containers, so those need
|
|
27
|
+
// a discriminator.
|
|
28
|
+
variables: ["CIRCLE_WORKFLOW_ID", "CIRCLE_BUILD_NUM"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
ci: "buildkite",
|
|
32
|
+
isActive: /* @__PURE__ */ __name((env) => env.BUILDKITE === "true", "isActive"),
|
|
33
|
+
// BUILDKITE_BUILD_ID covers the whole build, so BUILDKITE_JOB_ID separates
|
|
34
|
+
// the jobs within it and BUILDKITE_RETRY_COUNT separates a job's retries.
|
|
35
|
+
variables: [
|
|
36
|
+
"BUILDKITE_BUILD_ID",
|
|
37
|
+
"BUILDKITE_JOB_ID",
|
|
38
|
+
"BUILDKITE_RETRY_COUNT"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
ci: "jenkins",
|
|
43
|
+
// JENKINS_URL is unset on some agent configurations.
|
|
44
|
+
isActive: /* @__PURE__ */ __name((env) => env.JENKINS_URL !== void 0 || env.JENKINS_HOME !== void 0, "isActive"),
|
|
45
|
+
// BUILD_TAG is `jenkins-${JOB_NAME}-${BUILD_NUMBER}`, and Jenkins allocates
|
|
46
|
+
// a fresh BUILD_NUMBER for every externally visible re-run — Rebuild,
|
|
47
|
+
// Naginator, Replay and Restart from Stage — so it needs no attempt
|
|
48
|
+
// counter. An in-pipeline `retry {}` reuses it, but reports nothing new.
|
|
49
|
+
// A declarative `matrix` or `parallel` block runs its branches inside one
|
|
50
|
+
// build and so shares BUILD_TAG; those branches need a discriminator.
|
|
51
|
+
variables: ["BUILD_TAG"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
ci: "bitbucket",
|
|
55
|
+
isActive: /* @__PURE__ */ __name((env) => env.BITBUCKET_WORKSPACE !== void 0 || env.BITBUCKET_BUILD_NUMBER !== void 0, "isActive"),
|
|
56
|
+
// Rerunning a whole pipeline mints a new build number, but rerunning only
|
|
57
|
+
// the failed steps keeps it and increments the step's run number instead.
|
|
58
|
+
// Parallel steps share both, so BITBUCKET_STEP_UUID separates them.
|
|
59
|
+
variables: [
|
|
60
|
+
"BITBUCKET_BUILD_NUMBER",
|
|
61
|
+
"BITBUCKET_STEP_UUID",
|
|
62
|
+
"BITBUCKET_STEP_RUN_NUMBER"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
function detectProviderDeploymentId({
|
|
67
|
+
discriminator,
|
|
68
|
+
env = process.env
|
|
69
|
+
} = {}) {
|
|
70
|
+
const system = ciSystems.find((candidate) => candidate.isActive(env));
|
|
71
|
+
if (!system) return { outcome: "unsupported" };
|
|
72
|
+
const missingVariables = system.variables.filter((name) => !env[name]);
|
|
73
|
+
if (missingVariables.length > 0)
|
|
74
|
+
return { ci: system.ci, missingVariables, outcome: "missing-variables" };
|
|
75
|
+
const composed = system.variables.map((name) => env[name]).join("-");
|
|
76
|
+
return {
|
|
77
|
+
ci: system.ci,
|
|
78
|
+
// A colon separates the discriminator because a job id can itself contain
|
|
79
|
+
// a hyphen, so joining with one would let job "deploy-web" collide with
|
|
80
|
+
// job "deploy" discriminated by "web".
|
|
81
|
+
id: discriminator ? `${composed}:${discriminator}` : composed,
|
|
82
|
+
outcome: "detected"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
__name(detectProviderDeploymentId, "detectProviderDeploymentId");
|
|
86
|
+
|
|
5
87
|
// src/lib/other.ts
|
|
6
88
|
function sleep(ms) {
|
|
7
89
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -38,7 +120,7 @@ var defaultServiceBase = "https://app.qawolf.com/";
|
|
|
38
120
|
// package.json
|
|
39
121
|
var package_default = {
|
|
40
122
|
name: "@qawolf/ci-sdk",
|
|
41
|
-
version: "3.
|
|
123
|
+
version: "3.1.0",
|
|
42
124
|
private: false,
|
|
43
125
|
description: "A simple SDK for interacting with QAWolf in CI scripts.",
|
|
44
126
|
keywords: [],
|
|
@@ -61,18 +143,20 @@ var package_default = {
|
|
|
61
143
|
"lint:ci": "npm run lint:first-half; FIRST_HALF_EXIT=$?; npm run lint:second-half && exit $FIRST_HALF_EXIT",
|
|
62
144
|
"lint:dev": "npm run lint:first-half",
|
|
63
145
|
"lint:first-half": "oxlint . --fix --quiet --report-unused-disable-directives --type-aware && oxfmt --write .",
|
|
64
|
-
"lint:second-half": "
|
|
146
|
+
"lint:second-half": "rev-dep config run",
|
|
65
147
|
prepublishOnly: "nx run ci-sdk:build",
|
|
66
148
|
test: 'NODE_OPTIONS="--experimental-vm-modules" NODE_NO_WARNINGS=1 jest',
|
|
67
149
|
"tsc:check": "tsc --noEmit"
|
|
68
150
|
},
|
|
69
151
|
dependencies: {
|
|
152
|
+
"@qawolf/api-contracts": ">=0.68.0",
|
|
70
153
|
"@qawolf/ci-utils": "^2.0.0",
|
|
71
154
|
"@sinonjs/fake-timers": "^10.3.0",
|
|
72
|
-
tslib: "^2.6.2"
|
|
155
|
+
tslib: "^2.6.2",
|
|
156
|
+
zod: "^4.1.11"
|
|
73
157
|
},
|
|
74
158
|
devDependencies: {
|
|
75
|
-
"@qawolf/platform-builder": "^1.2.
|
|
159
|
+
"@qawolf/platform-builder": "^1.2.7",
|
|
76
160
|
tsx: "^4.19.4",
|
|
77
161
|
typescript: "^5.9.3"
|
|
78
162
|
},
|
|
@@ -1111,6 +1195,127 @@ async function pollCiGreenlightStatus(deps, apiConfig, {
|
|
|
1111
1195
|
}
|
|
1112
1196
|
__name(pollCiGreenlightStatus, "pollCiGreenlightStatus");
|
|
1113
1197
|
|
|
1198
|
+
// src/lib/sdk/domain/reportDeployment/index.ts
|
|
1199
|
+
import { publicContractsV1 } from "@qawolf/api-contracts";
|
|
1200
|
+
|
|
1201
|
+
// src/lib/sdk/publicApi/callContract.ts
|
|
1202
|
+
function isRecord2(value) {
|
|
1203
|
+
return typeof value === "object" && value !== null;
|
|
1204
|
+
}
|
|
1205
|
+
__name(isRecord2, "isRecord");
|
|
1206
|
+
function isSuperJsonEnvelope(data) {
|
|
1207
|
+
return "json" in data && Object.keys(data).every((key) => key === "json" || key === "meta");
|
|
1208
|
+
}
|
|
1209
|
+
__name(isSuperJsonEnvelope, "isSuperJsonEnvelope");
|
|
1210
|
+
function unwrapTrpcEnvelope(body) {
|
|
1211
|
+
if (!isRecord2(body) || !isRecord2(body.result)) return void 0;
|
|
1212
|
+
const data = body.result.data;
|
|
1213
|
+
return isRecord2(data) && isSuperJsonEnvelope(data) ? data.json : data;
|
|
1214
|
+
}
|
|
1215
|
+
__name(unwrapTrpcEnvelope, "unwrapTrpcEnvelope");
|
|
1216
|
+
function readTrpcError(body) {
|
|
1217
|
+
const wrapped = isRecord2(body) ? body.error : void 0;
|
|
1218
|
+
const error = isRecord2(wrapped) && isRecord2(wrapped.json) ? wrapped.json : wrapped;
|
|
1219
|
+
if (!isRecord2(error)) return { eventId: void 0, message: void 0 };
|
|
1220
|
+
const data = isRecord2(error.data) ? error.data : void 0;
|
|
1221
|
+
return {
|
|
1222
|
+
eventId: typeof data?.eventId === "string" ? data.eventId : void 0,
|
|
1223
|
+
message: typeof error.message === "string" ? error.message : void 0
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
__name(readTrpcError, "readTrpcError");
|
|
1227
|
+
function parseJson(text) {
|
|
1228
|
+
if (text === void 0) return void 0;
|
|
1229
|
+
try {
|
|
1230
|
+
return JSON.parse(text);
|
|
1231
|
+
} catch {
|
|
1232
|
+
return void 0;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
__name(parseJson, "parseJson");
|
|
1236
|
+
async function readHttpError(response, contractName, log) {
|
|
1237
|
+
const bodyText = await response.text().catch(() => void 0);
|
|
1238
|
+
const { eventId, message } = readTrpcError(parseJson(bodyText));
|
|
1239
|
+
const statusText = response.status >= 500 ? "Server Error" : response.statusText || "Unknown Error";
|
|
1240
|
+
const eventIdSuffix = eventId ? ` (Event ID: ${eventId})` : "";
|
|
1241
|
+
const messageSuffix = message ? `: ${message}` : "";
|
|
1242
|
+
log.error(
|
|
1243
|
+
`\u274C [${contractName}] ${response.status} ${statusText}${eventIdSuffix}${messageSuffix}`
|
|
1244
|
+
);
|
|
1245
|
+
return {
|
|
1246
|
+
eventId,
|
|
1247
|
+
httpStatus: response.status,
|
|
1248
|
+
message,
|
|
1249
|
+
outcome: "http-error"
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
__name(readHttpError, "readHttpError");
|
|
1253
|
+
async function callPublicContract({ fetch: localFetch, log }, apiConfig, {
|
|
1254
|
+
contract,
|
|
1255
|
+
input
|
|
1256
|
+
}) {
|
|
1257
|
+
const procedurePath = `public.${contract.name}`;
|
|
1258
|
+
let bodyText;
|
|
1259
|
+
try {
|
|
1260
|
+
const response = await localFetch(
|
|
1261
|
+
new URL(`/api/trpc/${procedurePath}`, apiConfig.serviceBase),
|
|
1262
|
+
{
|
|
1263
|
+
// The server's superjson transformer reads the input from `json`.
|
|
1264
|
+
body: JSON.stringify({ json: input }),
|
|
1265
|
+
headers: {
|
|
1266
|
+
Authorization: `Bearer ${apiConfig.apiKey}`,
|
|
1267
|
+
"Content-Type": "application/json",
|
|
1268
|
+
"User-Agent": apiConfig.userAgent
|
|
1269
|
+
},
|
|
1270
|
+
method: "POST"
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
1273
|
+
if (!response.ok) return readHttpError(response, contract.name, log);
|
|
1274
|
+
bodyText = await response.text();
|
|
1275
|
+
} catch {
|
|
1276
|
+
log.error(`\u{1F6AB} Network error from ${contract.name}, aborting.`);
|
|
1277
|
+
return { outcome: "network-error" };
|
|
1278
|
+
}
|
|
1279
|
+
const parsed = contract.output.safeParse(
|
|
1280
|
+
unwrapTrpcEnvelope(parseJson(bodyText))
|
|
1281
|
+
);
|
|
1282
|
+
if (!parsed.success) {
|
|
1283
|
+
log.error(
|
|
1284
|
+
`\u{1F6AB} Invalid response from ${contract.name} (${parsed.error.issues[0]?.message ?? "the response body did not match the contract"}), aborting.`
|
|
1285
|
+
);
|
|
1286
|
+
return { outcome: "invalid-response-body" };
|
|
1287
|
+
}
|
|
1288
|
+
return { data: parsed.data, outcome: "ok" };
|
|
1289
|
+
}
|
|
1290
|
+
__name(callPublicContract, "callPublicContract");
|
|
1291
|
+
|
|
1292
|
+
// src/lib/sdk/domain/reportDeployment/index.ts
|
|
1293
|
+
async function reportDeployment(deps, apiConfig, params) {
|
|
1294
|
+
const result = await callPublicContract(deps, apiConfig, {
|
|
1295
|
+
contract: publicContractsV1.deployment.reportStatus,
|
|
1296
|
+
input: params
|
|
1297
|
+
});
|
|
1298
|
+
if (result.outcome !== "ok") return mapFailure(result);
|
|
1299
|
+
const { deployment } = result.data;
|
|
1300
|
+
deps.log.info(
|
|
1301
|
+
`\u2705 Deployment ${deployment.id} reported as ${deployment.status}: ${deployment.url}`
|
|
1302
|
+
);
|
|
1303
|
+
return { deployment, outcome: "reported" };
|
|
1304
|
+
}
|
|
1305
|
+
__name(reportDeployment, "reportDeployment");
|
|
1306
|
+
function mapFailure(result) {
|
|
1307
|
+
if (result.outcome !== "http-error") return { outcome: result.outcome };
|
|
1308
|
+
const { httpStatus, message } = result;
|
|
1309
|
+
if (httpStatus === 400) return { message, outcome: "invalid-request" };
|
|
1310
|
+
if (httpStatus === 401 || httpStatus === 403)
|
|
1311
|
+
return { message, outcome: "unauthorized" };
|
|
1312
|
+
if (httpStatus === 404) return { message, outcome: "not-found" };
|
|
1313
|
+
if (httpStatus === 409) return { message, outcome: "conflict" };
|
|
1314
|
+
if (httpStatus >= 500) return { httpStatus, outcome: "server-error" };
|
|
1315
|
+
return { httpStatus, message, outcome: "unexpected-status" };
|
|
1316
|
+
}
|
|
1317
|
+
__name(mapFailure, "mapFailure");
|
|
1318
|
+
|
|
1114
1319
|
// src/lib/sdk/index.ts
|
|
1115
1320
|
function makeQaWolfSdk({
|
|
1116
1321
|
apiKey,
|
|
@@ -1135,7 +1340,14 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
|
|
|
1135
1340
|
};
|
|
1136
1341
|
const apiConfig = { apiKey, serviceBase, userAgent };
|
|
1137
1342
|
return {
|
|
1343
|
+
/**
|
|
1344
|
+
* @deprecated Reports through the legacy `deploy_success` webhook, which
|
|
1345
|
+
* triggers do not see. Use `deployment.reportStatus` instead.
|
|
1346
|
+
*/
|
|
1138
1347
|
attemptNotifyDeploy: attemptNotifyDeploy.bind(null, deps, apiConfig),
|
|
1348
|
+
deployment: {
|
|
1349
|
+
reportStatus: reportDeployment.bind(null, deps, apiConfig)
|
|
1350
|
+
},
|
|
1139
1351
|
generateSignedUrlForRunInputsExecutablesStorage: generateSignedUrlForRunInputsExecutablesStorage.bind(
|
|
1140
1352
|
null,
|
|
1141
1353
|
deps,
|
|
@@ -1148,11 +1360,16 @@ We recommend 'undici' package for that purpose. See the Requirement section of o
|
|
|
1148
1360
|
apiConfig
|
|
1149
1361
|
),
|
|
1150
1362
|
notifyTerminatedEphemeralEnvironment: notifyTerminatedEphemeralEnvironment.bind(null, deps, apiConfig),
|
|
1363
|
+
/**
|
|
1364
|
+
* @deprecated Polls a run id that only `attemptNotifyDeploy` returns, so it
|
|
1365
|
+
* keeps the pipeline on the legacy `deploy_success` webhook.
|
|
1366
|
+
*/
|
|
1151
1367
|
pollCiGreenlightStatus: pollCiGreenlightStatus.bind(null, deps, apiConfig)
|
|
1152
1368
|
};
|
|
1153
1369
|
}
|
|
1154
1370
|
__name(makeQaWolfSdk, "makeQaWolfSdk");
|
|
1155
1371
|
export {
|
|
1372
|
+
detectProviderDeploymentId,
|
|
1156
1373
|
makeQaWolfSdk
|
|
1157
1374
|
};
|
|
1158
1375
|
//# sourceMappingURL=index.js.map
|