@cloud-cli/on 0.1.9 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,180 +1,245 @@
1
- # Workflow Design
1
+ # 🏃 `@cloud-cli/on`
2
2
 
3
- This is a task runner using webhooks to process incoming events.
3
+ General-purpose workflows
4
4
 
5
- Every event is sent to a daemon as an HTTP request, with a JSON body, and can trigger one or more workflows.
6
- Every workflow is a set of steps, which can run on containers or in a shell on the host.
5
+ A self-hosted, lightweight, high-performance CI/CD runner engine built for Node.js.
7
6
 
8
- ## Running on Docker
7
+ Designed with a strict **security-first boundary**, native **JavaScript AST evaluation**, zero-DSL template literals, and a **built-in terminal log web dashboard**.
9
8
 
10
- - Steps run inside a docker container.
11
- - All steps run in the same workspace folder.
12
- - The current folder is mounted as a volume at /workspace by default. This can be changed by specifying a volume with `.` as the host path.
9
+ ---
13
10
 
14
- ## Running on the host machine
11
+ ## 🌟 Key Highlights
15
12
 
16
- - Steps are sent as stdin to a shell subprocess
13
+ - **Strict Code/Data Separation:** `run:` steps are executed verbatim as raw process scripts. Expressions and dynamic data bindings are isolated strictly to `env:`, eliminating shell-injection vectors entirely.
14
+ - **Standard ES Template Syntax (`${...}`):** No custom DSL wrappers like `${{ }}` or `{{ }}`. If a field contains `${...}`, it evaluates standard JavaScript template string logic via AST.
15
+ - **Deterministic Field Evaluation:** No silent fallbacks or ambiguous type conversions. Plain strings remain literal strings; conditions in `if:` fields run as strict JS boolean expressions.
16
+ - **Built-in Dark Mode Web UI (`/runs`):** Monitor job statuses live, inspect workspace inputs, and view ANSI-colored terminal log streams rendered in real-time.
17
+ - **System & Container Execution Drivers:** Run steps directly as detached host process groups or inside isolated Docker/Systemd transient units.
18
+ - **Automatic Secret Redaction:** Secrets loaded from `.env` are automatically masked (`***`) across all terminal log outputs and report snapshots.
17
19
 
18
- ## General configuration syntax
20
+ ---
19
21
 
20
- Running with Docker:
22
+ ## 📦 Project Structure
23
+
24
+ ```text
25
+ my-project/
26
+ ├── .on/ # Workflow definitions directory
27
+ │ ├── release.yml
28
+ │ └── test.yml
29
+ ├── .env # Local secrets (git-ignored)
30
+ ├── runner.config.mjs # (Optional) Engine configuration
31
+ └── package.json
21
32
 
22
- ```sh
23
- curl -X POST http://localhost:11235/ -d '{ "event-name": {...} }'
24
33
  ```
25
34
 
26
- ```yaml
27
- description: Run tests and build
35
+ ---
28
36
 
29
- vars: &vars
30
- image: node:latest
37
+ ## 🚀 Quick Start
38
+
39
+ ### 1. Install & Run
40
+
41
+ Run the engine directly via `npx` or `pnpm dlx`:
42
+
43
+ ```bash
44
+ # Start full engine (Ingress HTTP Gateway + 5 Worker Loops)
45
+ npx @cloud-cli/on start
31
46
 
32
- on:
33
- event-name:
34
- runner: docker
35
- if:
36
- - ${inputs.action} == 'published'
37
- secrets:
38
- - /path/to/secrets
39
- - /path/to/.env
40
- mappings:
41
- <field>: <path.to.value.in.inputs>
42
- env:
43
- A_SECRET: "${secrets.A_SECRET}"
44
- A_VALUE: "${inputs.some.value}"
45
- defaults:
46
- <<: *vars
47
- volumes:
48
- .: /home
49
- /dev/shm: /dev/shm
50
- args:
51
- net: host
52
- dns: 1.2.3.4
53
- steps:
54
- - pnpm i
55
- - pnpm run build
56
- - pnpm run test
57
- triggers:
58
- - path/to/output.json
59
47
  ```
