javi-forge 1.6.0 → 1.7.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 +45 -1
- package/ci-local/README.md +56 -11
- package/ci-local/ci-local.ps1 +445 -0
- package/ci-local/ci-local.sh +52 -9
- package/ci-local/docker/python.Dockerfile +7 -0
- package/ci-local/hooks/commit-msg +120 -44
- package/ci-local/hooks/commit-msg.test.sh +286 -0
- package/ci-local/hooks/pre-commit +9 -1
- package/ci-local/hooks/pre-push +13 -2
- package/ci-local/install.ps1 +367 -0
- package/ci-local/install.sh +151 -5
- package/ci-local/uninstall.ps1 +85 -0
- package/ci-local/uninstall.sh +95 -0
- package/dist/cli/dispatch/ci.d.ts +12 -0
- package/dist/cli/dispatch/ci.js +38 -0
- package/dist/cli/dispatch/security.d.ts +11 -0
- package/dist/cli/dispatch/security.js +68 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
- package/dist/cli/dispatch/simple-renderers.js +65 -0
- package/dist/cli/dispatch/skill-publish.d.ts +15 -0
- package/dist/cli/dispatch/skill-publish.js +44 -0
- package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
- package/dist/cli/dispatch/skills-cmd.js +93 -0
- package/dist/cli/dispatch/tdd.d.ts +11 -0
- package/dist/cli/dispatch/tdd.js +45 -0
- package/dist/cli/dispatch/types.d.ts +29 -0
- package/dist/cli/dispatch/types.js +2 -0
- package/dist/cli/dispatch/workflow.d.ts +11 -0
- package/dist/cli/dispatch/workflow.js +67 -0
- package/dist/cli/help.d.ts +127 -0
- package/dist/cli/help.js +138 -0
- package/dist/cli/runtime.d.ts +16 -0
- package/dist/cli/runtime.js +38 -0
- package/dist/cli/validators.d.ts +17 -0
- package/dist/cli/validators.js +35 -0
- package/dist/commands/analyze.d.ts +1 -1
- package/dist/commands/analyze.js +14 -16
- package/dist/commands/ci.d.ts +50 -3
- package/dist/commands/ci.js +537 -182
- package/dist/commands/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +112 -62
- package/dist/commands/init/report.d.ts +13 -0
- package/dist/commands/init/report.js +13 -0
- package/dist/commands/init/steps/agent-skills.d.ts +13 -0
- package/dist/commands/init/steps/agent-skills.js +45 -0
- package/dist/commands/init/steps/ai-sync.d.ts +15 -0
- package/dist/commands/init/steps/ai-sync.js +62 -0
- package/dist/commands/init/steps/ci.d.ts +24 -0
- package/dist/commands/init/steps/ci.js +74 -0
- package/dist/commands/init/steps/claude-md.d.ts +14 -0
- package/dist/commands/init/steps/claude-md.js +54 -0
- package/dist/commands/init/steps/code-graph.d.ts +16 -0
- package/dist/commands/init/steps/code-graph.js +68 -0
- package/dist/commands/init/steps/context-dir.d.ts +13 -0
- package/dist/commands/init/steps/context-dir.js +47 -0
- package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
- package/dist/commands/init/steps/docker-deploy.js +60 -0
- package/dist/commands/init/steps/ghagga.d.ts +15 -0
- package/dist/commands/init/steps/ghagga.js +58 -0
- package/dist/commands/init/steps/git.d.ts +25 -0
- package/dist/commands/init/steps/git.js +81 -0
- package/dist/commands/init/steps/gitignore.d.ts +13 -0
- package/dist/commands/init/steps/gitignore.js +39 -0
- package/dist/commands/init/steps/local-ai.d.ts +14 -0
- package/dist/commands/init/steps/local-ai.js +53 -0
- package/dist/commands/init/steps/manifest.d.ts +13 -0
- package/dist/commands/init/steps/manifest.js +58 -0
- package/dist/commands/init/steps/memory.d.ts +15 -0
- package/dist/commands/init/steps/memory.js +60 -0
- package/dist/commands/init/steps/mock.d.ts +13 -0
- package/dist/commands/init/steps/mock.js +64 -0
- package/dist/commands/init/steps/sdd.d.ts +12 -0
- package/dist/commands/init/steps/sdd.js +40 -0
- package/dist/commands/init/steps/security.d.ts +28 -0
- package/dist/commands/init/steps/security.js +103 -0
- package/dist/commands/init/types.d.ts +30 -0
- package/dist/commands/init/types.js +2 -0
- package/dist/commands/init.d.ts +5 -4
- package/dist/commands/init.js +42 -387
- package/dist/commands/llmstxt.d.ts +1 -1
- package/dist/commands/llmstxt.js +36 -34
- package/dist/commands/plugin.d.ts +10 -1
- package/dist/commands/plugin.js +92 -47
- package/dist/commands/security.d.ts +11 -5
- package/dist/commands/security.js +236 -77
- package/dist/commands/skills/analysis.d.ts +19 -0
- package/dist/commands/skills/analysis.js +211 -0
- package/dist/commands/skills/benchmark.d.ts +6 -0
- package/dist/commands/skills/benchmark.js +82 -0
- package/dist/commands/skills/constants.d.ts +10 -0
- package/dist/commands/skills/constants.js +27 -0
- package/dist/commands/skills/directives.d.ts +21 -0
- package/dist/commands/skills/directives.js +101 -0
- package/dist/commands/skills/parsing.d.ts +16 -0
- package/dist/commands/skills/parsing.js +108 -0
- package/dist/commands/skills/rules.d.ts +5 -0
- package/dist/commands/skills/rules.js +32 -0
- package/dist/commands/skills/scoring.d.ts +47 -0
- package/dist/commands/skills/scoring.js +306 -0
- package/dist/commands/skills.d.ts +8 -69
- package/dist/commands/skills.js +9 -490
- package/dist/commands/tdd-pipeline.d.ts +17 -0
- package/dist/commands/tdd-pipeline.js +144 -0
- package/dist/commands/tdd.d.ts +1 -1
- package/dist/commands/tdd.js +21 -18
- package/dist/commands/workflow.d.ts +23 -0
- package/dist/commands/workflow.js +116 -0
- package/dist/constants.d.ts +15 -1
- package/dist/constants.js +161 -122
- package/dist/index.js +43 -280
- package/dist/lib/agent-skills.d.ts +36 -1
- package/dist/lib/agent-skills.js +168 -19
- package/dist/lib/atlassian-mcp.d.ts +42 -0
- package/dist/lib/atlassian-mcp.js +98 -0
- package/dist/lib/auto-skill-install.d.ts +37 -0
- package/dist/lib/auto-skill-install.js +91 -0
- package/dist/lib/auto-wire.d.ts +20 -0
- package/dist/lib/auto-wire.js +240 -0
- package/dist/lib/ci-config.d.ts +65 -0
- package/dist/lib/ci-config.js +297 -0
- package/dist/lib/claudemd.d.ts +13 -1
- package/dist/lib/claudemd.js +174 -24
- package/dist/lib/codex-export.d.ts +1 -1
- package/dist/lib/codex-export.js +26 -32
- package/dist/lib/common.d.ts +1 -1
- package/dist/lib/common.js +52 -44
- package/dist/lib/context.d.ts +17 -2
- package/dist/lib/context.js +142 -13
- package/dist/lib/crash-recovery.d.ts +34 -0
- package/dist/lib/crash-recovery.js +123 -0
- package/dist/lib/docker.d.ts +23 -2
- package/dist/lib/docker.js +168 -131
- package/dist/lib/exec.d.ts +10 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/frontmatter.d.ts +1 -1
- package/dist/lib/frontmatter.js +29 -15
- package/dist/lib/harness-audit.d.ts +35 -0
- package/dist/lib/harness-audit.js +277 -0
- package/dist/lib/parallel-batch.d.ts +42 -0
- package/dist/lib/parallel-batch.js +88 -0
- package/dist/lib/plugin.d.ts +9 -3
- package/dist/lib/plugin.js +127 -70
- package/dist/lib/secret-scanner.d.ts +30 -0
- package/dist/lib/secret-scanner.js +274 -0
- package/dist/lib/security-analysis.d.ts +74 -0
- package/dist/lib/security-analysis.js +489 -0
- package/dist/lib/skill-publish.d.ts +40 -0
- package/dist/lib/skill-publish.js +164 -0
- package/dist/lib/skill-scanner.d.ts +63 -0
- package/dist/lib/skill-scanner.js +383 -0
- package/dist/lib/stack-detector.d.ts +38 -0
- package/dist/lib/stack-detector.js +207 -0
- package/dist/lib/supply-chain.d.ts +23 -0
- package/dist/lib/supply-chain.js +126 -0
- package/dist/lib/team-presets.d.ts +53 -0
- package/dist/lib/team-presets.js +201 -0
- package/dist/lib/template.d.ts +16 -1
- package/dist/lib/template.js +46 -17
- package/dist/lib/workflow/discovery.d.ts +27 -0
- package/dist/lib/workflow/discovery.js +85 -0
- package/dist/lib/workflow/index.d.ts +5 -0
- package/dist/lib/workflow/index.js +5 -0
- package/dist/lib/workflow/parser.d.ts +16 -0
- package/dist/lib/workflow/parser.js +198 -0
- package/dist/lib/workflow/renderer.d.ts +9 -0
- package/dist/lib/workflow/renderer.js +152 -0
- package/dist/lib/workflow/validator.d.ts +10 -0
- package/dist/lib/workflow/validator.js +189 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/scaffold-tasks.d.ts +3 -0
- package/dist/tasks/scaffold-tasks.js +14 -0
- package/dist/tasks/task-id.d.ts +30 -0
- package/dist/tasks/task-id.js +59 -0
- package/dist/tasks/task-tracker.d.ts +15 -0
- package/dist/tasks/task-tracker.js +81 -0
- package/dist/types/index.d.ts +134 -6
- package/dist/types/index.js +11 -1
- package/dist/ui/AnalyzeUI.d.ts +1 -1
- package/dist/ui/AnalyzeUI.js +38 -39
- package/dist/ui/App.d.ts +5 -3
- package/dist/ui/App.js +86 -46
- package/dist/ui/AutoSkills.d.ts +9 -0
- package/dist/ui/AutoSkills.js +124 -0
- package/dist/ui/CI.d.ts +2 -2
- package/dist/ui/CI.js +24 -26
- package/dist/ui/CIContext.d.ts +1 -1
- package/dist/ui/CIContext.js +2 -2
- package/dist/ui/CISelector.d.ts +2 -2
- package/dist/ui/CISelector.js +23 -15
- package/dist/ui/Doctor.d.ts +1 -1
- package/dist/ui/Doctor.js +35 -29
- package/dist/ui/Header.d.ts +1 -1
- package/dist/ui/Header.js +14 -14
- package/dist/ui/HookProfileSelector.d.ts +9 -0
- package/dist/ui/HookProfileSelector.js +54 -0
- package/dist/ui/LlmsTxt.d.ts +1 -1
- package/dist/ui/LlmsTxt.js +31 -22
- package/dist/ui/MemorySelector.d.ts +2 -2
- package/dist/ui/MemorySelector.js +28 -16
- package/dist/ui/NameInput.d.ts +1 -1
- package/dist/ui/NameInput.js +21 -21
- package/dist/ui/OptionSelector.d.ts +6 -2
- package/dist/ui/OptionSelector.js +83 -32
- package/dist/ui/Plugin.d.ts +4 -3
- package/dist/ui/Plugin.js +78 -35
- package/dist/ui/Progress.d.ts +3 -3
- package/dist/ui/Progress.js +23 -22
- package/dist/ui/Skills.d.ts +2 -2
- package/dist/ui/Skills.js +61 -32
- package/dist/ui/StackSelector.d.ts +2 -2
- package/dist/ui/StackSelector.js +27 -17
- package/dist/ui/Summary.d.ts +3 -3
- package/dist/ui/Summary.js +60 -50
- package/dist/ui/Welcome.d.ts +1 -1
- package/dist/ui/Welcome.js +15 -16
- package/dist/ui/theme.d.ts +1 -1
- package/dist/ui/theme.js +6 -6
- package/lib/common.psm1 +167 -0
- package/lib/common.sh +30 -0
- package/package.json +46 -24
- package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
- package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
- package/templates/common/repoforge/repoforge.yaml +34 -0
- package/templates/github/deploy-docker-zero-downtime.yml +140 -0
- package/templates/github/repoforge-graph.yml +45 -0
- package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
- package/templates/local-ai/.env.example +17 -0
- package/templates/local-ai/docker-compose.yml +95 -0
- package/templates/security-hooks/claude-settings-security.json +30 -0
- package/templates/security-hooks/commit-msg-signing +29 -0
- package/templates/security-hooks/pre-commit-permissions +74 -0
- package/templates/security-hooks/pre-commit-secrets +74 -0
- package/templates/security-hooks/pre-push-branch-protection +62 -0
- package/templates/security-hooks/pre-push-deps +83 -0
- package/templates/security-hooks/pre-push-signing +67 -0
- package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
- package/templates/workflows/ci-pipeline.dot +15 -0
- package/templates/workflows/feature-flow.dot +21 -0
- package/templates/workflows/release.dot +16 -0
- package/dist/__integration__/helpers.d.ts +0 -20
- package/dist/__integration__/helpers.d.ts.map +0 -1
- package/dist/__integration__/helpers.js +0 -31
- package/dist/__integration__/helpers.js.map +0 -1
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/ci.d.ts.map +0 -1
- package/dist/commands/ci.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/llmstxt.d.ts.map +0 -1
- package/dist/commands/llmstxt.js.map +0 -1
- package/dist/commands/plugin.d.ts.map +0 -1
- package/dist/commands/plugin.js.map +0 -1
- package/dist/commands/security.d.ts.map +0 -1
- package/dist/commands/security.js.map +0 -1
- package/dist/commands/skills.d.ts.map +0 -1
- package/dist/commands/skills.js.map +0 -1
- package/dist/commands/tdd.d.ts.map +0 -1
- package/dist/commands/tdd.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/agent-skills.d.ts.map +0 -1
- package/dist/lib/agent-skills.js.map +0 -1
- package/dist/lib/claudemd.d.ts.map +0 -1
- package/dist/lib/claudemd.js.map +0 -1
- package/dist/lib/codex-export.d.ts.map +0 -1
- package/dist/lib/codex-export.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/context.d.ts.map +0 -1
- package/dist/lib/context.js.map +0 -1
- package/dist/lib/docker.d.ts.map +0 -1
- package/dist/lib/docker.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/plugin.d.ts.map +0 -1
- package/dist/lib/plugin.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/AnalyzeUI.d.ts.map +0 -1
- package/dist/ui/AnalyzeUI.js.map +0 -1
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/CI.d.ts.map +0 -1
- package/dist/ui/CI.js.map +0 -1
- package/dist/ui/CIContext.d.ts.map +0 -1
- package/dist/ui/CIContext.js.map +0 -1
- package/dist/ui/CISelector.d.ts.map +0 -1
- package/dist/ui/CISelector.js.map +0 -1
- package/dist/ui/Doctor.d.ts.map +0 -1
- package/dist/ui/Doctor.js.map +0 -1
- package/dist/ui/Header.d.ts.map +0 -1
- package/dist/ui/Header.js.map +0 -1
- package/dist/ui/LlmsTxt.d.ts.map +0 -1
- package/dist/ui/LlmsTxt.js.map +0 -1
- package/dist/ui/MemorySelector.d.ts.map +0 -1
- package/dist/ui/MemorySelector.js.map +0 -1
- package/dist/ui/NameInput.d.ts.map +0 -1
- package/dist/ui/NameInput.js.map +0 -1
- package/dist/ui/OptionSelector.d.ts.map +0 -1
- package/dist/ui/OptionSelector.js.map +0 -1
- package/dist/ui/Plugin.d.ts.map +0 -1
- package/dist/ui/Plugin.js.map +0 -1
- package/dist/ui/Progress.d.ts.map +0 -1
- package/dist/ui/Progress.js.map +0 -1
- package/dist/ui/Skills.d.ts.map +0 -1
- package/dist/ui/Skills.js.map +0 -1
- package/dist/ui/StackSelector.d.ts.map +0 -1
- package/dist/ui/StackSelector.js.map +0 -1
- package/dist/ui/Summary.d.ts.map +0 -1
- package/dist/ui/Summary.js.map +0 -1
- package/dist/ui/Welcome.d.ts.map +0 -1
- package/dist/ui/Welcome.js.map +0 -1
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js.map +0 -1
package/dist/commands/ci.js
CHANGED
|
@@ -1,73 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import fs from
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { CI_STACKS, findCIConfig, loadCIConfig, } from "../lib/ci-config.js";
|
|
5
|
+
import { refreshContextDir } from "../lib/context.js";
|
|
6
|
+
import { ensureImage, getImageName, isDockerAvailable, openShell, runInContainer, } from "../lib/docker.js";
|
|
7
|
+
import { execFileAsync } from "../lib/exec.js";
|
|
7
8
|
// =============================================================================
|
|
8
9
|
// Stack detection
|
|
9
10
|
// =============================================================================
|
|
10
11
|
export async function detectCIStack(projectDir) {
|
|
11
|
-
let stackType =
|
|
12
|
-
let buildTool =
|
|
13
|
-
let javaVersion =
|
|
12
|
+
let stackType = "node";
|
|
13
|
+
let buildTool = "npm";
|
|
14
|
+
let javaVersion = "21";
|
|
14
15
|
// Java Gradle
|
|
15
|
-
if (await fs.pathExists(path.join(projectDir,
|
|
16
|
-
await fs.pathExists(path.join(projectDir,
|
|
17
|
-
stackType =
|
|
18
|
-
buildTool =
|
|
16
|
+
if ((await fs.pathExists(path.join(projectDir, "build.gradle.kts"))) ||
|
|
17
|
+
(await fs.pathExists(path.join(projectDir, "build.gradle")))) {
|
|
18
|
+
stackType = "java-gradle";
|
|
19
|
+
buildTool = "gradle";
|
|
19
20
|
// Try to read java version from build files
|
|
20
|
-
const ktsPath = path.join(projectDir,
|
|
21
|
-
const gradlePath = path.join(projectDir,
|
|
21
|
+
const ktsPath = path.join(projectDir, "build.gradle.kts");
|
|
22
|
+
const gradlePath = path.join(projectDir, "build.gradle");
|
|
22
23
|
if (await fs.pathExists(ktsPath)) {
|
|
23
|
-
const content = await fs.readFile(ktsPath,
|
|
24
|
+
const content = await fs.readFile(ktsPath, "utf-8");
|
|
24
25
|
const match = content.match(/JavaLanguageVersion\.of\((\d+)\)/);
|
|
25
26
|
if (match?.[1])
|
|
26
27
|
javaVersion = match[1];
|
|
27
28
|
}
|
|
28
29
|
else if (await fs.pathExists(gradlePath)) {
|
|
29
|
-
const content = await fs.readFile(gradlePath,
|
|
30
|
+
const content = await fs.readFile(gradlePath, "utf-8");
|
|
30
31
|
const match = content.match(/sourceCompatibility\s*=\s*['"]*(\d+)/);
|
|
31
32
|
if (match?.[1])
|
|
32
33
|
javaVersion = match[1];
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
// Java Maven
|
|
36
|
-
else if (await fs.pathExists(path.join(projectDir,
|
|
37
|
-
stackType =
|
|
38
|
-
buildTool =
|
|
37
|
+
else if (await fs.pathExists(path.join(projectDir, "pom.xml"))) {
|
|
38
|
+
stackType = "java-maven";
|
|
39
|
+
buildTool = "mvn";
|
|
39
40
|
}
|
|
40
41
|
// Node
|
|
41
|
-
else if (await fs.pathExists(path.join(projectDir,
|
|
42
|
-
stackType =
|
|
43
|
-
if (await fs.pathExists(path.join(projectDir,
|
|
44
|
-
buildTool =
|
|
45
|
-
else if (await fs.pathExists(path.join(projectDir,
|
|
46
|
-
buildTool =
|
|
42
|
+
else if (await fs.pathExists(path.join(projectDir, "package.json"))) {
|
|
43
|
+
stackType = "node";
|
|
44
|
+
if (await fs.pathExists(path.join(projectDir, "pnpm-lock.yaml")))
|
|
45
|
+
buildTool = "pnpm";
|
|
46
|
+
else if (await fs.pathExists(path.join(projectDir, "yarn.lock")))
|
|
47
|
+
buildTool = "yarn";
|
|
47
48
|
else
|
|
48
|
-
buildTool =
|
|
49
|
+
buildTool = "npm";
|
|
49
50
|
}
|
|
50
51
|
// Go
|
|
51
|
-
else if (await fs.pathExists(path.join(projectDir,
|
|
52
|
-
stackType =
|
|
53
|
-
buildTool =
|
|
52
|
+
else if (await fs.pathExists(path.join(projectDir, "go.mod"))) {
|
|
53
|
+
stackType = "go";
|
|
54
|
+
buildTool = "go";
|
|
54
55
|
}
|
|
55
56
|
// Rust
|
|
56
|
-
else if (await fs.pathExists(path.join(projectDir,
|
|
57
|
-
stackType =
|
|
58
|
-
buildTool =
|
|
57
|
+
else if (await fs.pathExists(path.join(projectDir, "Cargo.toml"))) {
|
|
58
|
+
stackType = "rust";
|
|
59
|
+
buildTool = "cargo";
|
|
59
60
|
}
|
|
60
61
|
// Python
|
|
61
|
-
else if (await fs.pathExists(path.join(projectDir,
|
|
62
|
-
await fs.pathExists(path.join(projectDir,
|
|
63
|
-
await fs.pathExists(path.join(projectDir,
|
|
64
|
-
stackType =
|
|
65
|
-
if (await fs.pathExists(path.join(projectDir,
|
|
66
|
-
buildTool =
|
|
67
|
-
else if (await fs.pathExists(path.join(projectDir,
|
|
68
|
-
buildTool =
|
|
62
|
+
else if ((await fs.pathExists(path.join(projectDir, "pyproject.toml"))) ||
|
|
63
|
+
(await fs.pathExists(path.join(projectDir, "requirements.txt"))) ||
|
|
64
|
+
(await fs.pathExists(path.join(projectDir, "setup.py")))) {
|
|
65
|
+
stackType = "python";
|
|
66
|
+
if (await fs.pathExists(path.join(projectDir, "uv.lock")))
|
|
67
|
+
buildTool = "uv";
|
|
68
|
+
else if (await fs.pathExists(path.join(projectDir, "poetry.lock")))
|
|
69
|
+
buildTool = "poetry";
|
|
69
70
|
else
|
|
70
|
-
buildTool =
|
|
71
|
+
buildTool = "pip";
|
|
71
72
|
}
|
|
72
73
|
// Build CI commands per stack
|
|
73
74
|
const { lintCmd, compileCmd, testCmd } = await buildCICommands(stackType, buildTool, projectDir);
|
|
@@ -75,63 +76,199 @@ export async function detectCIStack(projectDir) {
|
|
|
75
76
|
}
|
|
76
77
|
async function buildCICommands(stack, buildTool, projectDir) {
|
|
77
78
|
switch (stack) {
|
|
78
|
-
case
|
|
79
|
+
case "java-gradle":
|
|
79
80
|
return {
|
|
80
|
-
lintCmd:
|
|
81
|
-
compileCmd:
|
|
82
|
-
testCmd:
|
|
81
|
+
lintCmd: "./gradlew spotlessCheck --no-daemon",
|
|
82
|
+
compileCmd: "./gradlew clean classes testClasses --no-daemon && chown -R runner:runner build/ .gradle/ 2>/dev/null || true",
|
|
83
|
+
testCmd: "./gradlew test --no-daemon",
|
|
83
84
|
};
|
|
84
|
-
case
|
|
85
|
+
case "java-maven":
|
|
85
86
|
return {
|
|
86
|
-
lintCmd:
|
|
87
|
-
compileCmd:
|
|
88
|
-
testCmd:
|
|
87
|
+
lintCmd: "./mvnw spotless:check",
|
|
88
|
+
compileCmd: "./mvnw clean compile test-compile && chown -R runner:runner target/ .mvn/ 2>/dev/null || true",
|
|
89
|
+
testCmd: "./mvnw test",
|
|
89
90
|
};
|
|
90
|
-
case
|
|
91
|
-
const pkgPath = path.join(projectDir,
|
|
92
|
-
let pkgContent =
|
|
91
|
+
case "node": {
|
|
92
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
93
|
+
let pkgContent = "";
|
|
93
94
|
try {
|
|
94
|
-
pkgContent = await fs.readFile(pkgPath,
|
|
95
|
+
pkgContent = await fs.readFile(pkgPath, "utf-8");
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* no package.json */
|
|
95
99
|
}
|
|
96
|
-
catch { /* no package.json */ }
|
|
97
100
|
// Clean dist/ before build and chown after so tests (as runner) can access output.
|
|
98
101
|
// Runs as root inside the container to handle host-owned output dirs.
|
|
99
|
-
const buildPrefix =
|
|
100
|
-
const buildSuffix =
|
|
102
|
+
const buildPrefix = "rm -rf dist/ && ";
|
|
103
|
+
const buildSuffix = " && chown -R runner:runner dist/ 2>/dev/null || true";
|
|
101
104
|
return {
|
|
102
105
|
lintCmd: pkgContent.includes('"lint"') ? `${buildTool} run lint` : null,
|
|
103
|
-
compileCmd: pkgContent.includes('"build"')
|
|
104
|
-
|
|
106
|
+
compileCmd: pkgContent.includes('"build"')
|
|
107
|
+
? `${buildPrefix}${buildTool} run build${buildSuffix}`
|
|
108
|
+
: null,
|
|
109
|
+
testCmd: pkgContent.includes('"test"')
|
|
110
|
+
? `${buildTool} ${buildTool === "npm" ? "test" : "run test"}`
|
|
111
|
+
: null,
|
|
105
112
|
};
|
|
106
113
|
}
|
|
107
|
-
case
|
|
114
|
+
case "python":
|
|
108
115
|
return {
|
|
109
|
-
lintCmd:
|
|
116
|
+
lintCmd: "ruff check . && { pylint **/*.py 2>/dev/null || true; }",
|
|
110
117
|
compileCmd: null,
|
|
111
|
-
testCmd:
|
|
118
|
+
testCmd: "pytest",
|
|
112
119
|
};
|
|
113
|
-
case
|
|
120
|
+
case "go":
|
|
114
121
|
return {
|
|
115
|
-
lintCmd:
|
|
116
|
-
compileCmd:
|
|
117
|
-
testCmd:
|
|
122
|
+
lintCmd: "golangci-lint run",
|
|
123
|
+
compileCmd: "go clean -cache && go build ./... && chown -R runner:runner . 2>/dev/null || true",
|
|
124
|
+
testCmd: "go test ./...",
|
|
118
125
|
};
|
|
119
|
-
case
|
|
126
|
+
case "rust":
|
|
120
127
|
return {
|
|
121
|
-
lintCmd:
|
|
122
|
-
compileCmd:
|
|
123
|
-
testCmd:
|
|
128
|
+
lintCmd: "cargo clippy -- -D warnings",
|
|
129
|
+
compileCmd: "cargo clean && cargo build && chown -R runner:runner target/ 2>/dev/null || true",
|
|
130
|
+
testCmd: "cargo test",
|
|
124
131
|
};
|
|
125
132
|
default:
|
|
126
133
|
return { lintCmd: null, compileCmd: null, testCmd: null };
|
|
127
134
|
}
|
|
128
135
|
}
|
|
136
|
+
function freezeRunner(runner) {
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
...runner,
|
|
139
|
+
setupCmds: Object.freeze([...runner.setupCmds]),
|
|
140
|
+
lintCmds: Object.freeze([...runner.lintCmds]),
|
|
141
|
+
compileCmds: Object.freeze([...runner.compileCmds]),
|
|
142
|
+
testCmds: Object.freeze([...runner.testCmds]),
|
|
143
|
+
securityCmds: Object.freeze([...runner.securityCmds]),
|
|
144
|
+
requiredTools: Object.freeze([...runner.requiredTools]),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function freezeRunners(source, runners) {
|
|
148
|
+
return Object.freeze({ source, runners: Object.freeze(runners) });
|
|
149
|
+
}
|
|
150
|
+
/** Build-tool heuristic for a known stack in a given directory. */
|
|
151
|
+
async function detectBuildTool(stack, dir) {
|
|
152
|
+
switch (stack) {
|
|
153
|
+
case "node":
|
|
154
|
+
if (await fs.pathExists(path.join(dir, "pnpm-lock.yaml")))
|
|
155
|
+
return "pnpm";
|
|
156
|
+
if (await fs.pathExists(path.join(dir, "yarn.lock")))
|
|
157
|
+
return "yarn";
|
|
158
|
+
return "npm";
|
|
159
|
+
case "python":
|
|
160
|
+
if (await fs.pathExists(path.join(dir, "uv.lock")))
|
|
161
|
+
return "uv";
|
|
162
|
+
if (await fs.pathExists(path.join(dir, "poetry.lock")))
|
|
163
|
+
return "poetry";
|
|
164
|
+
return "pip";
|
|
165
|
+
case "java-gradle":
|
|
166
|
+
return "gradle";
|
|
167
|
+
case "java-maven":
|
|
168
|
+
return "mvn";
|
|
169
|
+
case "go":
|
|
170
|
+
return "go";
|
|
171
|
+
case "rust":
|
|
172
|
+
return "cargo";
|
|
173
|
+
default:
|
|
174
|
+
return "";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function toList(command) {
|
|
178
|
+
return command ? [command] : [];
|
|
179
|
+
}
|
|
180
|
+
async function resolveConfiguredRunner(projectDir, config) {
|
|
181
|
+
const runnerDir = path.join(projectDir, config.directory);
|
|
182
|
+
const buildTool = await detectBuildTool(config.stack, runnerDir);
|
|
183
|
+
const defaults = await buildCICommands(config.stack, buildTool, runnerDir);
|
|
184
|
+
return freezeRunner({
|
|
185
|
+
name: config.name,
|
|
186
|
+
stack: config.stack,
|
|
187
|
+
buildTool,
|
|
188
|
+
javaVersion: "21",
|
|
189
|
+
directory: config.directory,
|
|
190
|
+
image: config.image,
|
|
191
|
+
buildContext: config.buildContext,
|
|
192
|
+
setupCmds: config.setup,
|
|
193
|
+
lintCmds: config.lint.length > 0 ? config.lint : toList(defaults.lintCmd),
|
|
194
|
+
compileCmds: config.build.length > 0 ? config.build : toList(defaults.compileCmd),
|
|
195
|
+
testCmds: config.test.length > 0 ? config.test : toList(defaults.testCmd),
|
|
196
|
+
securityCmds: config.security,
|
|
197
|
+
requiredTools: config.requires,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function resolveExplicitStackRunner(projectDir, stack) {
|
|
201
|
+
const buildTool = await detectBuildTool(stack, projectDir);
|
|
202
|
+
const defaults = await buildCICommands(stack, buildTool, projectDir);
|
|
203
|
+
return freezeRunner({
|
|
204
|
+
name: stack,
|
|
205
|
+
stack,
|
|
206
|
+
buildTool,
|
|
207
|
+
javaVersion: "21",
|
|
208
|
+
directory: ".",
|
|
209
|
+
setupCmds: [],
|
|
210
|
+
lintCmds: toList(defaults.lintCmd),
|
|
211
|
+
compileCmds: toList(defaults.compileCmd),
|
|
212
|
+
testCmds: toList(defaults.testCmd),
|
|
213
|
+
securityCmds: [],
|
|
214
|
+
requiredTools: [],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the ordered runner list exactly once per CI run.
|
|
219
|
+
*
|
|
220
|
+
* Precedence (fail closed on ambiguity):
|
|
221
|
+
* 1. --config <path> (or discovered .javi-forge/ci.yaml) → configured runners
|
|
222
|
+
* 2. --stack <stack> → single explicit runner (single-stack repos only)
|
|
223
|
+
* 3. otherwise → single auto-detected runner (zero-config default)
|
|
224
|
+
*/
|
|
225
|
+
export async function resolveCIRunners(projectDir, options = {}) {
|
|
226
|
+
const { config, stack } = options;
|
|
227
|
+
if (config && stack) {
|
|
228
|
+
throw new Error("Ambiguous CI options: --config and --stack cannot be used together. " +
|
|
229
|
+
"Use --config for hybrid repositories.");
|
|
230
|
+
}
|
|
231
|
+
if (stack) {
|
|
232
|
+
if (!CI_STACKS.includes(stack)) {
|
|
233
|
+
throw new Error(`Unknown stack "${stack}". Valid stacks: ${CI_STACKS.join(", ")}`);
|
|
234
|
+
}
|
|
235
|
+
return freezeRunners("stack-override", [
|
|
236
|
+
await resolveExplicitStackRunner(projectDir, stack),
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
const configPath = config ?? (await findCIConfig(projectDir));
|
|
240
|
+
if (configPath) {
|
|
241
|
+
const ciConfig = await loadCIConfig(configPath);
|
|
242
|
+
const runners = [];
|
|
243
|
+
for (const runnerConfig of ciConfig.runners) {
|
|
244
|
+
runners.push(await resolveConfiguredRunner(projectDir, runnerConfig));
|
|
245
|
+
}
|
|
246
|
+
return freezeRunners("config", runners);
|
|
247
|
+
}
|
|
248
|
+
// Zero-config default: single auto-detected runner (unchanged behavior).
|
|
249
|
+
const info = await detectCIStack(projectDir);
|
|
250
|
+
return freezeRunners("auto", [
|
|
251
|
+
freezeRunner({
|
|
252
|
+
name: info.stackType,
|
|
253
|
+
stack: info.stackType,
|
|
254
|
+
buildTool: info.buildTool,
|
|
255
|
+
javaVersion: info.javaVersion,
|
|
256
|
+
directory: ".",
|
|
257
|
+
setupCmds: [],
|
|
258
|
+
lintCmds: toList(info.lintCmd),
|
|
259
|
+
compileCmds: toList(info.compileCmd),
|
|
260
|
+
testCmds: toList(info.testCmd),
|
|
261
|
+
securityCmds: [],
|
|
262
|
+
requiredTools: [],
|
|
263
|
+
}),
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
129
266
|
// =============================================================================
|
|
130
267
|
// GHAGGA check
|
|
131
268
|
// =============================================================================
|
|
132
269
|
async function isGhaggaAvailable() {
|
|
133
270
|
try {
|
|
134
|
-
await execFileAsync(
|
|
271
|
+
await execFileAsync("ghagga", ["--version"], { timeout: 3000 });
|
|
135
272
|
return true;
|
|
136
273
|
}
|
|
137
274
|
catch {
|
|
@@ -144,163 +281,354 @@ async function isGhaggaAvailable() {
|
|
|
144
281
|
function report(onStep, id, label, status, detail) {
|
|
145
282
|
onStep({ id, label, status, detail });
|
|
146
283
|
}
|
|
284
|
+
/** Detect-step label: legacy format for auto, explicit otherwise. */
|
|
285
|
+
function describeRunners(resolved) {
|
|
286
|
+
const first = resolved.runners[0];
|
|
287
|
+
if (resolved.source === "auto" && first) {
|
|
288
|
+
return `Stack: ${first.stack} (${first.buildTool})`;
|
|
289
|
+
}
|
|
290
|
+
if (resolved.source === "stack-override" && first) {
|
|
291
|
+
return `Stack: ${first.stack} (${first.buildTool}, --stack override)`;
|
|
292
|
+
}
|
|
293
|
+
const summary = resolved.runners
|
|
294
|
+
.map((r) => `${r.name} (${r.stack})`)
|
|
295
|
+
.join(", ");
|
|
296
|
+
return `Config: ${resolved.runners.length} runner(s) — ${summary}`;
|
|
297
|
+
}
|
|
147
298
|
export async function runCI(options, onStep) {
|
|
148
|
-
const { projectDir = process.cwd(), mode =
|
|
149
|
-
// ──
|
|
150
|
-
const stepDetect =
|
|
151
|
-
report(onStep, stepDetect,
|
|
152
|
-
let
|
|
299
|
+
const { projectDir = process.cwd(), mode = "full", noDocker = false, noGhagga = false, noSecurity = false, timeout = 600, } = options;
|
|
300
|
+
// ── Resolve runners (once — nothing downstream re-detects) ─────────────────
|
|
301
|
+
const stepDetect = "detect";
|
|
302
|
+
report(onStep, stepDetect, "Detecting stack", "running");
|
|
303
|
+
let resolved;
|
|
153
304
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
305
|
+
resolved = await resolveCIRunners(projectDir, {
|
|
306
|
+
config: options.config,
|
|
307
|
+
stack: options.stack,
|
|
308
|
+
});
|
|
309
|
+
report(onStep, stepDetect, describeRunners(resolved), "done");
|
|
156
310
|
}
|
|
157
311
|
catch (e) {
|
|
158
|
-
report(onStep, stepDetect,
|
|
312
|
+
report(onStep, stepDetect, "Detecting stack", "error", String(e));
|
|
159
313
|
throw e;
|
|
160
314
|
}
|
|
315
|
+
// Legacy single-runner view for the zero-config auto path. Keeping this
|
|
316
|
+
// shape guarantees single-stack repositories behave exactly as before.
|
|
317
|
+
const primary = resolved.runners[0];
|
|
318
|
+
const stackInfo = {
|
|
319
|
+
stackType: primary?.stack ?? "node",
|
|
320
|
+
buildTool: primary?.buildTool ?? "npm",
|
|
321
|
+
javaVersion: primary?.javaVersion ?? "21",
|
|
322
|
+
lintCmd: primary?.lintCmds[0] ?? null,
|
|
323
|
+
compileCmd: primary?.compileCmds[0] ?? null,
|
|
324
|
+
testCmd: primary?.testCmds[0] ?? null,
|
|
325
|
+
};
|
|
161
326
|
// ── Detect mode ─────────────────────────────────────────────────────────────
|
|
162
|
-
if (mode ===
|
|
327
|
+
if (mode === "detect")
|
|
163
328
|
return;
|
|
164
329
|
// ── Shell mode ──────────────────────────────────────────────────────────────
|
|
165
|
-
if (mode ===
|
|
330
|
+
if (mode === "shell") {
|
|
166
331
|
if (noDocker) {
|
|
167
|
-
report(onStep,
|
|
168
|
-
throw new Error(
|
|
332
|
+
report(onStep, "shell", "Shell", "error", "--shell requires Docker");
|
|
333
|
+
throw new Error("--shell requires Docker");
|
|
169
334
|
}
|
|
170
|
-
report(onStep,
|
|
335
|
+
report(onStep, "docker-image", "Building Docker image", "running");
|
|
336
|
+
let shellImage;
|
|
171
337
|
try {
|
|
172
|
-
await ensureImage({
|
|
173
|
-
|
|
338
|
+
shellImage = await ensureImage({
|
|
339
|
+
stack: stackInfo.stackType,
|
|
340
|
+
javaVersion: stackInfo.javaVersion,
|
|
341
|
+
});
|
|
342
|
+
report(onStep, "docker-image", "Docker image ready", "done");
|
|
174
343
|
}
|
|
175
344
|
catch (e) {
|
|
176
|
-
report(onStep,
|
|
345
|
+
report(onStep, "docker-image", "Building Docker image", "error", String(e));
|
|
177
346
|
throw e;
|
|
178
347
|
}
|
|
179
|
-
await openShell(projectDir);
|
|
348
|
+
await openShell(projectDir, shellImage);
|
|
180
349
|
return;
|
|
181
350
|
}
|
|
182
351
|
// ── Check Docker ─────────────────────────────────────────────────────────────
|
|
183
352
|
if (!noDocker) {
|
|
184
|
-
const stepDocker =
|
|
185
|
-
report(onStep, stepDocker,
|
|
353
|
+
const stepDocker = "docker-check";
|
|
354
|
+
report(onStep, stepDocker, "Checking Docker", "running");
|
|
186
355
|
const dockerOk = await isDockerAvailable();
|
|
187
356
|
if (!dockerOk) {
|
|
188
|
-
report(onStep, stepDocker,
|
|
189
|
-
throw new Error(
|
|
357
|
+
report(onStep, stepDocker, "Docker not available", "error", "Start Docker or use --no-docker");
|
|
358
|
+
throw new Error("Docker is not available");
|
|
190
359
|
}
|
|
191
|
-
report(onStep, stepDocker,
|
|
192
|
-
// Build image
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
360
|
+
report(onStep, stepDocker, "Docker available", "done");
|
|
361
|
+
// Build image (auto path only — configured runners ensure their own
|
|
362
|
+
// image inside the runner loop)
|
|
363
|
+
if (resolved.source === "auto") {
|
|
364
|
+
const stepImage = "docker-image";
|
|
365
|
+
report(onStep, stepImage, `Building image for ${stackInfo.stackType}`, "running");
|
|
366
|
+
try {
|
|
367
|
+
await ensureImage({
|
|
368
|
+
stack: stackInfo.stackType,
|
|
369
|
+
javaVersion: stackInfo.javaVersion,
|
|
370
|
+
});
|
|
371
|
+
report(onStep, stepImage, "Docker image ready", "done");
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
report(onStep, stepImage, "Building Docker image", "error", String(e));
|
|
375
|
+
throw e;
|
|
376
|
+
}
|
|
198
377
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
378
|
+
}
|
|
379
|
+
// ── Refresh .context/ ────────────────────────────────────────────────────────
|
|
380
|
+
const stepContext = "context-refresh";
|
|
381
|
+
report(onStep, stepContext, "Refresh .context/ directory", "running");
|
|
382
|
+
try {
|
|
383
|
+
const ctxResult = await refreshContextDir(projectDir);
|
|
384
|
+
if (ctxResult) {
|
|
385
|
+
report(onStep, stepContext, "Refresh .context/ directory", "done", "INDEX.md + summary.md updated");
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
report(onStep, stepContext, "Refresh .context/ directory", "skipped", "no .context/ or no manifest");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
// Non-fatal: context refresh failure should not block CI
|
|
393
|
+
report(onStep, stepContext, "Refresh .context/ directory", "error", String(e));
|
|
394
|
+
}
|
|
395
|
+
// ── Runner execution ─────────────────────────────────────────────────────────
|
|
396
|
+
if (resolved.source === "auto" && primary) {
|
|
397
|
+
// Legacy zero-config path — unchanged single-stack behavior.
|
|
398
|
+
await runLegacySteps(stackInfo, primary, {
|
|
399
|
+
projectDir,
|
|
400
|
+
mode,
|
|
401
|
+
noDocker,
|
|
402
|
+
timeout,
|
|
403
|
+
onStep,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// Configured (or --stack override) runners execute in declared order,
|
|
408
|
+
// each in its own working directory.
|
|
409
|
+
for (const runner of resolved.runners) {
|
|
410
|
+
await runConfiguredRunner(runner, {
|
|
411
|
+
projectDir,
|
|
412
|
+
mode,
|
|
413
|
+
noDocker,
|
|
414
|
+
noSecurity,
|
|
415
|
+
timeout,
|
|
416
|
+
onStep,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ── Security scan (full mode only) ──────────────────────────────────────────
|
|
421
|
+
if (mode === "full" && !noSecurity) {
|
|
422
|
+
const stepSecurity = "security";
|
|
423
|
+
const semgrepAvailable = await isSemgrepAvailable();
|
|
424
|
+
if (semgrepAvailable) {
|
|
425
|
+
report(onStep, stepSecurity, "Security scan (Semgrep)", "running");
|
|
426
|
+
try {
|
|
427
|
+
await runSemgrep(projectDir);
|
|
428
|
+
report(onStep, stepSecurity, "Security scan passed", "done");
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
report(onStep, stepSecurity, "Security scan failed", "error", String(e));
|
|
432
|
+
throw e;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
report(onStep, stepSecurity, "Security scan", "skipped", "Semgrep not available — install semgrep or Docker");
|
|
202
437
|
}
|
|
203
438
|
}
|
|
204
|
-
// ──
|
|
439
|
+
// ── GHAGGA review (full mode only) ──────────────────────────────────────────
|
|
440
|
+
if (mode === "full" && !noGhagga) {
|
|
441
|
+
const stepGhagga = "ghagga";
|
|
442
|
+
const ghagga = await isGhaggaAvailable();
|
|
443
|
+
if (ghagga) {
|
|
444
|
+
report(onStep, stepGhagga, "GHAGGA review", "running");
|
|
445
|
+
try {
|
|
446
|
+
await runGhagga(projectDir);
|
|
447
|
+
report(onStep, stepGhagga, "GHAGGA review passed", "done");
|
|
448
|
+
}
|
|
449
|
+
catch (e) {
|
|
450
|
+
report(onStep, stepGhagga, "GHAGGA review failed", "error", String(e));
|
|
451
|
+
throw e;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
report(onStep, stepGhagga, "GHAGGA review", "skipped", "ghagga not installed");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/** Legacy zero-config execution — identical to the pre-mixed-stack behavior. */
|
|
460
|
+
async function runLegacySteps(stackInfo, runner, ctx) {
|
|
461
|
+
const { projectDir, mode, noDocker, timeout, onStep } = ctx;
|
|
205
462
|
if (stackInfo.lintCmd) {
|
|
206
|
-
const stepLint =
|
|
207
|
-
report(onStep, stepLint, `Lint: ${stackInfo.lintCmd}`,
|
|
463
|
+
const stepLint = "lint";
|
|
464
|
+
report(onStep, stepLint, `Lint: ${stackInfo.lintCmd}`, "running");
|
|
208
465
|
try {
|
|
209
|
-
await runStep(stackInfo.lintCmd, projectDir, noDocker, timeout);
|
|
210
|
-
report(onStep, stepLint,
|
|
466
|
+
await runStep(stackInfo.lintCmd, projectDir, noDocker, timeout, runner);
|
|
467
|
+
report(onStep, stepLint, "Lint passed", "done");
|
|
211
468
|
}
|
|
212
469
|
catch (e) {
|
|
213
|
-
report(onStep, stepLint,
|
|
470
|
+
report(onStep, stepLint, "Lint failed", "error", String(e));
|
|
214
471
|
throw e;
|
|
215
472
|
}
|
|
216
473
|
}
|
|
217
|
-
// ── Compile ──────────────────────────────────────────────────────────────────
|
|
218
474
|
if (stackInfo.compileCmd) {
|
|
219
|
-
const stepCompile =
|
|
220
|
-
report(onStep, stepCompile, `Compile: ${stackInfo.compileCmd}`,
|
|
475
|
+
const stepCompile = "compile";
|
|
476
|
+
report(onStep, stepCompile, `Compile: ${stackInfo.compileCmd}`, "running");
|
|
221
477
|
try {
|
|
222
478
|
// Run as root inside the container to rm/build output dirs owned by any host user,
|
|
223
479
|
// then chown back to runner so subsequent test steps can read the output.
|
|
224
|
-
await runStep(stackInfo.compileCmd, projectDir, noDocker, timeout,
|
|
225
|
-
report(onStep, stepCompile,
|
|
480
|
+
await runStep(stackInfo.compileCmd, projectDir, noDocker, timeout, runner, "root");
|
|
481
|
+
report(onStep, stepCompile, "Compile passed", "done");
|
|
226
482
|
}
|
|
227
483
|
catch (e) {
|
|
228
|
-
report(onStep, stepCompile,
|
|
484
|
+
report(onStep, stepCompile, "Compile failed", "error", String(e));
|
|
229
485
|
throw e;
|
|
230
486
|
}
|
|
231
487
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
report(onStep, stepTest, `Test: ${stackInfo.testCmd}`, 'running');
|
|
488
|
+
if (mode === "full" && stackInfo.testCmd) {
|
|
489
|
+
const stepTest = "test";
|
|
490
|
+
report(onStep, stepTest, `Test: ${stackInfo.testCmd}`, "running");
|
|
236
491
|
try {
|
|
237
|
-
await runStep(stackInfo.testCmd, projectDir, noDocker, timeout);
|
|
238
|
-
report(onStep, stepTest,
|
|
492
|
+
await runStep(stackInfo.testCmd, projectDir, noDocker, timeout, runner);
|
|
493
|
+
report(onStep, stepTest, "Tests passed", "done");
|
|
239
494
|
}
|
|
240
495
|
catch (e) {
|
|
241
|
-
report(onStep, stepTest,
|
|
496
|
+
report(onStep, stepTest, "Tests failed", "error", String(e));
|
|
242
497
|
throw e;
|
|
243
498
|
}
|
|
244
499
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Execute one configured runner: resolve the image (default stack image,
|
|
503
|
+
* explicit/digest-pinned image, or deterministic build-context build),
|
|
504
|
+
* verify required tools fail-closed, then run setup/lint/compile/test/
|
|
505
|
+
* security commands in order, inside the runner's working directory.
|
|
506
|
+
* Any failure aborts the whole run — nothing is ever skipped silently.
|
|
507
|
+
*/
|
|
508
|
+
async function runConfiguredRunner(runner, ctx) {
|
|
509
|
+
const { projectDir, mode, noDocker, noSecurity, timeout, onStep } = ctx;
|
|
510
|
+
// ── Image resolution (Docker mode only) ────────────────────────────────────
|
|
511
|
+
let imageName;
|
|
512
|
+
if (!noDocker) {
|
|
513
|
+
const stepImage = `docker-image:${runner.name}`;
|
|
514
|
+
try {
|
|
515
|
+
if (runner.buildContext) {
|
|
516
|
+
report(onStep, stepImage, `Building image for ${runner.name} from ${runner.buildContext}`, "running");
|
|
517
|
+
imageName = await ensureImage({
|
|
518
|
+
stack: runner.stack,
|
|
519
|
+
buildContext: path.resolve(projectDir, runner.buildContext),
|
|
520
|
+
imageTag: `javi-forge-ci-${runner.name}`,
|
|
521
|
+
});
|
|
254
522
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
523
|
+
else if (runner.image) {
|
|
524
|
+
// Explicit image (digest pins pass through verbatim).
|
|
525
|
+
imageName = runner.image;
|
|
526
|
+
report(onStep, stepImage, `Using configured image ${runner.image}`, "done");
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
report(onStep, stepImage, `Building image for ${runner.name} (${runner.stack})`, "running");
|
|
530
|
+
imageName = await ensureImage({
|
|
531
|
+
stack: runner.stack,
|
|
532
|
+
javaVersion: runner.javaVersion,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (!runner.image) {
|
|
536
|
+
report(onStep, stepImage, `Docker image ready (${imageName})`, "done");
|
|
258
537
|
}
|
|
259
538
|
}
|
|
260
|
-
|
|
261
|
-
report(onStep,
|
|
539
|
+
catch (e) {
|
|
540
|
+
report(onStep, stepImage, "Building Docker image", "error", String(e));
|
|
541
|
+
throw e;
|
|
262
542
|
}
|
|
263
543
|
}
|
|
264
|
-
// ──
|
|
265
|
-
if (
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
544
|
+
// ── Required tools (fail closed, before any phase) ─────────────────────────
|
|
545
|
+
if (runner.requiredTools.length > 0) {
|
|
546
|
+
const stepTools = `tools:${runner.name}`;
|
|
547
|
+
const envDesc = noDocker
|
|
548
|
+
? `native environment (directory "${runner.directory}")`
|
|
549
|
+
: `image ${imageName}`;
|
|
550
|
+
report(onStep, stepTools, `Checking required tools [${runner.name}]: ${runner.requiredTools.join(", ")}`, "running");
|
|
551
|
+
for (const tool of runner.requiredTools) {
|
|
270
552
|
try {
|
|
271
|
-
await
|
|
272
|
-
|
|
553
|
+
await runStep(`command -v ${tool}`, projectDir, noDocker, timeout, runner, undefined, imageName);
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
const message = `runner "${runner.name}": required tool "${tool}" not found in ${envDesc} — ` +
|
|
557
|
+
"install it in the runner image/environment or remove it from requires";
|
|
558
|
+
report(onStep, stepTools, `Missing required tool [${runner.name}]: ${tool}`, "error", message);
|
|
559
|
+
throw new Error(message);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
report(onStep, stepTools, `Required tools OK [${runner.name}]`, "done");
|
|
563
|
+
}
|
|
564
|
+
const phases = [
|
|
565
|
+
{ id: "setup", label: "Setup", cmds: runner.setupCmds, skip: false },
|
|
566
|
+
{ id: "lint", label: "Lint", cmds: runner.lintCmds, skip: false },
|
|
567
|
+
{
|
|
568
|
+
id: "compile",
|
|
569
|
+
label: "Compile",
|
|
570
|
+
cmds: runner.compileCmds,
|
|
571
|
+
user: "root",
|
|
572
|
+
skip: false,
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: "test",
|
|
576
|
+
label: "Test",
|
|
577
|
+
cmds: runner.testCmds,
|
|
578
|
+
skip: mode !== "full",
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: "security",
|
|
582
|
+
label: "Security",
|
|
583
|
+
cmds: runner.securityCmds,
|
|
584
|
+
skip: mode !== "full" || noSecurity,
|
|
585
|
+
},
|
|
586
|
+
];
|
|
587
|
+
for (const phase of phases) {
|
|
588
|
+
if (phase.skip)
|
|
589
|
+
continue;
|
|
590
|
+
const stepId = `${phase.id}:${runner.name}`;
|
|
591
|
+
for (const cmd of phase.cmds) {
|
|
592
|
+
report(onStep, stepId, `${phase.label} [${runner.name}]: ${cmd}`, "running");
|
|
593
|
+
try {
|
|
594
|
+
await runStep(cmd, projectDir, noDocker, timeout, runner, phase.user, imageName);
|
|
595
|
+
report(onStep, stepId, `${phase.label} [${runner.name}] passed`, "done");
|
|
273
596
|
}
|
|
274
597
|
catch (e) {
|
|
275
|
-
report(onStep,
|
|
598
|
+
report(onStep, stepId, `${phase.label} [${runner.name}] failed`, "error", String(e));
|
|
276
599
|
throw e;
|
|
277
600
|
}
|
|
278
601
|
}
|
|
279
|
-
else {
|
|
280
|
-
report(onStep, stepGhagga, 'GHAGGA review', 'skipped', 'ghagga not installed');
|
|
281
|
-
}
|
|
282
602
|
}
|
|
283
603
|
}
|
|
284
|
-
|
|
285
|
-
// Step runners
|
|
286
|
-
// =============================================================================
|
|
287
|
-
async function runStep(command, projectDir, noDocker, timeout, user) {
|
|
604
|
+
async function runStep(command, projectDir, noDocker, timeout, runner, user, imageOverride) {
|
|
288
605
|
if (noDocker) {
|
|
289
|
-
// Run natively
|
|
606
|
+
// Run natively, in the runner's working directory.
|
|
607
|
+
const cwd = path.join(projectDir, runner.directory);
|
|
290
608
|
await new Promise((resolve, reject) => {
|
|
291
|
-
const proc = spawn(
|
|
292
|
-
cwd
|
|
293
|
-
stdio:
|
|
294
|
-
env: { ...process.env, CI:
|
|
609
|
+
const proc = spawn("bash", ["-c", command], {
|
|
610
|
+
cwd,
|
|
611
|
+
stdio: "inherit",
|
|
612
|
+
env: { ...process.env, CI: "true" },
|
|
295
613
|
});
|
|
296
|
-
proc.on(
|
|
297
|
-
|
|
614
|
+
proc.on("close", (code) => code === 0
|
|
615
|
+
? resolve()
|
|
616
|
+
: reject(new Error(`Command failed with code ${code}`)));
|
|
617
|
+
proc.on("error", reject);
|
|
298
618
|
});
|
|
299
619
|
}
|
|
300
620
|
else {
|
|
621
|
+
// The image is always resolved upstream (per-runner ensureImage,
|
|
622
|
+
// explicit config image, or the default per-stack image). Docker
|
|
623
|
+
// receives it verbatim and never re-detects anything.
|
|
624
|
+
const image = imageOverride ?? runner.image ?? getImageName(runner.stack);
|
|
625
|
+
const workdir = runner.directory !== "."
|
|
626
|
+
? `/home/runner/work/${runner.directory}`
|
|
627
|
+
: "/home/runner/work";
|
|
301
628
|
const result = await runInContainer({
|
|
302
629
|
projectDir,
|
|
303
|
-
|
|
630
|
+
image,
|
|
631
|
+
command: `cd ${workdir} && ${command}`,
|
|
304
632
|
timeout,
|
|
305
633
|
stream: true,
|
|
306
634
|
user,
|
|
@@ -312,7 +640,7 @@ async function runStep(command, projectDir, noDocker, timeout, user) {
|
|
|
312
640
|
}
|
|
313
641
|
async function isSemgrepAvailable() {
|
|
314
642
|
try {
|
|
315
|
-
await execFileAsync(
|
|
643
|
+
await execFileAsync("semgrep", ["--version"], { timeout: 3000 });
|
|
316
644
|
return true;
|
|
317
645
|
}
|
|
318
646
|
catch {
|
|
@@ -321,26 +649,30 @@ async function isSemgrepAvailable() {
|
|
|
321
649
|
}
|
|
322
650
|
async function runSemgrep(projectDir) {
|
|
323
651
|
// Look for semgrep config in project or use auto
|
|
324
|
-
const semgrepConfig = await fs.pathExists(path.join(projectDir,
|
|
325
|
-
? path.join(projectDir,
|
|
326
|
-
:
|
|
652
|
+
const semgrepConfig = (await fs.pathExists(path.join(projectDir, ".semgrep.yml")))
|
|
653
|
+
? path.join(projectDir, ".semgrep.yml")
|
|
654
|
+
: "auto";
|
|
327
655
|
await new Promise((resolve, reject) => {
|
|
328
|
-
const proc = spawn(
|
|
656
|
+
const proc = spawn("semgrep", ["--config", semgrepConfig, "--severity", "ERROR", "--quiet", "."], {
|
|
329
657
|
cwd: projectDir,
|
|
330
|
-
stdio:
|
|
658
|
+
stdio: "inherit",
|
|
331
659
|
});
|
|
332
|
-
proc.on(
|
|
333
|
-
|
|
660
|
+
proc.on("close", (code) => code === 0
|
|
661
|
+
? resolve()
|
|
662
|
+
: reject(new Error(`Semgrep found issues (exit ${code})`)));
|
|
663
|
+
proc.on("error", reject);
|
|
334
664
|
});
|
|
335
665
|
}
|
|
336
666
|
async function runGhagga(projectDir) {
|
|
337
667
|
await new Promise((resolve, reject) => {
|
|
338
|
-
const proc = spawn(
|
|
668
|
+
const proc = spawn("ghagga", ["review", "--plain", "--exit-on-issues"], {
|
|
339
669
|
cwd: projectDir,
|
|
340
|
-
stdio:
|
|
670
|
+
stdio: "inherit",
|
|
341
671
|
});
|
|
342
|
-
proc.on(
|
|
343
|
-
|
|
672
|
+
proc.on("close", (code) => code === 0
|
|
673
|
+
? resolve()
|
|
674
|
+
: reject(new Error(`GHAGGA review found issues (exit ${code})`)));
|
|
675
|
+
proc.on("error", reject);
|
|
344
676
|
});
|
|
345
677
|
}
|
|
346
678
|
// =============================================================================
|
|
@@ -386,8 +718,8 @@ fi || {
|
|
|
386
718
|
const COMMIT_MSG_HOOK = `#!/bin/bash
|
|
387
719
|
# Commit-msg: block AI attribution in commit messages
|
|
388
720
|
set -e
|
|
389
|
-
COMMIT_MSG_FILE="
|
|
390
|
-
COMMIT_MSG
|
|
721
|
+
COMMIT_MSG_FILE="$1"
|
|
722
|
+
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
391
723
|
|
|
392
724
|
AI_PATTERNS=(
|
|
393
725
|
"co-authored-by:.*claude" "co-authored-by:.*anthropic"
|
|
@@ -403,10 +735,10 @@ AI_PATTERNS=(
|
|
|
403
735
|
)
|
|
404
736
|
|
|
405
737
|
for pattern in "\${AI_PATTERNS[@]}"; do
|
|
406
|
-
if echo "
|
|
738
|
+
if echo "$COMMIT_MSG" | grep -iqE "$pattern"; then
|
|
407
739
|
echo ""
|
|
408
740
|
echo "COMMIT BLOCKED: AI Attribution Detected"
|
|
409
|
-
echo " Pattern:
|
|
741
|
+
echo " Pattern: $pattern"
|
|
410
742
|
echo " Remove AI attribution. You are the sole author."
|
|
411
743
|
echo ""
|
|
412
744
|
exit 1
|
|
@@ -415,22 +747,45 @@ done
|
|
|
415
747
|
exit 0
|
|
416
748
|
`;
|
|
417
749
|
export async function installCIHooks(projectDir) {
|
|
418
|
-
const gitDir = path.join(projectDir,
|
|
419
|
-
if (!await fs.pathExists(gitDir)) {
|
|
420
|
-
return {
|
|
750
|
+
const gitDir = path.join(projectDir, ".git");
|
|
751
|
+
if (!(await fs.pathExists(gitDir))) {
|
|
752
|
+
return {
|
|
753
|
+
installed: [],
|
|
754
|
+
errors: ["Not a git repository. Run git init first."],
|
|
755
|
+
};
|
|
421
756
|
}
|
|
422
|
-
const hooksDir = path.join(gitDir,
|
|
757
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
423
758
|
await fs.ensureDir(hooksDir);
|
|
424
759
|
const hooks = [
|
|
425
|
-
{ name:
|
|
426
|
-
{ name:
|
|
427
|
-
{ name:
|
|
760
|
+
{ name: "pre-commit", content: PRE_COMMIT_HOOK },
|
|
761
|
+
{ name: "pre-push", content: PRE_PUSH_HOOK },
|
|
762
|
+
{ name: "commit-msg", content: COMMIT_MSG_HOOK },
|
|
428
763
|
];
|
|
429
764
|
const installed = [];
|
|
430
765
|
const errors = [];
|
|
431
766
|
for (const hook of hooks) {
|
|
432
767
|
const hookPath = path.join(hooksDir, hook.name);
|
|
433
768
|
try {
|
|
769
|
+
// Refuse to follow symlinks. .git/hooks/<name> as a symlink to a
|
|
770
|
+
// sensitive file (e.g., ~/.ssh/authorized_keys) would otherwise be
|
|
771
|
+
// clobbered by writeFile. lstat reports the link, not the target.
|
|
772
|
+
let isSymlink = false;
|
|
773
|
+
try {
|
|
774
|
+
const stat = await fs.lstat(hookPath);
|
|
775
|
+
isSymlink = stat.isSymbolicLink();
|
|
776
|
+
}
|
|
777
|
+
catch (statErr) {
|
|
778
|
+
const code = statErr && typeof statErr === "object" && "code" in statErr
|
|
779
|
+
? statErr.code
|
|
780
|
+
: "";
|
|
781
|
+
if (code !== "ENOENT") {
|
|
782
|
+
throw statErr;
|
|
783
|
+
}
|
|
784
|
+
// ENOENT is expected if the hook file doesn't exist yet.
|
|
785
|
+
}
|
|
786
|
+
if (isSymlink) {
|
|
787
|
+
throw new Error(`refusing to write through a symlink at ${hookPath}`);
|
|
788
|
+
}
|
|
434
789
|
await fs.writeFile(hookPath, hook.content, { mode: 0o755 });
|
|
435
790
|
installed.push(hook.name);
|
|
436
791
|
}
|