Skip to content

Commit 4ba9878

Browse files
committed
chore: add oss clearance tools
1 parent d3a160c commit 4ba9878

File tree

4 files changed

+335
-5
lines changed

4 files changed

+335
-5
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#!/usr/bin/env ts-node-script
2+
3+
import { gh, GitHubDraftRelease, GitHubReleaseAsset } from "../src/github";
4+
import { join } from "path";
5+
import { exec, mkdir, mv, rm, zip } from "../src/shell";
6+
import { prompt } from "enquirer";
7+
import chalk from "chalk";
8+
import * as fs from "node:fs";
9+
import * as crypto from "crypto";
10+
import { pipeline } from "stream/promises";
11+
import { mkdtemp } from "node:fs/promises";
12+
import { homedir, tmpdir } from "node:os";
13+
14+
// ============================================================================
15+
// Constants
16+
// ============================================================================
17+
18+
const SBOM_GENERATOR_JAR = join(homedir(), "SBOM_Generator.jar");
19+
20+
// ============================================================================
21+
// Utility Functions
22+
// ============================================================================
23+
24+
function printHeader(title: string): void {
25+
console.log("\n" + chalk.bold.cyan("═".repeat(60)));
26+
console.log(chalk.bold.cyan(` ${title}`));
27+
console.log(chalk.bold.cyan("═".repeat(60)) + "\n");
28+
}
29+
30+
function printStep(step: number, total: number, message: string): void {
31+
console.log(chalk.bold.blue(`\n[${step}/${total}]`) + chalk.white(` ${message}`));
32+
}
33+
34+
function printSuccess(message: string): void {
35+
console.log(chalk.green(`✅ ${message}`));
36+
}
37+
38+
function printError(message: string): void {
39+
console.log(chalk.red(`❌ ${message}`));
40+
}
41+
42+
function printWarning(message: string): void {
43+
console.log(chalk.yellow(`⚠️ ${message}`));
44+
}
45+
46+
function printInfo(message: string): void {
47+
console.log(chalk.cyan(`ℹ️ ${message}`));
48+
}
49+
50+
function printProgress(message: string): void {
51+
console.log(chalk.gray(` → ${message}`));
52+
}
53+
54+
// ============================================================================
55+
// Core Functions
56+
// ============================================================================
57+
58+
async function verifyGitHubAuth(): Promise<void> {
59+
printStep(1, 6, "Verifying GitHub authentication...");
60+
61+
try {
62+
await gh.ensureAuth();
63+
printSuccess("GitHub authentication verified");
64+
} catch (error) {
65+
printError(`GitHub authentication failed: ${(error as Error).message}`);
66+
console.log(chalk.yellow("\n💡 Setup Instructions:\n"));
67+
console.log(chalk.white("1. Install GitHub CLI:"));
68+
console.log(chalk.cyan(" • Download: https://cli.github.com/"));
69+
console.log(chalk.cyan(" • Or via brew: brew install gh\n"));
70+
console.log(chalk.white("2. Authenticate (choose one option):"));
71+
console.log(chalk.cyan(" • Option A: export GITHUB_TOKEN=your_token_here"));
72+
console.log(chalk.cyan(" • Option B: export GH_PAT=your_token_here"));
73+
console.log(chalk.cyan(" • Option C: gh auth login\n"));
74+
console.log(chalk.white("3. For A and B get your token at:"));
75+
console.log(chalk.cyan(" https://github.com/settings/tokens\n"));
76+
throw new Error("GitHub authentication required");
77+
}
78+
}
79+
80+
async function selectRelease(): Promise<GitHubDraftRelease> {
81+
printStep(2, 6, "Fetching draft releases...");
82+
83+
const releases = await gh.getDraftReleases();
84+
printSuccess(`Found ${releases.length} draft release${releases.length !== 1 ? "s" : ""}`);
85+
86+
if (releases.length === 0) {
87+
printWarning("No draft releases found");
88+
throw new Error("No releases available");
89+
}
90+
91+
console.log(); // spacing
92+
const { tag_name } = await prompt<{ tag_name: string }>({
93+
type: "select",
94+
name: "tag_name",
95+
message: "Select a release to process:",
96+
choices: releases.map(r => ({
97+
name: r.tag_name,
98+
message: `${r.name} ${chalk.gray(`(${r.tag_name})`)}`
99+
}))
100+
});
101+
102+
const release = releases.find(r => r.tag_name === tag_name);
103+
if (!release) {
104+
throw new Error(`Release not found: ${tag_name}`);
105+
}
106+
107+
printInfo(`Selected release: ${chalk.bold(release.name)}`);
108+
return release;
109+
}
110+
111+
async function findAndValidateMpkAsset(release: GitHubDraftRelease): Promise<GitHubReleaseAsset> {
112+
printStep(3, 6, "Locating MPK asset...");
113+
114+
const mpkAsset = release.assets.find(asset => asset.name.endsWith(".mpk"));
115+
116+
if (!mpkAsset) {
117+
printError("No MPK asset found in release");
118+
printInfo(`Available assets: ${release.assets.map(a => a.name).join(", ")}`);
119+
throw new Error("MPK asset not found");
120+
}
121+
122+
printSuccess(`Found MPK asset: ${chalk.bold(mpkAsset.name)}`);
123+
printInfo(`Asset ID: ${mpkAsset.id}`);
124+
return mpkAsset;
125+
}
126+
127+
async function downloadAndVerifyAsset(mpkAsset: GitHubReleaseAsset, downloadPath: string): Promise<string> {
128+
printStep(4, 6, "Downloading and verifying MPK asset...");
129+
130+
printProgress(`Downloading to: ${downloadPath}`);
131+
await gh.downloadReleaseAsset(mpkAsset.id, downloadPath);
132+
printSuccess("Download completed");
133+
134+
printProgress("Computing SHA-256 hash...");
135+
const fileHash = await computeHash(downloadPath);
136+
printInfo(`Computed hash: ${fileHash}`);
137+
138+
const expectedDigest = mpkAsset.digest.replace("sha256:", "");
139+
if (fileHash !== expectedDigest) {
140+
printError("Hash mismatch detected!");
141+
printInfo(`Expected: ${expectedDigest}`);
142+
printInfo(`Got: ${fileHash}`);
143+
throw new Error("Asset integrity verification failed");
144+
}
145+
146+
printSuccess("Hash verification passed");
147+
return fileHash;
148+
}
149+
150+
async function runSbomGenerator(tmpFolder: string): Promise<void> {
151+
printStep(5, 6, "Running SBOM Generator...");
152+
153+
printProgress("Unzipping MPK...");
154+
await exec(`java -jar ${SBOM_GENERATOR_JAR} SBOM_GENERATOR unzip`, { cwd: tmpFolder });
155+
printSuccess("MPK unzipped");
156+
157+
printProgress("Scanning dependencies...");
158+
await exec(`java -jar ${SBOM_GENERATOR_JAR} SBOM_GENERATOR scan`, { cwd: tmpFolder });
159+
printSuccess("Scan completed");
160+
}
161+
162+
async function createOutputArchive(tmpFolder: string, releaseName: string, fileHash: string): Promise<void> {
163+
printStep(6, 6, "Creating output archive...");
164+
165+
const resultsFolder = join(tmpFolder, "CCA_JSON");
166+
const archiveName = `${releaseName}.zip`;
167+
const finalName = `${releaseName} [${fileHash}].zip`;
168+
169+
printProgress(`Archiving results from: ${resultsFolder}`);
170+
await zip(resultsFolder, archiveName);
171+
printSuccess("Archive created");
172+
173+
const ossArtifactZip = join(resultsFolder, archiveName);
174+
const downloadsFolder = join(homedir(), "Downloads");
175+
const finalPath = join(downloadsFolder, finalName);
176+
177+
printProgress(`Moving to: ${finalPath}`);
178+
mv(ossArtifactZip, finalPath);
179+
printSuccess("Archive moved to Downloads folder");
180+
181+
printProgress("Cleaning up temporary files...");
182+
rm("-rf", tmpFolder);
183+
printSuccess("Cleanup completed");
184+
185+
console.log(chalk.bold.green(`\n🎉 Success! Output file:`));
186+
console.log(chalk.cyan(` ${finalPath}\n`));
187+
}
188+
189+
async function computeHash(filepath: string): Promise<string> {
190+
const input = fs.createReadStream(filepath);
191+
const hash = crypto.createHash("sha256");
192+
await pipeline(input, hash);
193+
return hash.digest("hex");
194+
}
195+
196+
async function createFolderStructure(name: string): Promise<[string, string]> {
197+
const tmpFolder = await mkdtemp(join(tmpdir(), "tmp_OSS_Clearance_Artifacts_"));
198+
const artifactsFolder = join(tmpFolder, "SBOM_GENERATOR", name);
199+
await mkdir("-p", artifactsFolder);
200+
return [tmpFolder, join(artifactsFolder, `${name}.mpk`)];
201+
}
202+
203+
// ============================================================================
204+
// Main Function
205+
// ============================================================================
206+
207+
async function main(): Promise<void> {
208+
printHeader("OSS Clearance Artifacts Preparation Tool");
209+
210+
try {
211+
// Step 1: Verify authentication
212+
await verifyGitHubAuth();
213+
214+
// Step 2: Select release
215+
const release = await selectRelease();
216+
217+
// Step 3: Find MPK asset
218+
const mpkAsset = await findAndValidateMpkAsset(release);
219+
220+
// Prepare folder structure
221+
const [tmpFolder, downloadPath] = await createFolderStructure(release.name);
222+
printInfo(`Working directory: ${tmpFolder}`);
223+
224+
// Step 4: Download and verify
225+
const fileHash = await downloadAndVerifyAsset(mpkAsset, downloadPath);
226+
227+
// Step 5: Run SBOM Generator
228+
await runSbomGenerator(tmpFolder);
229+
230+
// Step 6: Create final archive
231+
await createOutputArchive(tmpFolder, release.name, fileHash);
232+
} catch (error) {
233+
console.log("\n" + chalk.bold.red("═".repeat(60)));
234+
printError(`Process failed: ${(error as Error).message}`);
235+
console.log(chalk.bold.red("═".repeat(60)) + "\n");
236+
process.exit(1);
237+
}
238+
}
239+
240+
// ============================================================================
241+
// Entry Point
242+
// ============================================================================
243+
244+
main().catch(e => {
245+
console.error(chalk.red("\n💥 Unexpected error:"), e);
246+
process.exit(1);
247+
});

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/github.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
import { mkdtemp, writeFile } from "fs/promises";
2+
import { createWriteStream } from "fs";
23
import { join } from "path";
4+
import { pipeline } from "stream/promises";
5+
import nodefetch from "node-fetch";
36
import { fetch } from "./fetch";
47
import { exec } from "./shell";
58