60
48
 
61
- Running with a shell on the same machine as the server:
49
+ ### 2. Configure Secrets (`.env`)
62
50
 
63
- ```sh
64
- curl -X POST http://localhost:11235/ -d '{ "package": {...} }'
51
+ Secrets are automatically loaded from `.env` at the root of your project. Prefix secrets with `SECRET_`:
52
+
53
+ ```env
54
+ SECRET_NPM_TOKEN="npm_1234567890abcdef"
55
+ SECRET_GITHUB_TOKEN="ghp_1234567890abcdef"
56
+ SECRET_GITHUB_WEBHOOK_SECRET="my-webhook-secret"
65
57
  ```
66
58
 
59
+ `SECRET_GITHUB_WEBHOOK_SECRET` is required to validate incoming webhooks from GitHub
60
+
61
+ ### 3. Define a Workflow (`.on/release.yml`)
62
+
67
63
  ```yaml
68
- description: Auto-release library
64
+ name: Build and Publish Release
65
+
69
66
  on:
70
- event-name:
71
- runner: shell
72
- secrets:
73
- - /path/to/secrets
74
- - /path/to/.env
75
- mappings:
76
- <field>: <path.to.value.in.json.payload>
67
+ github:
68
+ if: inputs.event === 'push' && inputs.branch === 'main'
69
+
70
+ concurrency:
71
+ group: release-${inputs.repo}
72
+ cancel-in-progress: true
73
+
74
+ steps:
75
+ - id: checkout
76
+ name: Checkout Code Repository
77
+ env:
78
+ CLONE_URL: ${inputs.clone_url}
79
+ COMMIT_SHA: ${inputs.commit_sha}
80
+ run: |
81
+ git clone --depth 1 "$CLONE_URL" .
82
+ git checkout "$COMMIT_SHA"
83
+
84
+ - id: install-and-build
85
+ name: Install Dependencies & Build
86
+ run: |
87
+ pnpm install
88
+ pnpm run build
89
+
90
+ - id: publish
91
+ name: Publish to NPM
77
92
  env:
78
- A_SECRET: "${secrets.A_SECRET}"
79
- A_VALUE: "${inputs.some.value}"
80
- steps:
81
- - pnpm i
82
- - pnpm run build
83
- - pnpm run release
84
- triggers:
85
- - path/to/output.json
93
+ NPM_TOKEN: ${secrets.NPM_TOKEN}
94
+ run: |
95
+ echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
96
+ npx --yes semantic-release@24 -b main --no-ci
86
97
  ```
87
98
 
88
- ### Event Payload
99
+ ---
89
100
 
90
- The webhooks can have any shape. To match events to workflows, we look for the top-level keys in the request body, and matching workflows that expect them to be present.
101
+ ## 💻 CLI Usage & Commands
91
102
 
92
- For example, given the request:
103
+ ```bash
104
+ npx @cloud-cli/on [command] [options]
93
105
 
94
- ```sh
95
- curl -X POST http://localhost:11235/ -d '{ "published": { "value" : 123 } }'
96
106
  ```
97
107
 
98
- Then the workflow should map the `published` key:
108
+ ### Commands
99
109
 
100
- ```yaml
101
- on:
102
- published:
103
- steps:
104
- - echo ${inputs.value}
105
- ```
110
+ | Command | Description |
111
+ | ----------------------- | ---------------------------------------------------------------------------------------- |
112
+ | **`start`** _(default)_ | Runs both Webhook Ingress Gateway and Worker execution loops together. |
113
+ | **`start-server`** | Runs Webhook Ingress Gateway only (API / Gateway mode). |
114
+ | **`start-workers`** | Runs Worker Polling loops only (Scalable Worker mode). |
115
+ | **`validate`** | Parses and validates all YAML workflows in your workflows folder without executing jobs. |
106
116
 
107
- Here, the expression `inputs` in the workflow context is defined as the value set in the `published` key from the parsed JSON.
108
- The expression `${inputs.value}` contains `123`.
117
+ ### CLI and Environment Options
109
118
 
