@tempo-ai/visual-review 0.0.108-staging.2 → 0.0.108-staging.3

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 CHANGED
@@ -24,6 +24,17 @@ every release is exercised there before customers resolve it.
24
24
  The current production ingest origin is the default. Set
25
25
  `VISUAL_REVIEW_INGEST_URL` only when using a Tempo preview deployment.
26
26
 
27
+ Every `visual-review` commit status is published by the Tempo Labs AI GitHub
28
+ App, including no-visual-change declarations, bypasses, and workflow failures.
29
+ The CLI authenticates these requests to Tempo with GitHub Actions OIDC;
30
+ app credentials stay in Tempo. The workflow token is used for PR comments.
31
+ If the app cannot publish, the job reports the failure instead of posting
32
+ under a different identity that cannot satisfy an app-bound required check.
33
+
34
+ The backend checks the signed PR identity against GitHub's current head and
35
+ reads skip declarations from the live PR. Declarations cannot be replayed
36
+ after removal, and reporting failures preserve an existing Tempo verdict.
37
+
27
38
  ## Releases and rollback
28
39
 
29
40
  Every production release publishes this package at the app version under
package/dist/bin.js CHANGED
@@ -6,11 +6,11 @@ async function main(argv = process.argv.slice(2)) {
6
6
  const [command, ...commandArgs] = argv;
7
7
  switch (command) {
8
8
  case "capture":
9
- return await (await import("./cli-DIZKG7EY.js")).main(commandArgs);
9
+ return await (await import("./cli-ASUZAROR.js")).main(commandArgs);
10
10
  case "claim":
11
- return await (await import("./claim-WI2ZVIDS.js")).main(commandArgs);
11
+ return await (await import("./claim-J654JAS6.js")).main(commandArgs);
12
12
  case "report":
13
- return await (await import("./report-RD2OQ5TP.js")).main(commandArgs);
13
+ return await (await import("./report-LTFCAIRI.js")).main(commandArgs);
14
14
  case "install-browsers":
15
15
  return await (await import("./install-browsers-5RCJN3AK.js")).main(commandArgs);
16
16
  default:
@@ -1,10 +1,11 @@
1
1
  import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
2
  import {
3
- DEFAULT_VISUAL_REVIEW_INGEST_URL
4
- } from "./chunk-QLAJNWA2.js";
3
+ publishWorkflowStatus
4
+ } from "./chunk-FFW3DB6S.js";
5
5
  import {
6
+ DEFAULT_VISUAL_REVIEW_INGEST_URL,
6
7
  githubPullRequestContext
7
- } from "./chunk-W7PBID4V.js";
8
+ } from "./chunk-HVWIRSNY.js";
8
9
 
9
10
  // src/claim.ts
10
11
  import { randomUUID } from "crypto";
@@ -108,6 +109,15 @@ async function main(argv = process.argv.slice(2)) {
108
109
  );
109
110
  return 0;
110
111
  }
