blob: 704bbbb64d78ce1c747dc3c7bf15644b292f8680 [file]
import crypto from "crypto";
import {
readFileSync,
mkdirSync,
existsSync,
copyFileSync,
writeFileSync,
createWriteStream,
unlink,
} from "fs";
import https from "https";
import { resolve } from "path";
/**
* Create a bcr entry for a new version of this repository.
*
* Usage: create-bcr-entry [project_path] [bcr_path] [owner_slash_repo] [tag]
*
* project_path: path to the project's repository; should contain a
* root level MODULE.bazel file and a .github/workflows/bcr folder
* with templated bcr entry files.
* bcr_path: path to the bcr repository
* owner_slash_repo: the github owner/repository name of the project
* tag: the github tag for this version, e.g., "v1.0.0" or "1.0.0"
*
*/
async function main(argv) {
if (argv.length !== 4) {
console.error(
"usage: create-bcr-entry [project_path] [bcr_path] [owner_slash_repo] [tag]"
);
process.exit(1);
}
const [projectPath, bcrPath, ownerSlashRepo, tag] = argv;
const version = getVersionFromTag(tag);
const moduleName = getModuleName(resolve(projectPath, "MODULE.bazel"));
const bcrTemplatesPath = resolve(projectPath, ".github/workflows/bcr");
const bcrEntryPath = resolve(bcrPath, "modules", moduleName);
const bcrVersionEntryPath = resolve(bcrEntryPath, version);
if (!existsSync(bcrEntryPath)) {
mkdirSync(bcrEntryPath);
}
updateMetadataFile(
resolve(bcrTemplatesPath, "metadata.template.json"),
resolve(bcrEntryPath, "metadata.json"),
version
);
mkdirSync(bcrVersionEntryPath);
stampModuleFile(
resolve(projectPath, "MODULE.bazel"),
resolve(bcrVersionEntryPath, "MODULE.bazel"),
version
);
await stampSourceFile(
resolve(bcrTemplatesPath, "source.template.json"),
resolve(bcrVersionEntryPath, "source.json"),
ownerSlashRepo,
version,
tag
);
// Copy over the presubmit file
copyFileSync(
resolve(bcrTemplatesPath, "presubmit.yml"),
resolve(bcrVersionEntryPath, "presubmit.yml")
);
}
function getModuleName(modulePath) {
const moduleContent = readFileSyncOrFail(
modulePath,
"Cannot find MODULE.bazel; bzlmod requires this file at the root of your workspace."
);
const regex = /module\(.*?name\s*=\s*"(\w+)"/s;
const match = moduleContent.match(regex);
if (match) {
return match[1];
}
throw new Error("Could not parse module name from module file");
}
function updateMetadataFile(sourcePath, destPath, version) {
let publishedVersions = [];
if (existsSync(destPath)) {
const existingMetadata = JSON.parse(
readFileSync(destPath, { encoding: "utf-8" })
);
publishedVersions = existingMetadata.versions;
}
if (publishedVersions.includes(version)) {
console.error(`Version ${version} is already published to this registry`);
process.exit(1);
}
const metadata = JSON.parse(
readFileSyncOrFail(sourcePath),
`Cannot find metadata template ${sourcePath}; did you forget to create it?`
);
metadata.versions = [...publishedVersions, version];
writeFileSync(destPath, JSON.stringify(metadata, null, 4) + "\n");
}
function stampModuleFile(sourcePath, destPath, version) {
const module = readFileSyncOrFail(
sourcePath,
"Cannot find MODULE.bazel; bzlmod requires this file at the root of your workspace."
);
const stampedModule = module.replace(
/(^.*?module\(.*?version\s*=\s*")[\w.]+(".*$)/s,
`$1${version}$2`
);
writeFileSync(destPath, stampedModule, {
encoding: "utf-8",
});
}
async function stampSourceFile(
sourcePath,
destPath,
ownerSlashRepo,
version,
tag
) {
const owner = ownerSlashRepo.substring(0, ownerSlashRepo.indexOf("/"));
const repo = ownerSlashRepo.substring(ownerSlashRepo.indexOf("/") + 1);
// Substitute variables into source.json
const sourceContent = readFileSyncOrFail(
sourcePath,
`Cannot find source template ${sourcePath}; did you forget to create it?`
);
const substituted = sourceContent
.replace(/{REPO}/g, repo)
.replace(/{OWNER}/g, owner)
.replace(/{VERSION}/g, version)
.replace(/{TAG}/g, tag);
// Compute the integrity hash
const sourceJson = JSON.parse(substituted);
const filename = sourceJson.url.substring(
sourceJson.url.lastIndexOf("/") + 1
);
console.log(`Downloading archive ${sourceJson.url}`);
await download(sourceJson.url, filename);
console.log("Finished downloading");
const hash = crypto.createHash("sha256");
hash.update(readFileSync(filename));
const digest = hash.digest("base64");
sourceJson.integrity = `sha256-${digest}`;
writeFileSync(destPath, JSON.stringify(sourceJson, undefined, 4), {
encoding: "utf-8",
});
}
function getVersionFromTag(version) {
if (version.startsWith("v")) {
return version.substring(1);
}
}
function download(url, dest) {
return new Promise((resolve, reject) => {
const request = https.get(url, (response) => {
if (response.statusCode === 200) {
const file = createWriteStream(dest, { flags: "wx" });
file.on("finish", () => resolve());
file.on("error", (err) => {
file.close();
unlink(dest, () => reject(err.message));
reject(err);
});
response.pipe(file);
} else if (response.statusCode === 302 || response.statusCode === 301) {
// Redirect
download(response.headers.location, dest).then(() => resolve());
} else {
reject(
`Server responded with ${response.statusCode}: ${response.statusMessage}`
);
}
});
request.on("error", (err) => {
reject(err.message);
});
});
}
function readFileSyncOrFail(filename, notExistsMsg) {
try {
return readFileSync(filename, { encoding: "utf-8" });
} catch (error) {
if (error.code === "ENOENT") {
console.error(notExistsMsg);
process.exit(1);
}
throw error;
}
}
(async () => {
const argv = process.argv.slice(2);
try {
await main(argv);
} catch (error) {
console.error(error);
process.exit(1);
}
})();