#!/usr/bin/env node import { Either, getRequiredEnvVars, type IEither, LogTraceable, LogMetricTraceable, Metric, prependWith, TraceUtil, getStdoutMany, } from '@emprespresso/pengueno'; import { Bitwarden, getPathOnHost, type SecureNote } from '@emprespresso/ci_worker'; import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; import { rmSync } from 'fs'; import { NpmPublishJob } from '@emprespresso/ci_model'; const eitherJob = getRequiredEnvVars(['source', 'registry']).mapRight( (baseArgs) => { type: 'npm_publish.js', arguments: baseArgs, }, ); const eitherVault = Bitwarden.getConfigFromEnvironment().mapRight((config) => new Bitwarden(config)); const READONLY_CREDENTIALS = { username: 'readonly', password: 'readonly' }; const REGISTRY = 'oci.liz.coffee'; const CI_PACKPUB_IMG = 'oci.liz.coffee/emprespresso/ci_packpub_npm:release'; const packPubMetric = Metric.fromName('npm_publish.packpub'); const _logJob = LogTraceable.of(eitherJob).flatMap(TraceUtil.withTrace('npm_publish')); await LogMetricTraceable.ofLogTraceable(_logJob) .flatMap(TraceUtil.withMetricTrace(packPubMetric)) .peek((tEitherJob) => tEitherJob.trace.trace('starting npm packpub job! (⑅˘꒳˘)')) .map((tEitherJob) => tEitherJob.get().flatMapAsync((job) => eitherVault.flatMapAsync(async (vault) => { const eitherKey = await vault.unlock(tEitherJob); tEitherJob.trace.trace('unlocked vault :3'); return eitherKey.mapRight((key) => ({ job, key, vault })); }), ), ) .map(async (tEitherJobVault) => (await tEitherJobVault.get()).flatMapAsync(({ job, key, vault }) => vault .fetchSecret(tEitherJobVault, key, 'npm_auth_token') .then((e) => e .mapRight(({ notes }) => notes) .mapRight((token) => [ `//${job.arguments.registry}/:_authToken=${token.trim()}`, `registry=https://${job.arguments.registry}/`, `always-auth=true`, ].join('\n'), ) .flatMapAsync((npmRc) => saveToTempFile(npmRc)), ) .then((e) => e.mapRight((npmRc) => ({ npmRc, job }))) .finally(() => vault.lock(tEitherJobVault, key)), ), ) .map(async (tEitherJobNpmRc) => { const jobNpmRc = await tEitherJobNpmRc.get(); return jobNpmRc.flatMapAsync(async ({ job, npmRc }) => { const [srcMount, npmRcMount] = await Promise.all( [join(process.cwd(), job.arguments.source), npmRc].map((x) => getPathOnHost(x).then((e) => e.right().get()), ), ); const volumes = [`${srcMount}:/src`, `${npmRcMount}:/root/.npmrc`]; const packPub = [ `docker login --username ${READONLY_CREDENTIALS.username} --password ${READONLY_CREDENTIALS.password} ${REGISTRY}`.split( ' ', ), ].concat([['docker', 'run', ...prependWith(volumes, '-v'), CI_PACKPUB_IMG]]); tEitherJobNpmRc.trace.trace(`running packpub magic~ (◕ᴗ◕✿) ${packPub}`); return tEitherJobNpmRc .move(packPub) .map((c) => getStdoutMany(c, { streamTraceable: ['stdout', 'stderr'] }).then((e) => { rmSync(npmRcMount!); return e; }), ) .get(); }); }) .map(async (tEitherJob) => { const eitherJob = await tEitherJob.get(); return eitherJob.fold( (e) => Promise.reject(e), () => Promise.resolve(0), ); }) .get(); async function saveToTempFile(text: string): Promise> { const dir = join(process.cwd(), '.secrets', crypto.randomUUID()); const file = join(dir, 'secret'); return Either.fromFailableAsync(() => mkdir(dir, { recursive: true }).then(async () => { await writeFile(file, text, { encoding: 'utf-8' }); return file; }), ); }