Skip to content

Commit c8ad9fc

Browse files
authored
[WC-3150] add oss clearance tools (#1960)
2 parents f67d586 + 617cdc2 commit c8ad9fc

File tree

9 files changed

+546
-36
lines changed

9 files changed

+546
-36
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#!/usr/bin/env ts-node-script
2+
3+
import { gh, GitHubDraftRelease, GitHubReleaseAsset } from "../src/github";
4+
import { basename, join } from "path";
5+
import { prompt } from "enquirer";
6+
import chalk from "chalk";
7+
import { createReadStream } from "node:fs";
8+
import * as crypto from "crypto";
9+
import { pipeline } from "stream/promises";
10+
import { homedir } from "node:os";
11+
import {
12+
createSBomGeneratorFolderStructure,
13+
findAllReadmeOssLocally,
14+
generateSBomArtifactsInFolder,
15+
getRecommendedReadmeOss,
16+
includeReadmeOssIntoMpk
17+
} from "../src/oss-clearance";
18+
19+
// ============================================================================
20+
// Constants
21+
// ============================================================================
22+
23+
const SBOM_GENERATOR_JAR = join(homedir(), "SBOM_Generator.jar");
24+
25+
// ============================================================================
26+
// Utility Functions
27+
// ============================================================================
28+
29+
function printHeader(title: string): void {
30+
console.log("\n" + chalk.bold.cyan("═".repeat(60)));
31+
console.log(chalk.bold.cyan(` ${title}`));
32+
console.log(chalk.bold.cyan("═".repeat(60)) + "\n");
33+
}
34+
35+
function printStep(step: number, total: number, message: string): void {
36+
console.log(chalk.bold.blue(`\n[${step}/${total}]`) + chalk.white(` ${message}`));
37+
}
38+
39+
function printSuccess(message: string): void {
40+
console.log(chalk.green(`✅ ${message}`));
41+
}
42+
43+
function printError(message: string): void {
44+
console.log(chalk.red(`❌ ${message}`));
45+
}
46+
47+
function printWarning(message: string): void {
48+
console.log(chalk.yellow(`⚠️ ${message}`));
49+
}
50+
51+
function printInfo(message: string): void {
52+
console.log(chalk.cyan(`ℹ️ ${message}`));
53+
}
54+
55+
function printProgress(message: string): void {
56+
console.log(chalk.gray(` → ${message}`));
57+
}
58+
59+
// ============================================================================
60+
// Core Functions
61+
// ============================================================================
62+
63+
async function verifyGitHubAuth(): Promise<void> {
64+
printStep(1, 5, "Verifying GitHub authentication...");
65+
66+
try {
67+
await gh.ensureAuth();
68+
printSuccess("GitHub authentication verified");
69+
} catch (error) {
70+
printError(`GitHub authentication failed: ${(error as Error).message}`);
71+
console.log(chalk.yellow("\n💡 Setup Instructions:\n"));
72+
console.log(chalk.white("1. Install GitHub CLI:"));
73+
console.log(chalk.cyan(" • Download: https://cli.github.com/"));
74+
console.log(chalk.cyan(" • Or via brew: brew install gh\n"));
75+
console.log(chalk.white("2. Authenticate (choose one option):"));
76+
console.log(chalk.cyan(" • Option A: export GITHUB_TOKEN=your_token_here"));
77+
console.log(chalk.cyan(" • Option B: export GH_PAT=your_token_here"));
78+
console.log(chalk.cyan(" • Option C: gh auth login\n"));
79+
console.log(chalk.white("3. For A and B get your token at:"));
80+
console.log(chalk.cyan(" https://github.com/settings/tokens\n"));
81+
throw new Error("GitHub authentication required");
82+
}
83+
}
84+
85+
async function selectRelease(): Promise<GitHubDraftRelease> {
86+
printStep(2, 5, "Fetching draft releases...");
87+
88+
const releases = await gh.getDraftReleases();
89+
printSuccess(`Found ${releases.length} draft release${releases.length !== 1 ? "s" : ""}`);
90+
91+
if (releases.length === 0) {
92+
printWarning(
93+
"No draft releases found. Please create a draft release before trying again using `prepare-release` tool"
94+
);
95+
throw new Error("No draft releases found");
96+
}
97+
98+
console.log(); // spacing
99+
const { tag_name } = await prompt<{ tag_name: string }>({
100+
type: "select",
101+
name: "tag_name",
102+
message: "Select a release to process:",
103+
choices: releases.map(r => ({
104+
name: r.tag_name,
105+
message: `${r.name} ${chalk.gray(`(${r.tag_name})`)}`
106+
}))
107+
});
108+
109+
const release = releases.find(r => r.tag_name === tag_name);
110+
if (!release) {
111+
throw new Error(`Release not found: ${tag_name}`);
112+
}
113+
114+
printInfo(`Selected release: ${chalk.bold(release.name)}`);
115+
return release;
116+
}
117+
118+
async function findAndValidateMpkAsset(release: GitHubDraftRelease): Promise<GitHubReleaseAsset> {
119+
printStep(3, 5, "Locating MPK asset...");
120+
121+
const mpkAsset = release.assets.find(asset => asset.name.endsWith(".mpk"));
122+
123+
if (!mpkAsset) {
124+
printError("No MPK asset found in release");
125+
printInfo(`Available assets: ${release.assets.map(a => a.name).join(", ")}`);
126+
throw new Error("MPK asset not found");
127+
}
128+
129+
printSuccess(`Found MPK asset: ${chalk.bold(mpkAsset.name)}`);
130+
printInfo(`Asset ID: ${mpkAsset.id}`);
131+
return mpkAsset;
132+
}
133+
134+
async function downloadAndVerifyAsset(mpkAsset: GitHubReleaseAsset, downloadPath: string): Promise<string> {
135+
printStep(4, 5, "Downloading and verifying MPK asset...");
136+
137+
printProgress(`Downloading to: ${downloadPath}`);
138+
await gh.downloadReleaseAsset(mpkAsset.id, downloadPath);
139+
printSuccess("Download completed");
140+
141+
printProgress("Computing SHA-256 hash...");
142+
const fileHash = await computeHash(downloadPath);
143+
printInfo(`Computed hash: ${fileHash}`);
144+
145+
const expectedDigest = mpkAsset.digest.replace("sha256:", "");
146+
if (fileHash !== expectedDigest) {
147+
printError("Hash mismatch detected!");
148+
printInfo(`Expected: ${expectedDigest}`);
149+
printInfo(`Got: ${fileHash}`);
150+
throw new Error("Asset integrity verification failed");
151+
}
152+
153+
printSuccess("Hash verification passed");
154+
return fileHash;
155+
}
156+
157+
async function runSbomGenerator(tmpFolder: string, releaseName: string, fileHash: string): Promise<string> {
158+
printStep(5, 5, "Running SBOM Generator...");
159+
160+
printProgress("Generating OSS Clearance artifacts...");
161+
162+
const finalName = `${releaseName} [${fileHash}].zip`;
163+
const finalPath = join(homedir(), "Downloads", finalName);
164+
165+
await generateSBomArtifactsInFolder(tmpFolder, SBOM_GENERATOR_JAR, releaseName, finalPath);
166+
printSuccess("Completed.");
167+
168+
return finalPath;
169+
}
170+
171+
async function computeHash(filepath: string): Promise<string> {
172+
const input = createReadStream(filepath);
173+
const hash = crypto.createHash("sha256");
174+
await pipeline(input, hash);
175+
return hash.digest("hex");
176+
}
177+
178+
// ============================================================================
179+
// Command Handlers
180+
// ============================================================================
181+
182+
async function handlePrepareCommand(): Promise<void> {
183+
printHeader("OSS Clearance Artifacts Preparation");
184+
185+
try {
186+
// Step 1: Verify authentication
187+
await verifyGitHubAuth();
188+
189+
// Step 2: Select release
190+
const release = await selectRelease();
191+
192+
// Step 3: Find MPK asset
193+
const mpkAsset = await findAndValidateMpkAsset(release);
194+
195+
// Prepare folder structure
196+
const [tmpFolder, downloadPath] = await createSBomGeneratorFolderStructure(release.name);
197+
printInfo(`Working directory: ${tmpFolder}`);
198+
199+
// Step 4: Download and verify
200+
const fileHash = await downloadAndVerifyAsset(mpkAsset, downloadPath);
201+
202+
// Step 5: Run SBOM Generator
203+
const finalPath = await runSbomGenerator(tmpFolder, release.name, fileHash);
204+
205+
console.log(chalk.bold.green(`\n🎉 Success! Output file:`));
206+
console.log(chalk.cyan(` ${finalPath}\n`));
207+
} catch (error) {
208+
console.log("\n" + chalk.bold.red("═".repeat(60)));
209+
printError(`Process failed: ${(error as Error).message}`);
210+
console.log(chalk.bold.red("═".repeat(60)) + "\n");
211+
process.exit(1);
212+
}
213+
}
214+
215+
async function handleIncludeCommand(): Promise<void> {
216+
printHeader("OSS Clearance Readme Include");
217+
218+
try {
219+
// TODO: Implement include command logic
220+
// Step 1: Verify authentication
221+
await verifyGitHubAuth();
222+
223+
// Step 2: Select release
224+
const release = await selectRelease();
225+
226+
// Step 3: Find MPK asset
227+
const mpkAsset = await findAndValidateMpkAsset(release);
228+
229+
// Step 4: Find and select OSS Readme
230+
const readmes = findAllReadmeOssLocally();
231+
const recommendedReadmeOss = getRecommendedReadmeOss(
232+
release.name.split(" ")[0],
233+
release.name.split(" ")[1],
234+
readmes
235+
);
236+
237+
let readmeToInclude: string;
238+
239+
if (!recommendedReadmeOss) {
240+
const { selectedReadme } = await prompt<{ selectedReadme: string }>({
241+
type: "select",
242+
name: "selectedReadme",
243+
message: "Select a release to process:",
244+
choices: readmes.map(r => ({
245+
name: r,
246+
message: basename(r)
247+
}))
248+
});
249+
250+
readmeToInclude = selectedReadme;
251+
} else {
252+
readmeToInclude = recommendedReadmeOss;
253+
}
254+
255+
printInfo(`Readme to include: ${readmeToInclude}`);
256+
257+
// Step 7: Upload updated asses to the draft release
258+
const newAsset = await gh.uploadReleaseAsset(release.id, readmeToInclude, basename(readmeToInclude));
259+
console.log(`Successfully uploaded asset ${newAsset.name} (ID: ${newAsset.id})`);
260+
261+
console.log(release.id);
262+
} catch (error) {
263+
console.log("\n" + chalk.bold.red("═".repeat(60)));
264+
printError(`Process failed: ${(error as Error).message}`);
265+
console.log(chalk.bold.red("═".repeat(60)) + "\n");
266+
process.exit(1);
267+
}
268+
}
269+
270+
// ============================================================================
271+
// Main Function
272+
// ============================================================================
273+
274+
async function main(): Promise<void> {
275+
const command = process.argv[2];
276+
277+
switch (command) {
278+
case "prepare":
279+
await handlePrepareCommand();
280+
break;
281+
case "include":
282+
await handleIncludeCommand();
283+
break;
284+
default:
285+
printError(command ? `Unknown command: ${command}` : "No command specified");
286+
console.log(chalk.white("\nUsage:"));
287+
console.log(
288+
chalk.cyan(" rui-oss-clearance.ts prepare ") +
289+
chalk.gray("- Prepare OSS clearance artifact from draft release")
290+
);
291+
console.log(
292+
chalk.cyan(" rui-oss-clearance.ts include ") +
293+
chalk.gray("- Include OSS Readme file into a draft release")
294+
);
295+
console.log();
296+
process.exit(1);
297+
}
298+
}
299+
300+
// ============================================================================
301+
// Entry Point
302+
// ============================================================================
303+
304+
main().catch(e => {
305+
console.error(chalk.red("\n💥 Unexpected error:"), e);
306+
process.exit(1);
307+
});

automation/utils/bin/rui-prepare-release.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,15 +353,13 @@ async function createReleaseBranch(packageName: string, version: string): Promis
353353
}
354354