110
- To differentiate between events coming from the same place, multiple webhooks can be created.
111
- The incoming webhook URL can have a path, and that is considered as the event source.
119
+ | Flag | Option | Default | Env | Description |
120
+ | ---- | ------------- | --------------------- | --------------------- | ----------------------------------------- |
121
+ | `-h` | `--help` | — | - | Prints CLI help message and exits. |
122
+ | `-c` | `--config` | `./runner.config.mjs` | `RUNNER_CONFIG_FILE` | Path to JavaScript configuration file. |
123
+ | `-d` | `--database` | - | `RUNNER_DATABASE_URL` | SQLite database file path or HTTP URL. |
124
+ | `-w` | `--workflows` | `.on/` | `RUNNER_WORKFLOWS` | Directory where workflow YAML files live. |
125
+ | `-p` | `--port` | `11235` | `PORT` | Port for the Ingress HTTP server. |
126
+ | `-k` | `--workers` | `5` | `RUNNER_WORKERS` | Number of worker loop threads to spawn. |
127
+ | | | | `RUNNER_ADMIN_SECRET` | Admin token to refresh secrets via API |
112
128
 
113
- Consider this request:
129
+ ### Secrets
114
130
 
115
- ```sh
116
- curl -X POST http://localhost:11235/source -d '{ "event": { "value" : 123 } }'
131
+ ---
132
+
133
+ ## ⚙️ Configuration Reference
134
+
135
+ You can customize engine behavior using `runner.config.mjs` in your project root:
136
+
137
+ ```javascript
138
+ // runner.config.mjs
139
+ import { HtmlReporter, SlackReporter, JsonFileReporter } from '@cloud-cli/on/reporters';
140
+
141
+ export default {
142
+ port: 3000,
143
+ workers: 5,
144
+ workflows: '/home/workflows/',
145
+ storagePath: '/tmp/workspaces',
146
+ database: 'https://remote.db.com/',
147
+
148
+ // Global environment variables passed to all steps
149
+ env: {
150
+ NODE_ENV: 'production',
151
+ },
152
+
153
+ // Custom execution reporters
154
+ reporters: [
155
+ new JsonFileReporter({ outputDir: './reports/json' }),
156
+ new HtmlReporter({ outputDir: './reports/html' }),
157
+ new SlackReporter({
158
+ webhookUrl: process.env.SLACK_WEBHOOK_URL,
159
+ channel: '#ci-deployments',
160
+ }),
161
+ ],
162
+ };
117
163
  ```
118
164
 
119
- The key `source` is added to the event payload. The workflow should now be defined as:
120
-
121
- ```yaml
122
- on:
123
- source:
124
- published:
125
- steps:
126
- - echo ${inputs.value}
127
- ```
165
+ ---
128
166
 
129
- This difference in payload processing allows multiple webhooks from the same source, to separate different events with a similar JSON body.
167
+ ## 📐 Deterministic Evaluation Rules
130
168
 
131
- ### Secrets
169
+ To prevent syntax ambiguity and injection risks, fields in workflow definitions operate under **three strict modes**:
132
170
 
133
- The daemon can fetch secrets from its host environment, or from a file.
171
+ ```
172
+ ┌──────────────────────────────────────────────────────────────────────────┐
173
+ │ 1. RAW PASSTHROUGH MODE (`run:`) │
174
+ │ • Executed verbatim as a shell process command. │
175
+ │ • No string replacements or engine parsing performed. │
176
+ │ • Access environment variables strictly via shell syntax: $MY_VAR. │
177
+ └──────────────────────────────────────────────────────────────────────────┘
178
+
179
+ ┌──────────────────────────────────────────────────────────────────────────┐
180
+ │ 2. EXPRESSION MODE (`if:`, `eval:`) │
181
+ │ • Evaluated strictly as pure JavaScript expressions via Acorn AST. │
182
+ │ • Must be valid JS syntax (e.g. `inputs.branch === 'main'`). │
183
+ │ • Automatically coerced to boolean in `if:` conditions. │
184
+ └──────────────────────────────────────────────────────────────────────────┘
185
+
186
+ ┌──────────────────────────────────────────────────────────────────────────┐
187
+ │ 3. DETERMINISTIC VALUE MODE (`env:`, `name:`, `image:`, `group:`) │
188
+ │ • Plain strings WITHOUT `${}` remain 100% raw literal strings. │
189
+ │ • Strings WITH `${...}` evaluate as standard ES Template Literals. │
190
+ │ • Example: `node:${inputs.node_version}-alpine` │
191
+ └──────────────────────────────────────────────────────────────────────────┘
134
192
 
