summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-07-26 21:07:36 -0700
committerElizabeth Hunt <me@liz.coffee>2025-07-27 00:00:12 -0700
commitdf76fa3c266f7f9b22d2bfaf98ad5accebcabd35 (patch)
tree8aaef9f227e385b8e31bd6c69fc6a8231326f361
parent9ee3bf3345b006a745b2ee28fee3613819011796 (diff)
downloadci-df76fa3c266f7f9b22d2bfaf98ad5accebcabd35.tar.gz
ci-df76fa3c266f7f9b22d2bfaf98ad5accebcabd35.zip
Fixes type inference for Either.joinRightAsync. Regarding ansible-docker. Will it ever be okay to trust docs?!!
-rw-r--r--README.md4
-rw-r--r--u/types/fn/either.ts6
-rwxr-xr-xworker/scripts/ansible_playbook.ts99
-rwxr-xr-xworker/scripts/fetch_code10
-rw-r--r--worker/secret.ts10
5 files changed, 74 insertions, 55 deletions
diff --git a/README.md b/README.md
index 3bc05a7..8607d48 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# @emprespresso/ci (⑅˘꒳˘)
-this is my ci server, built on top of [laminar](https://laminar.ohwg.net/docs.html), because while the jenkins logo looks hot and classy, i want something to hack on myself!
+this is my ci server, built on top of [laminar](https://laminar.ohwg.net/docs.html), because while the jenkins logo looks hot and classy, i want something to hack on myself
-also, to scrap out pengueno :3
+also! to scrap out pengueno :3
## how to use it? (。•̀ᴗ-)✧
diff --git a/u/types/fn/either.ts b/u/types/fn/either.ts
index 5e2dca0..0f65859 100644
--- a/u/types/fn/either.ts
+++ b/u/types/fn/either.ts
@@ -28,8 +28,8 @@ export interface IEither<E, T> extends Tagged<IEitherTag> {
readonly fold: <_T>(leftFolder: Mapper<E, _T>, rightFolder: Mapper<T, _T>) => _T;
readonly joinRight: <O, _T>(other: IEither<E, O>, mapper: (a: O, b: T) => _T) => IEither<E, _T>;
readonly joinRightAsync: <O, _T>(
- other: Supplier<Promise<IEither<E, O>>> | Promise<IEither<E, O>>,
- mapper: BiMapper<O, T, _T>,
+ other: (() => Promise<IEither<E, O>>) | Promise<IEither<E, O>>,
+ mapper: (a: O, b: T) => _T,
) => Promise<IEither<E, _T>>;
}
@@ -115,7 +115,7 @@ export class Either<E, T> extends _Tagged implements IEither<E, T> {
) {
return this.flatMapAsync(async (t) => {
const o = typeof other === 'function' ? other() : other;
- return o.then((other) => other.mapRight((o) => mapper(o, t)));
+ return await o.then((other) => other.mapRight((o) => mapper(o, t)));
});
}
diff --git a/worker/scripts/ansible_playbook.ts b/worker/scripts/ansible_playbook.ts
index 20f85d8..2048d44 100755
--- a/worker/scripts/ansible_playbook.ts
+++ b/worker/scripts/ansible_playbook.ts
@@ -12,10 +12,10 @@ import {
TraceUtil,
} from '@emprespresso/pengueno';
import type { AnsiblePlaybookJob } from '@emprespresso/ci_model';
-import { Bitwarden, type SecureNote } from '@emprespresso/ci_worker';
-import { writeFile, mkdtemp } from 'fs/promises';
+import { Bitwarden, BitwardenKey, getPathOnHost, type SecureNote } from '@emprespresso/ci_worker';
+import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
-import { tmpdir } from 'os';
+import { rmSync } from 'fs';
const eitherJob = getRequiredEnvVars(['path', 'playbooks']).mapRight(
(baseArgs) =>
@@ -41,51 +41,69 @@ await LogMetricTraceable.ofLogTraceable(_logJob)
}),
),
)
- .map(async (tEitherJobVault) => {
- const eitherJobVault = await tEitherJobVault.get();
-
- const eitherSshKey = await eitherJobVault.flatMapAsync(({ key, vault }) =>
- vault.fetchSecret<SecureNote>(tEitherJobVault, key, 'ssh_key'),
- );
- const eitherSshKeyFile = await eitherSshKey.mapRight(({ notes }) => notes).flatMapAsync(saveToTempFile);
- const eitherAnsibleSecrets = await eitherJobVault.flatMapAsync(({ key, vault }) =>
- vault.fetchSecret<SecureNote>(tEitherJobVault, key, 'ansible_secrets'),
- );
- const eitherAnsibleSecretsFile = await eitherAnsibleSecrets
- .mapRight(({ notes }) => notes)
- .flatMapAsync(saveToTempFile);
-
- return eitherJobVault.flatMapAsync(async ({ job, vault, key }) => {
- const eitherLocked = await vault.lock(tEitherJobVault, key);
- return eitherLocked.flatMap((_locked) =>
- eitherSshKeyFile.flatMap((sshKeyFile) =>
- eitherAnsibleSecretsFile.mapRight((secretsFile) => ({
- job,
- sshKeyFile,
- secretsFile,
- })),
- ),
- );
- });
- })
+ .map(
+ async (
+ tEitherJobVault,
+ ): Promise<
+ IEither<
+ Error,
+ {
+ secretFiles: { ssh_key: string; ansible_secrets: string };
+ key: BitwardenKey;
+ vault: Bitwarden;
+ job: AnsiblePlaybookJob;
+ }
+ >
+ > =>
+ ['ssh_key', 'ansible_secrets'].reduce(
+ async (_result, secret) => {
+ const result = await _result;
+ return result.joinRightAsync(
+ result.flatMapAsync(({ key, vault, job }) =>
+ vault
+ .fetchSecret<SecureNote>(tEitherJobVault, key, secret)
+ .then((eSecret) => eSecret.flatMapAsync(({ notes }) => saveToTempFile(job, notes))),
+ ),
+ (file, prev) => ({ ...prev, secretFiles: { ...prev.secretFiles, [secret]: file } }),
+ );
+ },
+ tEitherJobVault.get().then((e) => e.mapRight((r) => ({ ...r, secretFiles: <any>{} }))),
+ ),
+ )
.map(async (tEitherJobAndSecrets) => {
const eitherJobAndSecrets = await tEitherJobAndSecrets.get();
- return eitherJobAndSecrets.flatMapAsync(({ job, sshKeyFile, secretsFile }) => {
+ return eitherJobAndSecrets.flatMapAsync(async ({ job, secretFiles }) => {
+ const [src, sshKey, ansibleSecrets] = (
+ await Promise.all(
+ [join(process.cwd(), job.arguments.path), secretFiles.ssh_key, secretFiles.ansible_secrets].map(
+ (x) => getPathOnHost(x),
+ ),
+ )
+ ).map((x) => x.right().get());
const volumes = [
- `${job.arguments.path}:/ansible`,
- `${sshKeyFile}:/root/id_rsa`,
- `${secretsFile}:/ansible/secrets.yml`,
+ `${src}:/ansible`,
+ `${sshKey}:/root/.ssh/id_ed25519`,
+ `${ansibleSecrets}:/ansible/secrets.yml`,
];
const playbookCmd = `ansible-playbook -e @secrets.yml ${job.arguments.playbooks}`;
const deployCmd = [
'docker',
'run',
...prependWith(volumes, '-v'),
+ '--workdir=/ansible',
'willhallonline/ansible:latest',
...playbookCmd.split(' '),
];
tEitherJobAndSecrets.trace.trace(`running ansible magic~ (◕ᴗ◕✿) ${deployCmd}`);
- return tEitherJobAndSecrets.move(deployCmd).map(getStdout).get();
+ return tEitherJobAndSecrets
+ .move(deployCmd)
+ .map((c) =>
+ getStdout(c, { streamTraceable: ['stdout', 'stderr'] }).then((e) => {
+ Object.values(secretFiles).forEach((f) => rmSync(f));
+ return e;
+ }),
+ )
+ .get();
});
})
.map(async (tEitherJob) => {
@@ -97,12 +115,13 @@ await LogMetricTraceable.ofLogTraceable(_logJob)
})
.get();
-function saveToTempFile(text: string): Promise<IEither<Error, string>> {
+async function saveToTempFile(job: AnsiblePlaybookJob, text: string): Promise<IEither<Error, string>> {
+ const dir = join(process.cwd(), '.secrets', crypto.randomUUID());
+ const file = join(dir, 'secret');
return Either.fromFailableAsync(() =>
- mkdtemp(join(tmpdir(), 'ci-')).then(async (dir) => {
- const filePath = join(dir, 'temp-file');
- await writeFile(filePath, text);
- return filePath;
+ mkdir(dir, { recursive: true }).then(async () => {
+ await writeFile(file, text, { encoding: 'utf-8' });
+ return file;
}),
);
}
diff --git a/worker/scripts/fetch_code b/worker/scripts/fetch_code
index 2691832..d711f82 100755
--- a/worker/scripts/fetch_code
+++ b/worker/scripts/fetch_code
@@ -1,10 +1,10 @@
#!/bin/bash
-export LOG_PREFIX="[fetch_code $remote @ $checkout -> $path]"
+export LOG_PREFIX="[fetch_code $remoteUrl @ $checkout -> $path]"
-if [[ "$remote" == ssh://* ]]; then
- host=$(echo "$remote" | sed -E 's#ssh://([^:]+):[0-9]+/.*#\1#')
- port=$(echo "$remote" | sed -E 's#ssh://[^:]+:([0-9]+)/.*#\1#')
+if [[ "$remoteUrl" == ssh://* ]]; then
+ host=$(echo "$remoteUrl" | sed -E 's#ssh://([^:]+):[0-9]+/.*#\1#')
+ port=$(echo "$remoteUrl" | sed -E 's#ssh://[^:]+:([0-9]+)/.*#\1#')
log "populating host keyz~ $host:$port"
ssh-keyscan -p "$port" "$host" > ./cur_known_hosts
@@ -14,7 +14,7 @@ if [[ "$remote" == ssh://* ]]; then
fi
log "getting the codez~ time to fetch!"
-git clone "$remote" "$path"
+git clone "$remoteUrl" "$path"
if [ ! $? -eq 0 ]; then
log "D: oh nyo! couldn't clone the repo"
exit 1
diff --git a/worker/secret.ts b/worker/secret.ts
index 11daf06..071b539 100644
--- a/worker/secret.ts
+++ b/worker/secret.ts
@@ -39,12 +39,12 @@ export interface IVault<TClient, TKey, TItemId> {
// -- <Vault> --
type TClient = ITraceable<unknown, LogMetricTraceSupplier>;
-type TKey = {
+export type BitwardenKey = {
BW_SESSION: string;
BITWARDENCLI_APPDATA_DIR: string;
};
type TItemId = string;
-export class Bitwarden implements IVault<TClient, TKey, TItemId> {
+export class Bitwarden implements IVault<TClient, BitwardenKey, TItemId> {
constructor(private readonly config: BitwardenConfig) {}
public unlock(client: TClient) {
@@ -52,7 +52,7 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
.move(this.config)
.flatMap(TraceUtil.withMetricTrace(Bitwarden.loginMetric))
.map((tConfig) =>
- Either.fromFailable<Error, { config: BitwardenConfig; key: Pick<TKey, 'BITWARDENCLI_APPDATA_DIR'> }>(
+ Either.fromFailable<Error, { config: BitwardenConfig; key: Pick<BitwardenKey, 'BITWARDENCLI_APPDATA_DIR'> }>(
() => {
const sessionPath = path.join(this.config.sessionBaseDirectory, randomUUID());
mkdirSync(sessionPath, { recursive: true });
@@ -78,7 +78,7 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
.get();
}
- public fetchSecret<T extends SecretItem>(client: TClient, key: TKey, item: string): Promise<IEither<Error, T>> {
+ public fetchSecret<T extends SecretItem>(client: TClient, key: BitwardenKey, item: string): Promise<IEither<Error, T>> {
return client
.move(key)
.flatMap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric))
@@ -104,7 +104,7 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
.get();
}
- public lock(client: TClient, key: TKey) {
+ public lock(client: TClient, key: BitwardenKey) {
return client
.move(key)
.flatMap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric))