Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 307 additions & 0 deletions automation/utils/bin/rui-oss-clearance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
#!/usr/bin/env ts-node-script

import { gh, GitHubDraftRelease, GitHubReleaseAsset } from "../src/github";
import { basename, join } from "path";
import { prompt } from "enquirer";
import chalk from "chalk";
import { createReadStream } from "node:fs";
import * as crypto from "crypto";
import { pipeline } from "stream/promises";
import { homedir } from "node:os";
import {
createSBomGeneratorFolderStructure,
findAllReadmeOssLocally,
generateSBomArtifactsInFolder,
getRecommendedReadmeOss,
includeReadmeOssIntoMpk
} from "../src/oss-clearance";

// ============================================================================
// Constants
// ============================================================================

const SBOM_GENERATOR_JAR = join(homedir(), "SBOM_Generator.jar");

// ============================================================================
// Utility Functions
// ============================================================================

function printHeader(title: string): void {
console.log("\n" + chalk.bold.cyan("═".repeat(60)));
console.log(chalk.bold.cyan(` ${title}`));
console.log(chalk.bold.cyan("═".repeat(60)) + "\n");
}

function printStep(step: number, total: number, message: string): void {
console.log(chalk.bold.blue(`\n[${step}/${total}]`) + chalk.white(` ${message}`));
}

function printSuccess(message: string): void {
console.log(chalk.green(`✅ ${message}`));
}

function printError(message: string): void {
console.log(chalk.red(`❌ ${message}`));
}

function printWarning(message: string): void {
console.log(chalk.yellow(`⚠️ ${message}`));
}

function printInfo(message: string): void {
console.log(chalk.cyan(`ℹ️ ${message}`));
}

function printProgress(message: string): void {
console.log(chalk.gray(` → ${message}`));
}

// ============================================================================
// Core Functions
// ============================================================================

async function verifyGitHubAuth(): Promise<void> {
printStep(1, 5, "Verifying GitHub authentication...");

try {
await gh.ensureAuth();
printSuccess("GitHub authentication verified");
} catch (error) {
printError(`GitHub authentication failed: ${(error as Error).message}`);
console.log(chalk.yellow("\n💡 Setup Instructions:\n"));
console.log(chalk.white("1. Install GitHub CLI:"));
console.log(chalk.cyan(" • Download: https://cli.github.com/"));
console.log(chalk.cyan(" • Or via brew: brew install gh\n"));
console.log(chalk.white("2. Authenticate (choose one option):"));
console.log(chalk.cyan(" • Option A: export GITHUB_TOKEN=your_token_here"));
console.log(chalk.cyan(" • Option B: export GH_PAT=your_token_here"));
console.log(chalk.cyan(" • Option C: gh auth login\n"));
console.log(chalk.white("3. For A and B get your token at:"));
console.log(chalk.cyan(" https://github.com/settings/tokens\n"));
throw new Error("GitHub authentication required");
}
}

async function selectRelease(): Promise<GitHubDraftRelease> {
printStep(2, 5, "Fetching draft releases...");

const releases = await gh.getDraftReleases();
printSuccess(`Found ${releases.length} draft release${releases.length !== 1 ? "s" : ""}`);

if (releases.length === 0) {
printWarning(
"No draft releases found. Please create a draft release before trying again using `prepare-release` tool"
);
throw new Error("No draft releases found");
}

console.log(); // spacing
const { tag_name } = await prompt<{ tag_name: string }>({
type: "select",
name: "tag_name",
message: "Select a release to process:",
choices: releases.map(r => ({
name: r.tag_name,
message: `${r.name} ${chalk.gray(`(${r.tag_name})`)}`
}))
});

const release = releases.find(r => r.tag_name === tag_name);
if (!release) {
throw new Error(`Release not found: ${tag_name}`);
}

printInfo(`Selected release: ${chalk.bold(release.name)}`);
return release;
}

