@factiii/stack 0.7.2 → 0.8.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/bin/stack +1 -0
- package/dist/cli/execute-plugin-command.d.ts.map +1 -1
- package/dist/cli/execute-plugin-command.js +20 -0
- package/dist/cli/execute-plugin-command.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +25 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/plugins/pipelines/aws/scanfix/credentials.d.ts.map +1 -1
- package/dist/plugins/pipelines/aws/scanfix/credentials.js +105 -3
- package/dist/plugins/pipelines/aws/scanfix/credentials.js.map +1 -1
- package/dist/plugins/pipelines/factiii/index.d.ts.map +1 -1
- package/dist/plugins/pipelines/factiii/index.js +88 -36
- package/dist/plugins/pipelines/factiii/index.js.map +1 -1
- package/dist/plugins/pipelines/factiii/scanfix/claude-skills.d.ts +55 -0
- package/dist/plugins/pipelines/factiii/scanfix/claude-skills.d.ts.map +1 -0
- package/dist/plugins/pipelines/factiii/scanfix/claude-skills.js +474 -0
- package/dist/plugins/pipelines/factiii/scanfix/claude-skills.js.map +1 -0
- package/dist/plugins/pipelines/factiii/scanfix/ssh-verify.d.ts +10 -0
- package/dist/plugins/pipelines/factiii/scanfix/ssh-verify.d.ts.map +1 -0
- package/dist/plugins/pipelines/factiii/scanfix/ssh-verify.js +148 -0
- package/dist/plugins/pipelines/factiii/scanfix/ssh-verify.js.map +1 -0
- package/dist/plugins/pipelines/factiii/scanfix/workflows.d.ts.map +1 -1
- package/dist/plugins/pipelines/factiii/scanfix/workflows.js +2 -4
- package/dist/plugins/pipelines/factiii/scanfix/workflows.js.map +1 -1
- package/dist/plugins/pipelines/factiii/utils/workflows.d.ts +1 -0
- package/dist/plugins/pipelines/factiii/utils/workflows.d.ts.map +1 -1
- package/dist/plugins/pipelines/factiii/utils/workflows.js +5 -3
- package/dist/plugins/pipelines/factiii/utils/workflows.js.map +1 -1
- package/dist/types/plugin.d.ts +2 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/utils/config-helpers.d.ts +2 -1
- package/dist/utils/config-helpers.d.ts.map +1 -1
- package/dist/utils/config-helpers.js +1 -0
- package/dist/utils/config-helpers.js.map +1 -1
- package/dist/utils/ssh-helper.d.ts.map +1 -1
- package/dist/utils/ssh-helper.js +2 -1
- package/dist/utils/ssh-helper.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code skill scanfixes for Factiii Pipeline plugin
|
|
4
|
+
*
|
|
5
|
+
* Installs the three "standard" Claude Code skills the team uses across every
|
|
6
|
+
* factiii-pipeline repo, so any Claude Code session in any repo can rely on
|
|
7
|
+
* the same `/commit`, `/push`, and `/prod-check` workflows being available
|
|
8
|
+
* with the same gates.
|
|
9
|
+
*
|
|
10
|
+
* ~/.claude/skills/commit/SKILL.md — lint:fix + check-types gate, then commit
|
|
11
|
+
* ~/.claude/skills/push/SKILL.md — scoped tests for changed code, then push
|
|
12
|
+
* ~/.claude/skills/prod-check/SKILL.md — full pre-prod gate (lint/types/tests/build
|
|
13
|
+
* + branch diff vs production + secret scan
|
|
14
|
+
* + audit + auto-commit fixes)
|
|
15
|
+
*
|
|
16
|
+
* All three SKILL.md bodies are intentionally **generic across factiii-pipeline
|
|
17
|
+
* repos**: they live once at ~/.claude/skills/ and are shared by every repo on
|
|
18
|
+
* the machine, so they detect repo shape at runtime (apps/server, packages/*,
|
|
19
|
+
* Prisma, etc.) rather than hardcoding paths from any one repo.
|
|
20
|
+
*
|
|
21
|
+
* Local-only (no SSH, no GITHUB_ACTIONS check). Stage: dev.
|
|
22
|
+
* Severity: warning — missing skills don't break deploys, but having them
|
|
23
|
+
* available is what enforces the gates the team relies on.
|
|
24
|
+
*
|
|
25
|
+
* OPT-IN: These are "host-machine fixes" — they write to ~/.claude/, which is
|
|
26
|
+
* the developer's personal Claude Code config, not project state. Per
|
|
27
|
+
* STANDARDS.md "Host-Machine Fixes", they are gated behind an explicit opt-in
|
|
28
|
+
* in stack.local.yml (`claude_skills: true`). When the flag is unset or false,
|
|
29
|
+
* scan returns "no issue" and fix is a no-op. This keeps devs who don't use
|
|
30
|
+
* Claude Code (or who curate their own skills) from being surprised by files
|
|
31
|
+
* appearing in their home directory.
|
|
32
|
+
*
|
|
33
|
+
* REFRESH BEHAVIOR: Unlike the previous scanfix (which only wrote when the
|
|
34
|
+
* file was missing), this one **overwrites when the on-disk content drifts
|
|
35
|
+
* from the canonical content baked into stack**. That way `npx stack fix --dev`
|
|
36
|
+
* propagates skill updates to every dev who has opted in. If a dev wants to
|
|
37
|
+
* customize their local copy and not have stack stomp it, they can add the
|
|
38
|
+
* line `<!-- user-managed -->` anywhere in the file — the scanfix will detect
|
|
39
|
+
* the marker and skip that file.
|
|
40
|
+
*
|
|
41
|
+
* Split of concerns:
|
|
42
|
+
* - This scanfix owns the skill *workflows* (SKILL.md content). Process
|
|
43
|
+
* lives here so it ships with stack and is identical on every dev machine
|
|
44
|
+
* that opts in.
|
|
45
|
+
* - The audit *override reference table* (which packages, why) is optional
|
|
46
|
+
* per-repo at .specs/audit.md so PR reviewers can see it; the prod-check
|
|
47
|
+
* skill uses it if present and skips it if not.
|
|
48
|
+
* - Phase 4 of the prod-check SKILL.md auto-commits the deterministic fixes
|
|
49
|
+
* the gate produced. This is a sanctioned exception to factiii-stack's
|
|
50
|
+
* "never commit without approval" rule because prod-check is itself an
|
|
51
|
+
* explicit user-invoked gate; the skill always reports the resulting
|
|
52
|
+
* commit SHA back to the user.
|
|
53
|
+
*/
|
|
54
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
55
|
+
if (k2 === undefined) k2 = k;
|
|
56
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
57
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
58
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
59
|
+
}
|
|
60
|
+
Object.defineProperty(o, k2, desc);
|
|
61
|
+
}) : (function(o, m, k, k2) {
|
|
62
|
+
if (k2 === undefined) k2 = k;
|
|
63
|
+
o[k2] = m[k];
|
|
64
|
+
}));
|
|
65
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
66
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
67
|
+
}) : function(o, v) {
|
|
68
|
+
o["default"] = v;
|
|
69
|
+
});
|
|
70
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
71
|
+
var ownKeys = function(o) {
|
|
72
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
73
|
+
var ar = [];
|
|
74
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
75
|
+
return ar;
|
|
76
|
+
};
|
|
77
|
+
return ownKeys(o);
|
|
78
|
+
};
|
|
79
|
+
return function (mod) {
|
|
80
|
+
if (mod && mod.__esModule) return mod;
|
|
81
|
+
var result = {};
|
|
82
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
83
|
+
__setModuleDefault(result, mod);
|
|
84
|
+
return result;
|
|
85
|
+
};
|
|
86
|
+
})();
|
|
87
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
88
|
+
exports.claudeSkillFixes = void 0;
|
|
89
|
+
const fs = __importStar(require("fs"));
|
|
90
|
+
const os = __importStar(require("os"));
|
|
91
|
+
const path = __importStar(require("path"));
|
|
92
|
+
const config_helpers_js_1 = require("../../../../utils/config-helpers.js");
|
|
93
|
+
const SKILLS_ROOT = path.join(os.homedir(), '.claude', 'skills');
|
|
94
|
+
const USER_MANAGED_MARKER = '<!-- user-managed -->';
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// commit SKILL.md — gate: pnpm lint:fix + pnpm check-types, then commit
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
const COMMIT_SKILL_CONTENT = `---
|
|
99
|
+
name: commit
|
|
100
|
+
description: Create a git commit in any repo, gated by quality checks. Runs \`pnpm lint:fix\` and \`pnpm check-types\` first (skipping whichever isn't defined); only proceeds to commit if both pass. TRIGGER when the user invokes /commit or asks to commit changes.
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
# commit
|
|
104
|
+
|
|
105
|
+
Create a git commit, but only after the workspace passes the same quality gates that block CI. The point: never land a commit that breaks types or has trivially auto-fixable lint noise.
|
|
106
|
+
|
|
107
|
+
This skill is installed once globally in \`~/.claude/skills/\` and is shared by every repo on the machine. Detect what's available before running anything; skip steps whose script doesn't exist rather than failing.
|
|
108
|
+
|
|
109
|
+
## Phase 0 — Detect repo shape
|
|
110
|
+
|
|
111
|
+
Read the root \`package.json\` once and record which of these scripts exist:
|
|
112
|
+
|
|
113
|
+
- \`lint:fix\`
|
|
114
|
+
- \`check-types\`
|
|
115
|
+
|
|
116
|
+
Both are optional. A repo that defines neither still gets a commit (just with no gates).
|
|
117
|
+
|
|
118
|
+
## Steps
|
|
119
|
+
|
|
120
|
+
1. **Inspect what's being committed** — \`git status\` and \`git diff\` (both staged and unstaged) so you understand the change before gating it. If there are no changes, stop and tell the user.
|
|
121
|
+
|
|
122
|
+
2. **Auto-fix lint** — if \`lint:fix\` exists, run \`pnpm lint:fix\` from the repo root. This cleans import order, unused imports, prettier, prefer-const. Do not chase auto-fixable categories as findings — let the fixer handle them. If \`lint:fix\` itself errors out (not just modifies files), stop and report.
|
|
123
|
+
|
|
124
|
+
3. **Type check** — if \`check-types\` exists, run \`pnpm check-types\`. Type errors are blocking. Do NOT commit if this fails. If the errors are in code the user just changed, fix them and re-run. If they look pre-existing or need user judgment, stop and report before committing — do not paper over with \`@ts-ignore\` or \`any\`.
|
|
125
|
+
|
|
126
|
+
4. **Re-inspect** — \`lint:fix\` may have modified files. Run \`git status\` / \`git diff\` again so the commit message reflects the post-fix state.
|
|
127
|
+
|
|
128
|
+
5. **Commit** — follow the standard commit protocol from the system prompt:
|
|
129
|
+
- Stage specific files by name (never \`git add -A\` / \`git add .\`).
|
|
130
|
+
- Draft a concise message focused on the *why*.
|
|
131
|
+
- **Do NOT include a \`Co-Authored-By\` trailer** — factiii repos forbid it; absence is the safe default.
|
|
132
|
+
- Use a HEREDOC for the commit message to preserve formatting.
|
|
133
|
+
- Never \`--amend\`, never \`--no-verify\`.
|
|
134
|
+
|
|
135
|
+
6. **Verify** — run \`git status\` after the commit to confirm it landed and report the commit hash to the user.
|
|
136
|
+
|
|
137
|
+
## Notes
|
|
138
|
+
|
|
139
|
+
- Installed by the \`commit-skill\` scanfix in \`@factiii/stack\` (factiii pipeline). Run \`npx stack fix --dev\` from any factiii-pipeline repo to install or restore it.
|
|
140
|
+
- This file is shared across all factiii-pipeline repos on the machine — keep it generic. If you want to hand-edit your local copy and stop stack from refreshing it, add the line \`<!-- user-managed -->\` anywhere in the file.
|
|
141
|
+
- If pre-commit hooks fail, do NOT amend — fix the issue, re-stage, create a new commit.
|
|
142
|
+
- Do not push unless the user explicitly asks (\`/push\` is the right next step).
|
|
143
|
+
- If the user passes a message via args (\`/commit -m "..."\`), respect it but still run gates first.
|
|
144
|
+
`;
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
// push SKILL.md — gate: scoped tests for changed code, then push
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
const PUSH_SKILL_CONTENT = `---
|
|
149
|
+
name: push
|
|
150
|
+
description: Push the current branch in any repo, gated by tests scoped to the code that actually changed. Detects which apps/* and packages/* directories exist, maps the branch's diff onto them, and runs only those test suites. TRIGGER when the user invokes /push or asks to push the current branch.
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
# push
|
|
154
|
+
|
|
155
|
+
Push the current branch, but only after the tests that *matter for this change* are green. The point: don't run the entire monorepo's test matrix on every push, but never push code whose tests weren't run.
|
|
156
|
+
|
|
157
|
+
This skill sits between \`/commit\` (cheap local gates: lint:fix + check-types) and \`/prod-check\` (full pre-prod sweep including audit). Use it for the everyday "I'm ready to share this branch" moment.
|
|
158
|
+
|
|
159
|
+
This skill is installed once globally in \`~/.claude/skills/\` and is shared by every repo on the machine. Repo layouts vary — **detect what exists before running step-specific commands**. Never invent paths.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Phase 0 — Detect repo shape
|
|
164
|
+
|
|
165
|
+
Inspect the working directory and record:
|
|
166
|
+
|
|
167
|
+
- Every directory under \`apps/\` that contains a \`package.json\`. Each is a candidate **bucket**.
|
|
168
|
+
- Every directory under \`packages/\` that contains a \`package.json\`. Each is a candidate **bucket**.
|
|
169
|
+
- For every bucket, whether its \`package.json\` defines a \`test\` script (and a \`seed:test\` script — if present, run it before \`test\`).
|
|
170
|
+
- Whether a top-level \`shared/\` (or \`packages/shared/\`) directory exists.
|
|
171
|
+
- Whether the **root** \`package.json\` defines a \`test\` script (some repos use a single root suite instead of per-bucket).
|
|
172
|
+
|
|
173
|
+
The list of buckets *is* the test scope. A repo with only \`packages/stack\` and \`packages/auth\` has two buckets; a repo with \`apps/server\`, \`apps/client\`, and \`apps/mobile\` has three. Treat them uniformly.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Steps
|
|
178
|
+
|
|
179
|
+
1. **Sanity check the working tree** — \`git status\`. If there are uncommitted changes, stop and tell the user to \`/commit\` first. Pushing dirty state is never what they meant.
|
|
180
|
+
|
|
181
|
+
2. **Determine what actually changed** — figure out the diff against the upstream:
|
|
182
|
+
- If the branch tracks a remote: \`git diff --name-only @{u}...HEAD\`.
|
|
183
|
+
- Otherwise diff against \`origin/main\`: \`git fetch origin main\` then \`git diff --name-only origin/main...HEAD\`.
|
|
184
|
+
- If the diff is empty, stop and report — there's nothing to push.
|
|
185
|
+
|
|
186
|
+
3. **Map changed paths to buckets** — for each changed file, add to the run set whichever bucket(s) match:
|
|
187
|
+
|
|
188
|
+
| Pattern | Buckets to run |
|
|
189
|
+
|---|---|
|
|
190
|
+
| \`apps/<name>/**\` | the matching \`apps/<name>\` bucket |
|
|
191
|
+
| \`packages/<name>/**\` | the matching \`packages/<name>\` bucket |
|
|
192
|
+
| \`shared/**\` or \`packages/shared/**\` | every other bucket that imports shared (when in doubt, run them all — shared validators are exercised everywhere) |
|
|
193
|
+
| \`package.json\`, \`pnpm-lock.yaml\`, \`pnpm-workspace.yaml\`, \`tsconfig*.json\` (root) | **every** bucket — a workspace/dep change can break anything |
|
|
194
|
+
| \`.github/**\`, \`*.md\`, \`stack.yml\`, \`stackAuto.yml\`, \`docker-compose*.yml\` | none (no test-relevant changes) |
|
|
195
|
+
| anything else that doesn't match | the closest containing bucket; if none, run all buckets (be conservative) |
|
|
196
|
+
|
|
197
|
+
If *only* no-test-relevant files changed, skip Step 4 entirely and note "no test-relevant changes" in the report. If the run set ends up empty for any other reason, run **every** bucket — better to test too much than too little.
|
|
198
|
+
|
|
199
|
+
4. **Run the selected test commands** — for each bucket in the run set, in order (\`apps/*\` before \`packages/*\` is a fine default):
|
|
200
|
+
|
|
201
|
+
- If the bucket defines \`seed:test\`, run \`pnpm --filter <bucket-name> seed:test\` first. Skipping it produces misleading failures in repos that have a seed step.
|
|
202
|
+
- Then run \`pnpm --filter <bucket-name> test\`.
|
|
203
|
+
- If a bucket has no \`test\` script, skip it and note that in the report rather than failing.
|
|
204
|
+
- If a bucket fails, stop immediately and report. Do not push.
|
|
205
|
+
|
|
206
|
+
If the repo only defines a root \`test\` script (no per-bucket scripts), run \`pnpm test\` once instead of iterating.
|
|
207
|
+
|
|
208
|
+
5. **Push** — \`git push\`. If the branch has no upstream, use \`git push -u origin <branch>\`. Never \`--force\` or \`--no-verify\` unless the user explicitly asked.
|
|
209
|
+
|
|
210
|
+
6. **Report** — one short summary: which buckets ran, which were skipped (and why), the push result, and the remote branch URL if visible from \`git push\` output.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Notes
|
|
215
|
+
|
|
216
|
+
- Installed by the \`push-skill\` scanfix in \`@factiii/stack\` (factiii pipeline). Run \`npx stack fix --dev\` from any factiii-pipeline repo to install or restore it.
|
|
217
|
+
- This file is shared across all factiii-pipeline repos on the machine — keep it generic. If you want to hand-edit your local copy and stop stack from refreshing it, add the line \`<!-- user-managed -->\` anywhere in the file.
|
|
218
|
+
- This skill is about *scoping* tests, not skipping them. If you can't confidently determine the scope (weird path, generated file, unfamiliar package), run more tests rather than fewer.
|
|
219
|
+
- If the user wants the full matrix anyway, they can run \`pnpm test\` from the root themselves — don't second-guess that.
|
|
220
|
+
- Don't run lint or type checks here — those belong to \`/commit\`. Don't run audit or schema diffs — those belong to \`/prod-check\`.
|
|
221
|
+
- Don't create commits in this skill. If tests modify files (snapshots, etc.), stop and tell the user to review and \`/commit\` the updates.
|
|
222
|
+
`;
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
// prod-check SKILL.md — full pre-prod gate (already generic across repos)
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
const PROD_CHECK_SKILL_CONTENT = `---
|
|
227
|
+
name: prod-check
|
|
228
|
+
description: Pre-production verification for any factiii-pipeline repo. Runs quality gates (lint, types, tests, build), diffs the current branch against origin/production for schema/migration/env/secret/API changes, runs a security audit, and commits the resulting fixes. TRIGGER when the user asks to check a branch against production, prepare a production push, run pre-merge checks, or audit dependencies before release.
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
# prod-check
|
|
232
|
+
|
|
233
|
+
Pre-production verification workflow for **factiii-pipeline repos**. Runs in four phases: **fix everything locally**, **diff against production**, **security audit**, then **commit**. Report at the end groups findings as **BLOCKING** vs **INFORMATIONAL**.
|
|
234
|
+
|
|
235
|
+
The whole point: nothing should reach \`origin/production\` that hasn't passed local quality gates and been reviewed for schema, secret, and API-breaking changes.
|
|
236
|
+
|
|
237
|
+
This skill is installed once globally in \`~/.claude/skills/\` and is shared by every factiii-pipeline repo on your machine. Repo layouts vary — **detect what exists before running step-specific commands**. Skip steps that don't apply (e.g. no \`apps/mobile\` → no Expo doctor; no Prisma schema → no migration diff). Never invent paths.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Phase 0 — Detect repo shape
|
|
242
|
+
|
|
243
|
+
Before running anything, inspect the working directory and record which of these exist. Every later phase branches on this list:
|
|
244
|
+
|
|
245
|
+
- \`apps/server\` (Node server app)
|
|
246
|
+
- \`apps/client\` (web client)
|
|
247
|
+
- \`apps/mobile\` (Expo mobile app)
|
|
248
|
+
- Any \`packages/<name>\` directories with their own \`package.json\`
|
|
249
|
+
- \`apps/server/prisma/schema.prisma\` and \`apps/server/prisma/migrations/\`
|
|
250
|
+
- \`apps/server/src/routes/\` (tRPC route tree)
|
|
251
|
+
- \`shared/\` or \`packages/shared/\` validator dir (look for \`validators/\` underneath)
|
|
252
|
+
- \`.specs/audit.md\` (audit override reference table — optional)
|
|
253
|
+
- Root scripts: which of \`lint:fix\`, \`lint\`, \`check-types\`, \`test\`, \`build\` exist in root \`package.json\`
|
|
254
|
+
|
|
255
|
+
If a repo has only a server, run only server steps. If it has no Prisma, skip schema/migration diffing. If it's a packages-only repo (no \`apps/\`), iterate \`packages/*\` instead. **Do not fail a phase because an optional path is missing — note it as "n/a for this repo" and move on.**
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Phase 1 — Local quality gates (fix before continuing)
|
|
260
|
+
|
|
261
|
+
Run in this order against the scripts that exist. Each step must be green before moving on. If a step fails, fix it (or report blockers and stop) — do not skip.
|
|
262
|
+
|
|
263
|
+
1. **Sync** — \`git fetch origin production\` and confirm the working tree is clean (\`git status\`). Stash or commit anything dirty before continuing; uncommitted state contaminates the diff in Phase 2.
|
|
264
|
+
2. **Auto-fix lint noise** — \`pnpm lint:fix\` if the script exists. This cleans import order, unused imports, prettier, prefer-const so the type/test output isn't drowned in cosmetic churn. Always run the fixer first; ignore auto-fixable categories as *findings*.
|
|
265
|
+
3. **Type check** — \`pnpm check-types\`. Fix every error. No \`@ts-ignore\`, no \`any\` (use \`unknown\`). Type errors are blocking.
|
|
266
|
+
4. **Lint (real rules)** — \`pnpm lint\`. Focus on what matters: promise handling, hooks rules, real type issues. Auto-fixable categories are noise — don't chase them.
|
|
267
|
+
5. **Tests** — for each bucket detected in Phase 0 that has a \`test\` script: if it also has \`seed:test\`, run that first, then \`test\`. Skipping the seed step produces misleading failures in repos that have one.
|
|
268
|
+
6. **Build everything that exists** — \`pnpm build\` from the root. All present apps/packages must build. A green local dev does not imply a green build.
|
|
269
|
+
|
|
270
|
+
If any step surfaces failures you can fix safely, fix them. If failures need user judgment (e.g. a failing test reflects intended behavior change), stop and report before continuing.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Phase 2 — Diff against production
|
|
275
|
+
|
|
276
|
+
Only run after Phase 1 is fully green. Use \`origin/production\` as the base (refresh with \`git fetch origin production\` if Phase 1 took a while). For every path-based check below, only run it if the path exists in this repo.
|
|
277
|
+
|
|
278
|
+
1. **Overall diffstat** — \`git diff --stat origin/production...HEAD\` for orientation.
|
|
279
|
+
2. **Schema & migrations** — only if \`apps/server/prisma/schema.prisma\` exists. \`git diff origin/production...HEAD -- apps/server/prisma/schema.prisma 'apps/server/prisma/migrations/**'\`.
|
|
280
|
+
- Comment-only schema changes = informational.
|
|
281
|
+
- Field/model/index changes = blocking unless paired with a migration.
|
|
282
|
+
- **New migration files = blocking action**: they need to be applied to prod DB during deploy. Call this out explicitly with the migration name(s).
|
|
283
|
+
3. **Secrets scan** — always. Search the diff for committed credentials. Patterns to grep: \`AKIA[0-9A-Z]{16}\` (AWS access key), \`-----BEGIN .* PRIVATE KEY-----\`, \`sk_live_\`, \`sk_test_\`, \`xox[baprs]-\`, \`ghp_\`, \`Bearer [A-Za-z0-9_\\-]{20,}\`, raw \`password\\s*[:=]\`, \`client_secret\`. Run against \`git diff origin/production...HEAD\`. Anything matching is **BLOCKING** — instruct the user to rotate the credential in its provider before merging, since git history is forever.
|
|
284
|
+
4. **Env / infra surface** — diff whichever of these exist and flag any change:
|
|
285
|
+
- \`stack.yml\`, \`stackAuto.yml\`, \`docker-compose*.yml\`
|
|
286
|
+
- \`.github/workflows/**\`
|
|
287
|
+
- \`start.sh\`, root \`package.json\`, any \`apps/*/package.json\` or \`packages/*/package.json\` (deps)
|
|
288
|
+
- \`pnpm-workspace.yaml\` catalog
|
|
289
|
+
Changes here often need a corresponding action on the prod host (env var added, image rebuilt, workflow secret set).
|
|
290
|
+
5. **Breaking API changes for mobile** — only if \`apps/mobile/\` exists. Old mobile builds in the wild cannot be force-updated. Diff the server's route tree (e.g. \`apps/server/src/routes/**\`) and look for:
|
|
291
|
+
- Removed tRPC procedures
|
|
292
|
+
- Renamed/removed input or output fields
|
|
293
|
+
- Tightened Zod validators (new required fields, narrower enums)
|
|
294
|
+
- Changed HTTP status codes or error shapes
|
|
295
|
+
These are blocking unless the change is additive or behind a version gate.
|
|
296
|
+
6. **Shared validators** — if a shared validator dir exists, diff it. Tightening a Zod schema is the most common silent break for old clients.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Phase 3 — Security audit pass
|
|
301
|
+
|
|
302
|
+
**Always run** — even when \`pnpm-lock.yaml\` is unchanged. New CVEs get disclosed against packages already pinned in the lockfile, so a clean diff doesn't mean a clean audit.
|
|
303
|
+
|
|
304
|
+
Start with a quick \`pnpm audit\` against the current lockfile. If it's clean and (where present) \`.specs/audit.md\` is current, you're done with Phase 3 in one command. If there are new findings, run the full process below to triage them.
|
|
305
|
+
|
|
306
|
+
If the repo has a \`.specs/audit.md\`, it owns the override **reference table** (what's pinned and *why*). This skill owns the **process** for updating it. If the repo has no audit doc, just report findings — don't create the file unless the user asks.
|
|
307
|
+
|
|
308
|
+
### Audit process
|
|
309
|
+
|
|
310
|
+
1. Remove ALL overrides from \`package.json\` (security + dedup).
|
|
311
|
+
2. \`pnpm install --no-frozen-lockfile\`.
|
|
312
|
+
3. If \`apps/mobile/\` exists: \`cd apps/mobile && npx expo-doctor\` — check Expo SDK compatibility first.
|
|
313
|
+
4. Re-add dedup overrides if still needed, \`pnpm install\`.
|
|
314
|
+
5. \`pnpm audit\` — note what's still vulnerable.
|
|
315
|
+
6. For each vuln: \`pnpm why <pkg>\` — if upstream fixed it, no override needed.
|
|
316
|
+
7. Add security overrides only for transitive deps where the fix is a semver-compatible bump.
|
|
317
|
+
8. \`pnpm install && pnpm audit\` — verify.
|
|
318
|
+
9. If \`.specs/audit.md\` exists, **update it** with the new override row(s), source chain, and the *reason* (CVE, behavior, or compatibility issue). A row without a reason is worse than no row.
|
|
319
|
+
|
|
320
|
+
**Don't override** if: major version jump required, dev-only with no prod exposure, or it's a direct dep (just upgrade it).
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Phase 4 — Commit prod-check fixes
|
|
325
|
+
|
|
326
|
+
This phase **intentionally violates** the repo's "never commit without user approval" rule in CLAUDE.md. It is the one sanctioned exception: prod-check is explicitly invoked by the user as a pre-production gate, and the commit captures only the deterministic fixes the gate produced (lint:fix output, type-error fixes, audit override updates). Always tell the user the commit happened and what's in it.
|
|
327
|
+
|
|
328
|
+
Skip this phase entirely if:
|
|
329
|
+
|
|
330
|
+
- Phase 1 had unresolved failures (the user must triage first), OR
|
|
331
|
+
- \`git status\` is clean (nothing was changed), OR
|
|
332
|
+
- The diff includes changes you didn't make in this session — in that case, **stop and ask** rather than sweeping unrelated work into a prod-check commit.
|
|
333
|
+
|
|
334
|
+
### Commit steps
|
|
335
|
+
|
|
336
|
+
1. \`git status --short\` — confirm only files touched by Phase 1–3 are dirty.
|
|
337
|
+
2. \`git diff\` — sanity-check the contents one more time.
|
|
338
|
+
3. Stage **only** the files this session modified (name them explicitly; do **not** \`git add -A\`).
|
|
339
|
+
4. Commit with a HEREDOC message:
|
|
340
|
+
\`\`\`
|
|
341
|
+
chore(prod-check): apply pre-production gate fixes
|
|
342
|
+
|
|
343
|
+
- <bulleted list of what was fixed: lint:fix, type errors, audit overrides, etc>
|
|
344
|
+
\`\`\`
|
|
345
|
+
No \`Co-Authored-By\` trailer (factiii-stack CLAUDE.md forbids it).
|
|
346
|
+
5. Do **not** push. Pushing is always the user's call (\`/push\` is the right next step).
|
|
347
|
+
6. In your final report, surface a clearly-labeled note:
|
|
348
|
+
> **Auto-committed by prod-check:** \`<commit sha>\` — <one-line summary>. This skill is allowed to commit pre-prod gate fixes; review with \`git show <sha>\` and amend or revert if anything looks off.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Final report format
|
|
353
|
+
|
|
354
|
+
Group findings into two buckets. Be specific — name files, line numbers, migration filenames.
|
|
355
|
+
|
|
356
|
+
\`\`\`
|
|
357
|
+
BLOCKING (must resolve before merging to production):
|
|
358
|
+
- <finding> — <file:line> — <what to do>
|
|
359
|
+
|
|
360
|
+
INFORMATIONAL (no action needed, just FYI):
|
|
361
|
+
- <finding> — <file:line>
|
|
362
|
+
|
|
363
|
+
AUTO-COMMIT:
|
|
364
|
+
- <sha or "none — nothing to commit"> — <summary>
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
If Phase 1 had failures the user must triage, surface those at the top before any Phase 2 findings and skip Phase 4.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Notes
|
|
372
|
+
|
|
373
|
+
- Installed by the \`prod-check-skill\` scanfix in \`@factiii/stack\` (factiii pipeline). Run \`npx stack fix --dev\` from any factiii-pipeline repo to install or restore it.
|
|
374
|
+
- This file is shared across all factiii-pipeline repos on the machine — keep it generic. If you want to hand-edit your local copy and stop stack from refreshing it, add the line \`<!-- user-managed -->\` anywhere in the file.
|
|
375
|
+
- Per-repo specifics (audit override tables, etc.) belong in the consumer repo at \`.specs/audit.md\`.
|
|
376
|
+
`;
|
|
377
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
378
|
+
// Helpers
|
|
379
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
380
|
+
/**
|
|
381
|
+
* Read the per-developer opt-in flag from stack.local.yml.
|
|
382
|
+
* Defaults to false: host-machine fixes never run unless explicitly enabled.
|
|
383
|
+
*/
|
|
384
|
+
function isOptedIn(rootDir) {
|
|
385
|
+
try {
|
|
386
|
+
const local = (0, config_helpers_js_1.loadLocalConfig)(rootDir);
|
|
387
|
+
return local.claude_skills === true;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Decide whether the on-disk SKILL.md needs (re)writing.
|
|
395
|
+
*
|
|
396
|
+
* Returns true if:
|
|
397
|
+
* - the file is missing, OR
|
|
398
|
+
* - the file content differs from the canonical content AND the file does
|
|
399
|
+
* not contain the user-managed marker (so devs who hand-edit can opt out
|
|
400
|
+
* by adding `<!-- user-managed -->` anywhere in the file).
|
|
401
|
+
*/
|
|
402
|
+
function needsInstall(file, canonical) {
|
|
403
|
+
if (!fs.existsSync(file))
|
|
404
|
+
return true;
|
|
405
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
406
|
+
if (current.includes(USER_MANAGED_MARKER))
|
|
407
|
+
return false;
|
|
408
|
+
return current !== canonical;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Idempotently install a SKILL.md to ~/.claude/skills/<name>/SKILL.md,
|
|
412
|
+
* honoring the user-managed opt-out marker.
|
|
413
|
+
*/
|
|
414
|
+
function installSkill(name, content) {
|
|
415
|
+
const dir = path.join(SKILLS_ROOT, name);
|
|
416
|
+
const file = path.join(dir, 'SKILL.md');
|
|
417
|
+
if (!needsInstall(file, content))
|
|
418
|
+
return;
|
|
419
|
+
if (!fs.existsSync(dir))
|
|
420
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
421
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Build a Fix definition for one of the standard skills. The shape is the
|
|
425
|
+
* same for all three — only the id, description, name, and content vary.
|
|
426
|
+
*/
|
|
427
|
+
function makeSkillFix(args) {
|
|
428
|
+
const file = path.join(SKILLS_ROOT, args.skillName, 'SKILL.md');
|
|
429
|
+
return {
|
|
430
|
+
id: args.id,
|
|
431
|
+
stage: 'dev',
|
|
432
|
+
severity: 'warning',
|
|
433
|
+
description: args.description,
|
|
434
|
+
scan: async function (_config, rootDir) {
|
|
435
|
+
// Opt-in gate: invisible to devs who haven't enabled claude_skills.
|
|
436
|
+
if (!isOptedIn(rootDir))
|
|
437
|
+
return false;
|
|
438
|
+
// Returns true when there IS an issue (skill missing or stale).
|
|
439
|
+
return needsInstall(file, args.content);
|
|
440
|
+
},
|
|
441
|
+
fix: async function (_config, rootDir) {
|
|
442
|
+
// Belt-and-braces: re-check the opt-in before touching ~/.claude.
|
|
443
|
+
if (!isOptedIn(rootDir))
|
|
444
|
+
return true;
|
|
445
|
+
installSkill(args.skillName, args.content);
|
|
446
|
+
return true;
|
|
447
|
+
},
|
|
448
|
+
manualFix: `Set \`claude_skills: true\` in stack.local.yml and run \`npx stack fix --dev\`, or create ${file} by hand.`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
452
|
+
// Exported fixes
|
|
453
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
454
|
+
exports.claudeSkillFixes = [
|
|
455
|
+
makeSkillFix({
|
|
456
|
+
id: 'commit-skill-installed',
|
|
457
|
+
skillName: 'commit',
|
|
458
|
+
description: 'Claude Code commit skill is missing or stale at ~/.claude/skills/commit/SKILL.md — the lint:fix + check-types pre-commit gate cannot be invoked consistently without it (enable with `claude_skills: true` in stack.local.yml; add `<!-- user-managed -->` to the file to opt out of refresh)',
|
|
459
|
+
content: COMMIT_SKILL_CONTENT,
|
|
460
|
+
}),
|
|
461
|
+
makeSkillFix({
|
|
462
|
+
id: 'push-skill-installed',
|
|
463
|
+
skillName: 'push',
|
|
464
|
+
description: 'Claude Code push skill is missing or stale at ~/.claude/skills/push/SKILL.md — scoped pre-push tests cannot be invoked consistently without it (enable with `claude_skills: true` in stack.local.yml; add `<!-- user-managed -->` to the file to opt out of refresh)',
|
|
465
|
+
content: PUSH_SKILL_CONTENT,
|
|
466
|
+
}),
|
|
467
|
+
makeSkillFix({
|
|
468
|
+
id: 'prod-check-skill-installed',
|
|
469
|
+
skillName: 'prod-check',
|
|
470
|
+
description: 'Claude Code prod-check skill is missing or stale at ~/.claude/skills/prod-check/SKILL.md — pre-prod gates (lint/types/tests/build + branch diff vs production + secret scan + audit) cannot be invoked consistently without it (enable with `claude_skills: true` in stack.local.yml; add `<!-- user-managed -->` to the file to opt out of refresh)',
|
|
471
|
+
content: PROD_CHECK_SKILL_CONTENT,
|
|
472
|
+
}),
|
|
473
|
+
];
|
|
474
|
+
//# sourceMappingURL=claude-skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-skills.js","sourceRoot":"","sources":["../../../../../src/plugins/pipelines/factiii/scanfix/claude-skills.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Verification Scanfixes
|
|
3
|
+
*
|
|
4
|
+
* Two concerns:
|
|
5
|
+
* 1. Vault key extraction — if vault has {STAGE}_SSH but no key on disk, extract it
|
|
6
|
+
* 2. SSH connectivity — if key exists, verify the connection actually works
|
|
7
|
+
*/
|
|
8
|
+
import type { Fix } from '../../../../types/index.js';
|
|
9
|
+
export declare const sshVerifyFixes: Fix[];
|
|
10
|
+
//# sourceMappingURL=ssh-verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-verify.d.ts","sourceRoot":"","sources":["../../../../../src/plugins/pipelines/factiii/scanfix/ssh-verify.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAiB,GAAG,EAAE,MAAM,4BAA4B,CAAC;AA2IrE,eAAO,MAAM,cAAc,EAAE,GAAG,EAK/B,CAAC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SSH Verification Scanfixes
|
|
4
|
+
*
|
|
5
|
+
* Two concerns:
|
|
6
|
+
* 1. Vault key extraction — if vault has {STAGE}_SSH but no key on disk, extract it
|
|
7
|
+
* 2. SSH connectivity — if key exists, verify the connection actually works
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.sshVerifyFixes = void 0;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const ssh_helper_js_1 = require("../../../../utils/ssh-helper.js");
|
|
13
|
+
const config_helpers_js_1 = require("../../../../utils/config-helpers.js");
|
|
14
|
+
const secrets_js_1 = require("./secrets.js");
|
|
15
|
+
/**
|
|
16
|
+
* Get the Ansible vault store, or null if not configured.
|
|
17
|
+
*/
|
|
18
|
+
function getAnsibleStore(config, rootDir) {
|
|
19
|
+
if (!config.ansible?.vault_path)
|
|
20
|
+
return null;
|
|
21
|
+
// Lazy import to avoid circular deps
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
|
+
const { AnsibleVaultSecrets } = require('../../../../utils/ansible-vault-secrets.js');
|
|
24
|
+
return new AnsibleVaultSecrets({
|
|
25
|
+
vault_path: config.ansible.vault_path,
|
|
26
|
+
vault_password_file: config.ansible.vault_password_file,
|
|
27
|
+
rootDir,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Try to read a secret from vault. Returns the value or null.
|
|
32
|
+
*/
|
|
33
|
+
async function readVaultSecret(secretName, config, rootDir) {
|
|
34
|
+
const store = getAnsibleStore(config, rootDir);
|
|
35
|
+
if (!store)
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const result = await store.getSecret(secretName);
|
|
39
|
+
if (result.success && typeof result.value === 'string') {
|
|
40
|
+
return result.value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Vault read failed
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// ────────────────────────────────────────────────────────────
|
|
49
|
+
// Fix A: Extract vault SSH key to disk (secrets stage)
|
|
50
|
+
// ────────────────────────────────────────────────────────────
|
|
51
|
+
function makeVaultKeyFix(targetStage) {
|
|
52
|
+
const secretName = targetStage.toUpperCase() + '_SSH';
|
|
53
|
+
return {
|
|
54
|
+
id: 'ssh-vault-key-to-disk-' + targetStage,
|
|
55
|
+
stage: 'secrets',
|
|
56
|
+
targetStage,
|
|
57
|
+
severity: 'critical',
|
|
58
|
+
description: secretName + ' key is in vault but not on disk — extracting',
|
|
59
|
+
scan: async (config, _rootDir) => {
|
|
60
|
+
// No issue if key already exists on disk
|
|
61
|
+
const keyPath = (0, ssh_helper_js_1.findSshKeyForStage)(targetStage, config.name);
|
|
62
|
+
if (keyPath)
|
|
63
|
+
return false;
|
|
64
|
+
// Check if environments exist for this stage
|
|
65
|
+
if (!(0, config_helpers_js_1.hasEnvironments)(config))
|
|
66
|
+
return false;
|
|
67
|
+
const envs = (0, config_helpers_js_1.extractEnvironments)(config);
|
|
68
|
+
const stageEnvs = Object.entries(envs).filter(([name]) => name === targetStage || name.startsWith(targetStage + '_'));
|
|
69
|
+
if (stageEnvs.length === 0)
|
|
70
|
+
return false;
|
|
71
|
+
// Check if vault has the key
|
|
72
|
+
const value = await readVaultSecret(secretName, config, _rootDir);
|
|
73
|
+
if (!value || !value.includes('PRIVATE KEY'))
|
|
74
|
+
return false;
|
|
75
|
+
return true; // Issue: key in vault but not on disk
|
|
76
|
+
},
|
|
77
|
+
fix: async (config, rootDir) => {
|
|
78
|
+
const value = await readVaultSecret(secretName, config, rootDir);
|
|
79
|
+
if (!value || !value.includes('PRIVATE KEY')) {
|
|
80
|
+
console.log(' [!] Could not read ' + secretName + ' from vault');
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const keyPath = (0, secrets_js_1.writeSshKeyToDisk)(targetStage, value, config);
|
|
84
|
+
console.log(' [OK] Extracted ' + secretName + ' to ' + keyPath);
|
|
85
|
+
return true;
|
|
86
|
+
},
|
|
87
|
+
manualFix: 'Run: npx stack deploy --secrets write-ssh-keys',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ────────────────────────────────────────────────────────────
|
|
91
|
+
// Fix B: Verify SSH connectivity (staging/prod stage)
|
|
92
|
+
// ────────────────────────────────────────────────────────────
|
|
93
|
+
function makeSshVerifyFix(targetStage) {
|
|
94
|
+
return {
|
|
95
|
+
id: 'ssh-verify-' + targetStage,
|
|
96
|
+
stage: targetStage,
|
|
97
|
+
severity: 'warning',
|
|
98
|
+
description: 'SSH connection to ' + targetStage + ' server failed',
|
|
99
|
+
scan: async (config, _rootDir) => {
|
|
100
|
+
// Skip on server — no need to verify SSH to ourselves
|
|
101
|
+
if (process.env.GITHUB_ACTIONS || process.env.FACTIII_ON_SERVER)
|
|
102
|
+
return false;
|
|
103
|
+
// Need a key on disk to test
|
|
104
|
+
const keyPath = (0, ssh_helper_js_1.findSshKeyForStage)(targetStage, config.name);
|
|
105
|
+
if (!keyPath)
|
|
106
|
+
return false;
|
|
107
|
+
// Need environments with a domain to connect to
|
|
108
|
+
if (!(0, config_helpers_js_1.hasEnvironments)(config))
|
|
109
|
+
return false;
|
|
110
|
+
const envs = (0, config_helpers_js_1.extractEnvironments)(config);
|
|
111
|
+
const stageEnvs = Object.entries(envs).filter(([name]) => name === targetStage || name.startsWith(targetStage + '_'));
|
|
112
|
+
if (stageEnvs.length === 0)
|
|
113
|
+
return false;
|
|
114
|
+
const envConfig = stageEnvs[0]?.[1];
|
|
115
|
+
if (!envConfig)
|
|
116
|
+
return false;
|
|
117
|
+
const host = envConfig.domain;
|
|
118
|
+
const user = envConfig.ssh_user ?? 'ubuntu';
|
|
119
|
+
if (!host || host.toUpperCase().startsWith('EXAMPLE'))
|
|
120
|
+
return false;
|
|
121
|
+
// Test SSH connection
|
|
122
|
+
const result = (0, child_process_1.spawnSync)('ssh', [
|
|
123
|
+
'-i', keyPath,
|
|
124
|
+
'-o', 'BatchMode=yes',
|
|
125
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
126
|
+
'-o', 'ConnectTimeout=5',
|
|
127
|
+
user + '@' + host,
|
|
128
|
+
'echo ok',
|
|
129
|
+
], {
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
stdio: 'pipe',
|
|
132
|
+
timeout: 15000,
|
|
133
|
+
});
|
|
134
|
+
// Issue detected if connection fails
|
|
135
|
+
return result.status !== 0;
|
|
136
|
+
},
|
|
137
|
+
fix: null,
|
|
138
|
+
manualFix: 'Check that the server is running, the SSH key is authorized, and security groups allow port 22.\n' +
|
|
139
|
+
' Test manually: ssh -i ~/.ssh/' + targetStage + '_deploy_key ubuntu@<host> echo ok',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
exports.sshVerifyFixes = [
|
|
143
|
+
makeVaultKeyFix('staging'),
|
|
144
|
+
makeVaultKeyFix('prod'),
|
|
145
|
+
makeSshVerifyFix('staging'),
|
|
146
|
+
makeSshVerifyFix('prod'),
|
|
147
|
+
];
|
|
148
|
+
//# sourceMappingURL=ssh-verify.js.map
|