9+
export interface GitHubReleaseAsset {
10+
id: string;
11+
name: string;
12+
browser_download_url: string;
13+
size: number;
14+
content_type: string;
15+
digest: string;
16+
}
17+
18+
export interface GitHubDraftRelease {
19+
id: string;
20+
tag_name: string;
21+
name: string;
22+
draft: boolean;
23+
created_at: string;
24+
published_at: string | null;
25+
assets: GitHubReleaseAsset[];
26+
}
27+
628
interface GitHubReleaseInfo {
729
title: string;
830
tag: string;
@@ -29,12 +51,11 @@ interface GitHubPRInfo {
2951
export class GitHub {
3052
authSet = false;
3153
tmpPrefix = "gh-";
54+
authToken: string = "";
3255

3356
async ensureAuth(): Promise<void> {
3457
if (!this.authSet) {
35-
if (process.env.GITHUB_TOKEN) {
36-
// when using GITHUB_TOKEN, gh will automatically use it
37-
} else if (process.env.GH_PAT) {
58+
if (process.env.GH_PAT) {
3859
await exec(`echo "${process.env.GH_PAT}" | gh auth login --with-token`);
3960
} else {
4061
// No environment variables set, check if already authenticated
@@ -53,8 +74,10 @@ export class GitHub {
5374
try {
5475
// Try to run 'gh auth status' to check if authenticated
5576
await exec("gh auth status", { stdio: "pipe" });
77+
const { stdout: token } = await exec(`gh auth token`, { stdio: "pipe" });
78+
this.authToken = token.trim();
5679
return true;
57-
} catch (error) {
80+
} catch (error: unknown) {
5881
// If the command fails, the user is not authenticated
5982
return false;
6083
}
@@ -107,7 +130,7 @@ export class GitHub {
107130
get ghAPIHeaders(): Record<string, string> {
108131
return {
109132
"X-GitHub-Api-Version": "2022-11-28",
110-
Authorization: `Bearer ${process.env.GH_PAT}`
133+
Authorization: `Bearer ${this.authToken || process.env.GH_PAT}`
111134
};
112135
}
113136

@@ -165,6 +188,64 @@ export class GitHub {
165188
return downloadUrl;
166189
}
167190

191+
async getDraftReleases(owner = "mendix", repo = "web-widgets"): Promise<GitHubDraftRelease[]> {
192+
console.log(`Fetching draft releases from ${owner}/${repo}`);
193+
194+
const releases = await fetch<GitHubDraftRelease[]>(
195+
"GET",
196+
`https://api.github.com/repos/${owner}/${repo}/releases`,
197+
undefined,
198+
{
199+
...this.ghAPIHeaders
200+
}
201+
);
202+
203+
// Filter only draft releases
204+
return releases.filter(release => release.draft);
205+
}
206+
207+
async downloadReleaseAsset(
208+
assetId: string,
209+
destinationPath: string,
210+
owner = "mendix",
211+
repo = "web-widgets"
212+
): Promise<void> {
213+
await this.ensureAuth();
214+
215+
console.log(`Downloading release asset ${assetId} to ${destinationPath}`);
216+
217+
const url = `https://api.github.com/repos/${owner}/${repo}/releases/assets/${assetId}`;
218+
219+
try {
220+
const response = await nodefetch(url, {
221+
method: "GET",
222+
headers: {
223+
Accept: "application/octet-stream",
224+
...this.ghAPIHeaders
225+
},
226+
redirect: "follow"
227+
});
228+
229+
if (!response.ok) {
230+
throw new Error(`Failed to download asset ${assetId}: ${response.status} ${response.statusText}`);
231+
}
232+
233+
if (!response.body) {
234+
throw new Error(`No response body received for asset ${assetId}`);
235+
}
236+
237+
// Stream the response body to the file
238+
const fileStream = createWriteStream(destinationPath);
239+
await pipeline(response.body, fileStream);
240+
241+
console.log(`Successfully downloaded asset to ${destinationPath}`);
242+
} catch (error) {
243+
throw new Error(
244+
`Failed to download release asset ${assetId}: ${error instanceof Error ? error.message : String(error)}`
245+
);
246+
}
247+
}
248+
168249
async createReleaseNotesFile(releaseNotesText: string): Promise<string> {
169250
const filePath = await this.createTempFile();
170251
await writeFile(filePath, releaseNotesText);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"scripts": {
1010
"build": "turbo run build",
1111
"changelog": "pnpm --filter @mendix/automation-utils run changelog",
12+
"oss-clearance": "pnpm --filter @mendix/automation-utils run oss-clearance",
1213
"create-gh-release": "turbo run create-gh-release --concurrency 1",
1314
"create-translation": "turbo run create-translation",
1415
"postinstall": "turbo run agent-rules",

0 commit comments

Comments
 (0)