@allurereport/plugin-testops 3.12.0 → 3.13.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 +5 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/gitFlow/LaunchGitFlow.d.ts +14 -0
- package/dist/gitFlow/LaunchGitFlow.js +84 -0
- package/dist/gitFlow/context.d.ts +9 -0
- package/dist/gitFlow/context.js +32 -0
- package/dist/gitFlow/index.d.ts +4 -0
- package/dist/gitFlow/index.js +2 -0
- package/dist/gitFlow/options.d.ts +3 -0
- package/dist/gitFlow/options.js +42 -0
- package/dist/gitFlow/projection.d.ts +19 -0
- package/dist/gitFlow/projection.js +195 -0
- package/dist/gitFlow/types.d.ts +56 -0
- package/dist/gitFlow/types.js +1 -0
- package/dist/model.d.ts +2 -0
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.js +30 -12
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -78,6 +78,8 @@ The plugin accepts the following options:
|
|
|
78
78
|
| `endpoint` | TestOps API endpoint | `string` | `undefined` |
|
|
79
79
|
| `projectId` | TestOps project ID | `string` | `undefined` |
|
|
80
80
|
| `autocloseLaunch` | When `true` (default), the launch is closed automatically when the plugin finishes; set to `false` to keep the launch open | `boolean` | `true` |
|
|
81
|
+
| `gitFlow` | When `true`, collect Git metadata for TestOps Git Flow on CI uploads (opt-in) | `boolean` | `false` |
|
|
82
|
+
| `ancestorLimit` | How many ancestor commits to attach to the launch for history linking in TestOps | `number` | `100` |
|
|
81
83
|
|
|
82
84
|
### Using options from environment variables
|
|
83
85
|
|
|
@@ -90,5 +92,7 @@ The plugin automatically reads the following environment variables and uses them
|
|
|
90
92
|
| `ALLURE_ENDPOINT` | `endpoint` |
|
|
91
93
|
| `ALLURE_LAUNCH_NAME` | `launchName` |
|
|
92
94
|
| `ALLURE_LAUNCH_TAGS` | `launchTags` |
|
|
95
|
+
| `ALLURE_GIT_FLOW` | `gitFlow` |
|
|
96
|
+
| `ALLURE_GIT_ANCESTOR_LIMIT` | `ancestorLimit` |
|
|
93
97
|
|
|
94
|
-
`ALLURE_TESTOPS_ENABLED` and `CI` are not configuration options: they only control whether upload runs when no CI is detected. See [CI and local runs](#ci-and-local-runs).
|
|
98
|
+
`ALLURE_TESTOPS_ENABLED` and `CI` are not configuration options: they only control whether upload runs when no CI is detected. See [CI and local runs](#ci-and-local-runs).
|
package/dist/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ClientRequest } from "http";
|
|
|
2
2
|
import type { AttachmentLink, CiDescriptor, EnvironmentIdentity, TestError, TestResult, TestStatus } from "@allurereport/core-api";
|
|
3
3
|
import type { QualityGateValidationResult } from "@allurereport/plugin-api";
|
|
4
4
|
import { AxiosError, type AxiosResponse } from "axios";
|
|
5
|
+
import type { LaunchGitContextDto } from "./gitFlow/index.js";
|
|
5
6
|
import type { AttachmentForUpload, AttachmentsResolver, FixtureResolver, LaunchCategoryBulkItem, LaunchCategoryBulkResult, TestOpsClientParams, TestOpsNamedEnv, TestOpsPluginTestResult } from "./model.js";
|
|
6
7
|
declare class TestOpsClientError extends AxiosError<{
|
|
7
8
|
message: string;
|
|
@@ -25,7 +26,7 @@ export declare class TestOpsClient {
|
|
|
25
26
|
createLaunchCategoriesBulk(launchId: number, items: LaunchCategoryBulkItem[]): Promise<LaunchCategoryBulkResult[]>;
|
|
26
27
|
startUpload(ci: CiDescriptor): Promise<void>;
|
|
27
28
|
stopUpload(ci: CiDescriptor, status: TestStatus): Promise<void>;
|
|
28
|
-
createLaunch(launchName: string, launchTags: string[]): Promise<void>;
|
|
29
|
+
createLaunch(launchName: string, launchTags: string[], gitContext?: LaunchGitContextDto): Promise<void>;
|
|
29
30
|
createSession(environment?: Record<string, unknown>): Promise<void>;
|
|
30
31
|
get namedEnvs(): MapIterator<TestOpsNamedEnv>;
|
|
31
32
|
getNamedEnvFor(id: string): TestOpsNamedEnv | undefined;
|
package/dist/client.js
CHANGED
|
@@ -153,7 +153,7 @@ export class TestOpsClient {
|
|
|
153
153
|
__classPrivateFieldSet(this, _TestOpsClient_uploadInProgress, false, "f");
|
|
154
154
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`CI upload stopped (status: ${status})`);
|
|
155
155
|
}
|
|
156
|
-
async createLaunch(launchName, launchTags) {
|
|
156
|
+
async createLaunch(launchName, launchTags, gitContext) {
|
|
157
157
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Creating launch…");
|
|
158
158
|
const data = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch", {
|
|
159
159
|
body: {
|
|
@@ -162,6 +162,7 @@ export class TestOpsClient {
|
|
|
162
162
|
autoclose: true,
|
|
163
163
|
external: true,
|
|
164
164
|
tags: launchTags.map((tag) => ({ name: tag })),
|
|
165
|
+
...(gitContext ? { gitContext } : {}),
|
|
165
166
|
},
|
|
166
167
|
});
|
|
167
168
|
__classPrivateFieldSet(this, _TestOpsClient_launch, data, "f");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CiDescriptor } from "@allurereport/core-api";
|
|
2
|
+
import type { Logger } from "../logger.js";
|
|
3
|
+
import type { LaunchGitContextDto } from "./types.js";
|
|
4
|
+
export type LaunchGitFlowParams = {
|
|
5
|
+
ci: CiDescriptor;
|
|
6
|
+
gitFlow: boolean;
|
|
7
|
+
ancestorLimit: number;
|
|
8
|
+
logger: Logger;
|
|
9
|
+
};
|
|
10
|
+
export declare class LaunchGitFlow {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(params: LaunchGitFlowParams);
|
|
13
|
+
resolve(): LaunchGitContextDto | undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _LaunchGitFlow_instances, _LaunchGitFlow_ci, _LaunchGitFlow_gitFlow, _LaunchGitFlow_ancestorLimit, _LaunchGitFlow_logger, _LaunchGitFlow_classify;
|
|
13
|
+
import { collectGitFacts, isGitAvailable } from "@allurereport/git";
|
|
14
|
+
import { buildGitFlowContext, shouldAttachGitFlow } from "./context.js";
|
|
15
|
+
import { projectLaunchGitContext } from "./projection.js";
|
|
16
|
+
export class LaunchGitFlow {
|
|
17
|
+
constructor(params) {
|
|
18
|
+
_LaunchGitFlow_instances.add(this);
|
|
19
|
+
_LaunchGitFlow_ci.set(this, void 0);
|
|
20
|
+
_LaunchGitFlow_gitFlow.set(this, void 0);
|
|
21
|
+
_LaunchGitFlow_ancestorLimit.set(this, void 0);
|
|
22
|
+
_LaunchGitFlow_logger.set(this, void 0);
|
|
23
|
+
__classPrivateFieldSet(this, _LaunchGitFlow_ci, params.ci, "f");
|
|
24
|
+
__classPrivateFieldSet(this, _LaunchGitFlow_gitFlow, params.gitFlow, "f");
|
|
25
|
+
__classPrivateFieldSet(this, _LaunchGitFlow_ancestorLimit, params.ancestorLimit, "f");
|
|
26
|
+
__classPrivateFieldSet(this, _LaunchGitFlow_logger, params.logger, "f");
|
|
27
|
+
}
|
|
28
|
+
resolve() {
|
|
29
|
+
if (!shouldAttachGitFlow(__classPrivateFieldGet(this, _LaunchGitFlow_gitFlow, "f"))) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (!isGitAvailable()) {
|
|
33
|
+
__classPrivateFieldGet(this, _LaunchGitFlow_logger, "f").warn("git CLI is not available; continuing upload without git context");
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const facts = collectGitFacts({ ancestorLimit: __classPrivateFieldGet(this, _LaunchGitFlow_ancestorLimit, "f") });
|
|
37
|
+
const gitFlowContext = buildGitFlowContext({
|
|
38
|
+
ci: __classPrivateFieldGet(this, _LaunchGitFlow_ci, "f"),
|
|
39
|
+
facts,
|
|
40
|
+
ancestorLimit: __classPrivateFieldGet(this, _LaunchGitFlow_ancestorLimit, "f"),
|
|
41
|
+
});
|
|
42
|
+
if (!gitFlowContext) {
|
|
43
|
+
__classPrivateFieldGet(this, _LaunchGitFlow_logger, "f").warn("Git Flow metadata could not be collected; continuing upload without git context");
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const classified = __classPrivateFieldGet(this, _LaunchGitFlow_instances, "m", _LaunchGitFlow_classify).call(this, gitFlowContext);
|
|
47
|
+
if (!classified) {
|
|
48
|
+
__classPrivateFieldGet(this, _LaunchGitFlow_logger, "f").warn("Pull request git context is incomplete (missing source or target branch); continuing upload without git context");
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const launchGitContext = projectLaunchGitContext(classified, __classPrivateFieldGet(this, _LaunchGitFlow_ci, "f"));
|
|
52
|
+
if (!launchGitContext) {
|
|
53
|
+
__classPrivateFieldGet(this, _LaunchGitFlow_logger, "f").warn("Git provider is not supported by TestOps; continuing upload without git context");
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return launchGitContext;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
_LaunchGitFlow_ci = new WeakMap(), _LaunchGitFlow_gitFlow = new WeakMap(), _LaunchGitFlow_ancestorLimit = new WeakMap(), _LaunchGitFlow_logger = new WeakMap(), _LaunchGitFlow_instances = new WeakSet(), _LaunchGitFlow_classify = function _LaunchGitFlow_classify(context) {
|
|
60
|
+
if (context.pullRequest) {
|
|
61
|
+
const sourceBranch = context.branch;
|
|
62
|
+
const targetBranch = context.targetBranch;
|
|
63
|
+
if (!sourceBranch || !targetBranch) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
kind: "pull_request",
|
|
68
|
+
context,
|
|
69
|
+
sourceBranch,
|
|
70
|
+
targetBranch,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (context.branch) {
|
|
74
|
+
return {
|
|
75
|
+
kind: "branch",
|
|
76
|
+
context,
|
|
77
|
+
branch: context.branch,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
kind: "standalone",
|
|
82
|
+
context,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CiDescriptor, type GitFacts } from "@allurereport/core-api";
|
|
2
|
+
import type { GitFlowContext } from "./types.js";
|
|
3
|
+
export type BuildGitFlowContextParams = {
|
|
4
|
+
ci: CiDescriptor;
|
|
5
|
+
facts?: GitFacts;
|
|
6
|
+
ancestorLimit: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const shouldAttachGitFlow: (gitFlow: boolean) => boolean;
|
|
9
|
+
export declare const buildGitFlowContext: (params: BuildGitFlowContextParams) => GitFlowContext | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GitProvider } from "@allurereport/core-api";
|
|
2
|
+
export const shouldAttachGitFlow = (gitFlow) => gitFlow;
|
|
3
|
+
const nonBlank = (value) => {
|
|
4
|
+
const trimmed = value?.trim();
|
|
5
|
+
return trimmed || undefined;
|
|
6
|
+
};
|
|
7
|
+
export const buildGitFlowContext = (params) => {
|
|
8
|
+
const { ci, facts, ancestorLimit } = params;
|
|
9
|
+
if (!facts?.commit) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const fallbackRepoName = nonBlank(ci.repoName);
|
|
13
|
+
const repository = ci.repository?.slug ? ci.repository : fallbackRepoName ? { slug: fallbackRepoName } : undefined;
|
|
14
|
+
if (!repository?.slug) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const provider = ci.provider ?? GitProvider.Other;
|
|
18
|
+
const branch = nonBlank(ci.sourceBranch) ?? nonBlank(facts.branch) ?? nonBlank(ci.jobRunBranch);
|
|
19
|
+
const targetBranch = nonBlank(ci.targetBranch);
|
|
20
|
+
const pullRequest = ci.pullRequest;
|
|
21
|
+
return {
|
|
22
|
+
provider,
|
|
23
|
+
repository,
|
|
24
|
+
commit: facts.commit,
|
|
25
|
+
branch,
|
|
26
|
+
targetBranch,
|
|
27
|
+
pullRequest,
|
|
28
|
+
firstParentAncestors: facts.firstParentAncestors,
|
|
29
|
+
ancestorLimit,
|
|
30
|
+
localState: facts.localState,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { GitFlowContext, GitProviderType, LaunchGitBranchDto, LaunchGitCommitDto, LaunchGitContextDto, LaunchGitContextType, LaunchGitPullRequestDto, LaunchGitRepositoryDto, LaunchPullRequestGitflowMetadata, ResolvedGitFlowOptions, LaunchStandaloneGitflowMetadata, } from "./types.js";
|
|
2
|
+
export { LaunchGitFlow } from "./LaunchGitFlow.js";
|
|
3
|
+
export type { LaunchGitFlowParams } from "./LaunchGitFlow.js";
|
|
4
|
+
export { resolveGitFlowOptions } from "./options.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { env } from "node:process";
|
|
2
|
+
import { DEFAULT_ANCESTOR_LIMIT } from "@allurereport/git";
|
|
3
|
+
const parseEnvBool = (value) => {
|
|
4
|
+
if (value === undefined || value === "") {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const normalized = value.trim().toLowerCase();
|
|
8
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
};
|
|
16
|
+
const resolveGitFlow = (options) => {
|
|
17
|
+
if (options.gitFlow === true) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (options.gitFlow === false) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return parseEnvBool(env.ALLURE_GIT_FLOW) ?? false;
|
|
24
|
+
};
|
|
25
|
+
const parseAncestorLimit = (value) => {
|
|
26
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
27
|
+
return Math.floor(value);
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "string" && value !== "") {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
32
|
+
return Math.floor(parsed);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return DEFAULT_ANCESTOR_LIMIT;
|
|
36
|
+
};
|
|
37
|
+
export const resolveGitFlowOptions = (options) => {
|
|
38
|
+
return {
|
|
39
|
+
gitFlow: resolveGitFlow(options),
|
|
40
|
+
ancestorLimit: parseAncestorLimit(options.ancestorLimit ?? env.ALLURE_GIT_ANCESTOR_LIMIT),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CiDescriptor } from "@allurereport/core-api";
|
|
2
|
+
import type { GitFlowContext, LaunchGitContextDto } from "./types.js";
|
|
3
|
+
export type ClassifiedPullRequestGitFlowContext = {
|
|
4
|
+
kind: "pull_request";
|
|
5
|
+
context: GitFlowContext;
|
|
6
|
+
sourceBranch: string;
|
|
7
|
+
targetBranch: string;
|
|
8
|
+
};
|
|
9
|
+
export type ClassifiedBranchGitFlowContext = {
|
|
10
|
+
kind: "branch";
|
|
11
|
+
context: GitFlowContext;
|
|
12
|
+
branch: string;
|
|
13
|
+
};
|
|
14
|
+
export type ClassifiedStandaloneGitFlowContext = {
|
|
15
|
+
kind: "standalone";
|
|
16
|
+
context: GitFlowContext;
|
|
17
|
+
};
|
|
18
|
+
export type ClassifiedGitFlowContext = ClassifiedPullRequestGitFlowContext | ClassifiedBranchGitFlowContext | ClassifiedStandaloneGitFlowContext;
|
|
19
|
+
export declare const projectLaunchGitContext: (classified: ClassifiedGitFlowContext, ci?: CiDescriptor) => LaunchGitContextDto | undefined;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { GitProvider } from "@allurereport/core-api";
|
|
2
|
+
const mapProvider = (provider) => {
|
|
3
|
+
switch (provider) {
|
|
4
|
+
case GitProvider.Github:
|
|
5
|
+
return "github";
|
|
6
|
+
case GitProvider.Gitlab:
|
|
7
|
+
return "gitlab";
|
|
8
|
+
case GitProvider.Bitbucket:
|
|
9
|
+
return "bitbucket";
|
|
10
|
+
default:
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const hostMatchesProvider = (host, providerType) => {
|
|
15
|
+
switch (providerType) {
|
|
16
|
+
case "github":
|
|
17
|
+
return host === "github.com" || host.endsWith(".github.com") || host.startsWith("github.");
|
|
18
|
+
case "gitlab":
|
|
19
|
+
return host === "gitlab.com" || host.endsWith(".gitlab.com") || host.startsWith("gitlab.");
|
|
20
|
+
case "bitbucket":
|
|
21
|
+
return host === "bitbucket.org" || host.endsWith(".bitbucket.org") || host.startsWith("bitbucket.");
|
|
22
|
+
default:
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const inferProviderTypeFromUrl = (url) => {
|
|
27
|
+
if (!url) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
32
|
+
for (const providerType of ["github", "gitlab", "bitbucket"]) {
|
|
33
|
+
if (hostMatchesProvider(host, providerType)) {
|
|
34
|
+
return providerType;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
const resolveProviderType = (context) => mapProvider(context.provider) ?? inferProviderTypeFromUrl(context.repository.url);
|
|
44
|
+
const stripGitSuffix = (url) => url.replace(/\.git\/?$/i, "");
|
|
45
|
+
const buildRepositoryUrl = (providerType, slug, repositoryUrl) => {
|
|
46
|
+
if (repositoryUrl) {
|
|
47
|
+
return repositoryUrl;
|
|
48
|
+
}
|
|
49
|
+
if (providerType === "github") {
|
|
50
|
+
return `https://github.com/${slug}.git`;
|
|
51
|
+
}
|
|
52
|
+
if (providerType === "gitlab") {
|
|
53
|
+
return `https://gitlab.com/${slug}`;
|
|
54
|
+
}
|
|
55
|
+
if (providerType === "bitbucket") {
|
|
56
|
+
return `https://bitbucket.org/${slug}.git`;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
};
|
|
60
|
+
const buildRepositoryWebUrl = (providerType, slug, repositoryUrl) => {
|
|
61
|
+
if (repositoryUrl) {
|
|
62
|
+
return stripGitSuffix(repositoryUrl);
|
|
63
|
+
}
|
|
64
|
+
if (providerType === "github") {
|
|
65
|
+
return `https://github.com/${slug}`;
|
|
66
|
+
}
|
|
67
|
+
if (providerType === "gitlab") {
|
|
68
|
+
return `https://gitlab.com/${slug}`;
|
|
69
|
+
}
|
|
70
|
+
if (providerType === "bitbucket") {
|
|
71
|
+
return `https://bitbucket.org/${slug}`;
|
|
72
|
+
}
|
|
73
|
+
return repositoryUrl;
|
|
74
|
+
};
|
|
75
|
+
const buildCommitUrl = (repoWebUrl, hash, providerType) => {
|
|
76
|
+
if (!repoWebUrl) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (providerType === "github") {
|
|
80
|
+
return `${repoWebUrl}/commit/${hash}`;
|
|
81
|
+
}
|
|
82
|
+
if (providerType === "gitlab") {
|
|
83
|
+
return `${repoWebUrl}/-/commit/${hash}`;
|
|
84
|
+
}
|
|
85
|
+
return `${repoWebUrl}/commits/${hash}`;
|
|
86
|
+
};
|
|
87
|
+
const buildBranchUrl = (repoWebUrl, branchName, providerType) => {
|
|
88
|
+
if (!repoWebUrl) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (providerType === "github") {
|
|
92
|
+
return `${repoWebUrl}/tree/${branchName}`;
|
|
93
|
+
}
|
|
94
|
+
if (providerType === "gitlab") {
|
|
95
|
+
return `${repoWebUrl}/-/tree/${branchName}`;
|
|
96
|
+
}
|
|
97
|
+
return `${repoWebUrl}/branch/${branchName}`;
|
|
98
|
+
};
|
|
99
|
+
const buildPullRequestUrl = (repoWebUrl, externalId, providerType, pullRequestUrl) => {
|
|
100
|
+
if (pullRequestUrl) {
|
|
101
|
+
return pullRequestUrl;
|
|
102
|
+
}
|
|
103
|
+
if (!repoWebUrl) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (providerType === "github") {
|
|
107
|
+
return `${repoWebUrl}/pull/${externalId}`;
|
|
108
|
+
}
|
|
109
|
+
if (providerType === "gitlab") {
|
|
110
|
+
return `${repoWebUrl}/-/merge_requests/${externalId}`;
|
|
111
|
+
}
|
|
112
|
+
return `${repoWebUrl}/pull-requests/${externalId}`;
|
|
113
|
+
};
|
|
114
|
+
const toStandaloneMetadata = (localState) => {
|
|
115
|
+
if (!localState) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
hasUncommittedChanges: localState.uncommittedChanges,
|
|
120
|
+
isUnpublishedCommit: localState.unpublishedCommit,
|
|
121
|
+
isUnpublishedBranch: localState.unpublishedBranch,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
const toPullRequestMetadata = (ci) => {
|
|
125
|
+
if (!ci?.jobRunUid && !ci?.jobRunName) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
workflowRunId: ci.jobRunUid || undefined,
|
|
130
|
+
workflowRunName: ci.jobRunName || undefined,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
const toBranchDto = (name, providerType, repoWebUrl) => ({
|
|
134
|
+
name,
|
|
135
|
+
url: buildBranchUrl(repoWebUrl, name, providerType),
|
|
136
|
+
});
|
|
137
|
+
const buildRepositoryAndCommit = (context, providerType) => {
|
|
138
|
+
const repositoryUrl = buildRepositoryUrl(providerType, context.repository.slug, context.repository.url);
|
|
139
|
+
const repoWebUrl = buildRepositoryWebUrl(providerType, context.repository.slug, repositoryUrl);
|
|
140
|
+
const lineage = context.firstParentAncestors.length > 0 ? context.firstParentAncestors : undefined;
|
|
141
|
+
return {
|
|
142
|
+
repository: {
|
|
143
|
+
providerType,
|
|
144
|
+
name: context.repository.slug,
|
|
145
|
+
url: repositoryUrl,
|
|
146
|
+
},
|
|
147
|
+
commit: {
|
|
148
|
+
hash: context.commit,
|
|
149
|
+
url: buildCommitUrl(repoWebUrl, context.commit, providerType),
|
|
150
|
+
lineage,
|
|
151
|
+
},
|
|
152
|
+
repoWebUrl,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
export const projectLaunchGitContext = (classified, ci) => {
|
|
156
|
+
const context = classified.context;
|
|
157
|
+
const providerType = resolveProviderType(context);
|
|
158
|
+
if (!providerType) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const { repository, commit, repoWebUrl } = buildRepositoryAndCommit(context, providerType);
|
|
162
|
+
if (classified.kind === "pull_request") {
|
|
163
|
+
const { pullRequest } = context;
|
|
164
|
+
if (!pullRequest) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
contextType: "pull_request",
|
|
169
|
+
repository,
|
|
170
|
+
commit,
|
|
171
|
+
pullRequest: {
|
|
172
|
+
externalId: pullRequest.id,
|
|
173
|
+
title: pullRequest.title,
|
|
174
|
+
url: buildPullRequestUrl(repoWebUrl, pullRequest.id, providerType, pullRequest.url),
|
|
175
|
+
sourceBranch: toBranchDto(classified.sourceBranch, providerType, repoWebUrl),
|
|
176
|
+
targetBranch: toBranchDto(classified.targetBranch, providerType, repoWebUrl),
|
|
177
|
+
},
|
|
178
|
+
metadata: toPullRequestMetadata(ci),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (classified.kind === "branch") {
|
|
182
|
+
return {
|
|
183
|
+
contextType: "branch",
|
|
184
|
+
repository,
|
|
185
|
+
commit,
|
|
186
|
+
branch: toBranchDto(classified.branch, providerType, repoWebUrl),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
contextType: "standalone",
|
|
191
|
+
repository,
|
|
192
|
+
commit,
|
|
193
|
+
metadata: toStandaloneMetadata(context.localState),
|
|
194
|
+
};
|
|
195
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { GitLocalState, GitProvider, GitPullRequestRef, GitRepositoryRef } from "@allurereport/core-api";
|
|
2
|
+
export type LaunchGitContextType = "branch" | "standalone" | "pull_request";
|
|
3
|
+
export type GitProviderType = "github" | "gitlab" | "bitbucket";
|
|
4
|
+
export type ResolvedGitFlowOptions = {
|
|
5
|
+
gitFlow: boolean;
|
|
6
|
+
ancestorLimit: number;
|
|
7
|
+
};
|
|
8
|
+
export interface LaunchGitRepositoryDto {
|
|
9
|
+
providerType: GitProviderType;
|
|
10
|
+
name: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LaunchGitCommitDto {
|
|
14
|
+
hash: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
lineage?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface LaunchGitBranchDto {
|
|
19
|
+
name: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LaunchGitPullRequestDto {
|
|
23
|
+
externalId: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
url?: string;
|
|
26
|
+
sourceBranch: LaunchGitBranchDto;
|
|
27
|
+
targetBranch: LaunchGitBranchDto;
|
|
28
|
+
}
|
|
29
|
+
export interface LaunchStandaloneGitflowMetadata {
|
|
30
|
+
hasUncommittedChanges?: boolean;
|
|
31
|
+
isUnpublishedCommit?: boolean;
|
|
32
|
+
isUnpublishedBranch?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface LaunchPullRequestGitflowMetadata {
|
|
35
|
+
workflowRunId?: string;
|
|
36
|
+
workflowRunName?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface LaunchGitContextDto {
|
|
39
|
+
contextType: LaunchGitContextType;
|
|
40
|
+
repository: LaunchGitRepositoryDto;
|
|
41
|
+
commit: LaunchGitCommitDto;
|
|
42
|
+
branch?: LaunchGitBranchDto;
|
|
43
|
+
pullRequest?: LaunchGitPullRequestDto;
|
|
44
|
+
metadata?: LaunchStandaloneGitflowMetadata | LaunchPullRequestGitflowMetadata;
|
|
45
|
+
}
|
|
46
|
+
export interface GitFlowContext {
|
|
47
|
+
provider: GitProvider;
|
|
48
|
+
repository: GitRepositoryRef;
|
|
49
|
+
commit: string;
|
|
50
|
+
branch?: string;
|
|
51
|
+
targetBranch?: string;
|
|
52
|
+
pullRequest?: GitPullRequestRef;
|
|
53
|
+
firstParentAncestors: string[];
|
|
54
|
+
ancestorLimit: number;
|
|
55
|
+
localState?: GitLocalState;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/model.d.ts
CHANGED
package/dist/plugin.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { type AllureStore, type Plugin, type PluginContext } from "@allurereport/plugin-api";
|
|
1
|
+
import { type AllureStore, type Plugin, type PluginConstructorContext, type PluginContext } from "@allurereport/plugin-api";
|
|
2
2
|
import type { TestOpsPluginOptions } from "./model.js";
|
|
3
3
|
export declare class TestOpsPlugin implements Plugin {
|
|
4
4
|
#private;
|
|
5
5
|
readonly options: TestOpsPluginOptions;
|
|
6
|
-
constructor(options: TestOpsPluginOptions);
|
|
6
|
+
constructor(options: TestOpsPluginOptions, context?: PluginConstructorContext);
|
|
7
7
|
get isOverridenByEnv(): boolean;
|
|
8
|
+
get isManuallyEnabled(): boolean;
|
|
8
9
|
get enabled(): boolean;
|
|
9
10
|
start(context: PluginContext, store: AllureStore): Promise<void>;
|
|
10
11
|
update(context: PluginContext, store: AllureStore): Promise<void>;
|
package/dist/plugin.js
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
-
};
|
|
6
1
|
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
2
|
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
3
|
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
4
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
5
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
6
|
};
|
|
12
|
-
var
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _TestOpsPlugin_instances, _TestOpsPlugin_logger, _TestOpsPlugin_ci, _TestOpsPlugin_client, _TestOpsPlugin_launchName, _TestOpsPlugin_launchTags, _TestOpsPlugin_uploadedTestResultsIds, _TestOpsPlugin_autocloseLaunch, _TestOpsPlugin_gitFlow, _TestOpsPlugin_enabledByConfig, _TestOpsPlugin_uploadQualityGateResults, _TestOpsPlugin_uploadGlobalErrors, _TestOpsPlugin_uploadGlobalAttachments, _TestOpsPlugin_uploadTestResults, _TestOpsPlugin_upload, _TestOpsPlugin_trsToUpload, _TestOpsPlugin_enrichWithCategories, _TestOpsPlugin_syncLaunchCategories, _TestOpsPlugin_collectCategoryNamesByExternalId, _TestOpsPlugin_assignCreatedCategoryIds, _TestOpsPlugin_startUpload, _TestOpsPlugin_stopUpload;
|
|
13
13
|
import { env } from "node:process";
|
|
14
14
|
import { detect, isLocalCiDescriptor } from "@allurereport/ci";
|
|
15
15
|
import { getWorstStatus } from "@allurereport/core-api";
|
|
16
|
-
import { createPluginSummary } from "@allurereport/plugin-api";
|
|
16
|
+
import { createPluginSummary, } from "@allurereport/plugin-api";
|
|
17
17
|
import { uniqBy, stubTrue } from "lodash-es";
|
|
18
18
|
import { bold } from "yoctocolors";
|
|
19
19
|
import { TestOpsClient } from "./client.js";
|
|
20
|
+
import { LaunchGitFlow, resolveGitFlowOptions } from "./gitFlow/index.js";
|
|
20
21
|
import { Logger } from "./logger.js";
|
|
21
22
|
import { attachmentsResolverFactory, fixturesResolverFactory, resolvePluginOptions, unwrapStepsAttachments, } from "./utils/index.js";
|
|
22
23
|
import { toUploadCategory } from "./utils/uploadCategory.js";
|
|
23
24
|
import { uploadFilenameForLink } from "./utils/uploaderDto.js";
|
|
24
25
|
const categoryDisplayName = (cat) => cat.name ?? cat.grouping?.[0]?.name ?? cat.grouping?.[0]?.value ?? cat.grouping?.[0]?.key ?? cat.externalId;
|
|
25
26
|
export class TestOpsPlugin {
|
|
26
|
-
constructor(options) {
|
|
27
|
+
constructor(options, context = {}) {
|
|
27
28
|
_TestOpsPlugin_instances.add(this);
|
|
28
29
|
this.options = options;
|
|
29
30
|
_TestOpsPlugin_logger.set(this, new Logger("TestOpsPlugin"));
|
|
@@ -33,7 +34,13 @@ export class TestOpsPlugin {
|
|
|
33
34
|
_TestOpsPlugin_launchTags.set(this, []);
|
|
34
35
|
_TestOpsPlugin_uploadedTestResultsIds.set(this, new Set());
|
|
35
36
|
_TestOpsPlugin_autocloseLaunch.set(this, false);
|
|
36
|
-
|
|
37
|
+
_TestOpsPlugin_gitFlow.set(this, void 0);
|
|
38
|
+
_TestOpsPlugin_enabledByConfig.set(this, false);
|
|
39
|
+
__classPrivateFieldSet(this, _TestOpsPlugin_enabledByConfig, context.enabled === true, "f");
|
|
40
|
+
if (context.enabled === false) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (isLocalCiDescriptor(__classPrivateFieldGet(this, _TestOpsPlugin_ci, "f")) && !this.isManuallyEnabled) {
|
|
37
44
|
__classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`plugin is disabled - no CI environment detected. To enable, set ${bold("ALLURE_TESTOPS_ENABLED")}=true or ${bold("CI")}=true.`);
|
|
38
45
|
return;
|
|
39
46
|
}
|
|
@@ -48,6 +55,13 @@ export class TestOpsPlugin {
|
|
|
48
55
|
__classPrivateFieldSet(this, _TestOpsPlugin_launchTags, launchTags, "f");
|
|
49
56
|
}
|
|
50
57
|
__classPrivateFieldSet(this, _TestOpsPlugin_autocloseLaunch, autocloseLaunch, "f");
|
|
58
|
+
const gitFlowOptions = resolveGitFlowOptions(options);
|
|
59
|
+
__classPrivateFieldSet(this, _TestOpsPlugin_gitFlow, new LaunchGitFlow({
|
|
60
|
+
ci: __classPrivateFieldGet(this, _TestOpsPlugin_ci, "f"),
|
|
61
|
+
gitFlow: gitFlowOptions.gitFlow,
|
|
62
|
+
ancestorLimit: gitFlowOptions.ancestorLimit,
|
|
63
|
+
logger: __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f"),
|
|
64
|
+
}), "f");
|
|
51
65
|
if (!accessToken) {
|
|
52
66
|
__classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(`Allure TestOps ${bold("access token")} is missing. Please provide a valid access token in the plugin options.`);
|
|
53
67
|
}
|
|
@@ -67,11 +81,14 @@ export class TestOpsPlugin {
|
|
|
67
81
|
};
|
|
68
82
|
return isEnabled(env.ALLURE_TESTOPS_ENABLED) || isEnabled(env.CI);
|
|
69
83
|
}
|
|
84
|
+
get isManuallyEnabled() {
|
|
85
|
+
return __classPrivateFieldGet(this, _TestOpsPlugin_enabledByConfig, "f") || this.isOverridenByEnv;
|
|
86
|
+
}
|
|
70
87
|
get enabled() {
|
|
71
88
|
if (!(__classPrivateFieldGet(this, _TestOpsPlugin_client, "f") instanceof TestOpsClient)) {
|
|
72
89
|
return false;
|
|
73
90
|
}
|
|
74
|
-
if (this.
|
|
91
|
+
if (this.isManuallyEnabled) {
|
|
75
92
|
return true;
|
|
76
93
|
}
|
|
77
94
|
if (!__classPrivateFieldGet(this, _TestOpsPlugin_ci, "f") || __classPrivateFieldGet(this, _TestOpsPlugin_ci, "f").type === "local") {
|
|
@@ -150,7 +167,7 @@ export class TestOpsPlugin {
|
|
|
150
167
|
return summary;
|
|
151
168
|
}
|
|
152
169
|
}
|
|
153
|
-
_TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestOpsPlugin_client = new WeakMap(), _TestOpsPlugin_launchName = new WeakMap(), _TestOpsPlugin_launchTags = new WeakMap(), _TestOpsPlugin_uploadedTestResultsIds = new WeakMap(), _TestOpsPlugin_autocloseLaunch = new WeakMap(), _TestOpsPlugin_instances = new WeakSet(), _TestOpsPlugin_uploadQualityGateResults = async function _TestOpsPlugin_uploadQualityGateResults(store) {
|
|
170
|
+
_TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestOpsPlugin_client = new WeakMap(), _TestOpsPlugin_launchName = new WeakMap(), _TestOpsPlugin_launchTags = new WeakMap(), _TestOpsPlugin_uploadedTestResultsIds = new WeakMap(), _TestOpsPlugin_autocloseLaunch = new WeakMap(), _TestOpsPlugin_gitFlow = new WeakMap(), _TestOpsPlugin_enabledByConfig = new WeakMap(), _TestOpsPlugin_instances = new WeakSet(), _TestOpsPlugin_uploadQualityGateResults = async function _TestOpsPlugin_uploadQualityGateResults(store) {
|
|
154
171
|
const results = await store.qualityGateResults();
|
|
155
172
|
const uniqueResults = uniqBy(results.filter(({ success }) => !success), ({ rule, environment }) => `${rule}-${environment}`);
|
|
156
173
|
if (uniqueResults.length === 0) {
|
|
@@ -393,7 +410,8 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
|
|
|
393
410
|
}
|
|
394
411
|
}
|
|
395
412
|
}, _TestOpsPlugin_startUpload = async function _TestOpsPlugin_startUpload() {
|
|
396
|
-
|
|
413
|
+
const launchGitContext = __classPrivateFieldGet(this, _TestOpsPlugin_gitFlow, "f").resolve();
|
|
414
|
+
await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createLaunch(__classPrivateFieldGet(this, _TestOpsPlugin_launchName, "f"), __classPrivateFieldGet(this, _TestOpsPlugin_launchTags, "f"), launchGitContext);
|
|
397
415
|
await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").startUpload(__classPrivateFieldGet(this, _TestOpsPlugin_ci, "f"));
|
|
398
416
|
}, _TestOpsPlugin_stopUpload = async function _TestOpsPlugin_stopUpload(status) {
|
|
399
417
|
await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").stopUpload(__classPrivateFieldGet(this, _TestOpsPlugin_ci, "f"), status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allurereport/plugin-testops",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Allure Plugin TestOps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"allure",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
"lint:fix": "oxlint --import-plugin --fix src test features stories"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@allurereport/ci": "3.
|
|
36
|
-
"@allurereport/core-api": "3.
|
|
37
|
-
"@allurereport/
|
|
38
|
-
"@allurereport/
|
|
39
|
-
"@allurereport/
|
|
35
|
+
"@allurereport/ci": "3.13.0",
|
|
36
|
+
"@allurereport/core-api": "3.13.0",
|
|
37
|
+
"@allurereport/git": "3.13.0",
|
|
38
|
+
"@allurereport/plugin-api": "3.13.0",
|
|
39
|
+
"@allurereport/reader-api": "3.13.0",
|
|
40
|
+
"@allurereport/service": "3.13.0",
|
|
40
41
|
"axios": "^1.15.2",
|
|
41
42
|
"form-data": "^4.0.5",
|
|
42
43
|
"lodash-es": "^4.18.1",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"yoctocolors": "^2"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@types/d3-shape": "^3.1.
|
|
49
|
+
"@types/d3-shape": "^3.1.8",
|
|
49
50
|
"@types/lodash-es": "^4",
|
|
50
51
|
"@types/node": "^20",
|
|
51
52
|
"@types/progress": "^2",
|