async function findAndValidateMpkAsset(release: GitHubDraftRelease): Promise<GitHubReleaseAsset> {
printStep(3, 5, "Locating MPK asset...");

const mpkAsset = release.assets.find(asset => asset.name.endsWith(".mpk"));

if (!mpkAsset) {
printError("No MPK asset found in release");
printInfo(`Available assets: ${release.assets.map(a => a.name).join(", ")}`);
throw new Error("MPK asset not found");
}

printSuccess(`Found MPK asset: ${chalk.bold(mpkAsset.name)}`);
printInfo(`Asset ID: ${mpkAsset.id}`);
return mpkAsset;
}

async function downloadAndVerifyAsset(mpkAsset: GitHubReleaseAsset, downloadPath: string): Promise<string> {
printStep(4, 5, "Downloading and verifying MPK asset...");

printProgress(`Downloading to: ${downloadPath}`);
await gh.downloadReleaseAsset(mpkAsset.id, downloadPath);
printSuccess("Download completed");

printProgress("Computing SHA-256 hash...");
const fileHash = await computeHash(downloadPath);
printInfo(`Computed hash: ${fileHash}`);

const expectedDigest = mpkAsset.digest.replace("sha256:", "");
if (fileHash !== expectedDigest) {
printError("Hash mismatch detected!");
printInfo(`Expected: ${expectedDigest}`);
printInfo(`Got: ${fileHash}`);
throw new Error("Asset integrity verification failed");
}

printSuccess("Hash verification passed");
return fileHash;
}

async function runSbomGenerator(tmpFolder: string, releaseName: string, fileHash: string): Promise<string> {
printStep(5, 5, "Running SBOM Generator...");

printProgress("Generating OSS Clearance artifacts...");

const finalName = `${releaseName} [${fileHash}].zip`;
const finalPath = join(homedir(), "Downloads", finalName);

await generateSBomArtifactsInFolder(tmpFolder, SBOM_GENERATOR_JAR, releaseName, finalPath);
printSuccess("Completed.");

return finalPath;
}

async function computeHash(filepath: string): Promise<string> {
const input = createReadStream(filepath);
const hash = crypto.createHash("sha256");
await pipeline(input, hash);
return hash.digest("hex");
}

// ============================================================================
// Command Handlers
// ============================================================================

async function handlePrepareCommand(): Promise<void> {
printHeader("OSS Clearance Artifacts Preparation");

try {
// Step 1: Verify authentication
await verifyGitHubAuth();

// Step 2: Select release
const release = await selectRelease();

// Step 3: Find MPK asset
const mpkAsset = await findAndValidateMpkAsset(release);

// Prepare folder structure
const [tmpFolder, downloadPath] = await createSBomGeneratorFolderStructure(release.name);
printInfo(`Working directory: ${tmpFolder}`);

// Step 4: Download and verify
const fileHash = await downloadAndVerifyAsset(mpkAsset, downloadPath);

// Step 5: Run SBOM Generator
const finalPath = await runSbomGenerator(tmpFolder, release.name, fileHash);

console.log(chalk.bold.green(`\n🎉 Success! Output file:`));
console.log(chalk.cyan(` ${finalPath}\n`));
} catch (error) {
console.log("\n" + chalk.bold.red("═".repeat(60)));
printError(`Process failed: ${(error as Error).message}`);
console.log(chalk.bold.red("═".repeat(60)) + "\n");
process.exit(1);
}
}

