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
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:
parent
97c569fb35
commit
002bd382e9
199 changed files with 14268 additions and 185 deletions
164
Scripts/apple/create-asc-certificate.mjs
Executable file
164
Scripts/apple/create-asc-certificate.mjs
Executable 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue