summaryrefslogtreecommitdiff
path: root/worker/scripts/npm_publish.ts
blob: d2e14da0d1e57fc7d864e1be413b9eb84cf7bb7a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/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) =>
        <NpmPublishJob>{
            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<SecureNote>(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}:/etc/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<IEither<Error, string>> {
    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;
        }),
    );
}