112
+ if (!release && process.env.GITHUB_EVENT_NAME === "pull_request" && required.headSha) {
113
+ try {
114
+ await publishWorkflowStatus("capture-started", required.headSha);
115
+ } catch (error) {
116
+ console.warn(
117
+ `pending status unavailable: ${error instanceof Error ? error.message : error}`
118
+ );
119
+ }
120
+ }
111
121
  try {
112
122
  if (release) {
113
123
  const result2 = await releaseCaptureClaim(
@@ -143,4 +153,4 @@ export {
143
153
  releaseCaptureClaim,
144
154
  main
145
155
  };
146
- //# sourceMappingURL=chunk-DJGBIRN5.js.map
156
+ //# sourceMappingURL=chunk-2EPHXAPX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/claim.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { appendFile } from \"node:fs/promises\";\n\nimport { DEFAULT_VISUAL_REVIEW_INGEST_URL } from \"./constants\";\nimport { githubPullRequestContext } from \"./github-actions\";\nimport { publishWorkflowStatus } from \"./workflow-status\";\n\nconst PENDING_ENVIRONMENT = \"pending\";\nconst OIDC_AUDIENCE = \"tempo-visual-review\";\n\n/** The slice of `fetch` the claim protocol uses. A plain `(url, init)` mock\n * satisfies it without impersonating the overloaded global. */\ntype FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nasync function mintOidcToken(options: {\n oidcRequestUrl: string;\n oidcRequestToken: string;\n fetchImpl: FetchLike;\n}): Promise<string> {\n const separator = options.oidcRequestUrl.includes(\"?\") ? \"&\" : \"?\";\n const tokenResponse = await options.fetchImpl(\n `${options.oidcRequestUrl}${separator}audience=${OIDC_AUDIENCE}`,\n { headers: { Authorization: `Bearer ${options.oidcRequestToken}` } },\n );\n if (!tokenResponse.ok) {\n throw new Error(`OIDC token request failed: ${tokenResponse.status}`);\n }\n const { value } = (await tokenResponse.json()) as { value?: string };\n if (!value) throw new Error(\"OIDC token response had no value\");\n return value;\n}\n\nasync function postClaimRequest(options: {\n ingestUrl: string;\n oidcToken: string;\n path: \"claim\" | \"fail\";\n body: Record<string, string>;\n fetchImpl: FetchLike;\n}): Promise<Record<string, unknown>> {\n const response = await options.fetchImpl(\n `${options.ingestUrl.replace(/\\/$/, \"\")}/visual-review/baseline/${options.path}`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-visual-review-oidc\": options.oidcToken,\n },\n body: JSON.stringify(options.body),\n },\n );\n if (!response.ok) {\n throw new Error(\n `capture ${options.path} failed: ${response.status} ${(await response.text()).slice(0, 200)}`,\n );\n }\n return (await response.json()) as Record<string, unknown>;\n}\n\nexport async function announceCapture(options: {\n ingestUrl: string;\n repository: string;\n headSha: string;\n oidcRequestUrl: string;\n oidcRequestToken: string;\n ownerToken?: string;\n fetchImpl?: FetchLike;\n}): Promise<{ ownerToken?: string; status?: string }> {\n const ownerToken = options.ownerToken ?? randomUUID();\n const fetchImpl = options.fetchImpl ?? fetch;\n const oidcToken = await mintOidcToken({ ...options, fetchImpl });\n const result = await postClaimRequest({\n ingestUrl: options.ingestUrl,\n oidcToken,\n path: \"claim\",\n body: {\n repoKey: `github.com/${options.repository.toLowerCase()}`,\n commitSha: options.headSha,\n envFingerprint: PENDING_ENVIRONMENT,\n ownerToken,\n },\n fetchImpl,\n });\n const status = typeof result.status === \"string\" ? result.status : undefined;\n return {\n ownerToken: status === \"claimed\" ? ownerToken : undefined,\n status,\n };\n}\n\nexport async function releaseCaptureClaim(options: {\n ingestUrl: string;\n repository: string;\n headSha: string;\n ownerToken: string;\n oidcRequestUrl: string;\n oidcRequestToken: string;\n fetchImpl?: FetchLike;\n}): Promise<{ released?: boolean }> {\n const fetchImpl = options.fetchImpl ?? fetch;\n const oidcToken = await mintOidcToken({ ...options, fetchImpl });\n const result = await postClaimRequest({\n ingestUrl: options.ingestUrl,\n oidcToken,\n path: \"fail\",\n body: {\n repoKey: `github.com/${options.repository.toLowerCase()}`,\n commitSha: options.headSha,\n ownerToken: options.ownerToken,\n error: \"workflow ended before the capture claim completed\",\n },\n fetchImpl,\n });\n return { released: result.released === true };\n}\n\nfunction parseArgs(argv: string[]) {\n if (argv.some((arg) => arg !== \"--release\")) {\n throw new Error(\"claim accepts only the optional --release flag\");\n }\n return { release: argv.includes(\"--release\") };\n}\n\nexport async function main(argv = process.argv.slice(2)): Promise<number> {\n const { release } = parseArgs(argv);\n const pullRequest = githubPullRequestContext();\n const required = {\n ingestUrl:\n process.env.VISUAL_REVIEW_INGEST_URL ?? DEFAULT_VISUAL_REVIEW_INGEST_URL,\n repository: process.env.GITHUB_REPOSITORY,\n // GITHUB_SHA is a merge ref on pull_request runs; the event payload is\n // the only standard environment source for the actual PR head SHA.\n headSha:\n process.env.CAPTURE_HEAD_SHA ??\n pullRequest?.headSha ??\n process.env.GITHUB_SHA,\n oidcRequestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,\n oidcRequestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN,\n ...(release\n ? { ownerToken: process.env.VISUAL_REVIEW_CAPTURE_OWNER_TOKEN }\n : {}),\n };\n const missing = Object.entries(required)\n .filter(([, value]) => !value)\n .map(([name]) => name);\n if (missing.length > 0) {\n console.warn(\n `capture announcement skipped; missing ${missing.join(\", \")} (capture will claim after setup)`,\n );\n return 0;\n }\n\n if (\n !release &&\n process.env.GITHUB_EVENT_NAME === \"pull_request\" &&\n required.headSha\n ) {\n try {\n await publishWorkflowStatus(\"capture-started\", required.headSha);\n } catch (error) {\n // Capture still gets a chance to complete and publish its real gate.\n console.warn(\n `pending status unavailable: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n\n try {\n if (release) {\n const result = await releaseCaptureClaim(\n required as Parameters<typeof releaseCaptureClaim>[0],\n );\n console.log(\n `capture claim release: ${result.released ? \"released\" : \"already complete or transferred\"}`,\n );\n return 0;\n }\n const result = await announceCapture(\n required as Parameters<typeof announceCapture>[0],\n );\n console.log(`capture announcement: ${result.status}`);\n if (result.ownerToken && process.env.GITHUB_ENV) {\n await appendFile(\n process.env.GITHUB_ENV,\n `VISUAL_REVIEW_CAPTURE_OWNER_TOKEN=${result.ownerToken}\\n`,\n );\n }\n return 0;\n } catch (error) {\n // The capture command repeats the atomic claim with the real environment\n // fingerprint; an early announcement outage must not suppress coverage.\n console.warn(\n `capture announcement failed; deferring to capture: ${error instanceof Error ? error.message : error}`,\n );\n return 0;\n }\n}\n"],"mappings":"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","names":["result"]}
@@ -1,10 +1,9 @@
1
1
  import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
2
  import {
3
- DEFAULT_VISUAL_REVIEW_INGEST_URL
4
- } from "./chunk-QLAJNWA2.js";
5
- import {
6
- githubPullRequestContext
7
- } from "./chunk-W7PBID4V.js";
3
+ DEFAULT_VISUAL_REVIEW_INGEST_URL,
4
+ githubPullRequestContext,
5
+ resolveIngestCredential
6
+ } from "./chunk-HVWIRSNY.js";
8
7
 
9
8
  // src/cli.ts
10
9
  import { execFileSync, spawn as spawn6 } from "child_process";
@@ -12900,8 +12899,14 @@ var VisualReviewIngestClient = class {
12900
12899
  response.status
12901
12900
  );
12902
12901
  }
12902
+ if (reason === "visual_review_unconfigured") {
12903
+ throw new VisualReviewHttpError(
12904
+ "visual-review ingest is unconfigured on the target deployment (set VISUAL_REVIEW_UPLOAD_SECRET, or its OIDC producer credential)",
12905
+ response.status
12906
+ );
12907
+ }
12903
12908
  throw new VisualReviewHttpError(
12904
- "visual-review ingest is unconfigured on the target deployment (set VISUAL_REVIEW_UPLOAD_SECRET, or its OIDC producer credential)",
12909
+ `visual-review ingest unavailable: ${reason ?? "HTTP 503"}`,
12905
12910
  response.status
12906
12911
  );
12907
12912
  }
