pushci 1.3.0 → 1.4.0

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.
Files changed (3) hide show
  1. package/README.md +219 -27
  2. package/bin/pushci.js +126 -40
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -61,8 +61,9 @@ PushCI includes an MCP server for AI coding agents like **Claude Code**, **Curso
61
61
 
62
62
  ## Supported
63
63
 
64
- **19 Languages**: Go, Node/TS, Python, Rust, Java, C#,
65
- Ruby, PHP, Swift, Dart, Elixir, Zig, Docker +more
64
+ **33 Languages**: Go, Node/TS, Python, Rust, Java, C#,
65
+ Ruby, PHP, Swift, Dart, Elixir, Zig, Scala, Haskell,
66
+ Kotlin, Lua, Perl, R, Julia, OCaml, Nim, Crystal +more
66
67
 
67
68
  **40+ Frameworks**: Next.js, Nuxt, SvelteKit, Django, FastAPI,
68
69
  Flask, Spring Boot, Rails, Laravel, Phoenix, Flutter +more
@@ -103,44 +104,235 @@ Supported platforms:
103
104
  ## Commands
104
105
 
105
106
  ```
106
- pushci init Scan repo, generate config, install hooks
107
- pushci run Run full CI pipeline locally
108
- pushci deploy Deploy to configured target
109
- pushci diagnose AI-diagnose failed runs
110
- pushci status Show last run results
111
- pushci secret Manage encrypted secrets
112
- pushci heal AI-powered pipeline auto-fix
113
- pushci ask Natural language CI commands
114
- pushci generate AI-generate pushci.yml
115
- pushci migrate Convert GitHub Actions workflow
116
- pushci mcp Start MCP server for AI agents
117
- pushci agent Start webhook server (GitHub/GitLab/BB)
118
- pushci login Authenticate with PushCI (Pro)
119
- pushci logout Remove saved credentials
120
- pushci doctor Check environment health
107
+ pushci init Detect stack and generate pushci.yml
108
+ pushci run Execute pipeline checks
109
+ pushci deploy Deploy to target environment
110
+ pushci diagnose AI-diagnose failed runs
111
+ pushci status Show last run results
112
+ pushci secret Manage encrypted secrets
113
+ pushci heal AI self-heal broken pipeline
114
+ pushci ask Natural language CI commands
115
+ pushci generate AI-generate pushci.yml
116
+ pushci migrate Convert GitHub Actions workflow
117
+ pushci mcp Start MCP server for AI agents
118
+ pushci agent Start webhook agent server
119
+ pushci index Build dependency graph for blast radius
120
+ pushci skill Install/list/remove marketplace skills
121
+ pushci login Authenticate with PushCI (Pro)
122
+ pushci logout Remove saved credentials
123
+ pushci doctor Check environment health
121
124
  pushci troubleshoot Diagnose issues with actionable fixes
122
- pushci version Print version
125
+ pushci trace View Perfetto performance traces
126
+ pushci release Build & publish release locally ($0)
127
+ pushci promote Register with AI registries
128
+ pushci uninstall Remove hooks, config, and .pushci
129
+ pushci version Print version
123
130
  ```
124
131
 
125
132
  See [docs/CLI.md](docs/CLI.md) for the full CLI reference with flags, examples, and plan requirements.
126
133
 
127
134
  ## Configuration
128
135
 