355355
async function initializeJiraClient(): Promise<Jira> {
356-
const projectKey = process.env.JIRA_PROJECT_KEY;
357-
const baseUrl = process.env.JIRA_BASE_URL;
356+
const projectKey = process.env.JIRA_PROJECT_KEY ?? "WC";
357+
const baseUrl = process.env.JIRA_BASE_URL ?? "https://mendix.atlassian.net";
358358
const apiToken = process.env.JIRA_API_TOKEN;
359359

360360
if (!projectKey || !baseUrl || !apiToken) {
361361
console.error(chalk.red("❌ Missing Jira environment variables"));
362362
console.log(chalk.dim(" Required variables:"));
363-
console.log(chalk.dim(" export JIRA_PROJECT_KEY=WEB"));
364-
console.log(chalk.dim(" export JIRA_BASE_URL=https://your-company.atlassian.net"));
365363
console.log(chalk.dim(" export JIRA_API_TOKEN=username@your-company.com:ATATT3xFfGF0..."));
366364
console.log(chalk.dim(" Get your API token at: https://id.atlassian.com/manage-profile/security/api-tokens"));
367365
throw new Error("Missing Jira environment variables");

automation/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"compile:parser:widget": "peggy -o ./src/changelog-parser/parser/module/module.js ./src/changelog-parser/parser/module/module.pegjs",
3131
"format": "prettier --write .",
3232
"lint": "eslint --ext .jsx,.js,.ts,.tsx src/",
33+
"oss-clearance": "ts-node bin/rui-oss-clearance.ts",
3334
"prepare": "pnpm run compile:parser:widget && pnpm run compile:parser:module && tsc",
3435
"prepare-release": "ts-node bin/rui-prepare-release.ts",
3536
"start": "tsc --watch",

automation/utils/src/changelog.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { gh } from "./github";
22
import { PublishedInfo } from "./package-info";
33
import { exec, popd, pushd } from "./shell";
4-
import { findOssReadme } from "./oss-readme";
5-
import { join } from "path";
64

75
export async function updateChangelogsAndCreatePR(
86
info: PublishedInfo,
@@ -53,13 +51,6 @@ export async function updateChangelogsAndCreatePR(
5351
pushd(root.trim());
5452
await exec(`git add '*/CHANGELOG.md'`);
5553

56-
const path = process.cwd();
57-
const readmeossFile = findOssReadme(path, info.mxpackage.name, info.version.format());
58-
if (readmeossFile) {
59-
console.log(`Removing OSS clearance readme file '${readmeossFile}'...`);
60-
await exec(`git rm '${readmeossFile}'`);
61-
}
62-
6354
await exec(`git commit -m "chore(${info.name}): update changelog"`);
6455
await exec(`git push ${remoteName} ${releaseBranchName}`);
6556
popd();

0 commit comments

Comments
 (0)