@@ -12935,8 +12940,8 @@ var VisualReviewIngestClient = class {
12935
12940
  `negotiate failed: ${response.status}${routeMissingHint(response.status)}`
12936
12941
  );
12937
12942
  }
12938
- const { missing: missing2 } = await response.json();
12939
- return missing2;
12943
+ const { missing } = await response.json();
12944
+ return missing;
12940
12945
  }
12941
12946
  async uploadBlob(hash, bytes, size) {
12942
12947
  const response = await this.request(
@@ -13751,13 +13756,13 @@ async function main(argv = process.argv.slice(2)) {
13751
13756
  });
13752
13757
  }
13753
13758
  }
13754
- const missing2 = await timing.phase(
13759
+ const missing = await timing.phase(
13755
13760
  "negotiate",
13756
13761
  () => ingest.negotiate([...blobsByHash.keys()])
13757
13762
  );
13758
- log(`uploading ${missing2.length} of ${blobsByHash.size} blob(s)`);
13763
+ log(`uploading ${missing.length} of ${blobsByHash.size} blob(s)`);
13759
13764
  await timing.phase("upload", async () => {
13760
- for (const hash of missing2) {
13765
+ for (const hash of missing) {
13761
13766
  const blob = blobsByHash.get(hash);
13762
13767
  if (!blob) throw new Error(`negotiate returned unknown hash ${hash}`);
13763
13768
  await ingest.uploadBlob(hash, blob.bytes, blob);
@@ -13802,50 +13807,6 @@ status_url=${finalizeResult.statusUrl}
13802
13807
  process.removeListener("SIGTERM", onSigterm);
13803
13808
  }
13804
13809
  }
13805
- function missing(what) {
13806
- throw new Error(`${what} is required for --upload`);
13807
- }
13808
- var OIDC_TOKEN_LIFETIME_MS = 5 * 6e4;
13809
- var OIDC_TOKEN_REFRESH_MARGIN_MS = 6e4;
13810
- var VISUAL_REVIEW_OIDC_AUDIENCE = "tempo-visual-review";
13811
- function githubOidcProvider(requestUrl, requestToken) {
13812
- let cached = null;
13813
- return async () => {
13814
- if (cached && Date.now() - cached.mintedAtMs < OIDC_TOKEN_LIFETIME_MS - OIDC_TOKEN_REFRESH_MARGIN_MS) {
13815
- return { kind: "oidc", value: cached.value };
13816
- }
13817
- const separator = requestUrl.includes("?") ? "&" : "?";
13818
- const response = await fetch(
13819
- `${requestUrl}${separator}audience=${VISUAL_REVIEW_OIDC_AUDIENCE}`,
13820
- { headers: { Authorization: `Bearer ${requestToken}` } }
13821
- );
13822
- if (!response.ok) {
13823
- throw new Error(
13824
- `minting the GitHub OIDC token failed: ${response.status} ${(await response.text()).slice(0, 200)}`
13825
- );
13826
- }
13827
- const body = await response.json();
13828
- if (!body.value) {
13829
- throw new Error("GitHub OIDC token endpoint returned no value");
13830
- }
13831
- cached = { value: body.value, mintedAtMs: Date.now() };
13832
- return { kind: "oidc", value: body.value };
13833
- };
13834
- }
13835
- function resolveIngestCredential() {
13836
- const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
13837
- const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
13838
- if (requestUrl && requestToken) {
13839
- return githubOidcProvider(requestUrl, requestToken);
13840
- }
13841
- const oidc = process.env.VISUAL_REVIEW_OIDC_TOKEN;
13842
- if (oidc) return async () => ({ kind: "oidc", value: oidc });
13843
- const secret = process.env.VISUAL_REVIEW_UPLOAD_SECRET;
13844
- if (secret) return async () => ({ kind: "secret", value: secret });
13845
- return missing(
13846
- "GitHub Actions id-token permission, VISUAL_REVIEW_OIDC_TOKEN, or VISUAL_REVIEW_UPLOAD_SECRET"
13847
- );
13848
- }
13849
13810
  var isDirectRun = process.argv[1] !== void 0 && (process.argv[1].endsWith("cli.ts") || process.argv[1].endsWith("cli.js"));
13850
13811
  if (isDirectRun) {
13851
13812
  main().then((code) => {
@@ -13883,7 +13844,6 @@ export {
13883
13844
  resolveOrClaimBaseline,
13884
13845
  explicitTempoDirRelative,
13885
13846
  buildBaselineCaptureArgs,
13886
- main,
13887
- VISUAL_REVIEW_OIDC_AUDIENCE
13847
+ main
13888
13848
  };
13889
- //# sourceMappingURL=chunk-2EWJG76K.js.map
13849
+ //# sourceMappingURL=chunk-2EVRJIRQ.js.map