mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: pmg publish to npm * feat: add github action for publishing to npm * Update publish/npm/install.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * Update .github/workflows/publish-npm.yml Co-authored-by: Omkar Phansopkar <omkarphansopkar@gmail.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * refactor: define constants * refactor: improve npm package reliability with removing dynamic handlers and better error handling and validation * fix: publish-npm workflow * Update publish/npm/test.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * refactor(npm-workflow): use a more portable loop construct * refactor: remove version field from package.json * feat: use os.tmpdir() with mkdtempSync for cleaner temp directory handling * fix: add redirect limit and cleanup to prevent infinite redirects and resource leaks * remove test.js for npm publisher pack * chore: test cmd cleanup --------- Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
225 lines
6.1 KiB
JavaScript
225 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
const https = require("https");
|
|
const crypto = require("crypto");
|
|
const { execSync } = require("child_process");
|
|
const {
|
|
BINARY_NAME,
|
|
REPO_OWNER,
|
|
REPO_NAME,
|
|
GITHUB_RELEASES_BASE,
|
|
BINARY_PATTERNS,
|
|
} = require("./config");
|
|
|
|
// Read version from package.json with strict validation
|
|
function getValidatedVersion() {
|
|
try {
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
|
);
|
|
|
|
const version = packageJson.version;
|
|
|
|
// Strict validation: must be valid semver (x.y.z)
|
|
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
throw new Error(`Invalid version format: ${version}`);
|
|
}
|
|
|
|
return `v${version}`;
|
|
} catch (error) {
|
|
throw new Error(`Failed to read valid version: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
const RELEASE_VERSION = getValidatedVersion();
|
|
const BASE_URL = `${GITHUB_RELEASES_BASE}/${RELEASE_VERSION}`;
|
|
|
|
// Platform-specific binary URLs (constructed from config)
|
|
const BINARY_URLS = {};
|
|
Object.keys(BINARY_PATTERNS).forEach((platform) => {
|
|
BINARY_URLS[platform] = `${BASE_URL}/${BINARY_PATTERNS[platform]}`;
|
|
});
|
|
|
|
const CHECKSUMS_URL = `${BASE_URL}/checksums.txt`;
|
|
|
|
function getPlatformKey() {
|
|
const platform = process.platform;
|
|
const arch = process.arch;
|
|
return `${platform}-${arch}`;
|
|
}
|
|
|
|
function downloadFile(url, dest, maxRedirects = 5) {
|
|
return new Promise((resolve, reject) => {
|
|
if (maxRedirects < 0) {
|
|
reject(new Error("Too many redirects"));
|
|
return;
|
|
}
|
|
|
|
const file = fs.createWriteStream(dest);
|
|
|
|
https
|
|
.get(url, (response) => {
|
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
file.close();
|
|
fs.unlink(dest, () => {});
|
|
return downloadFile(response.headers.location, dest, maxRedirects - 1)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
}
|
|
|
|
if (response.statusCode !== 200) {
|
|
file.close();
|
|
fs.unlink(dest, () => {});
|
|
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
return;
|
|
}
|
|
|
|
response.pipe(file);
|
|
|
|
file.on("finish", () => {
|
|
file.close();
|
|
resolve();
|
|
});
|
|
|
|
file.on("error", (err) => {
|
|
fs.unlink(dest, () => {});
|
|
reject(err);
|
|
});
|
|
})
|
|
.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function calculateChecksum(filePath) {
|
|
const fileBuffer = fs.readFileSync(filePath);
|
|
const hashSum = crypto.createHash("sha256");
|
|
hashSum.update(fileBuffer);
|
|
return hashSum.digest("hex");
|
|
}
|
|
|
|
function validateChecksum(filePath, expectedChecksum) {
|
|
const actualChecksum = calculateChecksum(filePath);
|
|
return actualChecksum === expectedChecksum;
|
|
}
|
|
|
|
function extractArchive(archivePath, extractDir) {
|
|
const isZip = archivePath.endsWith(".zip");
|
|
|
|
if (isZip) {
|
|
execSync(`unzip -o "${archivePath}" -d "${extractDir}"`, { stdio: "pipe" });
|
|
} else {
|
|
execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, { stdio: "pipe" });
|
|
}
|
|
}
|
|
|
|
async function install() {
|
|
let tempWorkspace;
|
|
|
|
try {
|
|
console.log("📦 Installing PMG binary...");
|
|
|
|
// Get platform-specific URL
|
|
const platformKey = getPlatformKey();
|
|
const binaryUrl = BINARY_URLS[platformKey];
|
|
|
|
if (!binaryUrl) {
|
|
throw new Error(`Unsupported platform: ${platformKey}`);
|
|
}
|
|
|
|
console.log(`🔍 Platform: ${platformKey}`);
|
|
console.log(`📡 Version: ${RELEASE_VERSION}`);
|
|
|
|
// Create directories
|
|
const binDir = path.join(__dirname, "bin");
|
|
tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "pmg-install-"));
|
|
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
|
|
// Download binary archive
|
|
const archiveFilename = path.basename(binaryUrl);
|
|
const archivePath = path.join(tempWorkspace, archiveFilename);
|
|
|
|
console.log(`⬇️ Downloading binary...`);
|
|
await downloadFile(binaryUrl, archivePath);
|
|
|
|
// Download checksums
|
|
const checksumsPath = path.join(tempWorkspace, "checksums.txt");
|
|
console.log(`⬇️ Downloading checksums...`);
|
|
await downloadFile(CHECKSUMS_URL, checksumsPath);
|
|
|
|
// Parse checksums file
|
|
const checksumsContent = fs.readFileSync(checksumsPath, "utf8");
|
|
const checksumLines = checksumsContent.split("\n");
|
|
|
|
let expectedChecksum = null;
|
|
for (const line of checksumLines) {
|
|
if (line.includes(archiveFilename)) {
|
|
expectedChecksum = line.split(/\s+/)[0];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!expectedChecksum) {
|
|
throw new Error(`Checksum not found for ${archiveFilename}`);
|
|
}
|
|
|
|
// Validate checksum
|
|
console.log(`🔐 Validating checksum...`);
|
|
if (!validateChecksum(archivePath, expectedChecksum)) {
|
|
throw new Error(
|
|
"Checksum validation failed - binary may be corrupted or tampered",
|
|
);
|
|
}
|
|
|
|
console.log(`✅ Checksum validated`);
|
|
|
|
// Extract archive
|
|
console.log(`📂 Extracting binary...`);
|
|
extractArchive(archivePath, tempWorkspace);
|
|
|
|
// Find and move binary
|
|
const binaryName =
|
|
process.platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
const extractedBinaryPath = path.join(tempWorkspace, binaryName);
|
|
const finalBinaryPath = path.join(binDir, binaryName);
|
|
|
|
if (!fs.existsSync(extractedBinaryPath)) {
|
|
throw new Error(
|
|
`Binary not found at expected location: ${extractedBinaryPath}`,
|
|
);
|
|
}
|
|
|
|
// Move binary to final location
|
|
fs.renameSync(extractedBinaryPath, finalBinaryPath);
|
|
|
|
// Make executable on Unix systems
|
|
if (process.platform !== "win32") {
|
|
fs.chmodSync(finalBinaryPath, "755");
|
|
}
|
|
|
|
// Clean up
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
|
|
console.log("✅ PMG binary installed successfully!");
|
|
} catch (error) {
|
|
console.error("❌ Installation failed:", error.message);
|
|
|
|
// Clean up on failure
|
|
try {
|
|
if (tempWorkspace && fs.existsSync(tempWorkspace)) {
|
|
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
}
|
|
} catch (cleanupError) {
|
|
console.warn("⚠️ Failed to clean up:", cleanupError.message);
|
|
}
|
|
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run installation
|
|
install();
|