Wire Forge-native release infrastructure
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s

This commit is contained in:
Conrad Kramer 2026-06-07 05:51:12 -07:00
parent 97c569fb35
commit 002bd382e9
199 changed files with 14268 additions and 185 deletions

View file

@ -0,0 +1,164 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { webcrypto } from "node:crypto";
const crypto = globalThis.crypto ?? webcrypto;
function b64url(input) {
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
return buf
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function parseArgs(argv) {
const out = { _: [] };
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") out.help = true;
else if (arg.startsWith("--")) {
const key = arg.slice(2);
const value = argv[i + 1];
if (!value || value.startsWith("--")) throw new Error(`missing value for --${key}`);
out[key] = value;
i++;
} else {
out._.push(arg);
}
}
return out;
}
async function makeJwt({ keyId, issuerId, keyPem }) {
const header = { alg: "ES256", kid: keyId, typ: "JWT" };
const now = Math.floor(Date.now() / 1000);
const payload = { iss: issuerId, exp: now + 20 * 60, aud: "appstoreconnect-v1" };
const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
const pkcs8Der = Buffer.from(
keyPem.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\s+/g, ""),
"base64",
);
const key = await crypto.subtle.importKey(
"pkcs8",
pkcs8Der,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
key,
new TextEncoder().encode(input),
);
return `${input}.${b64url(Buffer.from(sig))}`;
}
async function ascFetch(jwt, url, opts = {}) {
const res = await fetch(url, {
...opts,
headers: {
...(opts.headers ?? {}),
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
},
});
const text = await res.text();
let body;
try {
body = text ? JSON.parse(text) : {};
} catch {
body = { raw: text };
}
if (!res.ok) {
throw new Error(`ASC ${res.status} ${url}: ${JSON.stringify(body).slice(0, 2000)}`);
}
return body;
}
function certificateFileName(type, id) {
return `${type.toLowerCase().replace(/_/g, "-")}-${id}.cer`;
}
function csrContentForApi(csrFile) {
const bytes = fs.readFileSync(csrFile);
const text = bytes.toString("ascii");
const pemMatch = text.match(/-----BEGIN CERTIFICATE REQUEST-----([\s\S]+?)-----END CERTIFICATE REQUEST-----/);
if (pemMatch) {
return pemMatch[1].replace(/\s+/g, "");
}
return bytes.toString("base64");
}
async function main() {
const args = parseArgs(process.argv);
if (args.help) {
console.log("Usage: create-asc-certificate.mjs --key-id ID --issuer-id ID --key-file AuthKey.p8 --certificate-type IOS_DISTRIBUTION --csr-file file.csr --out-dir DIR");
process.exit(0);
}
const keyId = args["key-id"] ?? process.env.APP_STORE_CONNECT_KEY_ID;
const issuerId = args["issuer-id"] ?? process.env.APP_STORE_CONNECT_ISSUER_ID;
const keyFile = args["key-file"] ?? process.env.APP_STORE_CONNECT_KEY_FILE;
const certificateType = args["certificate-type"] ?? "IOS_DISTRIBUTION";
const csrFile = args["csr-file"];
const outDir = args["out-dir"] ?? ".";
const outFile = args["out-file"];
const metadataFile = args["metadata-file"];
if (!keyId || !issuerId || !keyFile || !csrFile) {
throw new Error("required: --key-id --issuer-id --key-file --csr-file");
}
const jwt = await makeJwt({
keyId,
issuerId,
keyPem: fs.readFileSync(keyFile, "utf8"),
});
const csrContent = csrContentForApi(csrFile);
const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/certificates", {
method: "POST",
body: JSON.stringify({
data: {
type: "certificates",
attributes: {
certificateType,
csrContent,
},
},
}),
});
const cert = created?.data;
const id = cert?.id;
const content = cert?.attributes?.certificateContent;
if (!id || !content) {
throw new Error(`ASC response did not include certificate id/content: ${JSON.stringify(created).slice(0, 2000)}`);
}
fs.mkdirSync(outDir, { recursive: true });
const certificatePath = outFile ?? path.join(outDir, certificateFileName(certificateType, id));
fs.writeFileSync(certificatePath, Buffer.from(content, "base64"));
const metadata = {
id,
certificateType: cert?.attributes?.certificateType ?? certificateType,
displayName: cert?.attributes?.displayName,
name: cert?.attributes?.name,
platform: cert?.attributes?.platform,
expirationDate: cert?.attributes?.expirationDate,
serialNumber: cert?.attributes?.serialNumber,
certificatePath,
};
if (metadataFile) {
fs.writeFileSync(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
}
process.stdout.write(`${JSON.stringify(metadata, null, 2)}\n`);
}
main().catch((err) => {
console.error(err?.stack || String(err));
process.exit(1);
});