@qanalyzer/forge-commons 1.1.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.
- package/LICENSE +202 -0
- package/dist/ci/context.d.ts +6 -0
- package/dist/ci/context.js +71 -0
- package/dist/ci/frameworks/jest.d.ts +1 -0
- package/dist/ci/frameworks/jest.js +6 -0
- package/dist/ci/frameworks/reporter.d.ts +41 -0
- package/dist/ci/frameworks/reporter.js +184 -0
- package/dist/ci/frameworks/upload.d.ts +10 -0
- package/dist/ci/frameworks/upload.js +58 -0
- package/dist/ci/frameworks/vitest.d.ts +2 -0
- package/dist/ci/frameworks/vitest.js +10 -0
- package/dist/ci/generate-ci-template.d.ts +7 -0
- package/dist/ci/generate-ci-template.js +79 -0
- package/dist/ci/index.d.ts +7 -0
- package/dist/ci/index.js +36 -0
- package/dist/ci/templates/azure-devops.d.ts +5 -0
- package/dist/ci/templates/azure-devops.js +142 -0
- package/dist/ci/templates/bitbucket.d.ts +5 -0
- package/dist/ci/templates/bitbucket.js +113 -0
- package/dist/ci/templates/github.d.ts +6 -0
- package/dist/ci/templates/github.js +156 -0
- package/dist/ci/templates/gitlab.d.ts +5 -0
- package/dist/ci/templates/gitlab.js +124 -0
- package/dist/ci/templates/jenkins.d.ts +5 -0
- package/dist/ci/templates/jenkins.js +125 -0
- package/dist/ci/types.d.ts +58 -0
- package/dist/ci/types.js +16 -0
- package/dist/client/attach-client.d.ts +48 -0
- package/dist/client/attach-client.js +122 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -0
- package/dist/client/ingest-client.d.ts +19 -0
- package/dist/client/ingest-client.js +66 -0
- package/dist/config/config-loader.d.ts +8 -0
- package/dist/config/config-loader.js +62 -0
- package/dist/config/config-type.d.ts +24 -0
- package/dist/config/config-type.js +2 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +6 -0
- package/dist/env/ci-environment.d.ts +10 -0
- package/dist/env/ci-environment.js +59 -0
- package/dist/env/env-enum.d.ts +21 -0
- package/dist/env/env-enum.js +27 -0
- package/dist/env/env-to-config.d.ts +3 -0
- package/dist/env/env-to-config.js +53 -0
- package/dist/env/index.d.ts +4 -0
- package/dist/env/index.js +11 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +59 -0
- package/dist/models/attachment.d.ts +9 -0
- package/dist/models/attachment.js +2 -0
- package/dist/models/execution-sum.d.ts +7 -0
- package/dist/models/execution-sum.js +2 -0
- package/dist/models/host-data.d.ts +13 -0
- package/dist/models/host-data.js +2 -0
- package/dist/models/index.d.ts +14 -0
- package/dist/models/index.js +24 -0
- package/dist/models/ingest-payload.d.ts +56 -0
- package/dist/models/ingest-payload.js +56 -0
- package/dist/models/jest-vitest-report.d.ts +37 -0
- package/dist/models/jest-vitest-report.js +2 -0
- package/dist/models/meta-qa.d.ts +71 -0
- package/dist/models/meta-qa.js +257 -0
- package/dist/models/report.d.ts +15 -0
- package/dist/models/report.js +2 -0
- package/dist/models/short-result.d.ts +8 -0
- package/dist/models/short-result.js +2 -0
- package/dist/models/stats.d.ts +10 -0
- package/dist/models/stats.js +2 -0
- package/dist/models/step-data.d.ts +20 -0
- package/dist/models/step-data.js +2 -0
- package/dist/models/step-execution.d.ts +14 -0
- package/dist/models/step-execution.js +24 -0
- package/dist/models/test-execution.d.ts +18 -0
- package/dist/models/test-execution.js +30 -0
- package/dist/models/test-result.d.ts +34 -0
- package/dist/models/test-result.js +43 -0
- package/dist/models/test-step.d.ts +19 -0
- package/dist/models/test-step.js +46 -0
- package/dist/options/compose-options.d.ts +2 -0
- package/dist/options/compose-options.js +26 -0
- package/dist/options/index.d.ts +3 -0
- package/dist/options/index.js +7 -0
- package/dist/options/mode-enum.d.ts +5 -0
- package/dist/options/mode-enum.js +9 -0
- package/dist/options/options-type.d.ts +11 -0
- package/dist/options/options-type.js +2 -0
- package/dist/qanalyzer/index.d.ts +2 -0
- package/dist/qanalyzer/index.js +8 -0
- package/dist/qanalyzer/options-resolver.d.ts +11 -0
- package/dist/qanalyzer/options-resolver.js +31 -0
- package/dist/qanalyzer/reporter-factory.d.ts +11 -0
- package/dist/qanalyzer/reporter-factory.js +51 -0
- package/dist/qanalyzer.d.ts +29 -0
- package/dist/qanalyzer.js +96 -0
- package/dist/reporters/fallback-coordinator.d.ts +24 -0
- package/dist/reporters/fallback-coordinator.js +77 -0
- package/dist/reporters/file-reporter.d.ts +10 -0
- package/dist/reporters/file-reporter.js +16 -0
- package/dist/reporters/index.d.ts +4 -0
- package/dist/reporters/index.js +9 -0
- package/dist/reporters/ingest-reporter.d.ts +9 -0
- package/dist/reporters/ingest-reporter.js +14 -0
- package/dist/reporters/reporter-interface.d.ts +4 -0
- package/dist/reporters/reporter-interface.js +2 -0
- package/dist/utils/disabled-exception.d.ts +2 -0
- package/dist/utils/disabled-exception.js +6 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +17 -0
- package/dist/utils/issue-keys.d.ts +7 -0
- package/dist/utils/issue-keys.js +33 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/token-masker.d.ts +2 -0
- package/dist/utils/token-masker.js +17 -0
- package/dist/utils/upload-attachment.d.ts +35 -0
- package/dist/utils/upload-attachment.js +158 -0
- package/dist/writer/fs-writer.d.ts +9 -0
- package/dist/writer/fs-writer.js +19 -0
- package/dist/writer/index.d.ts +1 -0
- package/dist/writer/index.js +5 -0
- package/package.json +34 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderGitlabUpload = renderGitlabUpload;
|
|
4
|
+
const upload_1 = require("../frameworks/upload");
|
|
5
|
+
const reporter_1 = require("../frameworks/reporter");
|
|
6
|
+
const types_1 = require("../types");
|
|
7
|
+
function gitlabScriptLines(ctx, ...cmds) {
|
|
8
|
+
return [...(0, reporter_1.reporterPreRunScripts)(ctx), ...cmds].map((c) => ` - ${c}`).join('\n');
|
|
9
|
+
}
|
|
10
|
+
function gitlabSecrets() {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
name: 'QANALYZER_INGEST_URL',
|
|
14
|
+
description: 'Forge web trigger URL (launch ingest + binary attach / screenshots / qa.attach)',
|
|
15
|
+
platformHint: 'Settings → CI/CD → Variables → Add variable (Masked + Protected as needed)',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'QANALYZER_INGEST_TOKEN',
|
|
19
|
+
description: 'Bearer token from QAnalyzer configure page (shown once)',
|
|
20
|
+
platformHint: 'Settings → CI/CD → Variables → Add variable (Masked + Protected)',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
function gitlabVariables() {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
name: 'JIRA_PROJECT_KEY',
|
|
28
|
+
description: 'Jira project key allowlisted in QAnalyzer (e.g. AUTH)',
|
|
29
|
+
platformHint: 'Settings → CI/CD → Variables',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
function renderGitlabReporter(ctx) {
|
|
34
|
+
(0, reporter_1.assertReporterFramework)(ctx);
|
|
35
|
+
const nodeVersion = ctx.nodeVersion ?? '22';
|
|
36
|
+
const label = (0, reporter_1.reporterFrameworkLabel)(ctx);
|
|
37
|
+
const pkg = (0, reporter_1.reporterPackageName)(ctx);
|
|
38
|
+
const configHint = (0, reporter_1.reporterConfigHint)(ctx);
|
|
39
|
+
const jobName = ctx.framework;
|
|
40
|
+
const content = `# QAnalyzer fragment — ${label} ${pkg} reporter path
|
|
41
|
+
# Requires ${pkg} in package.json and ${configHint}
|
|
42
|
+
stages:
|
|
43
|
+
- test
|
|
44
|
+
|
|
45
|
+
${jobName}:
|
|
46
|
+
stage: test
|
|
47
|
+
image: node:${nodeVersion}-alpine
|
|
48
|
+
variables:
|
|
49
|
+
QANALYZER_MODE: ingest
|
|
50
|
+
QANALYZER_INGEST_URL: $QANALYZER_INGEST_URL
|
|
51
|
+
QANALYZER_INGEST_TOKEN: $QANALYZER_INGEST_TOKEN
|
|
52
|
+
QANALYZER_PROJECT_KEY: $JIRA_PROJECT_KEY
|
|
53
|
+
QANALYZER_LAUNCH_NAME: $CI_PIPELINE_ID
|
|
54
|
+
# Optional Test Plan: QANALYZER_PLAN_NAME / QANALYZER_PLAN_ID / QANALYZER_PLAN_KEY
|
|
55
|
+
# Optional tags: QANALYZER_FIX_VERSION / QANALYZER_SPRINT
|
|
56
|
+
script:
|
|
57
|
+
- npm ci
|
|
58
|
+
${gitlabScriptLines(ctx, (0, reporter_1.frameworkReporterRun)(ctx))}
|
|
59
|
+
`;
|
|
60
|
+
return {
|
|
61
|
+
platform: 'gitlab',
|
|
62
|
+
framework: ctx.framework,
|
|
63
|
+
ingestPath: 'reporter',
|
|
64
|
+
filename: '.gitlab-ci.yml',
|
|
65
|
+
content,
|
|
66
|
+
secretsSetup: gitlabSecrets(),
|
|
67
|
+
variablesSetup: [...gitlabVariables(), ...(0, reporter_1.planCiVariableHints)(), ...(0, reporter_1.versionTagCiVariableHints)()],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* GitLab CI — upload path or @qanalyzer/forge-vitest / @qanalyzer/forge-jest reporter path.
|
|
72
|
+
*/
|
|
73
|
+
function renderGitlabUpload(ctx) {
|
|
74
|
+
if (ctx.ingestPath === 'reporter') {
|
|
75
|
+
return renderGitlabReporter(ctx);
|
|
76
|
+
}
|
|
77
|
+
if (ctx.ingestPath !== 'upload') {
|
|
78
|
+
throw new types_1.UnsupportedVariantError(ctx.platform, ctx.framework, ctx.ingestPath);
|
|
79
|
+
}
|
|
80
|
+
const nodeVersion = ctx.nodeVersion ?? '22';
|
|
81
|
+
const reportFile = ctx.reportFile ?? 'qanalyzer-results.json';
|
|
82
|
+
const testCmd = (0, upload_1.frameworkTestCommand)(ctx);
|
|
83
|
+
const jobName = ctx.framework;
|
|
84
|
+
const label = (0, upload_1.frameworkLabel)(ctx);
|
|
85
|
+
const uploadBlock = (0, upload_1.indentUploadCli)(ctx, 6);
|
|
86
|
+
const content = `# QAnalyzer fragment — merge into your .gitlab-ci.yml
|
|
87
|
+
# ${label} upload path
|
|
88
|
+
stages:
|
|
89
|
+
- test
|
|
90
|
+
- report
|
|
91
|
+
|
|
92
|
+
${jobName}:
|
|
93
|
+
stage: test
|
|
94
|
+
image: node:${nodeVersion}-alpine
|
|
95
|
+
script:
|
|
96
|
+
- npm ci
|
|
97
|
+
${gitlabScriptLines(ctx, testCmd)}
|
|
98
|
+
artifacts:
|
|
99
|
+
when: always
|
|
100
|
+
paths:
|
|
101
|
+
- ${reportFile}
|
|
102
|
+
|
|
103
|
+
qanalyzer_upload:
|
|
104
|
+
stage: report
|
|
105
|
+
image: node:${nodeVersion}-alpine
|
|
106
|
+
when: always
|
|
107
|
+
needs: [${jobName}]
|
|
108
|
+
variables:
|
|
109
|
+
QANALYZER_INGEST_URL: $QANALYZER_INGEST_URL
|
|
110
|
+
QANALYZER_INGEST_TOKEN: $QANALYZER_INGEST_TOKEN
|
|
111
|
+
script:
|
|
112
|
+
- |
|
|
113
|
+
${uploadBlock}
|
|
114
|
+
`;
|
|
115
|
+
return {
|
|
116
|
+
platform: 'gitlab',
|
|
117
|
+
framework: ctx.framework,
|
|
118
|
+
ingestPath: 'upload',
|
|
119
|
+
filename: '.gitlab-ci.yml',
|
|
120
|
+
content,
|
|
121
|
+
secretsSetup: gitlabSecrets(),
|
|
122
|
+
variablesSetup: [...gitlabVariables(), ...(0, reporter_1.planCiVariableHints)(), ...(0, reporter_1.versionTagCiVariableHints)()],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderJenkinsUpload = renderJenkinsUpload;
|
|
4
|
+
const upload_1 = require("../frameworks/upload");
|
|
5
|
+
const reporter_1 = require("../frameworks/reporter");
|
|
6
|
+
const types_1 = require("../types");
|
|
7
|
+
function jenkinsShLines(indent, ctx, ...cmds) {
|
|
8
|
+
// Use sh -c so redirects in Playwright JSON upload (`> file`) work.
|
|
9
|
+
return [...(0, reporter_1.reporterPreRunScripts)(ctx), ...cmds]
|
|
10
|
+
.map((cmd) => `${indent}sh -c '${cmd.replace(/'/g, `'\"'\"'`)}'`)
|
|
11
|
+
.join('\n');
|
|
12
|
+
}
|
|
13
|
+
function jenkinsSecrets() {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
name: 'qanalyzer-ingest-url',
|
|
17
|
+
description: 'Secret text credential: Forge web trigger URL (ingest + attach)',
|
|
18
|
+
platformHint: "Manage Jenkins → Credentials → Add → Secret text; ID `qanalyzer-ingest-url`",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'qanalyzer-ingest-token',
|
|
22
|
+
description: 'Secret text credential: Bearer token from QAnalyzer configure page',
|
|
23
|
+
platformHint: "Manage Jenkins → Credentials → Add → Secret text; ID `qanalyzer-ingest-token`",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
function jenkinsVariables() {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
name: 'JIRA_PROJECT_KEY',
|
|
31
|
+
description: 'Jira project key allowlisted in QAnalyzer (set in Jenkinsfile environment)',
|
|
32
|
+
platformHint: 'pipeline environment { JIRA_PROJECT_KEY = \'…\' } or Jenkins folder property',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
function renderJenkinsReporter(ctx) {
|
|
37
|
+
(0, reporter_1.assertReporterFramework)(ctx);
|
|
38
|
+
const label = (0, reporter_1.reporterFrameworkLabel)(ctx);
|
|
39
|
+
const pkg = (0, reporter_1.reporterPackageName)(ctx);
|
|
40
|
+
const configHint = (0, reporter_1.reporterConfigHint)(ctx);
|
|
41
|
+
const content = `// QAnalyzer fragment — ${label} ${pkg} reporter path
|
|
42
|
+
// Requires ${pkg} in package.json and ${configHint}
|
|
43
|
+
pipeline {
|
|
44
|
+
agent any
|
|
45
|
+
environment {
|
|
46
|
+
JIRA_PROJECT_KEY = '${ctx.projectKey}'
|
|
47
|
+
QANALYZER_MODE = 'ingest'
|
|
48
|
+
QANALYZER_PROJECT_KEY = '${ctx.projectKey}'
|
|
49
|
+
}
|
|
50
|
+
stages {
|
|
51
|
+
stage('Test') {
|
|
52
|
+
steps {
|
|
53
|
+
withCredentials([
|
|
54
|
+
string(credentialsId: 'qanalyzer-ingest-url', variable: 'QANALYZER_INGEST_URL'),
|
|
55
|
+
string(credentialsId: 'qanalyzer-ingest-token', variable: 'QANALYZER_INGEST_TOKEN'),
|
|
56
|
+
]) {
|
|
57
|
+
sh 'npm ci'
|
|
58
|
+
${jenkinsShLines(' ', ctx, (0, reporter_1.frameworkReporterRun)(ctx))}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
return {
|
|
66
|
+
platform: 'jenkins',
|
|
67
|
+
framework: ctx.framework,
|
|
68
|
+
ingestPath: 'reporter',
|
|
69
|
+
filename: 'Jenkinsfile',
|
|
70
|
+
content,
|
|
71
|
+
secretsSetup: jenkinsSecrets(),
|
|
72
|
+
variablesSetup: [...jenkinsVariables(), ...(0, reporter_1.planCiVariableHints)(), ...(0, reporter_1.versionTagCiVariableHints)()],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Jenkins — upload path or @qanalyzer/forge-vitest / @qanalyzer/forge-jest reporter path.
|
|
77
|
+
*/
|
|
78
|
+
function renderJenkinsUpload(ctx) {
|
|
79
|
+
if (ctx.ingestPath === 'reporter') {
|
|
80
|
+
return renderJenkinsReporter(ctx);
|
|
81
|
+
}
|
|
82
|
+
if (ctx.ingestPath !== 'upload') {
|
|
83
|
+
throw new types_1.UnsupportedVariantError(ctx.platform, ctx.framework, ctx.ingestPath);
|
|
84
|
+
}
|
|
85
|
+
const testCmd = (0, upload_1.frameworkTestCommand)(ctx);
|
|
86
|
+
const label = (0, upload_1.frameworkLabel)(ctx);
|
|
87
|
+
const uploadBlock = (0, upload_1.indentUploadCli)(ctx, 10);
|
|
88
|
+
const content = `// QAnalyzer fragment — ${label} upload path — merge into your Jenkinsfile
|
|
89
|
+
pipeline {
|
|
90
|
+
agent any
|
|
91
|
+
environment {
|
|
92
|
+
JIRA_PROJECT_KEY = '${ctx.projectKey}'
|
|
93
|
+
}
|
|
94
|
+
stages {
|
|
95
|
+
stage('Test') {
|
|
96
|
+
steps {
|
|
97
|
+
sh 'npm ci'
|
|
98
|
+
${jenkinsShLines(' ', ctx, testCmd)}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
post {
|
|
103
|
+
always {
|
|
104
|
+
withCredentials([
|
|
105
|
+
string(credentialsId: 'qanalyzer-ingest-url', variable: 'QANALYZER_INGEST_URL'),
|
|
106
|
+
string(credentialsId: 'qanalyzer-ingest-token', variable: 'QANALYZER_INGEST_TOKEN'),
|
|
107
|
+
]) {
|
|
108
|
+
sh '''
|
|
109
|
+
${uploadBlock}
|
|
110
|
+
'''
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
return {
|
|
117
|
+
platform: 'jenkins',
|
|
118
|
+
framework: ctx.framework,
|
|
119
|
+
ingestPath: 'upload',
|
|
120
|
+
filename: 'Jenkinsfile',
|
|
121
|
+
content,
|
|
122
|
+
secretsSetup: jenkinsSecrets(),
|
|
123
|
+
variablesSetup: [...jenkinsVariables(), ...(0, reporter_1.planCiVariableHints)(), ...(0, reporter_1.versionTagCiVariableHints)()],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type CiPlatform = 'github' | 'gitlab' | 'azure-devops' | 'jenkins' | 'bitbucket';
|
|
2
|
+
export type CiFramework = 'vitest' | 'jest' | 'mocha' | 'cucumberjs' | 'cypress' | 'playwright' | 'wdio';
|
|
3
|
+
export type CiIngestPath = 'upload' | 'reporter';
|
|
4
|
+
export interface CiTemplateContext {
|
|
5
|
+
platform: CiPlatform;
|
|
6
|
+
framework: CiFramework;
|
|
7
|
+
ingestPath: CiIngestPath;
|
|
8
|
+
/** Pre-filled from Forge site config / user selection */
|
|
9
|
+
projectKey: string;
|
|
10
|
+
/** From site_config — never inlined as a token; YAML uses secret placeholders */
|
|
11
|
+
ingestUrl?: string;
|
|
12
|
+
ingestUrlSecret: string;
|
|
13
|
+
ingestTokenSecret: string;
|
|
14
|
+
nodeVersion?: string;
|
|
15
|
+
reportFile?: string;
|
|
16
|
+
/** Platform-specific expression for --project (may use vars.* or literal) */
|
|
17
|
+
projectKeyExpr?: string;
|
|
18
|
+
launchNameExpr?: string;
|
|
19
|
+
/** Platform expression for QANALYZER_INGEST_URL env value */
|
|
20
|
+
ingestUrlExpr?: string;
|
|
21
|
+
/** Platform expression for QANALYZER_INGEST_TOKEN env value */
|
|
22
|
+
ingestTokenExpr?: string;
|
|
23
|
+
/** Platform “always run” guard (e.g. if: always()) */
|
|
24
|
+
alwaysGuard?: string;
|
|
25
|
+
includeTestStep?: boolean;
|
|
26
|
+
includeUploadStep?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface CiSecretHint {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
platformHint: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CiVariableHint {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
platformHint: string;
|
|
37
|
+
}
|
|
38
|
+
export interface CiTemplateResult {
|
|
39
|
+
platform: CiPlatform;
|
|
40
|
+
framework: CiFramework;
|
|
41
|
+
ingestPath: CiIngestPath;
|
|
42
|
+
filename: string;
|
|
43
|
+
content: string;
|
|
44
|
+
secretsSetup: CiSecretHint[];
|
|
45
|
+
variablesSetup: CiVariableHint[];
|
|
46
|
+
}
|
|
47
|
+
export interface CiTemplateVariant {
|
|
48
|
+
platform: CiPlatform;
|
|
49
|
+
framework: CiFramework;
|
|
50
|
+
ingestPath: CiIngestPath;
|
|
51
|
+
}
|
|
52
|
+
export type CiTemplatePartial = Partial<CiTemplateContext> & Pick<CiTemplateContext, 'platform' | 'framework' | 'ingestPath' | 'projectKey'>;
|
|
53
|
+
export declare class UnsupportedVariantError extends Error {
|
|
54
|
+
readonly platform: CiPlatform;
|
|
55
|
+
readonly framework: CiFramework;
|
|
56
|
+
readonly ingestPath: CiIngestPath;
|
|
57
|
+
constructor(platform: CiPlatform, framework: CiFramework, ingestPath: CiIngestPath);
|
|
58
|
+
}
|
package/dist/ci/types.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UnsupportedVariantError = void 0;
|
|
4
|
+
class UnsupportedVariantError extends Error {
|
|
5
|
+
platform;
|
|
6
|
+
framework;
|
|
7
|
+
ingestPath;
|
|
8
|
+
constructor(platform, framework, ingestPath) {
|
|
9
|
+
super(`Unsupported CI template variant: ${platform} / ${framework} / ${ingestPath}`);
|
|
10
|
+
this.name = 'UnsupportedVariantError';
|
|
11
|
+
this.platform = platform;
|
|
12
|
+
this.framework = framework;
|
|
13
|
+
this.ingestPath = ingestPath;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.UnsupportedVariantError = UnsupportedVariantError;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { LoggerInterface } from '../utils';
|
|
2
|
+
export type AttachClientOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Shared Forge CI webtrigger URL (`QANALYZER_INGEST_URL`).
|
|
5
|
+
* Legacy `QANALYZER_ATTACH_URL` is accepted as a fallback alias.
|
|
6
|
+
*/
|
|
7
|
+
url?: string;
|
|
8
|
+
/** Same Bearer token as ingest (`QANALYZER_INGEST_TOKEN`). */
|
|
9
|
+
token?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** Max decoded file bytes (default 3_000_000). */
|
|
12
|
+
maxBytes?: number;
|
|
13
|
+
logger?: LoggerInterface;
|
|
14
|
+
};
|
|
15
|
+
export type AttachUploadInput = {
|
|
16
|
+
projectKey: string;
|
|
17
|
+
issueKey: string;
|
|
18
|
+
fileName: string;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
content: Buffer | Uint8Array | string;
|
|
21
|
+
};
|
|
22
|
+
export type AttachUploadResult = {
|
|
23
|
+
issueKey: string;
|
|
24
|
+
/**
|
|
25
|
+
* Jira attachment id when the proxy returns it. Static Forge webtriggers only echo
|
|
26
|
+
* `{"ok":true}`, so callers should treat this as optional and fall back to issue+filename.
|
|
27
|
+
*/
|
|
28
|
+
id?: string;
|
|
29
|
+
filename: string;
|
|
30
|
+
mimeType: string;
|
|
31
|
+
size: number;
|
|
32
|
+
};
|
|
33
|
+
export declare const DEFAULT_MAX_ATTACH_BYTES = 3000000;
|
|
34
|
+
/** Append `action=attach` so the shared ingest webtrigger dispatches correctly. */
|
|
35
|
+
export declare function withAttachAction(url: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Client for Phase 3 Forge attach proxy → Jira Attachment API.
|
|
38
|
+
* Uses the shared ingest webtrigger URL; sends JSON + base64 (not FR41).
|
|
39
|
+
*/
|
|
40
|
+
export declare class AttachClient {
|
|
41
|
+
private readonly url?;
|
|
42
|
+
private readonly token?;
|
|
43
|
+
private readonly timeoutMs;
|
|
44
|
+
private readonly maxBytes;
|
|
45
|
+
private readonly logger?;
|
|
46
|
+
constructor(options?: AttachClientOptions);
|
|
47
|
+
upload(input: AttachUploadInput): Promise<AttachUploadResult>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AttachClient = exports.DEFAULT_MAX_ATTACH_BYTES = void 0;
|
|
4
|
+
exports.withAttachAction = withAttachAction;
|
|
5
|
+
const env_enum_1 = require("../env/env-enum");
|
|
6
|
+
exports.DEFAULT_MAX_ATTACH_BYTES = 3_000_000;
|
|
7
|
+
function toBuffer(content) {
|
|
8
|
+
if (typeof content === 'string') {
|
|
9
|
+
return Buffer.from(content, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
if (Buffer.isBuffer(content)) {
|
|
12
|
+
return content;
|
|
13
|
+
}
|
|
14
|
+
return Buffer.from(content);
|
|
15
|
+
}
|
|
16
|
+
function readEnv(name) {
|
|
17
|
+
const value = process.env[name];
|
|
18
|
+
return value === '' ? undefined : value;
|
|
19
|
+
}
|
|
20
|
+
/** Append `action=attach` so the shared ingest webtrigger dispatches correctly. */
|
|
21
|
+
function withAttachAction(url) {
|
|
22
|
+
if (/[?&]action=/i.test(url)) {
|
|
23
|
+
return url;
|
|
24
|
+
}
|
|
25
|
+
return `${url}${url.includes('?') ? '&' : '?'}action=attach`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Client for Phase 3 Forge attach proxy → Jira Attachment API.
|
|
29
|
+
* Uses the shared ingest webtrigger URL; sends JSON + base64 (not FR41).
|
|
30
|
+
*/
|
|
31
|
+
class AttachClient {
|
|
32
|
+
url;
|
|
33
|
+
token;
|
|
34
|
+
timeoutMs;
|
|
35
|
+
maxBytes;
|
|
36
|
+
logger;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.url =
|
|
39
|
+
options.url ??
|
|
40
|
+
readEnv('QANALYZER_ATTACH_URL') ??
|
|
41
|
+
readEnv(env_enum_1.EnvIngestEnum.url);
|
|
42
|
+
this.token = options.token ?? readEnv(env_enum_1.EnvIngestEnum.token);
|
|
43
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
44
|
+
this.maxBytes = options.maxBytes ?? exports.DEFAULT_MAX_ATTACH_BYTES;
|
|
45
|
+
this.logger = options.logger;
|
|
46
|
+
}
|
|
47
|
+
async upload(input) {
|
|
48
|
+
if (!this.url) {
|
|
49
|
+
throw new Error('attach.url (or QANALYZER_INGEST_URL) is required to upload attachments');
|
|
50
|
+
}
|
|
51
|
+
if (!this.token) {
|
|
52
|
+
throw new Error('attach.token (or QANALYZER_INGEST_TOKEN) is required to upload attachments');
|
|
53
|
+
}
|
|
54
|
+
const content = toBuffer(input.content);
|
|
55
|
+
if (content.length > this.maxBytes) {
|
|
56
|
+
throw new Error(`Attachment is ${content.length} bytes; max allowed is ${this.maxBytes} bytes`);
|
|
57
|
+
}
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
60
|
+
const endpoint = withAttachAction(this.url);
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(endpoint, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
Authorization: `Bearer ${this.token}`,
|
|
67
|
+
'X-QAnalyzer-Action': 'attach',
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
projectKey: input.projectKey,
|
|
71
|
+
issueKey: input.issueKey,
|
|
72
|
+
fileName: input.fileName,
|
|
73
|
+
mimeType: input.mimeType,
|
|
74
|
+
contentBase64: content.toString('base64'),
|
|
75
|
+
}),
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
const text = await response.text();
|
|
79
|
+
let body = text;
|
|
80
|
+
try {
|
|
81
|
+
body = text ? JSON.parse(text) : null;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// keep raw
|
|
85
|
+
}
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const errMsg = typeof body === 'object' &&
|
|
88
|
+
body &&
|
|
89
|
+
'error' in body &&
|
|
90
|
+
typeof body.error === 'string'
|
|
91
|
+
? body.error
|
|
92
|
+
: typeof body === 'string'
|
|
93
|
+
? body
|
|
94
|
+
: JSON.stringify(body);
|
|
95
|
+
throw new Error(`Forge attach failed with HTTP ${response.status}: ${errMsg}`);
|
|
96
|
+
}
|
|
97
|
+
const ok = body;
|
|
98
|
+
// Static Forge webtrigger success body is fixed `{"ok":true}` — no attachment id.
|
|
99
|
+
const id = ok?.id != null && String(ok.id) !== '' ? String(ok.id) : undefined;
|
|
100
|
+
this.logger?.log(id
|
|
101
|
+
? `Attachment uploaded to ${ok.issueKey ?? input.issueKey} id=${id}`
|
|
102
|
+
: `Attachment uploaded to ${input.issueKey} (static ok)`);
|
|
103
|
+
return {
|
|
104
|
+
issueKey: ok?.issueKey ?? input.issueKey,
|
|
105
|
+
id,
|
|
106
|
+
filename: ok?.filename ?? input.fileName,
|
|
107
|
+
mimeType: ok?.mimeType ?? input.mimeType,
|
|
108
|
+
size: typeof ok?.size === 'number' ? ok.size : content.length,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
113
|
+
throw new Error(`Forge attach timed out after ${this.timeoutMs}ms`);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.AttachClient = AttachClient;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MAX_ATTACH_BYTES = exports.AttachClient = exports.IngestClient = void 0;
|
|
4
|
+
var ingest_client_1 = require("./ingest-client");
|
|
5
|
+
Object.defineProperty(exports, "IngestClient", { enumerable: true, get: function () { return ingest_client_1.IngestClient; } });
|
|
6
|
+
var attach_client_1 = require("./attach-client");
|
|
7
|
+
Object.defineProperty(exports, "AttachClient", { enumerable: true, get: function () { return attach_client_1.AttachClient; } });
|
|
8
|
+
Object.defineProperty(exports, "DEFAULT_MAX_ATTACH_BYTES", { enumerable: true, get: function () { return attach_client_1.DEFAULT_MAX_ATTACH_BYTES; } });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IngestOptionsType } from '../config';
|
|
2
|
+
import type { IngestPayload } from '../models';
|
|
3
|
+
import type { LoggerInterface } from '../utils';
|
|
4
|
+
export type IngestClientOptions = IngestOptionsType & {
|
|
5
|
+
logger?: LoggerInterface;
|
|
6
|
+
};
|
|
7
|
+
export type IngestResponse = {
|
|
8
|
+
status: number;
|
|
9
|
+
body: unknown;
|
|
10
|
+
};
|
|
11
|
+
export declare class IngestClient {
|
|
12
|
+
private readonly url?;
|
|
13
|
+
private readonly token?;
|
|
14
|
+
private readonly timeoutMs;
|
|
15
|
+
private readonly maxPayloadBytes;
|
|
16
|
+
private readonly logger?;
|
|
17
|
+
constructor(options?: IngestClientOptions);
|
|
18
|
+
send(payload: IngestPayload): Promise<IngestResponse>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IngestClient = void 0;
|
|
4
|
+
const models_1 = require("../models");
|
|
5
|
+
class IngestClient {
|
|
6
|
+
url;
|
|
7
|
+
token;
|
|
8
|
+
timeoutMs;
|
|
9
|
+
maxPayloadBytes;
|
|
10
|
+
logger;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.url = options.url;
|
|
13
|
+
this.token = options.token;
|
|
14
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
15
|
+
this.maxPayloadBytes = options.maxPayloadBytes ?? 4_500_000;
|
|
16
|
+
this.logger = options.logger;
|
|
17
|
+
}
|
|
18
|
+
async send(payload) {
|
|
19
|
+
if (!this.url) {
|
|
20
|
+
throw new Error('ingest.url (or QANALYZER_INGEST_URL) is required in ingest mode');
|
|
21
|
+
}
|
|
22
|
+
if (!this.token) {
|
|
23
|
+
throw new Error('ingest.token (or QANALYZER_INGEST_TOKEN) is required in ingest mode');
|
|
24
|
+
}
|
|
25
|
+
const bytes = (0, models_1.estimatePayloadBytes)(payload);
|
|
26
|
+
if (bytes > this.maxPayloadBytes) {
|
|
27
|
+
throw new Error(`Ingest payload is ${bytes} bytes; max allowed is ${this.maxPayloadBytes} bytes`);
|
|
28
|
+
}
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(this.url, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
Authorization: `Bearer ${this.token}`,
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify(payload),
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
let body = text;
|
|
43
|
+
try {
|
|
44
|
+
body = text ? JSON.parse(text) : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// keep raw text
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Forge ingest failed with HTTP ${response.status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
51
|
+
}
|
|
52
|
+
this.logger?.log(`Ingest accepted (HTTP ${response.status})`);
|
|
53
|
+
return { status: response.status, body };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
57
|
+
throw new Error(`Forge ingest timed out after ${this.timeoutMs}ms`);
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.IngestClient = IngestClient;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConfigType } from './config-type';
|
|
2
|
+
export declare class ConfigLoader {
|
|
3
|
+
private readonly paths;
|
|
4
|
+
constructor(paths?: string[]);
|
|
5
|
+
private read;
|
|
6
|
+
load(): ConfigType | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function loadConfig(paths?: string[]): ConfigType | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigLoader = void 0;
|
|
4
|
+
exports.loadConfig = loadConfig;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const DEFAULT_PATHS = ['qanalyzer.config.json', '.qanalyzerc'];
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function assertConfigShape(json, fileLabel) {
|
|
12
|
+
if (!isRecord(json)) {
|
|
13
|
+
throw new Error(`Invalid config: "${fileLabel}" must be a JSON object`);
|
|
14
|
+
}
|
|
15
|
+
if (json.mode !== undefined && typeof json.mode !== 'string') {
|
|
16
|
+
throw new Error(`Invalid config: "mode" must be a string`);
|
|
17
|
+
}
|
|
18
|
+
if (json.projectKey !== undefined && typeof json.projectKey !== 'string') {
|
|
19
|
+
throw new Error(`Invalid config: "projectKey" must be a string`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class ConfigLoader {
|
|
23
|
+
paths;
|
|
24
|
+
constructor(paths = DEFAULT_PATHS) {
|
|
25
|
+
this.paths = paths;
|
|
26
|
+
}
|
|
27
|
+
read() {
|
|
28
|
+
for (const configPath of this.paths) {
|
|
29
|
+
const filePath = (0, path_1.join)(process.cwd(), configPath);
|
|
30
|
+
try {
|
|
31
|
+
return (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const code = error instanceof Error && 'code' in error
|
|
35
|
+
? String(error.code)
|
|
36
|
+
: '';
|
|
37
|
+
if (code !== 'ENOENT' && code !== 'EISDIR') {
|
|
38
|
+
throw new Error(`Cannot read config file "${configPath}"`, { cause: error });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
load() {
|
|
45
|
+
const data = this.read();
|
|
46
|
+
if (!data)
|
|
47
|
+
return null;
|
|
48
|
+
let json;
|
|
49
|
+
try {
|
|
50
|
+
json = JSON.parse(data);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error('Invalid config: file is not valid JSON', { cause: error });
|
|
54
|
+
}
|
|
55
|
+
assertConfigShape(json, 'root');
|
|
56
|
+
return json;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.ConfigLoader = ConfigLoader;
|
|
60
|
+
function loadConfig(paths) {
|
|
61
|
+
return new ConfigLoader(paths).load();
|
|
62
|
+
}
|