mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
ci: migrate kestra-devtools to npm
This commit is contained in:
32
.github/workflows/kestra-devtools-test.yml
vendored
32
.github/workflows/kestra-devtools-test.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: kestra-devtools test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- 'dev-tools/kestra-devtools/**'
|
||||
|
||||
env:
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: kestra-devtools tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Npm - install
|
||||
working-directory: 'dev-tools/kestra-devtools'
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
working-directory: 'dev-tools/kestra-devtools'
|
||||
run: npm run test
|
||||
|
||||
- name: Npm - Run build
|
||||
working-directory: 'dev-tools/kestra-devtools'
|
||||
run: npm run build
|
||||
4
.github/workflows/workflow-backend-test.yml
vendored
4
.github/workflows/workflow-backend-test.yml
vendored
@@ -64,9 +64,7 @@ jobs:
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
||||
run: |
|
||||
export KESTRA_PWD=$(pwd) && sh -c 'cd dev-tools/kestra-devtools && npm ci && npm run build && node dist/kestra-devtools-cli.cjs generateTestReportSummary --only-errors --ci $KESTRA_PWD' > report.md
|
||||
cat report.md
|
||||
run: npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors --ci $(pwd)
|
||||
|
||||
# report test
|
||||
- name: Test - Publish Test Results
|
||||
|
||||
4
dev-tools/kestra-devtools/.gitignore
vendored
4
dev-tools/kestra-devtools/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
.DS_Store
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @ts-check
|
||||
import eslint from "@eslint/js";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig(
|
||||
{
|
||||
ignores: ["dist/**", "coverage/**", "node_modules/**"],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
);
|
||||
7175
dev-tools/kestra-devtools/package-lock.json
generated
7175
dev-tools/kestra-devtools/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"name": "kestra-devtools-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "a CLI tool to run various dev tasks to build, test, release Kestra",
|
||||
"bin": {
|
||||
"my-cli": "dist/kestra-devtools-cli.cjs"
|
||||
},
|
||||
"main": "dist/kestra-devtools-cli.cjs",
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vitest --watch",
|
||||
"build": "vite build && tsc -p tsconfig.types.json",
|
||||
"test": "npm run lint && vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"prepare": "npm run build",
|
||||
"start": "node dist/kestra-devtools-cli.cjs",
|
||||
"link": "npm link",
|
||||
"unlink": "npm unlink -g my-cli || true"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.43.0",
|
||||
"@typescript-eslint/parser": "^8.43.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"vite": "^7.1.5",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"octokit": "^5.0.3"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Octokit} from "octokit";
|
||||
|
||||
export async function commentPR(githubToken: string, owner: string, repo: string, prNumber: number, content: string){
|
||||
const octokit = new Octokit({ auth: githubToken });
|
||||
|
||||
await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number:prNumber,
|
||||
body: content,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import core from '@actions/core';
|
||||
import {context} from '@actions/github';
|
||||
import {strict as assert} from 'assert';
|
||||
|
||||
export function getPRContext():{token: string, owner: string, repo: string, prNumber: number}{
|
||||
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN') || process.env.GITHUB_TOKEN;
|
||||
|
||||
assert.ok(GITHUB_TOKEN, "GITHUB_TOKEN is mandatory");
|
||||
assert.ok(context.issue);
|
||||
assert.ok(context.issue.owner);
|
||||
assert.ok(context.issue.repo);
|
||||
assert.ok(context.issue.number);
|
||||
|
||||
return {token: GITHUB_TOKEN, owner: context.repo.owner, repo: context.repo.repo, prNumber: context.issue.number }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { main } from "./kestra-devtools-cli";
|
||||
|
||||
describe("cli tests", () => {
|
||||
it("prints hello with default", async () => {
|
||||
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await main(["node", "cli"]);
|
||||
expect(spy).toHaveBeenCalledWith("Hello, world!");
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("prints hello with name", async () => {
|
||||
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await main(["node", "cli", "Roman"]);
|
||||
expect(spy).toHaveBeenCalledWith("Hello, Roman!");
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
// Simple CLI entry point.
|
||||
// Built to dist/kestra-devtools-cli.cjs with a shebang so it can be executed directly.
|
||||
|
||||
import { getWorkingDir } from "./utilities/working-dir";
|
||||
import {exportTestReportSummary} from "./tests-reporting/export-test-report-summary";
|
||||
import {getPRContext} from "./github-context";
|
||||
|
||||
function parseArgs(argv: string[]) {
|
||||
// argv[0] = node, argv[1] = script, rest are args
|
||||
const args = argv.slice(2);
|
||||
const flags: Record<string, string | boolean> = {};
|
||||
const positionals: string[] = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a.startsWith("--")) {
|
||||
const [k, v] = a.slice(2).split("=");
|
||||
flags[k] = v ?? true;
|
||||
} else if (a.startsWith("-") && a.length > 1) {
|
||||
const letters = a.slice(1).split("");
|
||||
letters.forEach((l) => (flags[l] = true));
|
||||
} else {
|
||||
positionals.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
return { flags, positionals };
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv) {
|
||||
const { flags, positionals } = parseArgs(argv);
|
||||
|
||||
if (flags.h || flags.help) {
|
||||
console.log(`kestra-devtools-cli
|
||||
|
||||
Usage:
|
||||
kestra-devtools-cli [options] [name]
|
||||
|
||||
Options:
|
||||
-h, --help Show help
|
||||
-v, --version Show version
|
||||
|
||||
Examples:
|
||||
kestra-devtools-cli generateTestReportSummary /Users/roman/Documents/git-repos/kestra --only-errors
|
||||
|
||||
`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (positionals[0] === "generateTestReportSummary") {
|
||||
const dirArg = positionals[1];
|
||||
if (!dirArg) {
|
||||
console.error(
|
||||
"Error: missing working directory argument.\nUsage: kestra-devtools-cli generateTestReportSummary <absolute-path>",
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const ci = Boolean(flags["ci"]);
|
||||
const workingDir = getWorkingDir(dirArg);
|
||||
const summary = await exportTestReportSummary(workingDir, {
|
||||
onlyErrors: Boolean(flags["only-errors"]),
|
||||
githubContext: ci ? getPRContext() : undefined
|
||||
});
|
||||
// Print to stdout so it can be piped in CI or viewed in terminal
|
||||
console.log(summary);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (flags.v || flags.version) {
|
||||
// package.json is not bundled by default; prefer env-injected version if needed.
|
||||
console.log("kestra-devtools-cli v0.1.0");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const name = positionals[0] ?? "world";
|
||||
console.log(`Hello, ${name}!`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If executed directly, run main()
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main()
|
||||
.then((code) => process.exit(code))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import {commentPR} from "../github-api";
|
||||
import {WorkingDir} from "../utilities/working-dir";
|
||||
import {generateTestReportSummary} from "./generate-test-report-summary";
|
||||
import {strict as assert} from 'assert';
|
||||
|
||||
export async function exportTestReportSummary(workingDir: WorkingDir, options?: {
|
||||
onlyErrors?: boolean,
|
||||
githubContext?: { token: string, owner: string, repo: string, prNumber: number }
|
||||
}) {
|
||||
const report = await generateTestReportSummary(workingDir, {onlyErrors: options?.onlyErrors})
|
||||
if (options?.githubContext) {
|
||||
assert.ok(options.githubContext.token, "github token is mandatory");
|
||||
assert.ok(options.githubContext.owner);
|
||||
assert.ok(options.githubContext.repo);
|
||||
assert.ok(options.githubContext.prNumber);
|
||||
|
||||
await commentPR(options.githubContext.token, options.githubContext.owner, options.githubContext.repo, options.githubContext.prNumber, report);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getJavaProjectNameFromBuildAbsolutePath } from "./file-path-utils";
|
||||
|
||||
describe("test getJavaProjectNameFromBuildAbsolutePath", () => {
|
||||
it("should work for Kestra modules paths", async () => {
|
||||
expect(
|
||||
getJavaProjectNameFromBuildAbsolutePath(
|
||||
"/Users/roman/Documents/git-repos/kestra/core/build/test-results/junit/TEST-io.kestra.core.validations.ScheduleValidationTest.xml",
|
||||
),
|
||||
).toEqual("core");
|
||||
expect(
|
||||
getJavaProjectNameFromBuildAbsolutePath(
|
||||
"/kestra/runner-memory/build/test-results/junit/open-test-report.xml",
|
||||
),
|
||||
).toEqual("runner-memory");
|
||||
expect(
|
||||
getJavaProjectNameFromBuildAbsolutePath(
|
||||
"/kestra-ee/executor/build/test-results/junit/open-test-report.xml",
|
||||
),
|
||||
).toEqual("executor");
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
export function getJavaProjectNameFromBuildAbsolutePath(absoluteFilePath: string): string {
|
||||
const parts = absoluteFilePath.split("/");
|
||||
const buildIndex = parts.lastIndexOf("build");
|
||||
if (buildIndex > 0) {
|
||||
return parts[buildIndex - 1];
|
||||
}
|
||||
|
||||
// return full path if not handled
|
||||
return absoluteFilePath;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseJunitModuleReport } from "./parse-junit-module-report";
|
||||
|
||||
describe("parse-junit-report test", () => {
|
||||
it("parse OK for all tests success", async () => {
|
||||
const junitReport = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="io.kestra.core.validations.ScheduleValidationTest" tests="6" skipped="0" failures="0" errors="0" timestamp="2025-09-11T17:32:18.116Z" hostname="Romans-MacBook-Pro.local" time="0.202">
|
||||
<properties/>
|
||||
<testcase name="sundayDayOfTheWeekAlias()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="withSecondsValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="lateMaximumDelayValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="intervalValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="nicknameValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.203"/>
|
||||
<testcase name="cronValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<system-out><![CDATA[]]></system-out>
|
||||
<system-err><![CDATA[]]></system-err>
|
||||
</testsuite>
|
||||
`;
|
||||
|
||||
const res = parseJunitModuleReport(junitReport);
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(res.testsuites).toEqual([
|
||||
{
|
||||
name: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
errors: 0,
|
||||
failures: 0,
|
||||
skipped: 0,
|
||||
success: 6,
|
||||
tests: 6,
|
||||
status: "success",
|
||||
time: 0.202,
|
||||
testcases: [
|
||||
{
|
||||
name: "sundayDayOfTheWeekAlias()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "withSecondsValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "lateMaximumDelayValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "intervalValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "nicknameValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.203,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "cronValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("parse OK for test in error", async () => {
|
||||
const junitReport = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="io.kestra.core.validations.ScheduleValidationTest" tests="1" skipped="0" failures="1" errors="0" timestamp="2025-09-11T17:56:02.292Z" hostname="Romans-MacBook-Pro.local" time="0.265">
|
||||
<properties/>
|
||||
<testcase name="intervalValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.043">
|
||||
<failure message="java.lang.RuntimeException: I failed and this is my log" type="java.lang.RuntimeException">java.lang.RuntimeException: I failed and this is my log
|
||||
\tat io.kestra.core.validations.ScheduleValidationTest.intervalValidation(ScheduleValidationTest.java:93)
|
||||
\tat java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
\tat io.micronaut.test.extensions.junit5.MicronautJunit5Extension$2.proceed(MicronautJunit5Extension.java:142)
|
||||
\tat io.micronaut.test.extensions.AbstractMicronautExtension.interceptEach(AbstractMicronautExtension.java:162)
|
||||
\tat io.micronaut.test.extensions.AbstractMicronautExtension.interceptTest(AbstractMicronautExtension.java:119)
|
||||
\tat io.micronaut.test.extensions.junit5.MicronautJunit5Extension.interceptTestMethod(MicronautJunit5Extension.java:129)
|
||||
\tat java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
|
||||
\tat java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
|
||||
\tat java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
|
||||
\tat java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
|
||||
\tat java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
|
||||
</failure>
|
||||
</testcase>
|
||||
<system-out><![CDATA[]]></system-out>
|
||||
<system-err><![CDATA[]]></system-err>
|
||||
</testsuite>
|
||||
`;
|
||||
|
||||
const res = parseJunitModuleReport(junitReport);
|
||||
|
||||
expect(res.testsuites).length(1);
|
||||
expect(res.testsuites[0].testcases).length(1);
|
||||
expect(res.testsuites[0].testcases[0].status).equal("failed");
|
||||
expect(res.testsuites[0].testcases[0].message).contain("I failed and this is my log");
|
||||
expect(res.testsuites[0].testcases[0].details).contain("I failed and this is my log");
|
||||
expect(res.testsuites[0].testcases[0].details).contain("ForkJoinWorkerThread");
|
||||
});
|
||||
});
|
||||
@@ -1,241 +0,0 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
|
||||
export type JUnitModuleReport = {
|
||||
suites: number;
|
||||
tests: number;
|
||||
failures: number;
|
||||
errors: number;
|
||||
skipped: number;
|
||||
success: number;
|
||||
status: "success" | "failed" | "error" | "skipped";
|
||||
time: number; // total duration in seconds
|
||||
testsuites: Array<JunitTestSuite>;
|
||||
};
|
||||
|
||||
export interface JunitTestSuite {
|
||||
name?: string;
|
||||
tests: number;
|
||||
failures: number;
|
||||
errors: number;
|
||||
skipped: number;
|
||||
success: number;
|
||||
status: "success" | "failed" | "error" | "skipped";
|
||||
time: number;
|
||||
testcases: Array<JunitTestCase>;
|
||||
}
|
||||
|
||||
export interface JunitTestCase {
|
||||
classname?: string;
|
||||
name: string;
|
||||
time?: number;
|
||||
status: "success" | "failed" | "error" | "skipped";
|
||||
message?: string;
|
||||
type?: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
// for more info on the Junit test report format = https://github.com/testmoapp/junitxml
|
||||
export function parseJunitModuleReport(xml: string): JUnitModuleReport {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "",
|
||||
allowBooleanAttributes: true,
|
||||
parseAttributeValue: true,
|
||||
trimValues: false,
|
||||
});
|
||||
|
||||
const obj = parser.parse(xml);
|
||||
|
||||
// JUnit can be either <testsuites> or a single <testsuite>
|
||||
const rawSuites = obj?.testsuites?.testsuite ?? obj?.testsuite ?? [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const suites = toArray<any>(rawSuites);
|
||||
|
||||
const report: JUnitModuleReport = {
|
||||
suites: suites.length,
|
||||
tests: 0,
|
||||
failures: 0,
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
success: 0,
|
||||
status: "success",
|
||||
time: 0,
|
||||
testsuites: [],
|
||||
};
|
||||
|
||||
for (const s of suites) {
|
||||
const name: string | undefined = s.name;
|
||||
|
||||
// Attributes may exist on the suite OR we may need to infer from testcases
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const testcases = toArray<any>(s.testcase ?? []);
|
||||
|
||||
const suiteCounts = {
|
||||
tests: numeric(s.tests, testcases.length),
|
||||
failures: numeric(s.failures, 0),
|
||||
errors: numeric(s.errors, 0),
|
||||
skipped: numeric(s.skipped, 0),
|
||||
time: numeric(s.time, sum(testcases.map((tc) => numeric(tc.time, 0)))),
|
||||
};
|
||||
|
||||
// If suite attributes missing, infer from testcases
|
||||
if (
|
||||
!isFiniteNumber(s.failures) ||
|
||||
!isFiniteNumber(s.errors) ||
|
||||
!isFiniteNumber(s.skipped)
|
||||
) {
|
||||
let f = 0,
|
||||
e = 0,
|
||||
sk = 0;
|
||||
for (const tc of testcases) {
|
||||
if (hasKey(tc, "failed")) f += toArray(tc.failed).length;
|
||||
if (hasKey(tc, "error")) e += toArray(tc.error).length;
|
||||
if (hasKey(tc, "skipped")) sk += toArray(tc.skipped).length || 1; // some producers put empty <skipped/>
|
||||
}
|
||||
if (!isFiniteNumber(suiteCounts.failures)) suiteCounts.failures = f;
|
||||
if (!isFiniteNumber(suiteCounts.errors)) suiteCounts.errors = e;
|
||||
if (!isFiniteNumber(suiteCounts.skipped)) suiteCounts.skipped = sk;
|
||||
}
|
||||
|
||||
const successCount =
|
||||
suiteCounts.tests - suiteCounts.errors - suiteCounts.failures - suiteCounts.skipped;
|
||||
|
||||
let suiteStatus: "success" | "failed" | "error" | "skipped" = "success";
|
||||
if (suiteCounts.skipped === suiteCounts.tests) {
|
||||
suiteStatus = "skipped";
|
||||
} else if (suiteCounts.errors > 0) {
|
||||
suiteStatus = "error";
|
||||
} else if (suiteCounts.failures > 0) {
|
||||
suiteStatus = "failed";
|
||||
}
|
||||
|
||||
const suiteDetail: JunitTestSuite = {
|
||||
name,
|
||||
tests: suiteCounts.tests,
|
||||
failures: suiteCounts.failures,
|
||||
errors: suiteCounts.errors,
|
||||
skipped: suiteCounts.skipped,
|
||||
success: successCount,
|
||||
status: suiteStatus,
|
||||
time: suiteCounts.time,
|
||||
testcases: [],
|
||||
};
|
||||
|
||||
// Collect failed tests and build suiteDetail.testcases
|
||||
for (const tc of testcases) {
|
||||
const classname: string | undefined = tc.classname;
|
||||
const nameTc: string = tc.name;
|
||||
const time: number | undefined = isFiniteNumber(tc.time) ? Number(tc.time) : undefined;
|
||||
|
||||
// Determine status
|
||||
if (tc.failure) {
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "failed",
|
||||
message: tc.failure.message,
|
||||
type: tc.failure.type,
|
||||
details: textContent(tc.failure),
|
||||
});
|
||||
} else if (tc.error) {
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "error",
|
||||
message: tc.error.message,
|
||||
type: tc.error.message.type,
|
||||
details: textContent(tc.error),
|
||||
});
|
||||
} else if (tc.skipped) {
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "skipped",
|
||||
message: tc.skipped.message,
|
||||
details: textContent(tc.skipped),
|
||||
});
|
||||
} else {
|
||||
// success test
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "success",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
report.tests += suiteCounts.tests;
|
||||
report.failures += suiteCounts.failures;
|
||||
report.errors += suiteCounts.errors;
|
||||
report.skipped += suiteCounts.skipped;
|
||||
report.success += suiteDetail.success;
|
||||
report.time += suiteCounts.time;
|
||||
|
||||
report.testsuites.push(suiteDetail);
|
||||
}
|
||||
|
||||
if (report.skipped === report.tests) {
|
||||
report.status = "skipped";
|
||||
} else if (report.errors > 0) {
|
||||
report.status = "error";
|
||||
} else if (report.failures > 0) {
|
||||
report.status = "failed";
|
||||
} else {
|
||||
report.status = "success";
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: parse a file from disk.
|
||||
*/
|
||||
export async function summarizeJunitReportFromFile(filePath: string): Promise<JUnitModuleReport> {
|
||||
const xml = await fs.readFile(filePath, "utf8");
|
||||
return parseJunitModuleReport(xml);
|
||||
}
|
||||
|
||||
// -------------------- helpers --------------------
|
||||
|
||||
function toArray<T>(v: T | T[] | undefined | null): T[] {
|
||||
if (v == null) return [];
|
||||
return Array.isArray(v) ? v : [v];
|
||||
}
|
||||
|
||||
function numeric<T>(value: T, fallback = 0): number {
|
||||
const n = Number(value as unknown);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function sum(nums: number[]): number {
|
||||
return nums.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
function isFiniteNumber(v: unknown): v is number {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
// Some producers put the text of <failed> / <error> inside `#text` or as the value itself.
|
||||
function textContent(node: unknown): string | undefined {
|
||||
if (node == null) return undefined;
|
||||
if (typeof node === "string") return node;
|
||||
if (isRecord(node) && typeof node["#text"] === "string") {
|
||||
return node["#text"] as string;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasKey<O>(obj: O, key: PropertyKey): key is keyof O {
|
||||
return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { summarizeJunitReport, TestReport } from "./summarize-junit-report";
|
||||
|
||||
describe("summarize-junit-report test", () => {
|
||||
const testReportsWithGreenTests: TestReport[] = [
|
||||
{
|
||||
projectName: "java-module-1",
|
||||
projectReport: {
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 0,
|
||||
success: 1,
|
||||
status: "success",
|
||||
tests: 1,
|
||||
time: 3,
|
||||
suites: 1,
|
||||
testsuites: [
|
||||
{
|
||||
name: "io.kestra.core.some.Test",
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 0,
|
||||
success: 1,
|
||||
status: "success",
|
||||
tests: 1,
|
||||
time: 3,
|
||||
testcases: [
|
||||
{
|
||||
name: "sundayDayOfTheWeekAlias()",
|
||||
classname: "io.kestra.core.some.Test",
|
||||
time: 3,
|
||||
status: "success",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
it("summarizeJunitReport for one green module", async () => {
|
||||
const res = summarizeJunitReport(testReportsWithGreenTests);
|
||||
|
||||
expect(res.hasErrors).equal(false);
|
||||
expect(res.markdownContent).contains("java-module-1");
|
||||
expect(res.markdownContent).contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).contains("io.kestra.core.some.Test");
|
||||
});
|
||||
|
||||
it("summarizeJunitReport for one green module should not print tests when onlyErrors:true", async () => {
|
||||
const res = summarizeJunitReport(testReportsWithGreenTests, { onlyErrors: true });
|
||||
|
||||
expect(res.hasErrors).equal(false);
|
||||
expect(res.markdownContent).contains("java-module-1");
|
||||
expect(res.markdownContent).not.contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).not.contains(
|
||||
"io.kestra.core.validations.ScheduleValidationTest",
|
||||
);
|
||||
});
|
||||
|
||||
const testReportWithFailedTests: TestReport[] = [
|
||||
{
|
||||
projectName: "java-module-1",
|
||||
projectReport: {
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 1,
|
||||
success: 1,
|
||||
status: "failed",
|
||||
tests: 2,
|
||||
time: 3,
|
||||
suites: 1,
|
||||
testsuites: [
|
||||
{
|
||||
name: "io.kestra.core.someother.Test2",
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 1,
|
||||
success: 1,
|
||||
status: "failed",
|
||||
tests: 2,
|
||||
time: 3,
|
||||
testcases: [
|
||||
{
|
||||
name: "sundayDayOfTheWeekAlias()",
|
||||
classname: "io.kestra.core.someother.Test2",
|
||||
time: 3,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "failingTest()",
|
||||
classname: "io.kestra.core.someother.Test2",
|
||||
time: 3,
|
||||
status: "failed",
|
||||
message: "java.lang.RuntimeException: I failed and this is my log",
|
||||
details: "this is the error logs details",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
it("summarizeJunitReport for failed tests should summarize all by default without details", async () => {
|
||||
const res = summarizeJunitReport(testReportWithFailedTests);
|
||||
|
||||
expect(res.hasErrors).equal(true);
|
||||
expect(res.markdownContent).contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).contains("failingTest()");
|
||||
expect(res.markdownContent).contains(
|
||||
"java.lang.RuntimeException: I failed and this is my log",
|
||||
);
|
||||
expect(res.markdownContent).not.contains("this is the error logs details");
|
||||
});
|
||||
it("summarizeJunitReport for failed tests should summarize only errors with details when onlyErrors:true", async () => {
|
||||
const res = summarizeJunitReport(testReportWithFailedTests, { onlyErrors: true });
|
||||
|
||||
expect(res.hasErrors).equal(true);
|
||||
expect(res.markdownContent).not.contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).contains("failingTest()");
|
||||
expect(res.markdownContent).contains(
|
||||
"java.lang.RuntimeException: I failed and this is my log",
|
||||
);
|
||||
expect(res.markdownContent).contains("this is the error logs details");
|
||||
});
|
||||
|
||||
|
||||
it("summarizeJunitReport should merge module reports", async () => {
|
||||
// given 1 report the module name should appear twice
|
||||
const res1 = summarizeJunitReport(testReportWithFailedTests, { onlyErrors: true });
|
||||
|
||||
expect(res1.hasErrors).equal(true);
|
||||
expect(res1.markdownContent).contain("java-module-1");
|
||||
expect((res1.markdownContent.match(/java-module-1/g) || []).length).toBe(2);
|
||||
|
||||
// given 2 reports for the same module, but for different tests
|
||||
const reports = [...testReportsWithGreenTests, ...testReportWithFailedTests]
|
||||
const res2 = summarizeJunitReport(reports, { onlyErrors: true });
|
||||
|
||||
expect(res2.hasErrors).equal(true);
|
||||
expect(res2.markdownContent).contain("java-module-1");
|
||||
|
||||
// it should not be duplicated
|
||||
expect((res2.markdownContent.match(/java-module-1/g) || []).length).toBe(2);
|
||||
});
|
||||
|
||||
it("summarizeJunitReport should print totals", async () => {
|
||||
// given 2 reports
|
||||
const reports = [...testReportsWithGreenTests, ...testReportWithFailedTests]
|
||||
const res = summarizeJunitReport(reports, { onlyErrors: true });
|
||||
|
||||
// it should contains added/merged totals
|
||||
expect(res.markdownContent).contain("tests: 3");
|
||||
expect(res.markdownContent).contain("failed: 1");
|
||||
expect(res.markdownContent).contain("success: 2");
|
||||
expect(res.markdownContent).contain("skipped: 0");
|
||||
});
|
||||
});
|
||||
@@ -1,196 +0,0 @@
|
||||
import { JUnitModuleReport } from "./parse-junit-module-report";
|
||||
|
||||
export type MarkdownString = string;
|
||||
|
||||
export interface TestReport {
|
||||
projectName: string;
|
||||
projectReport: JUnitModuleReport;
|
||||
}
|
||||
|
||||
export interface TestReportSummary {
|
||||
hasErrors: boolean;
|
||||
markdownContent: MarkdownString;
|
||||
}
|
||||
|
||||
export function summarizeJunitReport(
|
||||
testReports: TestReport[],
|
||||
options?: { onlyErrors: boolean },
|
||||
): TestReportSummary {
|
||||
const onlyErrors = options?.onlyErrors ?? false;
|
||||
|
||||
const testReportQuickSummaryRows: string[] = [];
|
||||
const testReportDetailsRows: string[] = [];
|
||||
const testReportErrorLogs: string[] = [];
|
||||
let hasErrors = false;
|
||||
|
||||
const mergedReports = mergeSameProjectReports(testReports);
|
||||
for (const report of mergedReports) {
|
||||
const project = report.projectName;
|
||||
const projectReport: JUnitModuleReport = report.projectReport;
|
||||
testReportQuickSummaryRows.push(
|
||||
`| ${escapePipe(report.projectName)} | ${escapePipe(mapStatusToEmoji(projectReport.status))} | ${escapePipe(projectReport.success)} | ${escapePipe(projectReport.skipped)} | ${projectReport.errors + projectReport.failures} |`,
|
||||
);
|
||||
|
||||
for (const testsuite of projectReport.testsuites) {
|
||||
for (const testcase of testsuite.testcases) {
|
||||
const name = testcase.name ?? "";
|
||||
const duration = safeNum(testcase.time);
|
||||
const failed = testcase.status === "failed" || testcase.status === "error";
|
||||
if (failed) hasErrors = true;
|
||||
if (onlyErrors) {
|
||||
// then only print errors, and details like logs
|
||||
if (failed) {
|
||||
const message = testcase.message ?? "";
|
||||
const details = testcase.details ? "\n\n" + testcase.details : "";
|
||||
|
||||
const errorSummary= `${escapePipe(project)} > ${escapePipe(testsuite.name)} > ${escapePipe(name)} ${mapStatusToEmoji(testcase.status)} in ${duration}`;
|
||||
testReportErrorLogs.push(
|
||||
`${spoilerBlock(errorSummary, codeBlock(message + details))}\n`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
testReportDetailsRows.push(
|
||||
`| ${escapePipe(project)} | ${escapePipe(testsuite.name)} | ${escapePipe(name)} | ${mapStatusToEmoji(testcase.status)} | ${duration} | ${escapePipe(truncate(testcase.message ?? "", 200))} |`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let markdownContent = "## Tests report quick summary:";
|
||||
const totalTests = testReports.map(r => r.projectReport.tests).reduce((a,b) => a+b);
|
||||
const totalSuccess = testReports.map(r => r.projectReport.success).reduce((a,b) => a+b);
|
||||
const totalSkipped = testReports.map(r => r.projectReport.skipped).reduce((a,b) => a+b);
|
||||
const totalErrors = testReports.map(r => r.projectReport.failures + r.projectReport.errors).reduce((a,b) => a+b);
|
||||
markdownContent = markdownContent + `\ntotals > tests: ${totalTests}, success: ${totalSuccess}, skipped: ${totalSkipped}, failed: ${totalErrors}\n`;
|
||||
markdownContent =
|
||||
markdownContent +
|
||||
`\n| Project | Status | Success | Skipped | Failed |\n|---|---|---|---|---|`;
|
||||
markdownContent = markdownContent + "\n" + [...testReportQuickSummaryRows].join("\n");
|
||||
if (testReportDetailsRows.length > 0) {
|
||||
markdownContent = markdownContent + "\n\n" + "## Tests report details:";
|
||||
const header = `| Project | Suite | Test | Status | Duration (s) | Message |\n|---|---|---|---|---:|---|`;
|
||||
markdownContent = markdownContent + "\n" + [header, ...testReportDetailsRows].join("\n");
|
||||
}
|
||||
if (testReportErrorLogs.length > 0) {
|
||||
markdownContent = markdownContent + "\n## Failed tests:";
|
||||
markdownContent = markdownContent + "\n" + [...testReportErrorLogs].join("\n");
|
||||
}
|
||||
|
||||
return { hasErrors, markdownContent };
|
||||
|
||||
// merge reports that share the same projectName by concatenating testsuites
|
||||
function mergeSameProjectReports(reports: TestReport[]): TestReport[] {
|
||||
const byProject = new Map<string, JUnitModuleReport>();
|
||||
|
||||
for (const r of reports) {
|
||||
const key = r.projectName;
|
||||
const existing = byProject.get(key);
|
||||
if (!existing) {
|
||||
// clone a shallow copy so we don't mutate the original
|
||||
const cloned: JUnitModuleReport = {
|
||||
...r.projectReport,
|
||||
testsuites: [...r.projectReport.testsuites],
|
||||
} as JUnitModuleReport;
|
||||
computeModuleAggregates(cloned);
|
||||
byProject.set(key, cloned);
|
||||
} else {
|
||||
// concatenate testsuites and recompute aggregates
|
||||
existing.testsuites = [...existing.testsuites, ...r.projectReport.testsuites];
|
||||
computeModuleAggregates(existing);
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild TestReport array
|
||||
return Array.from(byProject.entries()).map(([projectName, projectReport]) => ({
|
||||
projectName,
|
||||
projectReport,
|
||||
}));
|
||||
}
|
||||
|
||||
// recompute success/skip/error/failure counts and overall status from testcases
|
||||
function computeModuleAggregates(moduleReport: JUnitModuleReport): void {
|
||||
let success = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
let failures = 0;
|
||||
|
||||
for (const suite of moduleReport.testsuites) {
|
||||
for (const tc of suite.testcases) {
|
||||
switch (tc.status) {
|
||||
case "success":
|
||||
success++; break;
|
||||
case "skipped":
|
||||
skipped++; break;
|
||||
case "error":
|
||||
errors++; break;
|
||||
case "failed":
|
||||
failures++; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = success + skipped + errors + failures;
|
||||
// update known aggregate fields if present on the type
|
||||
moduleReport.success = success;
|
||||
moduleReport.skipped = skipped;
|
||||
moduleReport.errors = errors;
|
||||
moduleReport.failures = failures;
|
||||
if ("tests" in moduleReport) {
|
||||
moduleReport.tests = total;
|
||||
}
|
||||
|
||||
// status rules: all skipped => skipped; any error => error; any failed => failed; else success
|
||||
let status: "success" | "failed" | "error" | "skipped";
|
||||
if (total > 0 && skipped === total) status = "skipped";
|
||||
else if (errors > 0) status = "error";
|
||||
else if (failures > 0) status = "failed";
|
||||
else status = "success";
|
||||
moduleReport.status = status;
|
||||
}
|
||||
|
||||
// helpers scoped below
|
||||
function escapePipe(s: string | number | undefined): string {
|
||||
const str = s == null ? "" : String(s);
|
||||
// escape pipe and newlines for markdown table cells
|
||||
return str.replace(/\|/g, "\\|").replace(/\r?\n/g, " ↵ ");
|
||||
}
|
||||
|
||||
function codeBlock(s: string | number | undefined): string {
|
||||
const str = s == null ? "" : String(s);
|
||||
return `\`\`\`\n${str}\n\`\`\`\n`;
|
||||
}
|
||||
|
||||
function spoilerBlock(summary: string, content: string): string {
|
||||
return `<details>
|
||||
<summary>${summary}</summary>
|
||||
|
||||
${content}
|
||||
</details>`;
|
||||
}
|
||||
|
||||
function truncate(s: string, max: number): string {
|
||||
return s && s.length > max ? s.slice(0, max - 1) + "…" : s || "";
|
||||
}
|
||||
|
||||
function safeNum(v: number | undefined): string {
|
||||
if (v === undefined || v === null) return "";
|
||||
const n = typeof v === "number" ? v : Number(String(v));
|
||||
if (Number.isFinite(n)) return n.toFixed(3).replace(/\.000$/, "");
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function mapStatusToEmoji(status: "success" | "failed" | "error" | "skipped"): string {
|
||||
switch (status) {
|
||||
case "failed":
|
||||
return "failed ❌";
|
||||
case "error":
|
||||
return "error ❌";
|
||||
case "skipped":
|
||||
return "skipped ⏭️";
|
||||
case "success":
|
||||
return "success ✅";
|
||||
default:
|
||||
throw new Error("Unhandled case");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import {WorkingDir} from "../utilities/working-dir";
|
||||
import {MarkdownString, summarizeJunitReport, TestReport,} from "./functions/summarize-junit-report";
|
||||
import {parseJunitModuleReport} from "./functions/parse-junit-module-report";
|
||||
import fg from "fast-glob";
|
||||
import fs from "fs";
|
||||
import {getJavaProjectNameFromBuildAbsolutePath} from "./functions/file-path-utils";
|
||||
|
||||
/**
|
||||
* parse files located at 'testReportsLocationPattern' and generate a summary in Markdown
|
||||
* @param workingDir
|
||||
* @param options
|
||||
*/
|
||||
export async function generateTestReportSummary(
|
||||
workingDir: WorkingDir,
|
||||
options?: {
|
||||
onlyErrors?: boolean;
|
||||
testReportsLocationPattern?: "**/build/test-results/test/*.xml";
|
||||
},
|
||||
): Promise<MarkdownString> {
|
||||
const onlyErrors = options?.onlyErrors ?? false;
|
||||
const pattern = options?.testReportsLocationPattern ?? "**/build/test-results/test/*.xml";
|
||||
|
||||
// Find matching report files under the provided working directory
|
||||
const junitXmlReportsFilenames = await fg.async(pattern, {
|
||||
cwd: workingDir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
dot: true,
|
||||
followSymbolicLinks: true,
|
||||
});
|
||||
|
||||
// Parse each JUnit report into a module-level structure
|
||||
const moduleReports: TestReport[] = junitXmlReportsFilenames.map((file) => {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
return {
|
||||
projectName: getJavaProjectNameFromBuildAbsolutePath(file),
|
||||
projectReport: parseJunitModuleReport(content),
|
||||
};
|
||||
});
|
||||
|
||||
// Summarize all parsed reports into a single Markdown string
|
||||
return summarizeJunitReport(moduleReports, {onlyErrors: onlyErrors}).markdownContent;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export type WorkingDir = string;
|
||||
|
||||
/**
|
||||
* helper to handle working dir passed in CLI
|
||||
* @param workingDir by default the repository root
|
||||
*/
|
||||
export function getWorkingDir(workingDir?: string): WorkingDir {
|
||||
if (!workingDir) {
|
||||
throw new Error(
|
||||
"an absolute working dir is for required, this can be improved for better DX",
|
||||
);
|
||||
}
|
||||
if (!path.isAbsolute(workingDir)) {
|
||||
throw new Error(`Working directory must be an absolute path: ${workingDir}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(workingDir)) {
|
||||
throw new Error(`Working directory does not exist: ${workingDir}`);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(workingDir);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Working directory is not a directory: ${workingDir}`);
|
||||
}
|
||||
|
||||
return workingDir;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "vitest"]
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "vitest.config.ts", "eslint.config.js"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
// ensure Node-builtins stay external
|
||||
const externals = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)];
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: "node18",
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: "src/kestra-devtools-cli.ts",
|
||||
formats: ["cjs"],
|
||||
fileName: () => "kestra-devtools-cli.cjs",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: externals,
|
||||
output: {
|
||||
// Make the output an executable CLI
|
||||
banner: "#!/usr/bin/env node",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.ts"],
|
||||
coverage: {
|
||||
reporter: ["text", "html"],
|
||||
reportsDirectory: "coverage",
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user