diff options
author | Elizabeth Hunt <me@liz.coffee> | 2025-07-01 12:38:35 -0700 |
---|---|---|
committer | Elizabeth Hunt <me@liz.coffee> | 2025-07-01 12:38:35 -0700 |
commit | b2ab472a677a22f8300b5beb061f426ad35e2a6a (patch) | |
tree | 1ca4465bf6322b514ad5cf0466b7953dedbcb719 /.ci | |
parent | 684d3768920786440278fd3745c7d6a69cb34a8b (diff) | |
download | ci-b2ab472a677a22f8300b5beb061f426ad35e2a6a.tar.gz ci-b2ab472a677a22f8300b5beb061f426ad35e2a6a.zip |
Build local ci exec
Diffstat (limited to '.ci')
-rwxr-xr-x | .ci/ci.cjs | 592 | ||||
-rw-r--r-- | .ci/ci.ts | 8 | ||||
-rw-r--r-- | .ci/package.json | 16 | ||||
-rw-r--r-- | .ci/tsconfig.json | 15 |
4 files changed, 628 insertions, 3 deletions
diff --git a/.ci/ci.cjs b/.ci/ci.cjs new file mode 100755 index 0000000..0e7ef6d --- /dev/null +++ b/.ci/ci.cjs @@ -0,0 +1,592 @@ +#!/usr/bin/env node +"use strict"; + +// ../u/leftpadesque/debug.ts +var _hasEnv = true; +var _env = _hasEnv && (process.env.ENVIRONMENT ?? "").toLowerCase().includes("prod") ? "production" : "development"; +var isProd = () => _env === "production"; +var _debug = !isProd() || _hasEnv && ["y", "t"].some((process.env.DEBUG ?? "").toLowerCase().startsWith); +var isDebug = () => _debug; + +// ../u/leftpadesque/memoize.ts +var memoize = (fn) => { + const cache = /* @__PURE__ */ new Map(); + return (...args) => { + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key); + } + const res = fn.apply(args); + cache.set(key, res); + return res; + }; +}; + +// ../u/trace/itrace.ts +var TraceableImpl = class _TraceableImpl { + constructor(item, trace) { + this.item = item; + this.trace = trace; + } + map(mapper) { + const result = mapper(this); + return new _TraceableImpl(result, this.trace); + } + coExtend(mapper) { + const results = mapper(this); + return Array.from(results).map((result) => this.move(result)); + } + flatMap(mapper) { + return mapper(this); + } + flatMapAsync(mapper) { + return new _TraceableImpl( + mapper(this).then((t) => t.get()), + this.trace + ); + } + traceScope(mapper) { + return new _TraceableImpl(this.get(), this.trace.traceScope(mapper(this))); + } + peek(peek) { + peek(this); + return this; + } + move(t) { + return this.map(() => t); + } + bimap(mapper) { + const { item, trace: _trace } = mapper(this); + return this.move(item).traceScope(() => _trace); + } + get() { + return this.item; + } +}; + +// ../u/trace/metric/emittable.ts +var EmittableMetric = class { + constructor(name, unit) { + this.name = name; + this.unit = unit; + } + withValue(value) { + return { + name: this.name, + unit: this.unit, + emissionTimestamp: Date.now(), + value, + _tag: MetricValueTag + }; + } +}; + +// ../u/trace/metric/metric.ts +var _Tagged = class { + constructor(_tag = IMetricTag) { + this._tag = _tag; + } +}; +var Metric = class _Metric extends _Tagged { + constructor(name, parent = void 0, count = new EmittableMetric(_Metric.join(name, "count"), "COUNT" /* COUNT */), time = new EmittableMetric(_Metric.join(name, "time"), "MILLISECONDS" /* MILLISECONDS */)) { + super(); + this.name = name; + this.parent = parent; + this.count = count; + this.time = time; + } + static DELIM = "."; + child(_name) { + const childName = _Metric.join(this.name, _name); + return new _Metric(childName, this); + } + asResult() { + return ResultMetric.from(this); + } + static join(...name) { + return name.join(_Metric.DELIM); + } + static fromName(name) { + return new _Metric(name); + } +}; +var ResultMetric = class _ResultMetric extends Metric { + constructor(name, parent = void 0, failure, success, warn) { + super(name, parent); + this.name = name; + this.parent = parent; + this.failure = failure; + this.success = success; + this.warn = warn; + } + static from(metric) { + const failure = metric.child("failure"); + const success = metric.child("success"); + const warn = metric.child("warn"); + return new _ResultMetric(metric.name, metric.parent, failure, success, warn); + } +}; + +// ../u/trace/metric/trace.ts +var MetricsTrace = class _MetricsTrace { + constructor(metricConsumer, activeTraces = /* @__PURE__ */ new Map(), completedTraces = /* @__PURE__ */ new Set()) { + this.metricConsumer = metricConsumer; + this.activeTraces = activeTraces; + this.completedTraces = completedTraces; + } + traceScope(trace) { + const now = Date.now(); + const metricsToTrace = (Array.isArray(trace) ? trace : [trace]).filter(isIMetric); + const initialTraces = new Map(metricsToTrace.map((metric) => [metric, now])); + return new _MetricsTrace(this.metricConsumer, initialTraces); + } + trace(metrics) { + if (!metrics || typeof metrics === "string") { + return this; + } + const now = Date.now(); + const allMetrics = Array.isArray(metrics) ? metrics : [metrics]; + const valuesToEmit = allMetrics.filter(isMetricValue); + const traceableMetrics = allMetrics.filter(isIMetric); + const metricsToStart = traceableMetrics.filter((m) => !this.activeTraces.has(m)); + const metricsToEnd = traceableMetrics.filter((m) => this.activeTraces.has(m) && !this.completedTraces.has(m)); + const endedMetricValues = metricsToEnd.flatMap((metric) => [ + metric.count.withValue(1), + metric.time.withValue(now - this.activeTraces.get(metric)) + ]); + const allMetricsToEmit = [...valuesToEmit, ...endedMetricValues]; + if (allMetricsToEmit.length > 0) { + this.metricConsumer(allMetricsToEmit); + } + const nextActiveTraces = new Map([ + ...this.activeTraces, + ...metricsToStart.map((m) => [m, now]) + ]); + const nextCompletedTraces = /* @__PURE__ */ new Set([...this.completedTraces, ...metricsToEnd]); + return new _MetricsTrace(this.metricConsumer, nextActiveTraces, nextCompletedTraces); + } +}; + +// ../u/trace/metric/index.ts +var MetricValueTag = "MetricValue"; +var isMetricValue = (t) => isTagged(t, MetricValueTag); +var IMetricTag = "IMetric"; +var isIMetric = (t) => isTagged(t, IMetricTag); + +// ../u/trace/log/ansi.ts +var ANSI = { + RESET: "\x1B[0m", + BOLD: "\x1B[1m", + DIM: "\x1B[2m", + RED: "\x1B[31m", + GREEN: "\x1B[32m", + YELLOW: "\x1B[33m", + BLUE: "\x1B[34m", + MAGENTA: "\x1B[35m", + CYAN: "\x1B[36m", + WHITE: "\x1B[37m", + BRIGHT_RED: "\x1B[91m", + BRIGHT_YELLOW: "\x1B[93m", + GRAY: "\x1B[90m" +}; + +// ../u/trace/log/level.ts +var logLevelOrder = [ + "DEBUG" /* DEBUG */, + "INFO" /* INFO */, + "WARN" /* WARN */, + "ERROR" /* ERROR */, + "SYS" /* SYS */ +]; +var isLogLevel = (l) => typeof l === "string" && logLevelOrder.some((level) => level === l); + +// ../u/trace/log/pretty_json_console.ts +var PrettyJsonConsoleLogger = class { + log(level, ...trace) { + const message = JSON.stringify( + { + level, + trace + }, + null, + 4 + ); + const styled = `${this.getStyle(level)}${message}${ANSI.RESET} +`; + this.getStream(level)(styled); + } + getStream(level) { + if (level === "ERROR" /* ERROR */) { + return console.error; + } + return console.log; + } + getStyle(level) { + switch (level) { + case "UNKNOWN" /* UNKNOWN */: + case "INFO" /* INFO */: + return `${ANSI.MAGENTA}`; + case "DEBUG" /* DEBUG */: + return `${ANSI.CYAN}`; + case "WARN" /* WARN */: + return `${ANSI.BRIGHT_YELLOW}`; + case "ERROR" /* ERROR */: + return `${ANSI.BRIGHT_RED}`; + case "SYS" /* SYS */: + return `${ANSI.DIM}${ANSI.BLUE}`; + } + } +}; + +// ../u/trace/log/trace.ts +var LogTrace = class _LogTrace { + constructor(logger = new PrettyJsonConsoleLogger(), traces = [defaultTrace], defaultLevel = "INFO" /* INFO */, allowedLevels = defaultAllowedLevelsSupplier) { + this.logger = logger; + this.traces = traces; + this.defaultLevel = defaultLevel; + this.allowedLevels = allowedLevels; + } + traceScope(trace) { + return new _LogTrace(this.logger, this.traces.concat(trace), this.defaultLevel, this.allowedLevels); + } + trace(trace) { + const { traces, level: _level } = this.foldTraces(this.traces.concat(trace)); + if (!this.allowedLevels().has(_level)) return; + const level = _level === "UNKNOWN" /* UNKNOWN */ ? this.defaultLevel : _level; + this.logger.log(level, ...traces); + } + foldTraces(_traces) { + const _logTraces = _traces.map((trace) => typeof trace === "function" ? trace() : trace); + const _level = _logTraces.filter((trace) => isLogLevel(trace)).reduce((acc, level2) => Math.max(logLevelOrder.indexOf(level2), acc), -1); + const level = logLevelOrder[_level] ?? "UNKNOWN" /* UNKNOWN */; + const traces = _logTraces.filter((trace) => !isLogLevel(trace)).map((trace) => { + if (typeof trace === "object") { + return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}`; + } + return trace; + }); + return { + level, + traces + }; + } +}; +var defaultTrace = () => `TimeStamp = ${(/* @__PURE__ */ new Date()).toISOString()}`; +var defaultAllowedLevels = memoize( + (isDebug2) => /* @__PURE__ */ new Set([ + "UNKNOWN" /* UNKNOWN */, + ...isDebug2 ? ["DEBUG" /* DEBUG */] : [], + "INFO" /* INFO */, + "WARN" /* WARN */, + "ERROR" /* ERROR */, + "SYS" /* SYS */ + ]) +); +var defaultAllowedLevelsSupplier = () => defaultAllowedLevels(isDebug()); + +// ../u/trace/trace.ts +var LogTraceable = class _LogTraceable extends TraceableImpl { + static LogTrace = new LogTrace(); + static of(t) { + return new _LogTraceable(t, _LogTraceable.LogTrace); + } +}; +var getEmbeddedMetricConsumer = (logTrace) => (metrics) => { + if (metrics.length === 0) return; + logTrace.traceScope("SYS" /* SYS */).trace(`Metrics = <metrics>${JSON.stringify(metrics)}</metrics>`); +}; +var EmbeddedMetricsTraceable = class _EmbeddedMetricsTraceable extends TraceableImpl { + static MetricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(LogTraceable.LogTrace)); + static of(t, metricsTrace = _EmbeddedMetricsTraceable.MetricsTrace) { + return new _EmbeddedMetricsTraceable(t, metricsTrace); + } +}; + +// ../u/process/run.ts +var import_node_util = require("node:util"); +var import_node_child_process = require("node:child_process"); +var exec = (0, import_node_util.promisify)(import_node_child_process.exec); +var CmdMetric = Metric.fromName("Exec").asResult(); + +// ../u/process/signals.ts +var SigIntMetric = Metric.fromName("SigInt").asResult(); +var SigTermMetric = Metric.fromName("SigTerm").asResult(); + +// ../u/server/response/pengueno.ts +var ResponseCodeMetrics = [0, 1, 2, 3, 4, 5].map((x) => Metric.fromName(`response.${x}xx`).asResult()); + +// ../u/server/activity/health.ts +var healthCheckMetric = Metric.fromName("Health").asResult(); + +// ../u/server/filter/json.ts +var ParseJsonMetric = Metric.fromName("JsonParse").asResult(); + +// ../u/server/filter/index.ts +var ErrorSource = ((ErrorSource2) => { + ErrorSource2[ErrorSource2["USER"] = "WARN" /* WARN */] = "USER"; + ErrorSource2[ErrorSource2["SYSTEM"] = "ERROR" /* ERROR */] = "SYSTEM"; + return ErrorSource2; +})(ErrorSource || {}); + +// ../u/types/object.ts +var isObject = (o) => typeof o === "object" && !Array.isArray(o) && !!o; + +// ../u/types/tagged.ts +var isTagged = (o, tag) => !!(isObject(o) && "_tag" in o && o._tag === tag); + +// ../u/types/fn/either.ts +var IEitherTag = "IEither"; +var ELeftTag = "E.Left"; +var isLeft = (o) => isTagged(o, ELeftTag); +var ERightTag = "E.Right"; +var isRight = (o) => isTagged(o, ERightTag); +var _Tagged2 = class { + constructor(_tag = IEitherTag) { + this._tag = _tag; + } +}; +var Either = class _Either extends _Tagged2 { + constructor(self) { + super(); + this.self = self; + } + moveRight(t) { + return this.mapRight(() => t); + } + mapBoth(errBranch, okBranch) { + if (isLeft(this.self)) return _Either.left(errBranch(this.self.err)); + return _Either.right(okBranch(this.self.ok)); + } + mapRight(mapper) { + if (isRight(this.self)) return _Either.right(mapper(this.self.ok)); + return _Either.left(this.self.err); + } + mapLeft(mapper) { + if (isLeft(this.self)) return _Either.left(mapper(this.self.err)); + return _Either.right(this.self.ok); + } + flatMap(mapper) { + if (isRight(this.self)) return mapper(this.self.ok); + return _Either.left(this.self.err); + } + async flatMapAsync(mapper) { + if (isLeft(this.self)) return Promise.resolve(_Either.left(this.self.err)); + return await mapper(this.self.ok).catch((err) => _Either.left(err)); + } + fold(leftFolder, rightFolder) { + if (isLeft(this.self)) return leftFolder(this.self.err); + return rightFolder(this.self.ok); + } + left() { + if (isLeft(this.self)) return Optional.from(this.self.err); + return Optional.none(); + } + right() { + if (isRight(this.self)) return Optional.from(this.self.ok); + return Optional.none(); + } + static left(e) { + return new _Either({ err: e, _tag: ELeftTag }); + } + static right(t) { + return new _Either({ ok: t, _tag: ERightTag }); + } + static fromFailable(s) { + try { + return _Either.right(s()); + } catch (e) { + return _Either.left(e); + } + } + static async fromFailableAsync(s) { + return await (typeof s === "function" ? s() : s).then((t) => _Either.right(t)).catch((e) => _Either.left(e)); + } +}; + +// ../u/types/fn/optional.ts +var IOptionalTag = "IOptional"; +var IOptionalEmptyError = class extends Error { +}; +var OSomeTag = "O.Some"; +var ONoneTag = "O.None"; +var isNone = (o) => isTagged(o, ONoneTag); +var isSome = (o) => isTagged(o, OSomeTag); +var _Tagged3 = class { + constructor(_tag = IOptionalTag) { + this._tag = _tag; + } +}; +var Optional = class _Optional extends _Tagged3 { + constructor(self) { + super(); + this.self = self; + } + move(t) { + return this.map(() => t); + } + orSome(supplier) { + if (isNone(this.self)) return _Optional.from(supplier()); + return this; + } + get() { + if (isNone(this.self)) throw new IOptionalEmptyError("empty value"); + return this.self.value; + } + filter(mapper) { + if (isNone(this.self) || !mapper(this.self.value)) return _Optional.none(); + return _Optional.some(this.self.value); + } + map(mapper) { + if (isNone(this.self)) return _Optional.none(); + return _Optional.from(mapper(this.self.value)); + } + flatMap(mapper) { + if (isNone(this.self)) return _Optional.none(); + return _Optional.from(mapper(this.self.value)).orSome(() => _Optional.none()).get(); + } + present() { + return isSome(this.self); + } + *[Symbol.iterator]() { + if (isSome(this.self)) yield this.self.value; + } + static some(value) { + return new _Optional({ value, _tag: OSomeTag }); + } + static _none = new _Optional({ _tag: ONoneTag }); + static none() { + return this._none; + } + static from(value) { + if (value === null || value === void 0) return _Optional.none(); + return _Optional.some(value); + } +}; + +// ../model/job/index.ts +var isJob = (j) => !!(isObject(j) && "arguments" in j && isObject(j.arguments) && "type" in j && typeof j.type === "string" && j); + +// ../model/pipeline/builder.ts +var BasePipelineBuilder = class { + stages = []; + addStage(stage) { + this.stages.push(stage); + return this; + } + build() { + return new PipelineImpl(this.stages); + } +}; +var DefaultGitHookPipelineBuilder = class extends BasePipelineBuilder { + constructor(remoteUrl = process.env.remote, rev = process.env.rev, ref = process.env.ref) { + super(); + this.remoteUrl = remoteUrl; + this.ref = ref; + this.addStage({ + parallelJobs: [ + { + type: "fetch_code", + arguments: { + remoteUrl, + checkout: rev, + path: this.getSourceDestination() + } + } + ] + }); + } + getSourceDestination() { + return this.remoteUrl.split("/").at(-1) ?? "src"; + } + getBranch() { + const branchRefPrefix = "refs/heads/"; + return this.ref.split(branchRefPrefix).at(1); + } +}; + +// ../model/pipeline/impl.ts +var PipelineImpl = class _PipelineImpl { + constructor(serialJobs) { + this.serialJobs = serialJobs; + } + serialize() { + return JSON.stringify(this.serialJobs); + } + static from(s) { + return Either.fromFailable(() => JSON.parse(s)).flatMap( + (eitherPipelineJson) => isPipeline(eitherPipelineJson) ? Either.right(eitherPipelineJson) : Either.left(new Error("oh noes D: its a bad pipewine :((")) + ).mapRight((pipeline) => new _PipelineImpl(pipeline.serialJobs)); + } +}; + +// ../model/pipeline/index.ts +var isPipelineStage = (t) => isObject(t) && "parallelJobs" in t && Array.isArray(t.parallelJobs) && t.parallelJobs.every((j) => isJob(j)); +var isPipeline = (t) => isObject(t) && "serialJobs" in t && Array.isArray(t.serialJobs) && t.serialJobs.every((p) => isPipelineStage(p)); + +// dist/ci.js +var REGISTRY = "oci.liz.coffee"; +var NAMESPACE = "emprespresso"; +var IMG = "ci"; +var REMOTE = "ssh://src.liz.coffee:2222"; +var getPipeline = () => { + const gitHookPipeline = new DefaultGitHookPipelineBuilder(); + const branch = gitHookPipeline.getBranch(); + if (!branch) + return gitHookPipeline.build(); + const commonBuildArgs = { + registry: REGISTRY, + namespace: NAMESPACE, + imageTag: branch + }; + const baseCiPackageBuild = { + type: "build_docker_image.js", + arguments: { + ...commonBuildArgs, + context: gitHookPipeline.getSourceDestination(), + repository: IMG + "_base", + buildTarget: IMG + "_base", + dockerfile: "Dockerfile" + } + }; + gitHookPipeline.addStage({ + parallelJobs: [baseCiPackageBuild] + }); + const subPackages = ["worker", "hooks"].map((_package) => ({ + type: "build_docker_image.js", + arguments: { + ...commonBuildArgs, + repository: `${IMG}_${_package}`, + buildTarget: _package, + dockerfile: `${_package}/Dockerfile` + } + })); + gitHookPipeline.addStage({ + parallelJobs: subPackages + }); + const isRelease = branch === "release"; + if (!isRelease) { + return gitHookPipeline.build(); + } + const fetchAnsibleCode = { + type: "fetch_code", + arguments: { + remoteUrl: `${REMOTE}/infra`, + checkout: "main", + path: "infra" + } + }; + const thenDeploy = { + type: "ansible_playbook.js", + arguments: { + path: "infra", + playbooks: "playbooks/ci.yml" + } + }; + [fetchAnsibleCode, thenDeploy].forEach((deploymentStage) => gitHookPipeline.addStage({ parallelJobs: [deploymentStage] })); + return gitHookPipeline.build(); +}; +var main = () => { + const data = getPipeline().serialize(); + process.stdout.write(data); +}; +main(); @@ -1,4 +1,4 @@ -#!/usr/bin/env ts-node +#!/usr/bin/env node import { AnsiblePlaybookJob, @@ -77,7 +77,9 @@ const getPipeline = () => { return gitHookPipeline.build(); }; -if (import.meta.url === `file://${process.argv[1]}`) { +const main = () => { const data = getPipeline().serialize(); process.stdout.write(data); -} +}; + +main(); diff --git a/.ci/package.json b/.ci/package.json new file mode 100644 index 0000000..9d8bfa7 --- /dev/null +++ b/.ci/package.json @@ -0,0 +1,16 @@ +{ + "scripts": { + "build": "tsc && esbuild --format=cjs --target=node22 --platform=node --bundle --outfile=ci.cjs dist/ci.js && chmod +x ci.cjs", + "clean": "rm -rf dist ci.cjs" + }, + "dependencies": { + "@emprespresso/ci_model": "*" + }, + "devDependencies": { + "esbuild": "0.25.5" + }, + "files": [ + "dist/**/*", + "package.json" + ] +} diff --git a/.ci/tsconfig.json b/.ci/tsconfig.json new file mode 100644 index 0000000..58e9147 --- /dev/null +++ b/.ci/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "references": [{ "path": "../u" }, { "path": "../model" }] +} |