@flakiness/sdk 0.150.1 → 0.151.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/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@flakiness/sdk",
3
- "version": "0.150.1",
3
+ "version": "0.151.0",
4
4
  "private": false,
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/flakiness/nodejs-sdk.git"
8
+ },
5
9
  "exports": {
6
10
  ".": {
7
11
  "types": "./types/src/index.d.ts",
@@ -1,90 +0,0 @@
1
- import { spawnSync } from "child_process";
2
- import crypto from "crypto";
3
- import fs from "fs";
4
- import util from "util";
5
- import zlib from "zlib";
6
- const asyncBrotliCompress = util.promisify(zlib.brotliCompress);
7
- async function compressTextAsync(text) {
8
- return asyncBrotliCompress(text, {
9
- chunkSize: 32 * 1024,
10
- params: {
11
- [zlib.constants.BROTLI_PARAM_QUALITY]: 6,
12
- [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT
13
- }
14
- });
15
- }
16
- const FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
17
- function errorText(error) {
18
- return FLAKINESS_DBG ? error.stack : error.message;
19
- }
20
- async function retryWithBackoff(job, backoff = []) {
21
- for (const timeout of backoff) {
22
- try {
23
- return await job();
24
- } catch (e) {
25
- if (e instanceof AggregateError)
26
- console.error(`[flakiness.io err]`, errorText(e.errors[0]));
27
- else if (e instanceof Error)
28
- console.error(`[flakiness.io err]`, errorText(e));
29
- else
30
- console.error(`[flakiness.io err]`, e);
31
- await new Promise((x) => setTimeout(x, timeout));
32
- }
33
- }
34
- return await job();
35
- }
36
- function shell(command, args, options) {
37
- try {
38
- const result = spawnSync(command, args, { encoding: "utf-8", ...options });
39
- if (result.status !== 0) {
40
- return void 0;
41
- }
42
- return result.stdout.trim();
43
- } catch (e) {
44
- console.error(e);
45
- return void 0;
46
- }
47
- }
48
- function sha1Text(data) {
49
- const hash = crypto.createHash("sha1");
50
- hash.update(data);
51
- return hash.digest("hex");
52
- }
53
- function sha1File(filePath) {
54
- return new Promise((resolve, reject) => {
55
- const hash = crypto.createHash("sha1");
56
- const stream = fs.createReadStream(filePath);
57
- stream.on("data", (chunk) => {
58
- hash.update(chunk);
59
- });
60
- stream.on("end", () => {
61
- resolve(hash.digest("hex"));
62
- });
63
- stream.on("error", (err) => {
64
- reject(err);
65
- });
66
- });
67
- }
68
- function randomUUIDBase62() {
69
- const BASE62_CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
70
- let num = BigInt("0x" + crypto.randomUUID().replace(/-/g, ""));
71
- if (num === 0n)
72
- return BASE62_CHARSET[0];
73
- const chars = [];
74
- while (num > 0n) {
75
- const remainder = Number(num % 62n);
76
- num /= 62n;
77
- chars.push(BASE62_CHARSET[remainder]);
78
- }
79
- return chars.reverse().join("");
80
- }
81
- export {
82
- compressTextAsync,
83
- errorText,
84
- randomUUIDBase62,
85
- retryWithBackoff,
86
- sha1File,
87
- sha1Text,
88
- shell
89
- };
90
- //# sourceMappingURL=_internalUtils.js.map
@@ -1 +0,0 @@
1
- //# sourceMappingURL=_stable-hash.d.js.map
package/lib/ciUtils.js DELETED
@@ -1,41 +0,0 @@
1
- var CIUtils;
2
- ((CIUtils2) => {
3
- function runUrl() {
4
- return githubActions() ?? azure() ?? process.env.CI_JOB_URL ?? process.env.BUILD_URL;
5
- }
6
- CIUtils2.runUrl = runUrl;
7
- })(CIUtils || (CIUtils = {}));
8
- function githubActions() {
9
- const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
10
- const repo = process.env.GITHUB_REPOSITORY;
11
- const runId = process.env.GITHUB_RUN_ID;
12
- if (!repo || !runId) return void 0;
13
- try {
14
- const url = new URL(`${serverUrl}/${repo}/actions/runs/${runId}`);
15
- const attempt = process.env.GITHUB_RUN_ATTEMPT;
16
- if (attempt) url.searchParams.set("attempt", attempt);
17
- url.searchParams.set("check_suite_focus", "true");
18
- return url.toString();
19
- } catch (error) {
20
- return void 0;
21
- }
22
- }
23
- function azure() {
24
- const collectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
25
- const project = process.env.SYSTEM_TEAMPROJECT;
26
- const buildId = process.env.BUILD_BUILDID;
27
- if (!collectionUri || !project || !buildId)
28
- return void 0;
29
- try {
30
- const baseUrl = collectionUri.endsWith("/") ? collectionUri : `${collectionUri}/`;
31
- const url = new URL(`${baseUrl}${project}/_build/results`);
32
- url.searchParams.set("buildId", buildId);
33
- return url.toString();
34
- } catch (error) {
35
- return void 0;
36
- }
37
- }
38
- export {
39
- CIUtils
40
- };
41
- //# sourceMappingURL=ciUtils.js.map
@@ -1,65 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import { shell } from "./_internalUtils.js";
4
- function readLinuxOSRelease() {
5
- const osReleaseText = fs.readFileSync("/etc/os-release", "utf-8");
6
- return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
7
- line = line.trim();
8
- let [key, value] = line.split("=");
9
- if (value.startsWith('"') && value.endsWith('"'))
10
- value = value.substring(1, value.length - 1);
11
- return [key, value];
12
- }));
13
- }
14
- function osLinuxInfo() {
15
- const arch = shell(`uname`, [`-m`]);
16
- const osReleaseMap = readLinuxOSRelease();
17
- const name = osReleaseMap.get("name") ?? shell(`uname`);
18
- const version = osReleaseMap.get("version_id");
19
- return { name, arch, version };
20
- }
21
- function osDarwinInfo() {
22
- const name = "macos";
23
- const arch = shell(`uname`, [`-m`]);
24
- const version = shell(`sw_vers`, [`-productVersion`]);
25
- return { name, arch, version };
26
- }
27
- function osWinInfo() {
28
- const name = "win";
29
- const arch = process.arch;
30
- const version = os.release();
31
- return { name, arch, version };
32
- }
33
- function getOSInfo() {
34
- if (process.platform === "darwin")
35
- return osDarwinInfo();
36
- if (process.platform === "win32")
37
- return osWinInfo();
38
- return osLinuxInfo();
39
- }
40
- function extractEnvConfiguration() {
41
- const ENV_PREFIX = "FK_ENV_";
42
- return Object.fromEntries(
43
- Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
44
- );
45
- }
46
- function createEnvironment(options) {
47
- const osInfo = getOSInfo();
48
- return {
49
- name: options.name,
50
- systemData: {
51
- osArch: osInfo.arch,
52
- osName: osInfo.name,
53
- osVersion: osInfo.version
54
- },
55
- userSuppliedData: {
56
- ...extractEnvConfiguration(),
57
- ...options.userSuppliedData ?? {}
58
- },
59
- opaqueData: options.opaqueData
60
- };
61
- }
62
- export {
63
- createEnvironment
64
- };
65
- //# sourceMappingURL=createEnvironment.js.map
@@ -1,47 +0,0 @@
1
- import { codeFrameColumns } from "@babel/code-frame";
2
- import fs from "fs";
3
- import { visitTests } from "./visitTests.js";
4
- function createTestStepSnippetsInplace(worktree, report) {
5
- const allSteps = /* @__PURE__ */ new Map();
6
- visitTests(report, (test) => {
7
- for (const attempt of test.attempts) {
8
- for (const step of attempt.steps ?? []) {
9
- if (!step.location)
10
- continue;
11
- let fileSteps = allSteps.get(step.location.file);
12
- if (!fileSteps) {
13
- fileSteps = /* @__PURE__ */ new Set();
14
- allSteps.set(step.location.file, fileSteps);
15
- }
16
- fileSteps.add(step);
17
- }
18
- }
19
- });
20
- for (const [gitFilePath, steps] of allSteps) {
21
- let source;
22
- try {
23
- source = fs.readFileSync(worktree.absolutePath(gitFilePath), "utf-8");
24
- } catch (e) {
25
- continue;
26
- }
27
- const lines = source.split("\n").length;
28
- const highlighted = codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
29
- const highlightedLines = highlighted.split("\n");
30
- const lineWithArrow = highlightedLines[highlightedLines.length - 1];
31
- for (const step of steps) {
32
- if (!step.location)
33
- continue;
34
- if (step.location.line < 2 || step.location.line >= lines)
35
- continue;
36
- const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
37
- const index = lineWithArrow.indexOf("^");
38
- const shiftedArrow = lineWithArrow.slice(0, index) + " ".repeat(step.location.column - 1) + lineWithArrow.slice(index);
39
- snippetLines.splice(2, 0, shiftedArrow);
40
- step.snippet = snippetLines.join("\n");
41
- }
42
- }
43
- }
44
- export {
45
- createTestStepSnippetsInplace
46
- };
47
- //# sourceMappingURL=createTestStepSnippets.js.map
@@ -1,143 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { GitWorktree } from "./gitWorktree.js";
4
- function createConfigPath(dir) {
5
- return path.join(dir, ".flakiness", "config.json");
6
- }
7
- let gConfigPath;
8
- function ensureConfigPath() {
9
- if (!gConfigPath)
10
- gConfigPath = computeConfigPath();
11
- return gConfigPath;
12
- }
13
- function computeConfigPath() {
14
- for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
15
- const configPath = createConfigPath(p);
16
- if (fs.existsSync(configPath))
17
- return configPath;
18
- }
19
- try {
20
- const worktree = GitWorktree.create(process.cwd());
21
- return createConfigPath(worktree.rootPath());
22
- } catch (e) {
23
- return createConfigPath(process.cwd());
24
- }
25
- }
26
- class FlakinessProjectConfig {
27
- constructor(_configPath, _config) {
28
- this._configPath = _configPath;
29
- this._config = _config;
30
- }
31
- /**
32
- * Loads the Flakiness project configuration from disk.
33
- *
34
- * Searches for an existing `.flakiness/config.json` file starting from the current working
35
- * directory and walking up the directory tree. If no config exists, it determines the
36
- * appropriate location (git root or current directory) for future saves.
37
- *
38
- * @returns {Promise<FlakinessProjectConfig>} Promise that resolves to a FlakinessProjectConfig
39
- * instance. If no config file exists, returns an instance with default/empty values.
40
- *
41
- * @example
42
- * ```typescript
43
- * const config = await FlakinessProjectConfig.load();
44
- * const projectId = config.projectPublicId();
45
- * ```
46
- */
47
- static async load() {
48
- const configPath = ensureConfigPath();
49
- const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
50
- const json = data ? JSON.parse(data) : {};
51
- return new FlakinessProjectConfig(configPath, json);
52
- }
53
- /**
54
- * Creates a new empty Flakiness project configuration.
55
- *
56
- * Creates a configuration instance with no values set. Use this when you want to build
57
- * a configuration from scratch. Call `save()` to persist it to disk.
58
- *
59
- * @returns {FlakinessProjectConfig} A new empty configuration instance.
60
- *
61
- * @example
62
- * ```typescript
63
- * const config = FlakinessProjectConfig.createEmpty();
64
- * config.setProjectPublicId('my-project-id');
65
- * await config.save();
66
- * ```
67
- */
68
- static createEmpty() {
69
- return new FlakinessProjectConfig(ensureConfigPath(), {});
70
- }
71
- /**
72
- * Returns the absolute path to the configuration file.
73
- *
74
- * @returns {string} Absolute path to `.flakiness/config.json`.
75
- */
76
- path() {
77
- return this._configPath;
78
- }
79
- /**
80
- * Returns the project's public ID, if configured.
81
- *
82
- * The project public ID is used to associate reports with a specific Flakiness.io project.
83
- *
84
- * @returns {string | undefined} Project public ID, or `undefined` if not set.
85
- */
86
- projectPublicId() {
87
- return this._config.projectPublicId;
88
- }
89
- /**
90
- * Returns the report viewer URL, either custom or default.
91
- *
92
- * @returns {string} Custom report viewer URL if configured, otherwise the default
93
- * `https://report.flakiness.io`.
94
- */
95
- reportViewerUrl() {
96
- return this._config.customReportViewerUrl ?? "https://report.flakiness.io";
97
- }
98
- /**
99
- * Sets or clears the custom report viewer URL.
100
- *
101
- * @param {string | undefined} url - Custom report viewer URL to use, or `undefined` to
102
- * clear and use the default URL.
103
- */
104
- setCustomReportViewerUrl(url) {
105
- if (url)
106
- this._config.customReportViewerUrl = url;
107
- else
108
- delete this._config.customReportViewerUrl;
109
- }
110
- /**
111
- * Sets the project's public ID.
112
- *
113
- * @param {string | undefined} projectId - Project public ID to set, or `undefined` to clear.
114
- */
115
- setProjectPublicId(projectId) {
116
- this._config.projectPublicId = projectId;
117
- }
118
- /**
119
- * Saves the configuration to disk.
120
- *
121
- * Writes the current configuration values to `.flakiness/config.json`. Creates the
122
- * `.flakiness` directory if it doesn't exist.
123
- *
124
- * @returns {Promise<void>} Promise that resolves when the file has been written.
125
- *
126
- * @throws {Error} Throws if unable to create directories or write the file.
127
- *
128
- * @example
129
- * ```typescript
130
- * const config = await FlakinessProjectConfig.load();
131
- * config.setProjectPublicId('my-project');
132
- * await config.save();
133
- * ```
134
- */
135
- async save() {
136
- await fs.promises.mkdir(path.dirname(this._configPath), { recursive: true });
137
- await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
138
- }
139
- }
140
- export {
141
- FlakinessProjectConfig
142
- };
143
- //# sourceMappingURL=flakinessProjectConfig.js.map
@@ -1,208 +0,0 @@
1
- import assert from "assert";
2
- import { exec } from "child_process";
3
- import debug from "debug";
4
- import { posix as posixPath, win32 as win32Path } from "path";
5
- import { promisify } from "util";
6
- import { shell } from "./_internalUtils.js";
7
- const log = debug("fk:git");
8
- const execAsync = promisify(exec);
9
- const IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
10
- const IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
11
- function toPosixAbsolutePath(absolutePath) {
12
- if (IS_WIN32_PATH.test(absolutePath)) {
13
- absolutePath = absolutePath.split(win32Path.sep).join(posixPath.sep);
14
- }
15
- if (IS_ALMOST_POSIX_PATH.test(absolutePath))
16
- return "/" + absolutePath[0] + absolutePath.substring(2);
17
- return absolutePath;
18
- }
19
- function toNativeAbsolutePath(posix) {
20
- if (process.platform !== "win32")
21
- return posix;
22
- assert(posix.startsWith("/"), "The path must be absolute");
23
- const m = posix.match(/^\/([a-zA-Z])(\/.*)?$/);
24
- assert(m, `Invalid POSIX path: ${posix}`);
25
- const drive = m[1];
26
- const rest = (m[2] ?? "").split(posixPath.sep).join(win32Path.sep);
27
- return drive.toUpperCase() + ":" + rest;
28
- }
29
- class GitWorktree {
30
- constructor(_gitRoot) {
31
- this._gitRoot = _gitRoot;
32
- this._posixGitRoot = toPosixAbsolutePath(this._gitRoot);
33
- }
34
- /**
35
- * Creates a GitWorktree instance from any path inside a git repository.
36
- *
37
- * @param {string} somePathInsideGitRepo - Any path (file or directory) within a git repository.
38
- * Can be absolute or relative. The function will locate the git root directory.
39
- *
40
- * @returns {GitWorktree} A new GitWorktree instance bound to the discovered git root.
41
- *
42
- * @throws {Error} Throws if the path is not inside a git repository or if git commands fail.
43
- *
44
- * @example
45
- * ```typescript
46
- * const worktree = GitWorktree.create('./src/my-test.ts');
47
- * const gitRoot = worktree.rootPath();
48
- * ```
49
- */
50
- static create(somePathInsideGitRepo) {
51
- const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
52
- cwd: somePathInsideGitRepo,
53
- encoding: "utf-8"
54
- });
55
- assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
56
- return new GitWorktree(root);
57
- }
58
- _posixGitRoot;
59
- /**
60
- * Returns the native absolute path of the git repository root directory.
61
- *
62
- * @returns {string} Native absolute path to the git root. Format matches the current platform
63
- * (Windows or POSIX).
64
- *
65
- * @example
66
- * ```typescript
67
- * const root = worktree.rootPath();
68
- * // On Windows: 'D:\project'
69
- * // On Unix: '/project'
70
- * ```
71
- */
72
- rootPath() {
73
- return this._gitRoot;
74
- }
75
- /**
76
- * Returns the commit ID (SHA-1 hash) of the current HEAD commit.
77
- *
78
- * @returns {FlakinessReport.CommitId} Full 40-character commit hash of the HEAD commit.
79
- *
80
- * @throws {Error} Throws if git command fails or repository is in an invalid state.
81
- *
82
- * @example
83
- * ```typescript
84
- * const commitId = worktree.headCommitId();
85
- * // Returns: 'a1b2c3d4e5f6...' (40-character SHA-1)
86
- * ```
87
- */
88
- headCommitId() {
89
- const sha = shell(`git`, ["rev-parse", "HEAD"], {
90
- cwd: this._gitRoot,
91
- encoding: "utf-8"
92
- });
93
- assert(sha, `FAILED: git rev-parse HEAD @ ${this._gitRoot}`);
94
- return sha.trim();
95
- }
96
- /**
97
- * Converts a native absolute path to a git-relative POSIX path.
98
- *
99
- * Takes any absolute path (Windows or POSIX format) and converts it to a POSIX path
100
- * relative to the git repository root. This is essential for Flakiness reports where
101
- * all file paths must be git-relative and use POSIX separators.
102
- *
103
- * @param {string} absolutePath - Native absolute path to convert. Can be in Windows format
104
- * (e.g., `D:\project\src\test.ts`) or POSIX format (e.g., `/project/src/test.ts`).
105
- *
106
- * @returns {FlakinessReport.GitFilePath} POSIX path relative to git root (e.g., `src/test.ts`).
107
- * Returns an empty string if the path is the git root itself.
108
- *
109
- * @example
110
- * ```typescript
111
- * const gitPath = worktree.gitPath('/Users/project/src/test.ts');
112
- * // Returns: 'src/test.ts'
113
- * ```
114
- */
115
- gitPath(absolutePath) {
116
- return posixPath.relative(this._posixGitRoot, toPosixAbsolutePath(absolutePath));
117
- }
118
- /**
119
- * Converts a git-relative POSIX path to a native absolute path.
120
- *
121
- * Takes a POSIX path relative to the git root and converts it to the native absolute path
122
- * format for the current platform (Windows or POSIX). This is the inverse of `gitPath()`.
123
- *
124
- * @param {FlakinessReport.GitFilePath} relativePath - POSIX path relative to git root
125
- * (e.g., `src/test.ts`).
126
- *
127
- * @returns {string} Native absolute path. On Windows, returns Windows format (e.g., `D:\project\src\test.ts`).
128
- * On POSIX systems, returns POSIX format (e.g., `/project/src/test.ts`).
129
- *
130
- * @example
131
- * ```typescript
132
- * const absolutePath = worktree.absolutePath('src/test.ts');
133
- * // On Windows: 'D:\project\src\test.ts'
134
- * // On Unix: '/project/src/test.ts'
135
- * ```
136
- */
137
- absolutePath(relativePath) {
138
- return toNativeAbsolutePath(posixPath.join(this._posixGitRoot, relativePath));
139
- }
140
- /**
141
- * Lists recent commits from the repository.
142
- *
143
- * Retrieves commit information including commit ID, timestamp, author, message, and parent commits.
144
- * Note: CI environments often have shallow checkouts with limited history, which may affect
145
- * the number of commits returned.
146
- *
147
- * @param {number} count - Maximum number of commits to retrieve, starting from HEAD.
148
- *
149
- * @returns {Promise<GitCommit[]>} Promise that resolves to an array of commit objects, ordered
150
- * from most recent to oldest. Each commit includes:
151
- * - `commitId` - Full commit hash
152
- * - `timestamp` - Commit timestamp in milliseconds since Unix epoch
153
- * - `message` - Commit message (subject line)
154
- * - `author` - Author name
155
- * - `parents` - Array of parent commit IDs
156
- *
157
- * @example
158
- * ```typescript
159
- * const commits = await worktree.listCommits(10);
160
- * console.log(`Latest commit: ${commits[0].message}`);
161
- * ```
162
- */
163
- async listCommits(count) {
164
- return await listCommits(this._gitRoot, "HEAD", count);
165
- }
166
- }
167
- async function listCommits(gitRoot, head, count) {
168
- const FIELD_SEPARATOR = "|~|";
169
- const RECORD_SEPARATOR = "\0";
170
- const prettyFormat = [
171
- "%H",
172
- // Full commit hash
173
- "%ct",
174
- // Commit timestamp (Unix seconds)
175
- "%an",
176
- // Author name
177
- "%s",
178
- // Subject line
179
- "%P"
180
- // Parent hashes (space-separated)
181
- ].join(FIELD_SEPARATOR);
182
- const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
183
- try {
184
- const { stdout } = await execAsync(command, { cwd: gitRoot });
185
- if (!stdout) {
186
- return [];
187
- }
188
- return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
189
- const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
190
- const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
191
- return {
192
- commitId,
193
- timestamp: parseInt(timestampStr, 10) * 1e3,
194
- author,
195
- message,
196
- parents,
197
- walkIndex: 0
198
- };
199
- });
200
- } catch (error) {
201
- log(`Failed to list commits for repository at ${gitRoot}:`, error);
202
- return [];
203
- }
204
- }
205
- export {
206
- GitWorktree
207
- };
208
- //# sourceMappingURL=gitWorktree.js.map