@cashu/cashu-ts
Version:
cashu library for communicating with a cashu mint
206 lines (173 loc) • 5.73 kB
text/typescript
import fs from 'fs';
import fetch from 'node-fetch';
// -----------------------------
// Config per dependency
// -----------------------------
const MAKEFILE = 'Makefile';
type Dep = {
name: string;
githubRepo: string;
dockerTagPrefix: string;
stableVar: string;
rcVar: string;
};
const DEPENDENCIES: Dep[] = [
{
name: 'cashubtc/mintd',
githubRepo: 'cashubtc/cdk',
dockerTagPrefix: 'v',
stableVar: 'CDK_IMAGE',
rcVar: 'CDK_IMAGE_RC',
},
{
name: 'cashubtc/nutshell',
githubRepo: 'cashubtc/nutshell',
dockerTagPrefix: '',
stableVar: 'NUT_IMAGE',
rcVar: 'NUT_IMAGE_RC',
},
];
// for github output
interface Update {
name: string;
version: string;
}
const updates: Update[] = [];
// -----------------------------
// Helpers
// -----------------------------
function setGithubOutput(name: string, value: string) {
const outputPath = process.env.GITHUB_OUTPUT;
if (!outputPath) return;
fs.appendFileSync(outputPath, `${name}<<EOF\n${value}\nEOF\n`);
}
// Write PR outputs
function writePrOutputs(updates: Update[]): void {
if (updates.length === 0) {
setGithubOutput('changed', 'false');
return;
}
setGithubOutput('changed', 'true');
const summary = updates.map((u) => `- ${u.name} -> ${u.version}`).join('\n');
setGithubOutput('summary', summary);
const title =
updates.length === 1
? `chore(docker): update ${updates[0].name} to ${updates[0].version}`
: 'chore(docker): update mint images';
setGithubOutput('title', title);
setGithubOutput('branch', 'automation/update-mint-images');
}
type GithubRelease = {
tag_name: string;
prerelease: boolean;
draft: boolean;
};
async function fetchGithubReleases(repo: string): Promise<GithubRelease[]> {
const url = `https://api.github.com/repos/${repo}/releases?per_page=100`;
const headers: Record<string, string> = { Accept: 'application/vnd.github+json' };
const token = process.env.GITHUB_TOKEN;
if (token) headers['Authorization'] = `Bearer ${token}`;
const resp = await fetch(url, { headers });
if (!resp.ok) {
throw new Error(`Failed to fetch releases for ${repo}: ${resp.statusText}`);
}
return (await resp.json()) as GithubRelease[];
}
function stripV(tag: string): string {
return tag.replace(/^v/, '');
}
function semverKey(tag: string): number[] {
const base = tag.split('-')[0];
return base.split('.').map((x) => parseInt(x, 10));
}
function isRc(version: string): boolean {
return /^\d+\.\d+\.\d+-rc\.\d+$/.test(version);
}
function getLatestStable(releases: GithubRelease[]): string | null {
const stable = releases
.filter((r) => !r.draft && !r.prerelease)
.map((r) => stripV(r.tag_name))
.filter((v) => /^\d+\.\d+\.\d+$/.test(v));
if (stable.length === 0) return null;
return stable.sort(compareSemver).pop()!;
}
function getLatestRc(releases: GithubRelease[]): string | null {
const rcs = releases
.filter((r) => !r.draft && r.prerelease)
.map((r) => stripV(r.tag_name))
.filter(isRc);
if (rcs.length === 0) return null;
return rcs.sort(compareSemver).pop()!;
}
function compareSemver(a: string, b: string): number {
const parse = (v: string) => {
const [base, suffix] = v.split('-');
const parts = base.split('.').map((x) => parseInt(x, 10));
const rcMatch = suffix?.match(/^rc\.(\d+)$/);
const rc = rcMatch ? parseInt(rcMatch[1], 10) : null;
return { parts, rc };
};
const av = parse(a);
const bv = parse(b);
for (let i = 0; i < 3; i++) {
if (av.parts[i] !== bv.parts[i]) return av.parts[i] - bv.parts[i];
}
if (av.rc !== null && bv.rc !== null) return av.rc - bv.rc;
return 0;
}
async function dockerTagExists(repo: string, tag: string): Promise<boolean> {
const resp = await fetch(`https://hub.docker.com/v2/repositories/${repo}/tags/${tag}`);
return resp.ok;
}
function updateMakefile(varName: string, newTag: string) {
const content = fs.readFileSync(MAKEFILE, 'utf-8');
const regex = new RegExp(`^${varName}\\s*\\?=.*$`, 'm');
const updated = content.replace(regex, `${varName} ?= ${newTag}`);
if (updated !== content) {
fs.writeFileSync(MAKEFILE, updated);
console.log(`Updated ${varName} to ${newTag}`);
// Only writes in CI
updates.push({ name: varName, version: newTag });
return true;
} else {
console.log(`No update needed for ${varName}`);
return false;
}
}
// -----------------------------
// Main
// -----------------------------
async function main() {
for (const dep of DEPENDENCIES) {
const releases = await fetchGithubReleases(dep.githubRepo);
const stable = getLatestStable(releases);
const rc = getLatestRc(releases);
if (stable) {
const dockerTag = `${dep.dockerTagPrefix}${stable}`;
if (await dockerTagExists(dep.name, dockerTag)) {
updateMakefile(dep.stableVar, `${dep.name}:${dockerTag}`);
} else {
console.log(`Docker image ${dep.name}:${dockerTag} not yet available, skipping`);
}
}
if (rc) {
const stableVer = stable ? semverKey(stable) : [0, 0, 0];
const rcVer = semverKey(rc.split('-')[0]);
if (rcVer.some((v, i) => v > (stableVer[i] || 0))) {
const dockerTag = `${dep.dockerTagPrefix}${rc}`;
if (await dockerTagExists(dep.name, dockerTag)) {
updateMakefile(dep.rcVar, `${dep.name}:${dockerTag}`);
} else {
console.log(`Docker image ${dep.name}:${dockerTag} not yet available, skipping`);
}
} else {
console.log(`RC ${rc} is not newer than stable ${stable}, skipping RC update`);
}
}
}
writePrOutputs(updates);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});