@gr8ful/spf 0.11.0 → 0.11.1
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.
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bitbucket Cloud REST API v2.0 implementation of `CodeHostProvider` —
|
|
3
|
-
* `spf watch`'s PR seam, not its tracker seam (see `provider.ts`'s module
|
|
4
|
-
* comment): this class never touches issues/labels, so it's paired with an
|
|
5
|
-
* `IssueProvider` (`github_provider.ts` or `jira_provider.ts`) at the CLI
|
|
6
|
-
* layer.
|
|
7
|
-
*
|
|
8
|
-
* Auth is HTTP Basic with an Atlassian account email + API token
|
|
9
|
-
* (`BITBUCKET_EMAIL` / `BITBUCKET_API_TOKEN`) — verified directly against
|
|
10
|
-
* Atlassian's current docs before writing this, not assumed from training
|
|
11
|
-
* data: Bitbucket Cloud app passwords are being fully removed (brownout
|
|
12
|
-
* window closes July 28, 2026), so this project only supports the
|
|
13
|
-
* replacement — API tokens, same auth shape as `jira_provider.ts`.
|
|
14
|
-
*
|
|
15
|
-
* `repo` is `"workspace/repo_slug"` (Bitbucket's own two-part identifier),
|
|
16
|
-
* the same config field GitHub uses for `"owner/name"` — the shape just
|
|
17
|
-
* means something different per `code_host`.
|
|
18
|
-
*/
|
|
19
1
|
import type { CodeHostProvider, PrRef, PrStatus } from "./provider.ts";
|
|
20
2
|
export declare class BitbucketProvider implements CodeHostProvider {
|
|
21
3
|
private readonly email;
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket Cloud REST API v2.0 implementation of `CodeHostProvider` —
|
|
3
|
+
* `spf watch`'s PR seam, not its tracker seam (see `provider.ts`'s module
|
|
4
|
+
* comment): this class never touches issues/labels, so it's paired with an
|
|
5
|
+
* `IssueProvider` (`github_provider.ts` or `jira_provider.ts`) at the CLI
|
|
6
|
+
* layer.
|
|
7
|
+
*
|
|
8
|
+
* Auth is HTTP Basic with an Atlassian account email + API token
|
|
9
|
+
* (`BITBUCKET_EMAIL` / `BITBUCKET_API_TOKEN`) — verified directly against
|
|
10
|
+
* Atlassian's current docs before writing this, not assumed from training
|
|
11
|
+
* data: Bitbucket Cloud app passwords are being fully removed (brownout
|
|
12
|
+
* window closes July 28, 2026), so this project only supports the
|
|
13
|
+
* replacement — API tokens, same auth shape as `jira_provider.ts`.
|
|
14
|
+
*
|
|
15
|
+
* `repo` is `"workspace/repo_slug"` (Bitbucket's own two-part identifier),
|
|
16
|
+
* the same config field GitHub uses for `"owner/name"` — the shape just
|
|
17
|
+
* means something different per `code_host`.
|
|
18
|
+
*/
|
|
19
|
+
import { fetchRetryTransient } from "../utils.js";
|
|
1
20
|
const API = "https://api.bitbucket.org/2.0";
|
|
2
21
|
export class BitbucketProvider {
|
|
3
22
|
email;
|
|
@@ -15,7 +34,7 @@ export class BitbucketProvider {
|
|
|
15
34
|
this.repoSlug = repoSlug;
|
|
16
35
|
}
|
|
17
36
|
async bb(path, init) {
|
|
18
|
-
const response = await
|
|
37
|
+
const response = await fetchRetryTransient(`${API}${path}`, {
|
|
19
38
|
...init,
|
|
20
39
|
headers: {
|
|
21
40
|
Authorization: `Basic ${Buffer.from(`${this.email}:${this.apiToken}`).toString("base64")}`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetchRetryTransient } from "../utils.js";
|
|
1
2
|
const STATES = [
|
|
2
3
|
"ready",
|
|
3
4
|
"working",
|
|
@@ -65,7 +66,7 @@ export class JiraProvider {
|
|
|
65
66
|
const debug = Boolean(process.env["SPF_JIRA_DEBUG"]);
|
|
66
67
|
if (debug)
|
|
67
68
|
console.error(`[jira debug] ${init?.method ?? "GET"} ${this.baseUrl}${path} body=${init?.body ?? "(none)"}`);
|
|
68
|
-
const response = await
|
|
69
|
+
const response = await fetchRetryTransient(`${this.baseUrl}${path}`, {
|
|
69
70
|
...init,
|
|
70
71
|
headers: {
|
|
71
72
|
Authorization: this.authHeader(),
|
|
@@ -139,7 +140,7 @@ export class JiraProvider {
|
|
|
139
140
|
* never discovers it from the issue body itself.
|
|
140
141
|
*/
|
|
141
142
|
async getIssue(id) {
|
|
142
|
-
const response = await
|
|
143
|
+
const response = await fetchRetryTransient(`${this.baseUrl}/rest/api/3/issue/${id}?fields=summary,description,labels`, {
|
|
143
144
|
headers: { Authorization: this.authHeader(), Accept: "application/json" },
|
|
144
145
|
});
|
|
145
146
|
if (response.status === 404)
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -19,6 +19,18 @@ import path from "node:path";
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function operatorEnv(): Record<string, string>;
|
|
21
21
|
export declare function newId(length?: number): string;
|
|
22
|
+
/**
|
|
23
|
+
* `fetch`, but a transport-level blip on the FIRST attempt gets one silent
|
|
24
|
+
* retry before it's allowed to throw. Node's global `fetch` (undici) pools
|
|
25
|
+
* keep-alive connections across calls; Atlassian's Cloud APIs (Jira,
|
|
26
|
+
* Bitbucket) close idle ones from their end, which surfaces here as
|
|
27
|
+
* `ECONNRESET` the next time a long-lived poller (`spf watch`) reuses one —
|
|
28
|
+
* a stale-socket race, not a real problem with the request. Only retries
|
|
29
|
+
* error codes that mean "the transport failed," never an HTTP error status
|
|
30
|
+
* (a 4xx/5xx response is not a thrown error here, and must keep surfacing
|
|
31
|
+
* on the first attempt so callers see it immediately).
|
|
32
|
+
*/
|
|
33
|
+
export declare function fetchRetryTransient(input: string, init?: RequestInit): Promise<Response>;
|
|
22
34
|
/** Matches Python's `datetime.now(timezone.utc).isoformat(timespec="milliseconds")`. */
|
|
23
35
|
export declare function nowIso(): string;
|
|
24
36
|
export declare function ensureDir(dirPath: string): string;
|
package/dist/core/utils.js
CHANGED
|
@@ -31,6 +31,30 @@ export function operatorEnv() {
|
|
|
31
31
|
export function newId(length = 8) {
|
|
32
32
|
return randomBytes(Math.floor(length / 2)).toString("hex");
|
|
33
33
|
}
|
|
34
|
+
/** Transport-level blips worth one silent retry — see `fetchRetryTransient` below. */
|
|
35
|
+
const TRANSIENT_FETCH_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "EPIPE", "ECONNREFUSED", "EAI_AGAIN"]);
|
|
36
|
+
/**
|
|
37
|
+
* `fetch`, but a transport-level blip on the FIRST attempt gets one silent
|
|
38
|
+
* retry before it's allowed to throw. Node's global `fetch` (undici) pools
|
|
39
|
+
* keep-alive connections across calls; Atlassian's Cloud APIs (Jira,
|
|
40
|
+
* Bitbucket) close idle ones from their end, which surfaces here as
|
|
41
|
+
* `ECONNRESET` the next time a long-lived poller (`spf watch`) reuses one —
|
|
42
|
+
* a stale-socket race, not a real problem with the request. Only retries
|
|
43
|
+
* error codes that mean "the transport failed," never an HTTP error status
|
|
44
|
+
* (a 4xx/5xx response is not a thrown error here, and must keep surfacing
|
|
45
|
+
* on the first attempt so callers see it immediately).
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchRetryTransient(input, init) {
|
|
48
|
+
try {
|
|
49
|
+
return await fetch(input, init);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const code = error.cause?.code;
|
|
53
|
+
if (!code || !TRANSIENT_FETCH_CODES.has(code))
|
|
54
|
+
throw error;
|
|
55
|
+
return await fetch(input, init);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
34
58
|
/** Matches Python's `datetime.now(timezone.utc).isoformat(timespec="milliseconds")`. */
|
|
35
59
|
export function nowIso() {
|
|
36
60
|
const iso = new Date().toISOString(); // e.g. 2024-01-01T12:00:00.123Z
|