Skip to content

Commit eb2b9b4

Browse files
committed
chore: add oss clearance tools
1 parent 0c647d5 commit eb2b9b4

File tree

8 files changed

+572
-28
lines changed

8 files changed

+572
-28
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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("No draft releases found");
93+
throw new Error("No releases available");
94+
}
95+
96+
console.log(); // spacing
97+
const { tag_name } = await prompt<{ tag_name: string }>({
98+
type: "select",
99+
name: "tag_name",
100+
message: "Select a release to process:",
101+
choices: releases.map(r => ({
102+
name: r.tag_name,
103+
message: `${r.name} ${chalk.gray(`(${r.tag_name})`)}`
104+
}))
105+
});
106+
107+
const release = releases.find(r => r.tag_name === tag_name);
108+
if (!release) {
109+
throw new Error(`Release not found: ${tag_name}`);
110+
}
111+
112+
printInfo(`Selected release: ${chalk.bold(release.name)}`);
113+
return release;
114+
}
115+
116+
async function findAndValidateMpkAsset(release: GitHubDraftRelease): Promise<GitHubReleaseAsset> {
117+
printStep(3, 5, "Locating MPK asset...");
118+
119+
const mpkAsset = release.assets.find(asset => asset.name.endsWith(".mpk"));
120+
121+
if (!mpkAsset) {
122+
printError("No MPK asset found in release");
123+
printInfo(`Available assets: ${release.assets.map(a => a.name).join(", ")}`);
124+
throw new Error("MPK asset not found");
125+
}
126+
127+
printSuccess(`Found MPK asset: ${chalk.bold(mpkAsset.name)}`);
128+
printInfo(`Asset ID: ${mpkAsset.id}`);
129+
return mpkAsset;
130+
}
131+
132+
async function downloadAndVerifyAsset(mpkAsset: GitHubReleaseAsset, downloadPath: string): Promise<string> {
133+
printStep(4, 5, "Downloading and verifying MPK asset...");
134+
135+
printProgress(`Downloading to: ${downloadPath}`);
136+
await gh.downloadReleaseAsset(mpkAsset.id, downloadPath);
137+
printSuccess("Download completed");
138+
139+
printProgress("Computing SHA-256 hash...");
140+
const fileHash = await computeHash(downloadPath);
141+
printInfo(`Computed hash: ${fileHash}`);
142+
143+
const expectedDigest = mpkAsset.digest.replace("sha256:", "");
144+
if (fileHash !== expectedDigest) {
145+
printError("Hash mismatch detected!");
146+
printInfo(`Expected: ${expectedDigest}`);
147+
printInfo(`Got: ${fileHash}`);
148+
throw new Error("Asset integrity verification failed");
149+
}
150+
151+
printSuccess("Hash verification passed");
152+
return fileHash;
153+
}
154+
155+
async function runSbomGenerator(tmpFolder: string, releaseName: string, fileHash: string): Promise<string> {
156+
printStep(5, 5, "Running SBOM Generator...");
157+
158+
printProgress("Generating OSS Clearance artifacts...");
159+
160+
const finalName = `${releaseName} [${fileHash}].zip`;
161+
const finalPath = join(homedir(), "Downloads", finalName);
162+
163+
await generateSBomArtifactsInFolder(tmpFolder, SBOM_GENERATOR_JAR, releaseName, finalPath);
164+
printSuccess("Completed.");
165+
166+
return finalPath;
167+
}
168+
169+
async function computeHash(filepath: string): Promise<string> {
170+
const input = createReadStream(filepath);
171+
const hash = crypto.createHash("sha256");
172+
await pipeline(input, hash);
173+
return hash.digest("hex");
174+
}
175+
176+
// ============================================================================
177+
// Command Handlers
178+
// ============================================================================
179+
180+
async function handlePrepareCommand(): Promise<void> {
181+
printHeader("OSS Clearance Artifacts Preparation");
182+
183+
try {
184+
// Step 1: Verify authentication
185+
await verifyGitHubAuth();
186+
187+
// Step 2: Select release
188+
const release = await selectRelease();
189+
190+
// Step 3: Find MPK asset
191+
const mpkAsset = await findAndValidateMpkAsset(release);
192+
193+
// Prepare folder structure
194+
const [tmpFolder, downloadPath] = await createSBomGeneratorFolderStructure(release.name);
195+
printInfo(`Working directory: ${tmpFolder}`);
196+
197+
// Step 4: Download and verify
198+
const fileHash = await downloadAndVerifyAsset(mpkAsset, downloadPath);
199+
200+
// Step 5: Run SBOM Generator
201+
const finalPath = await runSbomGenerator(tmpFolder, release.name, fileHash);
202+
203+
console.log(chalk.bold.green(`\n🎉 Success! Output file:`));
204+
console.log(chalk.cyan(` ${finalPath}\n`));
205+
} catch (error) {
206+
console.log("\n" + chalk.bold.red("═".repeat(60)));
207+
printError(`Process failed: ${(error as Error).message}`);
208+
console.log(chalk.bold.red("═".repeat(60)) + "\n");
209+
process.exit(1);
210+
}
211+
}
212+
213+
async function handleIncludeCommand(): Promise<void> {
214+
printHeader("OSS Clearance Readme Include");
215+
216+
try {
217+
// TODO: Implement include command logic
218+
// Step 1: Verify authentication
219+
await verifyGitHubAuth();
220+
221+
// Step 2: Select release
222+
const release = await selectRelease();
223+
224+
// Step 3: Find MPK asset
225+
const mpkAsset = await findAndValidateMpkAsset(release);
226+
227+
// Step 4: Find and select OSS Readme
228+
const readmes = findAllReadmeOssLocally();
229+
const recommendedReadmeOss = getRecommendedReadmeOss(
230+
release.name.split(" ")[0],
231+
release.name.split(" ")[1],
232+
readmes
233+
);
234+
235+
let readmeToInclude: string;
236+
237+
if (!recommendedReadmeOss) {
238+
const { selectedReadme } = await prompt<{ selectedReadme: string }>({
239+
type: "select",
240+
name: "selectedReadme",
241+
message: "Select a release to process:",
242+
choices: readmes.map(r => ({
243+
name: r,
244+
message: basename(r)
245+
}))
246+
});
247+
248+
readmeToInclude = selectedReadme;
249+
} else {
250+
readmeToInclude = recommendedReadmeOss;
251+
}
252+
253+
printInfo(`Readme to include: ${readmeToInclude}`);
254+
255+
// Prepare folder structure
256+
const [tmpFolder, downloadPath] = await createSBomGeneratorFolderStructure(release.name);
257+
printInfo(`Working directory: ${tmpFolder}`);
258+
259+
// Step 5: Download and verify
260+
await downloadAndVerifyAsset(mpkAsset, downloadPath);
261+
262+
// Step 6: Include readmeToInclude into the mpk
263+
await includeReadmeOssIntoMpk(readmeToInclude, downloadPath);
264+
265+
// Step 7: Upload updated asses to the draft release
266+
const newAsset = await gh.updateReleaseAsset(release.id, mpkAsset, downloadPath);
267+
console.log(`Successfully uploaded asset ${newAsset.name} (ID: ${newAsset.id})`);
268+
269+
console.log(release.id);
270+
} catch (error) {
271+
console.log("\n" + chalk.bold.red("═".repeat(60)));
272+
printError(`Process failed: ${(error as Error).message}`);
273+
console.log(chalk.bold.red("═".repeat(60)) + "\n");
274+
process.exit(1);
275+
}
276+
}
277+
278+
// ============================================================================
279+
// Main Function
280+
// ============================================================================
281+
282+
async function main(): Promise<void> {
283+
const command = process.argv[2];
284+
285+
switch (command) {
286+
case "prepare":
287+
await handlePrepareCommand();
288+
break;
289+
case "include":
290+
await handleIncludeCommand();
291+
break;
292+
default:
293+
printError(command ? `Unknown command: ${command}` : "No command specified");
294+
console.log(chalk.white("\nUsage:"));
295+
console.log(
296+
chalk.cyan(" rui-oss-clearance.ts prepare ") +
297+
chalk.gray("- Prepare OSS clearance artifact from draft release")
298+
);
299+
console.log(
300+
chalk.cyan(" rui-oss-clearance.ts include ") +
301+
chalk.gray("- Include OSS Readme file into a draft release")
302+
);
303+
console.log();
304+
process.exit(1);
305+
}
306+
}
307+
308+
// ============================================================================
309+
// Entry Point
310+
// ============================================================================
311+
312+
main().catch(e => {
313+
console.error(chalk.red("\n💥 Unexpected error:"), e);
314+
process.exit(1);
315+
});

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)