135
- ### Inputs
193
+ ```
136
194
 
137
- Inputs are defined from the incoming JSON payload. The payload is parsed and made available as the `inputs` variable
195
+ ### Context Scope Available in Expressions
138
196
 
139
- ### Mappings
197
+ Within `${...}`, `if:`, and `eval:` contexts, the following object scopes are exposed:
140
198
 
141
- These are shortcuts to make scripting easier.
199
+ - **`inputs`**: Payload key-values received from incoming webhooks.
200
+ - **`env`**: Merged environment variables from global config and workflow definitions.
201
+ - **`secrets`**: Unmasked secret values loaded from `.env` or environment variables (`SECRET_` prefix stripped).
202
+ - **`steps`**: Execution statuses and outputs from previous steps in the workflow (`steps.<id>.status`, `steps.<id>.outputs`).
203
+ - **`BUILTIN_HELPERS`**: JS utilities including `String`, `Number`, `Boolean`, and `JSON.parse` / `JSON.stringify`.
142
204
 
143
- After the JSON payload is parsed, these mappings are evaluated, and added to `inputs` as shortcuts for long/deep properties in the payload.
205
+ ---
144
206
 
145
- For example: from a GitHub webhook event that contains a lot of fields, we can define `image` from `package.package_version.package_url`
207
+ ## 🌐 Webhook Ingress Gateway & Dashboard
146
208
 
147
- ```yaml
148
- mappings:
149
- image: ${inputs.package.package_version.package_url}
150
- ```
209
+ The Ingress Gateway listens for incoming HTTP requests and serves the live web UI.
151
210
 
152
- ### Environment variables
211
+ ### Endpoint Matrix
153
212
 
154
- After resolving secrets and mappings, we proceed to resolve env variables from template strings or literal strings.
213
+ | Method | Endpoint | Description |
214
+ | ---------- | ------------------ | -------------------------------------------------------------------------------------- |
215
+ | **`POST`** | `/webhooks/github` | Webhook endpoint for GitHub events. Evaluates `on.github.if` triggers. |
216
+ | **`GET`** | `/runs` | **Dashboard:** Live dark-mode monitoring page listing recent jobs and worker health. |
217
+ | **`GET`** | `/runs/:jobId` | **Job Report:** Interactive HTML trace view with step timings and terminal log output. |
155
218
 
156
- An env variable is interpreted a JS template string, with `${value}` syntax used to interpolate values from `inputs` or `secrets`.
219
+ ### Dashboard Features
157
220
 
158
- ## Sequence of operation
221
+ - **Real-time Auto-Refresh:** `/runs` automatically refreshes job queue statuses (`PENDING`, `RUNNING`, `SUCCESS`, `FAILED`, `CANCELLED`).
222
+ - **ANSI Terminal Rendering:** Uses `ansi_up` to render bash colors, bold highlights, and console outputs accurately in step log boxes.
223
+ - **Payload Inspection:** View JSON inputs received from webhooks for easy debugging.
159
224
 
160
- For every incoming event, these steps are followed:
225
+ ---
161
226
 
162
- - Parse and validate payload
163
- - Load secrets
164
- - Map inputs
165
- - Populate env with secrets
166
- - Populate env with additional workflow definitions (section `env`)
167
- - Create a temporary working directory
168
- - Add a volume to defaults at `/workspace`, or a custom path, if a volume with a host path `.` is defined in the workflow
169
- - Run steps:
170
- - For every step, either a string, or a step definition is accepted.
171
- - If string, run with the defaults defined in the workflow
172
- - If a definition, merge defaults into it, and run the step
173
- - The step is a shell command, executed inside a short-lived container
174
- - Trigger new events
227
+ ## 🔒 Security & Hardening
175
228
 
176
- ## Triggers
229
+ 1. **Environment Variable Boundary:**
230
+ By forcing shell steps to consume data via process environment variables (`$CLONE_URL`), malicious webhook payloads containing shell delimiters (e.g. `; rm -rf /`) cannot mutate shell script execution trees.
231
+ 2. **Prototype Pollution Protection:**
232
+ AST evaluation explicitly blocks access to dangerous JS properties (`constructor`, `__proto__`, `prototype`).
233
+ 3. **Payload Size Guard:**
234
+ The Ingress server enforces a strict 5MB payload limit to prevent Out-Of-Memory (OOM) denial-of-service attacks.
235
+ 4. **Signal Traps & Resource Cleanup:**
236
+ Graceful process traps (`SIGINT`, `SIGTERM`) ensure active job handles are safely terminated, file descriptors are closed, and temp `.env`/`.out` files are removed via `try ... finally` blocks.
177
237
 
178
- After steps are executed, a list of one or more JSON files can be defined to trigger new workflows.
238
+ ## Development
179
239
 
180
- These files are read one by one and sent back to the daemon as new events.
240
+ ```bash
241
+ pnpm i
242
+ pnpm run lint
243
+ pnpm run test
244
+ pnpm run build
245
+ ```
@@ -0,0 +1,14 @@
1
+ declare function query(method: 'get' | 'run' | 'all', statement: string, data?: Array<string | number | null>, pragma?: string[]): Promise<any>;
2
+ export declare const get: (statement: string, data?: (string | number | null)[] | undefined, pragma?: string[] | undefined) => Promise<any>;
3
+ export declare const run: (statement: string, data?: (string | number | null)[] | undefined, pragma?: string[] | undefined) => Promise<any>;
4
+ export declare const all: (statement: string, data?: (string | number | null)[] | undefined, pragma?: string[] | undefined) => Promise<any>;
5
+ export declare function pragma(p: any): void;
6
+ declare const _default: {
7
+ query: typeof query;
8
+ get: (statement: string, data?: (string | number | null)[] | undefined, pragma?: string[] | undefined) => Promise<any>;
9
+ run: (statement: string, data?: (string | number | null)[] | undefined, pragma?: string[] | undefined) => Promise<any>;
10
+ all: (statement: string, data?: (string | number | null)[] | undefined, pragma?: string[] | undefined) => Promise<any>;
11
+ pragma: typeof pragma;
12
+ };
13
+ export default _default;
14
+ export declare function setUrl(u: any): void;
@@ -0,0 +1,2 @@
1
+ import { ExecutionDriver } from '../types.js';
2
+ export declare function resolveDriver(): Promise<ExecutionDriver>;
@@ -0,0 +1,10 @@
1
+ import { ExecutionDriver, StepContext, StepExecutionHandle } from '../types.js';
2
+ export declare class StandardProcessDriver implements ExecutionDriver {
3
+ name: string;
4
+ isSupported(): Promise<boolean>;
5
+ execute(ctx: StepContext): Promise<StepExecutionHandle>;
6
+ /**
7
+ * Kills the entire process group tree (-PID) with unref escalation
8
+ */
9
+ private killProcessGroup;
10
+ }
@@ -0,0 +1,6 @@
1
+ import { ExecutionDriver, StepContext, StepExecutionHandle } from '../types.js';
2
+ export declare class SystemdDriver implements ExecutionDriver {
3
+ name: string;
4
+ isSupported(): Promise<boolean>;
5
+ execute(ctx: StepContext): Promise<StepExecutionHandle>;
6
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export { HtmlReporter } from './reporters/html.reporter.js';
3
+ export { JsonFileReporter } from './reporters/json-file.reporter.js';
4
+ export { SlackReporter } from './reporters/slack.reporter.js';
@@ -0,0 +1,6 @@
1
+ import { Transform, TransformCallback } from 'node:stream';
2
+ export declare class SecretRedactorStream extends Transform {
3
+ private secretValues;
4
+ constructor(secretValues: string[]);
5
+ _transform(chunk: any, _encoding: BufferEncoding, callback: TransformCallback): void;
6
+ }