async function handleIncludeCommand(): Promise<void> {
printHeader("OSS Clearance Readme Include");

try {
// TODO: Implement include command logic
// Step 1: Verify authentication
await verifyGitHubAuth();

// Step 2: Select release
const release = await selectRelease();

// Step 3: Find MPK asset
const mpkAsset = await findAndValidateMpkAsset(release);

// Step 4: Find and select OSS Readme
const readmes = findAllReadmeOssLocally();
const recommendedReadmeOss = getRecommendedReadmeOss(
release.name.split(" ")[0],
release.name.split(" ")[1],
readmes
);

let readmeToInclude: string;

if (!recommendedReadmeOss) {
const { selectedReadme } = await prompt<{ selectedReadme: string }>({
type: "select",
name: "selectedReadme",
message: "Select a release to process:",
choices: readmes.map(r => ({
name: r,
message: basename(r)
}))
});

readmeToInclude = selectedReadme;
} else {
readmeToInclude = recommendedReadmeOss;
}

printInfo(`Readme to include: ${readmeToInclude}`);

// Step 7: Upload updated asses to the draft release
const newAsset = await gh.uploadReleaseAsset(release.id, readmeToInclude, basename(readmeToInclude));
console.log(`Successfully uploaded asset ${newAsset.name} (ID: ${newAsset.id})`);

console.log(release.id);
} catch (error) {
console.log("\n" + chalk.bold.red("═".repeat(60)));
printError(`Process failed: ${(error as Error).message}`);
console.log(chalk.bold.red("═".repeat(60)) + "\n");
process.exit(1);
}
}

// ============================================================================
// Main Function
// ============================================================================

async function main(): Promise<void> {
const command = process.argv[2];

switch (command) {
case "prepare":
await handlePrepareCommand();
break;
case "include":
await handleIncludeCommand();
break;
default:
printError(command ? `Unknown command: ${command}` : "No command specified");
console.log(chalk.white("\nUsage:"));
console.log(
chalk.cyan(" rui-oss-clearance.ts prepare ") +
chalk.gray("- Prepare OSS clearance artifact from draft release")
);
console.log(
chalk.cyan(" rui-oss-clearance.ts include ") +
chalk.gray("- Include OSS Readme file into a draft release")
);
console.log();
process.exit(1);
}
}

// ============================================================================
// Entry Point
// ============================================================================

main().catch(e => {
console.error(chalk.red("\n💥 Unexpected error:"), e);
process.exit(1);
});
6 changes: 2 additions & 4 deletions automation/utils/bin/rui-prepare-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,15 +353,13 @@ async function createReleaseBranch(packageName: string, version: string): Promis
}

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

if (!projectKey || !baseUrl || !apiToken) {
console.error(chalk.red("❌ Missing Jira environment variables"));
console.log(chalk.dim(" Required variables:"));
console.log(chalk.dim(" export JIRA_PROJECT_KEY=WEB"));
console.log(chalk.dim(" export JIRA_BASE_URL=https://your-company.atlassian.net"));
console.log(chalk.dim(" export JIRA_API_TOKEN=username@your-company.com:ATATT3xFfGF0..."));
console.log(chalk.dim(" Get your API token at: https://id.atlassian.com/manage-profile/security/api-tokens"));
throw new Error("Missing Jira environment variables");
Expand Down
1 change: 1 addition & 0 deletions automation/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"compile:parser:widget": "peggy -o ./src/changelog-parser/parser/module/module.js ./src/changelog-parser/parser/module/module.pegjs",
"format": "prettier --write .",
"lint": "eslint --ext .jsx,.js,.ts,.tsx src/",
"oss-clearance": "ts-node bin/rui-oss-clearance.ts",
"prepare": "pnpm run compile:parser:widget && pnpm run compile:parser:module && tsc",
"prepare-release": "ts-node bin/rui-prepare-release.ts",
"start": "tsc --watch",
Expand Down
9 changes: 0 additions & 9 deletions automation/utils/src/changelog.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { gh } from "./github";
import { PublishedInfo } from "./package-info";
import { exec, popd, pushd } from "./shell";
import { findOssReadme } from "./oss-readme";
import { join } from "path";

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

const path = process.cwd();
const readmeossFile = findOssReadme(path, info.mxpackage.name, info.version.format());
if (readmeossFile) {
console.log(`Removing OSS clearance readme file '${readmeossFile}'...`);
await exec(`git rm '${readmeossFile}'`);
}

await exec(`git commit -m "chore(${info.name}): update changelog"`);
await exec(`git push ${remoteName} ${releaseBranchName}`);
popd();
Expand Down
Loading
Loading