@solidactions/cli 1.16.0 → 1.17.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.
- package/README.md +18 -0
- package/dist/commands/deploy.js +30 -10
- package/dist/utils/deploy-ignore.js +134 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -61,6 +61,24 @@ If you run multiple AI coding agents in different project folders simultaneously
|
|
|
61
61
|
- Run `solidactions login <key> --local` in each folder so each has its own config, or
|
|
62
62
|
- Set `SOLIDACTIONS_API_KEY` / `SOLIDACTIONS_WORKSPACE_ID` in the environment each agent uses (no files to share or stomp).
|
|
63
63
|
|
|
64
|
+
### Deploy bundle exclusions (`solidactions.yaml`)
|
|
65
|
+
|
|
66
|
+
When you run `solidactions project deploy`, the CLI bundles your project directory and uploads it. You can control what goes into that bundle with an optional `deploy:` block in `solidactions.yaml`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
deploy:
|
|
70
|
+
exclude: # additive, gitignore-style patterns
|
|
71
|
+
- web/ # a large local-only sub-app you don't deploy
|
|
72
|
+
- "*.tmp"
|
|
73
|
+
gitignore: true # opt-in: also honor this project's .gitignore
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **`deploy.exclude`** — a list of gitignore-style patterns (anchoring, `*.log`-at-any-depth, and `!` negation all work). Use it to keep large local-only directories (e.g. a `.venv` or a legacy `web/` app) out of the upload — this avoids `413 Request Entity Too Large` failures. Additive on top of the always-excluded defaults: `node_modules/`, `.git/`, `dist/`, `vendor/`.
|
|
77
|
+
- **`deploy.gitignore`** — `false` by default. Set to `true` to also apply your project's root `.gitignore` to the bundle. This is opt-in so deploys don't silently change behavior.
|
|
78
|
+
- **`.env` and `.env.*` are always excluded**, regardless of config, and no `!` negation can re-include them. Secrets must come from `solidactions env set` (they are injected at runtime), and must never be baked into the deploy bundle.
|
|
79
|
+
|
|
80
|
+
The CLI prints a one-line summary of what it bundled, e.g. `Bundling 312 files (.env excluded; .gitignore applied; 2 exclude rules)`, and warns about any symlinks it skipped.
|
|
81
|
+
|
|
64
82
|
## Commands
|
|
65
83
|
|
|
66
84
|
Use `solidactions <command> --help` for full flag details on any command.
|
package/dist/commands/deploy.js
CHANGED
|
@@ -13,6 +13,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
13
13
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
14
14
|
const env_1 = require("../utils/env");
|
|
15
15
|
const api_1 = require("../utils/api");
|
|
16
|
+
const deploy_ignore_1 = require("../utils/deploy-ignore");
|
|
16
17
|
/**
|
|
17
18
|
* Validate project structure before deployment.
|
|
18
19
|
* Checks for required files and SDK dependency.
|
|
@@ -282,6 +283,29 @@ async function deploy(projectName, sourcePath, options = {}) {
|
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
285
|
const archivePath = path_1.default.join(sourceDir, '.steps-deploy.tar.gz');
|
|
286
|
+
// Plan the file list BEFORE creating the archive write stream so a walk error
|
|
287
|
+
// (permission error, unreadable dir) aborts cleanly with no orphan tarball.
|
|
288
|
+
let plan;
|
|
289
|
+
try {
|
|
290
|
+
plan = (0, deploy_ignore_1.planDeployFiles)(sourceDir, yamlConfig);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
console.error(chalk_1.default.red('Deployment failed:'));
|
|
294
|
+
console.error(error.message);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
// Summary line so silent truncation never reads as "shipped everything".
|
|
298
|
+
const parts = ['.env excluded'];
|
|
299
|
+
if (plan.summary.gitignoreApplied) {
|
|
300
|
+
parts.push('.gitignore applied');
|
|
301
|
+
}
|
|
302
|
+
if (plan.summary.excludeRuleCount > 0) {
|
|
303
|
+
parts.push(`${plan.summary.excludeRuleCount} exclude rule${plan.summary.excludeRuleCount === 1 ? '' : 's'}`);
|
|
304
|
+
}
|
|
305
|
+
console.log(chalk_1.default.gray(`Bundling ${plan.files.length} files (${parts.join('; ')})`));
|
|
306
|
+
if (plan.summary.symlinksSkipped.length > 0) {
|
|
307
|
+
console.log(chalk_1.default.yellow(`⚠ Skipped ${plan.summary.symlinksSkipped.length} symlink(s) (not followed): ${plan.summary.symlinksSkipped.join(', ')}`));
|
|
308
|
+
}
|
|
285
309
|
const output = fs_1.default.createWriteStream(archivePath);
|
|
286
310
|
const archive = (0, archiver_1.default)('tar', { gzip: true, gzipOptions: { level: 9 } });
|
|
287
311
|
output.on('close', async () => {
|
|
@@ -386,16 +410,12 @@ async function deploy(projectName, sourcePath, options = {}) {
|
|
|
386
410
|
throw err;
|
|
387
411
|
});
|
|
388
412
|
archive.pipe(output);
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
dot: true
|
|
396
|
-
}, {
|
|
397
|
-
prefix: 'tenantcode'
|
|
398
|
-
});
|
|
413
|
+
// User code goes under tenantcode/ so it never conflicts with our Dockerfile.
|
|
414
|
+
// Add each planned file explicitly (per-file walk computed above).
|
|
415
|
+
for (const relPosixPath of plan.files) {
|
|
416
|
+
const absPath = path_1.default.join(sourceDir, relPosixPath);
|
|
417
|
+
archive.file(absPath, { name: 'tenantcode/' + relPosixPath });
|
|
418
|
+
}
|
|
399
419
|
// Dockerfile always at archive root, referencing tenantcode/
|
|
400
420
|
const universalDockerfile = [
|
|
401
421
|
'FROM node:24-alpine',
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HARD_DENY_BASENAMES = exports.DEFAULT_DEPLOY_IGNORES = void 0;
|
|
7
|
+
exports.isHardDenied = isHardDenied;
|
|
8
|
+
exports.buildDeployMatcher = buildDeployMatcher;
|
|
9
|
+
exports.isIgnoredDirectory = isIgnoredDirectory;
|
|
10
|
+
exports.planDeployFiles = planDeployFiles;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const ignore_1 = __importDefault(require("ignore"));
|
|
14
|
+
/**
|
|
15
|
+
* Layer-2 matcher defaults (gitignore syntax). These are excluded unless a later
|
|
16
|
+
* pattern (deploy.exclude or .gitignore) re-includes them via a `!` negation.
|
|
17
|
+
*/
|
|
18
|
+
exports.DEFAULT_DEPLOY_IGNORES = [
|
|
19
|
+
'node_modules/',
|
|
20
|
+
'.git/',
|
|
21
|
+
'dist/',
|
|
22
|
+
'vendor/',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Layer-1 hard deny — matched against the *basename* of a path. Never
|
|
26
|
+
* re-includable by any matcher negation. Covers:
|
|
27
|
+
* - `.env` and any `.env.*` (secret-leak fix). Anchored so `.envrc` and
|
|
28
|
+
* `environment.ts` do NOT match.
|
|
29
|
+
* - `.steps-deploy.tar.gz` / `.steps-deploy.zip` (our own deploy artifacts,
|
|
30
|
+
* written into sourceDir while we walk).
|
|
31
|
+
*/
|
|
32
|
+
exports.HARD_DENY_BASENAMES = /^(?:\.env(?:$|\..*)|\.steps-deploy\.(?:tar\.gz|zip))$/;
|
|
33
|
+
/**
|
|
34
|
+
* True if a path is hard-denied (Layer 1). Checked before the matcher so that
|
|
35
|
+
* no `!` negation in deploy.exclude or .gitignore can re-include it.
|
|
36
|
+
* Tests the basename of the (POSIX) path.
|
|
37
|
+
*/
|
|
38
|
+
function isHardDenied(relPosixPath) {
|
|
39
|
+
const base = relPosixPath.split('/').pop() ?? '';
|
|
40
|
+
return exports.HARD_DENY_BASENAMES.test(base);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the Layer-2 ignore matcher from defaults + deploy.exclude + optional
|
|
44
|
+
* .gitignore. Patterns are added in order; with negations, later patterns win
|
|
45
|
+
* (standard ordered gitignore semantics).
|
|
46
|
+
*/
|
|
47
|
+
function buildDeployMatcher(sourceDir, config) {
|
|
48
|
+
const matcher = (0, ignore_1.default)();
|
|
49
|
+
// 1. Defaults.
|
|
50
|
+
matcher.add(exports.DEFAULT_DEPLOY_IGNORES);
|
|
51
|
+
// 2. deploy.exclude entries.
|
|
52
|
+
const exclude = config?.deploy?.exclude ?? [];
|
|
53
|
+
const excludeRuleCount = exclude.length;
|
|
54
|
+
if (excludeRuleCount > 0) {
|
|
55
|
+
matcher.add(exclude);
|
|
56
|
+
}
|
|
57
|
+
// 3. .gitignore, only when opted in and the file exists.
|
|
58
|
+
let gitignoreApplied = false;
|
|
59
|
+
if (config?.deploy?.gitignore === true) {
|
|
60
|
+
const gitignorePath = path_1.default.join(sourceDir, '.gitignore');
|
|
61
|
+
if (fs_1.default.existsSync(gitignorePath)) {
|
|
62
|
+
matcher.add(fs_1.default.readFileSync(gitignorePath, 'utf8'));
|
|
63
|
+
gitignoreApplied = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { matcher, summary: { gitignoreApplied, excludeRuleCount } };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* True if a directory should be pruned (not descended into). The `ignore`
|
|
70
|
+
* package only matches trailing-slash dir patterns (e.g. `node_modules/`) when
|
|
71
|
+
* the tested path itself carries a trailing slash, so we normalize the dir path
|
|
72
|
+
* to exactly one trailing slash before testing. This also still matches plain
|
|
73
|
+
* (non-trailing-slash) patterns like a bare `foo`.
|
|
74
|
+
*/
|
|
75
|
+
function isIgnoredDirectory(relPosixDir, matcher) {
|
|
76
|
+
const stripped = relPosixDir.replace(/\/+$/, '');
|
|
77
|
+
if (stripped === '') {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return matcher.ignores(stripped + '/');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Walk sourceDir, returning { files, summary }. files = relative POSIX paths to
|
|
84
|
+
* include, with Layer-1 hard deny + Layer-2 matcher applied and ignored
|
|
85
|
+
* directories pruned (so we never read into a 400 MB .venv). Symlinks (file and
|
|
86
|
+
* directory) are skipped and collected in summary.symlinksSkipped.
|
|
87
|
+
*/
|
|
88
|
+
function planDeployFiles(sourceDir, config) {
|
|
89
|
+
const { matcher, summary } = buildDeployMatcher(sourceDir, config);
|
|
90
|
+
const files = [];
|
|
91
|
+
const symlinksSkipped = [];
|
|
92
|
+
const toPosix = (p) => p.split(path_1.default.sep).join('/');
|
|
93
|
+
const walk = (absDir, relDir) => {
|
|
94
|
+
const entries = fs_1.default.readdirSync(absDir, { withFileTypes: true });
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
97
|
+
const relPosix = toPosix(relPath);
|
|
98
|
+
const absPath = path_1.default.join(absDir, entry.name);
|
|
99
|
+
// Layer 1 — hard deny (files and dirs), independent of the matcher.
|
|
100
|
+
if (isHardDenied(relPosix)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (entry.isSymbolicLink()) {
|
|
104
|
+
// Skip symlinks (both file and dir); never follow.
|
|
105
|
+
symlinksSkipped.push(relPosix);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
// Layer 2 — prune ignored directories before descending.
|
|
110
|
+
if (isIgnoredDirectory(relPosix, matcher)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
walk(absPath, relPath);
|
|
114
|
+
}
|
|
115
|
+
else if (entry.isFile()) {
|
|
116
|
+
// Layer 2 — matcher for files.
|
|
117
|
+
if (matcher.ignores(relPosix)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
files.push(relPosix);
|
|
121
|
+
}
|
|
122
|
+
// Other entry types (sockets, fifos, etc.) are ignored.
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
walk(sourceDir, '');
|
|
126
|
+
return {
|
|
127
|
+
files,
|
|
128
|
+
summary: {
|
|
129
|
+
gitignoreApplied: summary.gitignoreApplied,
|
|
130
|
+
excludeRuleCount: summary.excludeRuleCount,
|
|
131
|
+
symlinksSkipped,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidactions/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "SolidActions CLI - Deploy and manage workflow automation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"dotenv": "^17.3.1",
|
|
46
46
|
"form-data": "^4.0.0",
|
|
47
47
|
"fs-extra": "^11.0.0",
|
|
48
|
+
"ignore": "^7.0.5",
|
|
48
49
|
"js-yaml": "^4.1.0",
|
|
49
50
|
"prompts": "^2.4.2",
|
|
50
51
|
"tar": "^7.5.12"
|