129
- Optional `pushci.yml`:
136
+ `pushci.yml` is optional — `pushci init` generates one that works, and
137
+ zero-config mode auto-detects your stack. When you want more control,
138
+ PushCI supports three authoring styles. Full reference is at
139
+ [pushci.dev/docs/pushci-yaml](https://pushci.dev/docs/pushci-yaml).
140
+
141
+ ### Simple example
142
+
143
+ Zero-config Node.js app with linear stages, single-target deploy on
144
+ `main`, and Slack notifications. Drop this into your repo as
145
+ `pushci.yml` and run `pushci run`.
130
146
 
131
147
  ```yaml
132
148
  on: [push, pull_request]
133
- checks:
134
- - build
135
- - test
136
- - lint
137
- - line-limit: 100
149
+
150
+ stages:
151
+ - name: install
152
+ checks:
153
+ - name: deps
154
+ run: pnpm install --frozen-lockfile
155
+
156
+ - name: lint
157
+ depends_on: [install]
158
+ checks:
159
+ - name: eslint
160
+ run: pnpm lint
161
+
162
+ - name: test
163
+ depends_on: [install]
164
+ checks:
165
+ - name: vitest
166
+ run: pnpm test
167
+
168
+ - name: build
169
+ depends_on: [install, lint, test]
170
+ checks:
171
+ - name: next-build
172
+ run: pnpm build
173
+
174
+ deploy:
175
+ trigger: push
176
+ only_on: [main]
177
+ run: npx wrangler pages deploy dist --project-name=my-app
178
+
179
+ notify:
180
+ slack: "${{ secrets.SLACK_WEBHOOK }}"
181
+ ```
182
+
183
+ ### Complex example — every feature
184
+
185
+ Parallel stages, cross-stage DAG, conditional checks, retries,
186
+ timeouts, Docker-isolated steps, multi-environment staged deploy with
187
+ an approval gate on production, and stage-scoped secrets.
188
+
189
+ ```yaml
190
+ on: [push, pull_request, workflow_dispatch]
191
+
192
+ stages:
193
+ - name: install
194
+ checks:
195
+ - name: pnpm-install
196
+ run: pnpm install --frozen-lockfile
197
+ retry: 2
198
+ timeout: 3m
199
+
200
+ - name: quality
201
+ depends_on: [install]
202
+ parallel: true # every check runs concurrently
203
+ env:
204
+ NODE_ENV: test
205
+ checks:
206
+ - name: typecheck
207
+ run: pnpm tsc --noEmit
208
+ - name: lint
209
+ run: pnpm lint
210
+ - name: format
211
+ run: pnpm prettier --check .
212
+ - name: audit
213
+ run: pnpm audit --audit-level=high
214
+ on_fail: warn # log but don't fail the stage
215
+
216
+ - name: test
217
+ depends_on: [install]
218
+ parallel: true
219
+ env:
220
+ DATABASE_URL: postgres://test:test@localhost:5432/test
221
+ checks:
222
+ - name: unit
223
+ run: pnpm test:unit --coverage
224
+ - name: integration
225
+ run: pnpm test:integration
226
+ retry: 1
227
+ timeout: 5m
228
+ - name: e2e
229
+ if: branch == 'main' || branch =~ '^release/'
230
+ run: pnpm test:e2e
231
+ docker: mcr.microsoft.com/playwright:v1.49.0-focal
232
+ timeout: 10m
233
+
234
+ - name: security
235
+ depends_on: [install]
236
+ checks:
237
+ - name: secret-scan
238
+ run: npx gitleaks detect --no-git
239
+ - name: sast
240
+ run: pushci scan --engine claude --fail-on high
241
+
242
+ - name: build
243
+ depends_on: [quality, test, security]
244
+ checks:
245
+ - name: next-build
246
+ run: pnpm build
247
+ - name: bundle-size
248
+ run: npx size-limit
249
+ line-limit: 10
250
+
138
251
  deploy:
139
- target: cloudflare-pages
140
- trigger: merge to main
252
+ trigger: push
253
+ environments:
254
+ - name: staging
255
+ only_on: [develop]
256
+ run: npx wrangler pages deploy dist --project-name=app-staging
257
+ env:
258
+ CF_API_TOKEN: "${{ secrets.CF_API_TOKEN_STAGING }}"
259
+ - name: production
260
+ only_on: [main]
261
+ approve: true # requires interactive approval
262
+ run: npx wrangler pages deploy dist --project-name=app-prod
263
+ env:
264
+ CF_API_TOKEN: "${{ secrets.CF_API_TOKEN_PROD }}"
265
+
266
+ notify:
267
+ slack: "${{ secrets.SLACK_WEBHOOK }}"
268
+ discord: "${{ secrets.DISCORD_WEBHOOK }}"
269
+ email: oncall@example.com
270
+ ```
271
+
272
+ ### GitHub Actions parity — no rewrite needed
273
+
274
+ PushCI v1.3.1+ runs your existing `.github/workflows/*.yml` files
275
+ end-to-end via the embedded [nektos/act](https://github.com/nektos/act)
276
+ runtime. `actions/checkout@v4`, matrix builds, service containers,
277
+ composite actions, secret masking, `needs.*.outputs.*` — all work.
278
+ Just drop in a workflow and run:
279
+
280
+ ```bash
281
+ pushci actions run # runs all .github/workflows/*.yml
282
+ pushci actions run --job test # run one job
283
+ pushci actions run --dry-run # validate without containers
284
+ pushci actions doctor # check act + docker + workflow status
285
+ ```
286
+
287
+ A real complex workflow that runs unchanged:
288
+
289
+ ```yaml
290
+ name: CI
291
+ on: [push, pull_request]
292
+
293
+ jobs:
294
+ test:
295
+ runs-on: ubuntu-latest
296
+ strategy:
297
+ fail-fast: false
298
+ matrix:
299
+ node: [20, 22]
300
+ services:
301
+ postgres:
302
+ image: postgres:16
303
+ env:
304
+ POSTGRES_PASSWORD: test
305
+ ports: ['5432:5432']
306
+ options: >-
307
+ --health-cmd pg_isready --health-interval 10s
308
+ --health-timeout 5s --health-retries 5
309
+ steps:
310
+ - uses: actions/checkout@v4
311
+ - uses: actions/setup-node@v4
312
+ with:
313
+ node-version: ${{ matrix.node }}
314
+ - run: npm ci
315
+ - run: npm test
316
+ env:
317
+ DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
318
+ - id: coverage
319
+ run: echo "pct=$(npm test --silent)" >> $GITHUB_OUTPUT
320
+ outputs:
321
+ coverage: ${{ steps.coverage.outputs.pct }}
322
+
323
+ deploy:
324
+ needs: test
325
+ runs-on: ubuntu-latest
326
+ if: github.ref == 'refs/heads/main'
327
+ steps:
328
+ - uses: actions/checkout@v4
329
+ - name: Deploy
330
+ run: echo "coverage was ${{ needs.test.outputs.coverage }}"
331
+ env:
332
+ CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
141
333
  ```
142
334
 
143
- Or just run `pushci init` — AI figures everything out.
335
+ Or just run `pushci init` — PushCI figures everything out automatically.
144
336
 
145
337
  ## Git Hook
146
338
 
package/bin/pushci.js CHANGED
@@ -1,30 +1,53 @@
1
1
  #!/usr/bin/env node
2
+ // pushci npm shim. Resolution order: local dev build → bundled
3
+ // bin/pushci-<os>-<arch> → pushci on PATH → download from GitHub
4
+ // Releases → go build → install help. VERSION reads from package.json
5
+ // so the shim and npm package never drift. See CLAUDE.md
6
+ // "Release & Distribution" for the full pipeline.
7
+
2
8
  const { execSync, spawn } = require('child_process');
3
9
  const fs = require('fs');
4
10
  const path = require('path');
5
11
  const os = require('os');
6
12
 
7
- const VERSION = '1.1.0';
13
+ const pkg = require('../package.json');
14
+ const VERSION = pkg.version;
15
+ const REPO = 'finsavvyai/push-ci.dev';
16
+
8
17
  const BINARY_NAME = os.platform() === 'win32' ? 'pushci.exe' : 'pushci';
9
- const CDN = 'https://pushci-releases.broad-dew-49ad.workers.dev';
10
18
 
11
- const PLATFORM_MAP = {
12
- darwin: 'darwin', linux: 'linux', win32: 'windows',
13
- };
14
- const ARCH_MAP = {
15
- x64: 'amd64', arm64: 'arm64',
16
- };
19
+ const PLATFORM_MAP = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
20
+ const ARCH_MAP = { x64: 'amd64', arm64: 'arm64' };
17
21
 
18
22
  function getBinaryPath() {
23
+ // 1. Local dev build at the package root (set by `go build` during
24
+ // development or by goreleaser-like bundling).
19
25
  const local = path.join(__dirname, '..', BINARY_NAME);
20
- if (fs.existsSync(local)) return local;
26
+ if (fs.existsSync(local) && isValidBinary(local)) return local;
27
+
28
+ // 2. Bundled platform-specific binary shipped inside the npm
29
+ // tarball at bin/pushci-<os>-<arch>[.exe]. This is the
30
+ // fastest path — zero network, works offline, deterministic.
31
+ // It's also the only reliable path for users who don't have
32
+ // GitHub Releases access (corporate proxies, air-gapped CI).
33
+ const plat = PLATFORM_MAP[os.platform()];
34
+ const arch = ARCH_MAP[os.arch()];
35
+ if (plat && arch) {
36
+ const winExt = os.platform() === 'win32' ? '.exe' : '';
37
+ const bundled = path.join(__dirname, `pushci-${plat}-${arch}${winExt}`);
38
+ if (fs.existsSync(bundled) && isValidBinary(bundled)) return bundled;
39
+ }
21
40
 
41
+ // 3. Existing install on PATH (Homebrew, go install, curl
42
+ // installer). The `which` result may be this same shim, so
43
+ // validate it's a real binary before trusting it.
22
44
  try {
23
45
  const cmd = os.platform() === 'win32' ? 'where' : 'which';
24
46
  const found = execSync(`${cmd} pushci`, { encoding: 'utf8' }).trim();
25
- if (found) return found;
47
+ if (found && found !== __filename && isValidBinary(found)) return found;
26
48
  } catch (_) {}
27
49
 
50
+ // 4-6. Download, build, or give up.
28
51
  return downloadOrBuild();
29
52
  }
30
53
 
@@ -37,31 +60,16 @@ function downloadOrBuild() {
37
60
  if (fs.existsSync(target) && isValidBinary(target)) return target;
38
61
 
39
62
  if (plat && arch) {
40
- const url = `${CDN}/v${VERSION}/pushci-${plat}-${arch}${ext}`;
41
- console.log(`Downloading pushci v${VERSION}...`);
42
- try {
43
- execSync(`curl -sfL --retry 2 -o "${target}" "${url}"`, { timeout: 30000 });
44
- if (isValidBinary(target)) {
45
- fs.chmodSync(target, 0o755);
46
- return target;
47
- }
48
- try { fs.unlinkSync(target); } catch (_) {}
49
- } catch (_) {}
63
+ if (downloadFromReleases(plat, arch, target)) {
64
+ return target;
65
+ }
50
66
  }
51
67
 
52
- try {
53
- execSync('go version', { stdio: 'ignore' });
54
- console.log('Building pushci from source...');
55
- const out = path.join(os.tmpdir(), BINARY_NAME);
56
- execSync(`go build -o "${out}" ./cmd/pushci`, {
57
- cwd: path.join(__dirname, '..'),
58
- stdio: 'inherit',
59
- timeout: 120000,
60
- });
61
- if (fs.existsSync(out)) return out;
62
- } catch (_) {}
68
+ if (buildFromSource(target)) {
69
+ return target;
70
+ }
63
71
 
64
- // If invoked from a git hook, don't block the push
72
+ // If invoked from a git hook, don't block the push.
65
73
  if (process.env.GIT_DIR) {
66
74
  console.error('pushci: binary unavailable, skipping checks.');
67
75
  process.exit(0);
@@ -70,6 +78,81 @@ function downloadOrBuild() {
70
78
  process.exit(1);
71
79
  }
72
80
 
81
+ // downloadFromReleases pulls the goreleaser archive for the current
82
+ // platform from GitHub Releases, extracts the binary, and moves it
83
+ // into place. Returns true on success, false on any failure so the
84
+ // caller can fall through to buildFromSource.
85
+ function downloadFromReleases(plat, arch, target) {
86
+ const isWin = os.platform() === 'win32';
87
+ const archiveExt = isWin ? 'zip' : 'tar.gz';
88
+ const archiveName = `pushci_${VERSION}_${plat}_${arch}.${archiveExt}`;
89
+ const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
90
+
91
+ const scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pushci-install-'));
92
+ const archivePath = path.join(scratchDir, archiveName);
93
+
94
+ console.log(`Downloading pushci v${VERSION} from GitHub Releases...`);
95
+ try {
96
+ execSync(
97
+ `curl -sfL --retry 2 --retry-delay 1 -o "${archivePath}" "${url}"`,
98
+ { timeout: 60000 },
99
+ );
100
+ } catch (_) {
101
+ fs.rmSync(scratchDir, { recursive: true, force: true });
102
+ return false;
103
+ }
104
+
105
+ try {
106
+ if (isWin) {
107
+ // tar has shipped with Windows 10 (1803+) — `tar -xf` handles
108
+ // .zip the same as .tar.gz so we avoid a PowerShell branch.
109
+ execSync(`tar -xf "${archivePath}" -C "${scratchDir}"`, {
110
+ timeout: 30000,
111
+ });
112
+ } else {
113
+ execSync(`tar -xzf "${archivePath}" -C "${scratchDir}"`, {
114
+ timeout: 30000,
115
+ });
116
+ }
117
+ const extracted = path.join(scratchDir, BINARY_NAME);
118
+ if (!fs.existsSync(extracted) || !isValidBinary(extracted)) {
119
+ fs.rmSync(scratchDir, { recursive: true, force: true });
120
+ return false;
121
+ }
122
+ fs.copyFileSync(extracted, target);
123
+ if (!isWin) fs.chmodSync(target, 0o755);
124
+ fs.rmSync(scratchDir, { recursive: true, force: true });
125
+ return true;
126
+ } catch (_) {
127
+ fs.rmSync(scratchDir, { recursive: true, force: true });
128
+ return false;
129
+ }
130
+ }
131
+
132
+ // buildFromSource is the Go-install fallback. Only runs when download
133
+ // fails AND the user has Go on PATH. Useful for air-gapped dev setups.
134
+ function buildFromSource(target) {
135
+ try {
136
+ execSync('go version', { stdio: 'ignore' });
137
+ } catch (_) {
138
+ return false;
139
+ }
140
+ console.log('Downloading failed, building pushci from source...');
141
+ try {
142
+ execSync(`go build -o "${target}" ./cmd/pushci`, {
143
+ cwd: path.join(__dirname, '..'),
144
+ stdio: 'inherit',
145
+ timeout: 180000,
146
+ });
147
+ return fs.existsSync(target) && isValidBinary(target);
148
+ } catch (_) {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ // isValidBinary sanity-checks the magic bytes so a half-downloaded
154
+ // archive doesn't pass for a real binary. ELF (Linux), Mach-O
155
+ // (darwin little- and big-endian), PE (Windows).
73
156
  function isValidBinary(filepath) {
74
157
  try {
75
158
  const stat = fs.statSync(filepath);
@@ -78,12 +161,14 @@ function isValidBinary(filepath) {
78
161
  const fd = fs.openSync(filepath, 'r');
79
162
  fs.readSync(fd, buf, 0, 4, 0);
80
163
  fs.closeSync(fd);
81
- if (buf[0] === 0x7f && buf[1] === 0x45) return true;
82
- if (buf[0] === 0xcf && buf[1] === 0xfa) return true;
83
- if (buf[0] === 0xfe && buf[1] === 0xed) return true;
84
- if (buf[0] === 0x4d && buf[1] === 0x5a) return true;
164
+ if (buf[0] === 0x7f && buf[1] === 0x45) return true; // ELF
165
+ if (buf[0] === 0xcf && buf[1] === 0xfa) return true; // Mach-O (64-bit LE)
166
+ if (buf[0] === 0xfe && buf[1] === 0xed) return true; // Mach-O (32-bit BE)
167
+ if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // PE (MZ)
85
168
  return false;
86
- } catch (_) { return false; }
169
+ } catch (_) {
170
+ return false;
171
+ }
87
172
  }
88
173
 
89
174
  function printInstallHelp() {
@@ -95,12 +180,13 @@ function printInstallHelp() {
95
180
  console.error(' brew install finsavvyai/tap/pushci');
96
181
  console.error(' go install github.com/finsavvyai/pushci/cmd/pushci@latest');
97
182
  console.error('');
98
- console.error("Don't have npm/npx? Install Node.js first:");
183
+ console.error("Don't have Node installed?");
99
184
  console.error(' macOS: brew install node');
100
- console.error(' Linux: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs');
185
+ console.error(' Linux: see https://nodejs.org/en/download/package-manager');
101
186
  console.error(' Windows: https://nodejs.org');
102
187
  console.error('');
103
- console.error('Or install Go: https://go.dev/dl');
188
+ console.error('Offline? Install Go and the shim will build from source:');
189
+ console.error(' https://go.dev/dl');
104
190
  }
105
191
 
106
192
  const binary = getBinaryPath();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pushci",
3
- "version": "1.3.0",
4
- "description": "AI-native CI/CD that runs on your machine. Zero config, zero cost. 19 languages, 69 skills, Tailscale mesh, blast radius analysis.",
3
+ "version": "1.4.0",
4
+ "description": "AI-native CI/CD that runs on your machine. Zero config, zero cost. 33 languages, 69 skills, Tailscale mesh, blast radius analysis.",
5
5
  "bin": {
6
6
  "pushci": "bin/pushci.js"
7
7
  },