@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 +191 -126
- package/dist/db-client.d.ts +14 -0
- package/dist/drivers/index.d.ts +2 -0
- package/dist/drivers/standard-process.driver.d.ts +10 -0
- package/dist/drivers/systemd.driver.d.ts +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/log-redactor.d.ts +6 -0
- package/dist/on.js +9218 -5173
- package/dist/parser/include-resolver.d.ts +9 -0
- package/dist/parser/matrix-expander.d.ts +5 -0
- package/dist/parser/yaml-loader.d.ts +6 -0
- package/dist/plugins/github-status.plugin.d.ts +7 -0
- package/dist/plugins/manager.d.ts +7 -0
- package/dist/queue.d.ts +41 -0
- package/dist/reporters/html.reporter.d.ts +14 -0
- package/dist/reporters/json-file.reporter.d.ts +9 -0
- package/dist/reporters/slack.reporter.d.ts +15 -0
- package/dist/runner/step-runner.d.ts +1 -0
- package/dist/safe-eval.d.ts +28 -0
- package/dist/secrets.d.ts +15 -0
- package/dist/server/preprocessors/github.d.ts +5 -0
- package/dist/server/server.d.ts +32 -0
- package/dist/types.d.ts +181 -0
- package/dist/worker.d.ts +8 -0
- package/package.json +35 -16
package/README.md
CHANGED
|
@@ -1,180 +1,245 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🏃 `@cloud-cli/on`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
General-purpose workflows
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
11
|
+
## 🌟 Key Highlights
|
|
15
12
|
|
|
16
|
-
-
|
|
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
|
-
|
|
20
|
+
---
|
|
19
21
|
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
description: Run tests and build
|
|
35
|
+
---
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
49
|
+
### 2. Configure Secrets (`.env`)
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
64
|
+
name: Build and Publish Release
|
|
65
|
+
|
|
69
66
|
on:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
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
|
-
|
|
99
|
+
---
|
|
89
100
|
|
|
90
|
-
|
|
101
|
+
## 💻 CLI Usage & Commands
|
|
91
102
|
|
|
92
|
-
|
|
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
|
-
|
|
108
|
+
### Commands
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
The expression `${inputs.value}` contains `123`.
|
|
117
|
+
### CLI and Environment Options
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
129
|
+
### Secrets
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
```yaml
|
|
122
|
-
on:
|
|
123
|
-
source:
|
|
124
|
-
published:
|
|
125
|
-
steps:
|
|
126
|
-
- echo ${inputs.value}
|
|
127
|
-
```
|
|
165
|
+
---
|
|
128
166
|
|
|
129
|
-
|
|
167
|
+
## 📐 Deterministic Evaluation Rules
|
|
130
168
|
|
|
131
|
-
|
|
169
|
+
To prevent syntax ambiguity and injection risks, fields in workflow definitions operate under **three strict modes**:
|
|
132
170
|
|
|
133
|
-
|
|
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
|
-
|
|
193
|
+
```
|
|
136
194
|
|
|
137
|
-
|
|
195
|
+
### Context Scope Available in Expressions
|
|
138
196
|
|
|
139
|
-
|
|
197
|
+
Within `${...}`, `if:`, and `eval:` contexts, the following object scopes are exposed:
|
|
140
198
|
|
|
141
|
-
|
|
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
|
-
|
|
205
|
+
---
|
|
144
206
|
|
|
145
|
-
|
|
207
|
+
## 🌐 Webhook Ingress Gateway & Dashboard
|
|
146
208
|
|
|
147
|
-
|
|
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
|
-
###
|
|
211
|
+
### Endpoint Matrix
|
|
153
212
|
|
|
154
|
-
|
|
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
|
-
|
|
219
|
+
### Dashboard Features
|
|
157
220
|
|
|
158
|
-
|
|
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
|
-
|
|
225
|
+
---
|
|
161
226
|
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
+
## Development
|
|
179
239
|
|
|
180
|
-
|
|
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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|