gaia-framework 1.87.0 → 1.105.1
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/.claude/commands/gaia-ci-edit.md +17 -0
- package/CLAUDE.md +10 -0
- package/_gaia/_config/environment-presets.yaml +140 -0
- package/_gaia/_config/global.yaml +1 -1
- package/_gaia/_config/lifecycle-sequence.yaml +9 -0
- package/_gaia/_config/workflow-manifest.csv +1 -0
- package/_gaia/core/engine/workflow.xml +2 -0
- package/_gaia/core/validators/ci-edit-audit.js +181 -0
- package/_gaia/core/validators/ci-edit-test-env-scan.js +89 -0
- package/_gaia/core/validators/dev-story-security-controls.js +264 -0
- package/_gaia/core/validators/promotion-chain-env-resolver.js +140 -0
- package/_gaia/core/validators/test-environment-validator.js +292 -0
- package/_gaia/lifecycle/knowledge/brownfield/test-execution-scan.md +56 -9
- package/_gaia/lifecycle/workflows/4-implementation/create-story/instructions.xml +4 -0
- package/_gaia/lifecycle/workflows/4-implementation/dev-story/checklist.md +2 -0
- package/_gaia/lifecycle/workflows/4-implementation/dev-story/instructions.xml +94 -3
- package/_gaia/testing/workflows/ci-edit/checklist.md +41 -0
- package/_gaia/testing/workflows/ci-edit/instructions.xml +132 -0
- package/_gaia/testing/workflows/ci-edit/workflow.yaml +30 -0
- package/_gaia/testing/workflows/ci-setup/instructions.xml +75 -7
- package/_gaia/testing/workflows/test-gap-analysis/checklist.md +12 -0
- package/_gaia/testing/workflows/test-gap-analysis/instructions.xml +61 -2
- package/_gaia/testing/workflows/test-gap-analysis/workflow.yaml +2 -0
- package/gaia-install.sh +81 -76
- package/lib/copy-lib.sh +81 -0
- package/package.json +2 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 'ci-edit'
|
|
3
|
+
description: 'Edit the ci_cd.promotion_chain in global.yaml — add, remove, edit, or reorder environments.'
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS:
|
|
8
|
+
|
|
9
|
+
<steps CRITICAL="TRUE">
|
|
10
|
+
1. LOAD the FULL {project-root}/_gaia/core/engine/workflow.xml
|
|
11
|
+
2. READ its entire contents — this is the CORE OS
|
|
12
|
+
3. Pass {project-root}/_gaia/testing/workflows/ci-edit/workflow.yaml as 'workflow-config'
|
|
13
|
+
4. Follow workflow.xml instructions EXACTLY
|
|
14
|
+
5. Save outputs after EACH section
|
|
15
|
+
</steps>
|
|
16
|
+
|
|
17
|
+
$ARGUMENTS
|
package/CLAUDE.md
CHANGED
|
@@ -254,6 +254,16 @@ Publishing is fully automated — no manual steps required beyond creating the p
|
|
|
254
254
|
|
|
255
255
|
After a release, verify: `npm view gaia-framework version`
|
|
256
256
|
|
|
257
|
+
## Sprint Gate (Upgrade Protection)
|
|
258
|
+
|
|
259
|
+
The installer's `update` command includes a sprint gate that prevents framework upgrades while a sprint is active. Before any files are modified, the installer reads `docs/implementation-artifacts/sprint-status.yaml` and checks whether any story has status `in-progress`, `review`, or `ready-for-dev`.
|
|
260
|
+
|
|
261
|
+
- **Active sprint detected:** the upgrade halts with exit code 1 and a message identifying the sprint and the number of active stories.
|
|
262
|
+
- **No active sprint** (all stories `done` or `backlog`): the gate passes and the upgrade proceeds.
|
|
263
|
+
- **No sprint file:** the gate passes silently (fresh project or no sprint started).
|
|
264
|
+
|
|
265
|
+
To bypass the gate (not recommended): `gaia-install.sh update --skip-sprint-gate [target]`
|
|
266
|
+
|
|
257
267
|
## Do Not
|
|
258
268
|
|
|
259
269
|
- Pre-load files — load at runtime when needed
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# GAIA Environment Presets — E20-S2 (FR-245, ADR-033)
|
|
2
|
+
#
|
|
3
|
+
# User-facing entry point for promotion-chain configuration. Each preset is a
|
|
4
|
+
# complete, ready-to-use promotion_chain array that can be copied into
|
|
5
|
+
# global.yaml under `ci_cd.promotion_chain`. The presets are consumed by
|
|
6
|
+
# /gaia-ci-setup (E20-S3) which offers them during initial project setup.
|
|
7
|
+
#
|
|
8
|
+
# The schema of each promotion_chain entry is defined in architecture §10.24.1
|
|
9
|
+
# and enforced by the E20-S1 validator (test/validators/promotion-chain-validator.js).
|
|
10
|
+
# Required fields per entry: id, name, branch, ci_provider.
|
|
11
|
+
# Optional fields used here: merge_strategy, environment, test_tiers, auto_merge,
|
|
12
|
+
# approval_required, description, ci_checks.
|
|
13
|
+
#
|
|
14
|
+
# Test-tier convention (§10.24.3): additive — later environments include all
|
|
15
|
+
# earlier tiers plus additional ones (e.g., [1] -> [1, 2] -> [1, 2, 3]).
|
|
16
|
+
#
|
|
17
|
+
# Backward compatibility (NFR-045): this file is purely additive. Projects that
|
|
18
|
+
# do not set `ci_cd.promotion_chain` in global.yaml are unaffected.
|
|
19
|
+
|
|
20
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
# solo — Solo developer shipping direct to main
|
|
22
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
solo:
|
|
24
|
+
description: "Solo developer shipping direct to main. Single environment, auto-merge, full test coverage on the only branch."
|
|
25
|
+
promotion_chain:
|
|
26
|
+
- id: "prod"
|
|
27
|
+
name: "Production"
|
|
28
|
+
branch: "main"
|
|
29
|
+
environment: "production"
|
|
30
|
+
ci_provider: "github_actions"
|
|
31
|
+
merge_strategy: "squash"
|
|
32
|
+
test_tiers: [1, 2, 3]
|
|
33
|
+
auto_merge: true
|
|
34
|
+
description: "Solo preset — single environment on main. auto_merge is enabled because there is no upstream environment to promote from."
|
|
35
|
+
|
|
36
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
# small-team — 2-5 developers with a single pre-prod gate
|
|
38
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
small-team:
|
|
40
|
+
description: "Small team (2-5 developers) with a single pre-prod gate. Feature branches merge to develop for integration, then promote to main for release."
|
|
41
|
+
promotion_chain:
|
|
42
|
+
- id: "dev"
|
|
43
|
+
name: "Development"
|
|
44
|
+
branch: "develop"
|
|
45
|
+
environment: "development"
|
|
46
|
+
ci_provider: "github_actions"
|
|
47
|
+
merge_strategy: "squash"
|
|
48
|
+
test_tiers: [1]
|
|
49
|
+
auto_merge: false
|
|
50
|
+
description: "Integration branch for the small team. Runs tier-1 (unit + lint) on every feature merge."
|
|
51
|
+
- id: "prod"
|
|
52
|
+
name: "Production"
|
|
53
|
+
branch: "main"
|
|
54
|
+
environment: "production"
|
|
55
|
+
ci_provider: "github_actions"
|
|
56
|
+
merge_strategy: "squash"
|
|
57
|
+
test_tiers: [1, 2, 3]
|
|
58
|
+
auto_merge: false
|
|
59
|
+
description: "Release branch. Runs the full tier-1/2/3 suite before promotion from develop."
|
|
60
|
+
|
|
61
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
# standard — Standard three-tier flow (dev -> staging -> prod)
|
|
63
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
standard:
|
|
65
|
+
description: "Standard three-tier flow for most product teams. Feature branches land on develop, promote to staging for pre-prod validation, then promote to main for release."
|
|
66
|
+
promotion_chain:
|
|
67
|
+
- id: "dev"
|
|
68
|
+
name: "Development"
|
|
69
|
+
branch: "develop"
|
|
70
|
+
environment: "development"
|
|
71
|
+
ci_provider: "github_actions"
|
|
72
|
+
merge_strategy: "squash"
|
|
73
|
+
test_tiers: [1]
|
|
74
|
+
auto_merge: false
|
|
75
|
+
description: "Integration branch. Tier-1 tests run on every feature merge."
|
|
76
|
+
- id: "staging"
|
|
77
|
+
name: "Staging"
|
|
78
|
+
branch: "staging"
|
|
79
|
+
environment: "staging"
|
|
80
|
+
ci_provider: "github_actions"
|
|
81
|
+
merge_strategy: "squash"
|
|
82
|
+
test_tiers: [1, 2]
|
|
83
|
+
auto_merge: false
|
|
84
|
+
description: "Pre-production validation. Adds tier-2 (integration) tests on top of tier-1."
|
|
85
|
+
- id: "prod"
|
|
86
|
+
name: "Production"
|
|
87
|
+
branch: "main"
|
|
88
|
+
environment: "production"
|
|
89
|
+
ci_provider: "github_actions"
|
|
90
|
+
merge_strategy: "squash"
|
|
91
|
+
test_tiers: [1, 2, 3]
|
|
92
|
+
auto_merge: false
|
|
93
|
+
description: "Production release. Runs the full tier-1/2/3 suite before promotion from staging."
|
|
94
|
+
|
|
95
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
# enterprise — Four-stage flow with dedicated UAT and approval gates
|
|
97
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
enterprise:
|
|
99
|
+
description: "Enterprise four-stage flow with a dedicated UAT environment and mandatory approval gates on staging and production. Uses merge (not squash) to preserve individual commit history for audit trails."
|
|
100
|
+
promotion_chain:
|
|
101
|
+
- id: "dev"
|
|
102
|
+
name: "Development"
|
|
103
|
+
branch: "develop"
|
|
104
|
+
environment: "development"
|
|
105
|
+
ci_provider: "github_actions"
|
|
106
|
+
merge_strategy: "merge"
|
|
107
|
+
test_tiers: [1]
|
|
108
|
+
auto_merge: false
|
|
109
|
+
approval_required: false
|
|
110
|
+
description: "Integration branch. Tier-1 tests (unit + lint) on every feature merge."
|
|
111
|
+
- id: "uat"
|
|
112
|
+
name: "User Acceptance Testing"
|
|
113
|
+
branch: "uat"
|
|
114
|
+
environment: "uat"
|
|
115
|
+
ci_provider: "github_actions"
|
|
116
|
+
merge_strategy: "merge"
|
|
117
|
+
test_tiers: [1, 2]
|
|
118
|
+
auto_merge: false
|
|
119
|
+
approval_required: false
|
|
120
|
+
description: "User acceptance testing environment. Adds tier-2 integration tests. Business stakeholders validate features here before staging promotion."
|
|
121
|
+
- id: "staging"
|
|
122
|
+
name: "Staging"
|
|
123
|
+
branch: "staging"
|
|
124
|
+
environment: "staging"
|
|
125
|
+
ci_provider: "github_actions"
|
|
126
|
+
merge_strategy: "merge"
|
|
127
|
+
test_tiers: [1, 2, 3]
|
|
128
|
+
auto_merge: false
|
|
129
|
+
approval_required: true
|
|
130
|
+
description: "Pre-production staging. Runs the full tier-1/2/3 suite. Requires explicit approval before promotion — enforced by branch protection."
|
|
131
|
+
- id: "prod"
|
|
132
|
+
name: "Production"
|
|
133
|
+
branch: "main"
|
|
134
|
+
environment: "production"
|
|
135
|
+
ci_provider: "github_actions"
|
|
136
|
+
merge_strategy: "merge"
|
|
137
|
+
test_tiers: [1, 2, 3]
|
|
138
|
+
auto_merge: false
|
|
139
|
+
approval_required: true
|
|
140
|
+
description: "Production release. Requires explicit approval before promotion from staging — enforced by branch protection and a release manager sign-off."
|
|
@@ -531,6 +531,15 @@ sequence:
|
|
|
531
531
|
next:
|
|
532
532
|
primary: /gaia-readiness-check
|
|
533
533
|
|
|
534
|
+
ci-edit:
|
|
535
|
+
module: testing
|
|
536
|
+
command: /gaia-ci-edit
|
|
537
|
+
next:
|
|
538
|
+
standalone: true
|
|
539
|
+
suggestions:
|
|
540
|
+
- command: /gaia-build-configs
|
|
541
|
+
context: "Regenerate resolved configs after editing the promotion chain"
|
|
542
|
+
|
|
534
543
|
atdd:
|
|
535
544
|
module: testing
|
|
536
545
|
command: /gaia-atdd
|
|
@@ -40,6 +40,7 @@ name,displayName,description,module,phase,path,command,agent
|
|
|
40
40
|
"test-design","Test Design","Create risk-based test plans","testing","anytime","_gaia/testing/workflows/test-design/workflow.yaml","gaia-test-design","test-architect"
|
|
41
41
|
"test-framework","Test Framework","Initialize test framework","testing","anytime","_gaia/testing/workflows/test-framework/workflow.yaml","gaia-test-framework","test-architect"
|
|
42
42
|
"ci-setup","CI Setup","Scaffold CI/CD quality pipeline","testing","anytime","_gaia/testing/workflows/ci-setup/workflow.yaml","gaia-ci-setup","test-architect"
|
|
43
|
+
"ci-edit","CI Edit","Edit ci_cd.promotion_chain in global.yaml (add/remove/edit/reorder environments)","testing","anytime","_gaia/testing/workflows/ci-edit/workflow.yaml","gaia-ci-edit","test-architect"
|
|
43
44
|
"atdd","ATDD","Generate failing acceptance tests","testing","anytime","_gaia/testing/workflows/atdd/workflow.yaml","gaia-atdd","test-architect"
|
|
44
45
|
"test-automation","Test Automation","Expand automated test coverage","testing","anytime","_gaia/testing/workflows/test-automation/workflow.yaml","gaia-test-automate","test-architect"
|
|
45
46
|
"test-review","Test Review","Review test quality","testing","anytime","_gaia/testing/workflows/test-review/workflow.yaml","gaia-test-review","test-architect"
|
|
@@ -51,6 +51,8 @@ execution modes (normal/yolo/planning), checkpoints, and quality gates.
|
|
|
51
51
|
<action>Resolve {date} to current date</action>
|
|
52
52
|
<action>Ask user for any remaining unresolved variables</action>
|
|
53
53
|
|
|
54
|
+
<!-- ci_cd backward-compat tolerance (E20-S11 / NFR-045 / ADR-033) — Config resolution treats a missing or empty ci_cd block as a no-op: no defaults are injected, no warning is emitted, and downstream workflows that depend on ci_cd.promotion_chain MUST consult the canonical chainPresent() predicate in scripts/lib/promotion-chain-guard.js rather than re-implementing the absent-variant check. -->
|
|
55
|
+
|
|
54
56
|
<!-- Template Resolution (ADR-020, FR-101) — custom/templates/ overrides _gaia/lifecycle/templates/ -->
|
|
55
57
|
<!-- Resolution order for template reads: custom/templates/{filename} > _gaia/lifecycle/templates/{filename} -->
|
|
56
58
|
<!-- Resolution order for template writes: custom/templates/ ONLY — NEVER _gaia/lifecycle/templates/ -->
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /gaia-ci-edit Audit Trail (E20-S13 AC5)
|
|
3
|
+
*
|
|
4
|
+
* Writes an audit checkpoint every time a user adds, removes, reorders, or
|
|
5
|
+
* modifies an entry in `ci_cd.promotion_chain`. The checkpoint format
|
|
6
|
+
* follows the existing `_memory/checkpoints/` YAML conventions so
|
|
7
|
+
* `/gaia-resume` and future audit tooling can discover and replay the
|
|
8
|
+
* history deterministically.
|
|
9
|
+
*
|
|
10
|
+
* Checkpoint schema:
|
|
11
|
+
* user: <string>
|
|
12
|
+
* timestamp: <ISO 8601 UTC>
|
|
13
|
+
* operation: add|remove|reorder|modify
|
|
14
|
+
* before_state: <full promotion_chain array before the edit>
|
|
15
|
+
* after_state: <full promotion_chain array after the edit>
|
|
16
|
+
* diff_summary: <human-readable change list: added/removed/modified/reordered>
|
|
17
|
+
*
|
|
18
|
+
* Design principles:
|
|
19
|
+
* - Deterministic filenames: `ci-edit-<iso-8601>.yaml` with colons
|
|
20
|
+
* replaced by hyphens so the name is filesystem-safe on every OS.
|
|
21
|
+
* - No external YAML dependency — the checkpoint writer emits a small,
|
|
22
|
+
* strict YAML subset. Round-tripping through a real YAML parser is
|
|
23
|
+
* covered in the ci-edit test suite; this module stays dependency-free.
|
|
24
|
+
* - Pure function at the edges: the filesystem write is the only side
|
|
25
|
+
* effect, and the caller provides `checkpointDir` and `now`.
|
|
26
|
+
*
|
|
27
|
+
* @module ci-edit-audit
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the filesystem-safe ISO-8601 timestamp component.
|
|
35
|
+
* @param {Date} date
|
|
36
|
+
* @returns {string} e.g., "2026-04-08T09-15-30Z"
|
|
37
|
+
*/
|
|
38
|
+
function buildTimestampSlug(date) {
|
|
39
|
+
return date.toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "Z");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compute a human-readable diff summary between two promotion_chain arrays.
|
|
44
|
+
*
|
|
45
|
+
* - Entries present only in `after` → added
|
|
46
|
+
* - Entries present only in `before` → removed
|
|
47
|
+
* - Entries present in both with mutated contents → modified
|
|
48
|
+
* - Entries present in both identical but in different positions → reordered
|
|
49
|
+
*
|
|
50
|
+
* @param {Array<object>} before
|
|
51
|
+
* @param {Array<object>} after
|
|
52
|
+
* @returns {{added:string[], removed:string[], modified:string[], reordered:boolean}}
|
|
53
|
+
*/
|
|
54
|
+
export function computeDiffSummary(before, after) {
|
|
55
|
+
const beforeList = Array.isArray(before) ? before : [];
|
|
56
|
+
const afterList = Array.isArray(after) ? after : [];
|
|
57
|
+
const beforeById = new Map(beforeList.map((e) => [e?.id, e]));
|
|
58
|
+
const afterById = new Map(afterList.map((e) => [e?.id, e]));
|
|
59
|
+
|
|
60
|
+
const added = [];
|
|
61
|
+
const removed = [];
|
|
62
|
+
const modified = [];
|
|
63
|
+
|
|
64
|
+
for (const [id, afterEntry] of afterById.entries()) {
|
|
65
|
+
if (!beforeById.has(id)) {
|
|
66
|
+
added.push(id);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const beforeEntry = beforeById.get(id);
|
|
70
|
+
if (JSON.stringify(beforeEntry) !== JSON.stringify(afterEntry)) {
|
|
71
|
+
modified.push(id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const id of beforeById.keys()) {
|
|
75
|
+
if (!afterById.has(id)) removed.push(id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reorder detection: same set of ids, different ordering, no content changes.
|
|
79
|
+
let reordered = false;
|
|
80
|
+
if (added.length === 0 && removed.length === 0 && modified.length === 0) {
|
|
81
|
+
const beforeOrder = beforeList.map((e) => e?.id).join("|");
|
|
82
|
+
const afterOrder = afterList.map((e) => e?.id).join("|");
|
|
83
|
+
if (beforeOrder !== afterOrder) reordered = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { added, removed, modified, reordered };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Minimal YAML emitter for the audit checkpoint. Handles strings, numbers,
|
|
91
|
+
* booleans, arrays, and nested objects — enough for promotion_chain entries.
|
|
92
|
+
*
|
|
93
|
+
* @param {*} value
|
|
94
|
+
* @param {number} indent
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function emitYaml(value, indent = 0) {
|
|
98
|
+
const pad = " ".repeat(indent);
|
|
99
|
+
if (value === null || value === undefined) return "null";
|
|
100
|
+
if (typeof value === "string") {
|
|
101
|
+
// Quote if the string contains YAML-significant characters.
|
|
102
|
+
if (/[:#\-?\[\]{}&*!|>'"%@`,\n]/.test(value) || value === "") {
|
|
103
|
+
return JSON.stringify(value);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
if (value.length === 0) return "[]";
|
|
110
|
+
return value
|
|
111
|
+
.map((item) => {
|
|
112
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
113
|
+
const inner = emitYaml(item, indent + 1)
|
|
114
|
+
.split("\n")
|
|
115
|
+
.map((line, idx) => (idx === 0 ? line : `${pad} ${line}`))
|
|
116
|
+
.join("\n");
|
|
117
|
+
return `${pad}- ${inner}`;
|
|
118
|
+
}
|
|
119
|
+
return `${pad}- ${emitYaml(item, indent + 1)}`;
|
|
120
|
+
})
|
|
121
|
+
.join("\n");
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "object") {
|
|
124
|
+
const entries = Object.entries(value);
|
|
125
|
+
if (entries.length === 0) return "{}";
|
|
126
|
+
return entries
|
|
127
|
+
.map(([k, v]) => {
|
|
128
|
+
if (v && typeof v === "object") {
|
|
129
|
+
if (Array.isArray(v) && v.length === 0) return `${pad}${k}: []`;
|
|
130
|
+
if (!Array.isArray(v) && Object.keys(v).length === 0) return `${pad}${k}: {}`;
|
|
131
|
+
const nested = emitYaml(v, indent + 1);
|
|
132
|
+
return `${pad}${k}:\n${nested}`;
|
|
133
|
+
}
|
|
134
|
+
return `${pad}${k}: ${emitYaml(v, indent + 1)}`;
|
|
135
|
+
})
|
|
136
|
+
.join("\n");
|
|
137
|
+
}
|
|
138
|
+
return JSON.stringify(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Write a /gaia-ci-edit audit checkpoint to disk.
|
|
143
|
+
*
|
|
144
|
+
* @param {{
|
|
145
|
+
* operation: "add"|"remove"|"reorder"|"modify",
|
|
146
|
+
* user: string,
|
|
147
|
+
* beforeState: Array<object>,
|
|
148
|
+
* afterState: Array<object>,
|
|
149
|
+
* checkpointDir: string,
|
|
150
|
+
* now?: Date,
|
|
151
|
+
* }} params
|
|
152
|
+
* @returns {string} Absolute path to the written checkpoint file.
|
|
153
|
+
*/
|
|
154
|
+
export function writeCiEditAuditCheckpoint(params) {
|
|
155
|
+
const { operation, user, beforeState, afterState, checkpointDir, now } = params || {};
|
|
156
|
+
if (!checkpointDir || typeof checkpointDir !== "string") {
|
|
157
|
+
throw new Error("writeCiEditAuditCheckpoint: checkpointDir is required");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const stamp = now instanceof Date ? now : new Date();
|
|
161
|
+
const timestamp = stamp.toISOString();
|
|
162
|
+
const slug = buildTimestampSlug(stamp);
|
|
163
|
+
const filename = `ci-edit-${slug}.yaml`;
|
|
164
|
+
|
|
165
|
+
const diffSummary = computeDiffSummary(beforeState, afterState);
|
|
166
|
+
|
|
167
|
+
const payload = {
|
|
168
|
+
workflow: "ci-edit",
|
|
169
|
+
user: user || "unknown",
|
|
170
|
+
timestamp,
|
|
171
|
+
operation,
|
|
172
|
+
before_state: beforeState || [],
|
|
173
|
+
after_state: afterState || [],
|
|
174
|
+
diff_summary: diffSummary,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
mkdirSync(checkpointDir, { recursive: true });
|
|
178
|
+
const filePath = join(checkpointDir, filename);
|
|
179
|
+
writeFileSync(filePath, emitYaml(payload) + "\n", "utf8");
|
|
180
|
+
return filePath;
|
|
181
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ci-edit Remove Safety Scan — test-environment.yaml target (E20-S10, AC5)
|
|
3
|
+
*
|
|
4
|
+
* Scans a `test-environment.yaml` payload for tier entries that reference a
|
|
5
|
+
* given promotion chain environment id via `promotion_chain_env_id`, so the
|
|
6
|
+
* `/gaia-ci-edit` remove operation (E20-S4) can surface impacted tiers and
|
|
7
|
+
* warn the user before deleting an environment from `ci_cd.promotion_chain`.
|
|
8
|
+
*
|
|
9
|
+
* This module intentionally does NOT rely on a full YAML parser — it uses a
|
|
10
|
+
* minimal line-based scanner tuned to the manifest schema (flat runner list
|
|
11
|
+
* with scalar fields). This keeps the scan dependency-free and resilient to
|
|
12
|
+
* partial manifests.
|
|
13
|
+
*
|
|
14
|
+
* Architecture references:
|
|
15
|
+
* - ADR-033: Multi-Environment Promotion Chain
|
|
16
|
+
* - §10.24.4: /gaia-ci-edit cascade updates
|
|
17
|
+
*
|
|
18
|
+
* @module ci-edit-test-env-scan
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scan a test-environment.yaml content string for tier entries that reference
|
|
23
|
+
* the given environment id.
|
|
24
|
+
*
|
|
25
|
+
* @param {string|null|undefined} content — The raw YAML content.
|
|
26
|
+
* @param {string} targetEnvId — The id being removed from the promotion chain.
|
|
27
|
+
* @returns {string[]} List of tier/runner names that reference the id. Empty
|
|
28
|
+
* list if nothing matches, the content is empty, or the target id is falsy.
|
|
29
|
+
*/
|
|
30
|
+
export function scanTestEnvironmentForEnvId(content, targetEnvId) {
|
|
31
|
+
if (!content || typeof content !== "string") return [];
|
|
32
|
+
if (!targetEnvId) return [];
|
|
33
|
+
|
|
34
|
+
const lines = content.split("\n");
|
|
35
|
+
const references = [];
|
|
36
|
+
let currentRunnerName = null;
|
|
37
|
+
|
|
38
|
+
for (const rawLine of lines) {
|
|
39
|
+
// Strip trailing comments and whitespace
|
|
40
|
+
const line = rawLine.replace(/#.*$/, "").trimEnd();
|
|
41
|
+
if (line.trim() === "") continue;
|
|
42
|
+
|
|
43
|
+
const trimmed = line.trimStart();
|
|
44
|
+
|
|
45
|
+
// Detect a new runner entry: "- name: <value>"
|
|
46
|
+
const runnerNameMatch = trimmed.match(/^-\s+name:\s*(.+)$/);
|
|
47
|
+
if (runnerNameMatch) {
|
|
48
|
+
currentRunnerName = stripQuotes(runnerNameMatch[1].trim());
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Detect an indented "name:" line for the current runner (alternative form)
|
|
53
|
+
const altNameMatch = trimmed.match(/^name:\s*(.+)$/);
|
|
54
|
+
if (altNameMatch && !trimmed.startsWith("- ")) {
|
|
55
|
+
// Only honor if we're already inside a runner block (indent > 0)
|
|
56
|
+
const indent = line.length - line.trimStart().length;
|
|
57
|
+
if (indent > 0) {
|
|
58
|
+
currentRunnerName = stripQuotes(altNameMatch[1].trim());
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Detect "promotion_chain_env_id: <value>" within the current runner
|
|
64
|
+
const envIdMatch = trimmed.match(/^promotion_chain_env_id:\s*(.+)$/);
|
|
65
|
+
if (envIdMatch && currentRunnerName) {
|
|
66
|
+
const value = stripQuotes(envIdMatch[1].trim());
|
|
67
|
+
if (value === targetEnvId) {
|
|
68
|
+
references.push(currentRunnerName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return references;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Strip surrounding single or double quotes from a YAML scalar value.
|
|
78
|
+
* @param {string} value
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function stripQuotes(value) {
|
|
82
|
+
if (
|
|
83
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
84
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
85
|
+
) {
|
|
86
|
+
return value.slice(1, -1);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|