pipecraft 0.0.0-releaseit

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/.claude/settings.local.json +35 -0
  2. package/.github/actions/calculate-version/action.yml +106 -0
  3. package/.github/actions/create-pr/action.yml +122 -0
  4. package/.github/actions/create-release/action.yml +74 -0
  5. package/.github/actions/create-tag/action.yml +94 -0
  6. package/.github/actions/detect-changes/action.yml +62 -0
  7. package/.github/actions/manage-branch/action.yml +113 -0
  8. package/.github/actions/promote-branch/action.yml +352 -0
  9. package/.github/example/workflows/job.analyze.code.yml +26 -0
  10. package/.github/example/workflows/job.analyze.docker.yml +32 -0
  11. package/.github/example/workflows/job.app.api.deploy.yml +127 -0
  12. package/.github/example/workflows/job.app.api.test.yml +102 -0
  13. package/.github/example/workflows/job.app.docs.deploy.yml +40 -0
  14. package/.github/example/workflows/job.app.docs.test.yml +50 -0
  15. package/.github/example/workflows/job.app.web.deploy.yml +96 -0
  16. package/.github/example/workflows/job.app.web.test.yml +49 -0
  17. package/.github/example/workflows/job.changes.yml +82 -0
  18. package/.github/example/workflows/job.create-pr.yml +96 -0
  19. package/.github/example/workflows/job.env-check.yml +43 -0
  20. package/.github/example/workflows/job.fast-forward.yml +103 -0
  21. package/.github/example/workflows/job.lint.yml +64 -0
  22. package/.github/example/workflows/job.pr-name.yml +70 -0
  23. package/.github/example/workflows/job.pr-squash-name.yml +27 -0
  24. package/.github/example/workflows/job.tag.yml +38 -0
  25. package/.github/example/workflows/job.version.yml +99 -0
  26. package/.github/example/workflows/lib.plugin-base.publish.yml +59 -0
  27. package/.github/example/workflows/pipe.yml +178 -0
  28. package/.github/example/workflows/scripts/fastforward.sh +51 -0
  29. package/.github/example/workflows/scripts/pipe-cleaner.sh +70 -0
  30. package/.github/scripts/fastforward.sh +51 -0
  31. package/.github/scripts/pipe-cleaner.sh +70 -0
  32. package/.github/workflows/pipeline.yml +220 -0
  33. package/.github/workflows/pr-title-check.yml +70 -0
  34. package/.github/workflows/publish.yml +39 -0
  35. package/.pipecraftrc.json +78 -0
  36. package/.release-it.cjs +71 -0
  37. package/PIPELINE_TESTING_PLAN.md +499 -0
  38. package/README.md +1101 -0
  39. package/TRUNK_FLOW_PLAN.md +401 -0
  40. package/assets/logo_banner.png +0 -0
  41. package/assets/logo_banner.webp +0 -0
  42. package/dist/cli/index.d.ts +3 -0
  43. package/dist/cli/index.d.ts.map +1 -0
  44. package/dist/cli/index.js +364 -0
  45. package/dist/cli/index.js.map +1 -0
  46. package/dist/generators/init.tpl.d.ts +3 -0
  47. package/dist/generators/init.tpl.d.ts.map +1 -0
  48. package/dist/generators/init.tpl.js +117 -0
  49. package/dist/generators/init.tpl.js.map +1 -0
  50. package/dist/generators/workflows.tpl.d.ts +7 -0
  51. package/dist/generators/workflows.tpl.d.ts.map +1 -0
  52. package/dist/generators/workflows.tpl.js +73 -0
  53. package/dist/generators/workflows.tpl.js.map +1 -0
  54. package/dist/templates/actions/calculate-version.yml.tpl.d.ts +3 -0
  55. package/dist/templates/actions/calculate-version.yml.tpl.d.ts.map +1 -0
  56. package/dist/templates/actions/calculate-version.yml.tpl.js +122 -0
  57. package/dist/templates/actions/calculate-version.yml.tpl.js.map +1 -0
  58. package/dist/templates/actions/create-pr.yml.tpl.d.ts +3 -0
  59. package/dist/templates/actions/create-pr.yml.tpl.d.ts.map +1 -0
  60. package/dist/templates/actions/create-pr.yml.tpl.js +145 -0
  61. package/dist/templates/actions/create-pr.yml.tpl.js.map +1 -0
  62. package/dist/templates/actions/create-tag.yml.tpl.d.ts +3 -0
  63. package/dist/templates/actions/create-tag.yml.tpl.d.ts.map +1 -0
  64. package/dist/templates/actions/create-tag.yml.tpl.js +111 -0
  65. package/dist/templates/actions/create-tag.yml.tpl.js.map +1 -0
  66. package/dist/templates/actions/detect-changes.yml.tpl.d.ts +3 -0
  67. package/dist/templates/actions/detect-changes.yml.tpl.d.ts.map +1 -0
  68. package/dist/templates/actions/detect-changes.yml.tpl.js +79 -0
  69. package/dist/templates/actions/detect-changes.yml.tpl.js.map +1 -0
  70. package/dist/templates/actions/manage-branch.yml.tpl.d.ts +3 -0
  71. package/dist/templates/actions/manage-branch.yml.tpl.d.ts.map +1 -0
  72. package/dist/templates/actions/manage-branch.yml.tpl.js +130 -0
  73. package/dist/templates/actions/manage-branch.yml.tpl.js.map +1 -0
  74. package/dist/templates/actions/promote-branch.yml.tpl.d.ts +3 -0
  75. package/dist/templates/actions/promote-branch.yml.tpl.d.ts.map +1 -0
  76. package/dist/templates/actions/promote-branch.yml.tpl.js +362 -0
  77. package/dist/templates/actions/promote-branch.yml.tpl.js.map +1 -0
  78. package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts +16 -0
  79. package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts.map +1 -0
  80. package/dist/templates/workflows/pipeline-path-based.yml.tpl.js +652 -0
  81. package/dist/templates/workflows/pipeline-path-based.yml.tpl.js.map +1 -0
  82. package/dist/types/index.d.ts +63 -0
  83. package/dist/types/index.d.ts.map +1 -0
  84. package/dist/types/index.js +2 -0
  85. package/dist/types/index.js.map +1 -0
  86. package/dist/utils/ast-path-operations.d.ts +287 -0
  87. package/dist/utils/ast-path-operations.d.ts.map +1 -0
  88. package/dist/utils/ast-path-operations.js +419 -0
  89. package/dist/utils/ast-path-operations.js.map +1 -0
  90. package/dist/utils/config.d.ts +3 -0
  91. package/dist/utils/config.d.ts.map +1 -0
  92. package/dist/utils/config.js +49 -0
  93. package/dist/utils/config.js.map +1 -0
  94. package/dist/utils/github-setup.d.ts +74 -0
  95. package/dist/utils/github-setup.d.ts.map +1 -0
  96. package/dist/utils/github-setup.js +418 -0
  97. package/dist/utils/github-setup.js.map +1 -0
  98. package/dist/utils/idempotency.d.ts +47 -0
  99. package/dist/utils/idempotency.d.ts.map +1 -0
  100. package/dist/utils/idempotency.js +176 -0
  101. package/dist/utils/idempotency.js.map +1 -0
  102. package/dist/utils/preflight.d.ts +55 -0
  103. package/dist/utils/preflight.d.ts.map +1 -0
  104. package/dist/utils/preflight.js +261 -0
  105. package/dist/utils/preflight.js.map +1 -0
  106. package/dist/utils/versioning.d.ts +41 -0
  107. package/dist/utils/versioning.d.ts.map +1 -0
  108. package/dist/utils/versioning.js +219 -0
  109. package/dist/utils/versioning.js.map +1 -0
  110. package/docs/USER_JOURNEY_ERRORS.md +352 -0
  111. package/eslint.config.js +77 -0
  112. package/examples/basic-config.json +41 -0
  113. package/examples/monorepo-config.json +49 -0
  114. package/examples/usage.md +289 -0
  115. package/package.json +58 -0
  116. package/scripts/debug-workflows.sh +339 -0
  117. package/scripts/parse-pipeline.js +29 -0
  118. package/scripts/test-job-order.cjs +140 -0
  119. package/scripts/validate-pipeline.cjs +379 -0
  120. package/scripts/verify-job-order.sh +30 -0
  121. package/src/cli/index.ts +414 -0
  122. package/src/generators/init.tpl.ts +126 -0
  123. package/src/generators/workflows.tpl.ts +80 -0
  124. package/src/templates/actions/calculate-version.yml.tpl.ts +124 -0
  125. package/src/templates/actions/create-pr.yml.tpl.ts +147 -0
  126. package/src/templates/actions/create-tag.yml.tpl.ts +112 -0
  127. package/src/templates/actions/detect-changes.yml.tpl.ts +87 -0
  128. package/src/templates/actions/manage-branch.yml.tpl.ts +132 -0
  129. package/src/templates/actions/promote-branch.yml.tpl.ts +364 -0
  130. package/src/templates/workflows/pipeline-path-based.yml.tpl.ts +706 -0
  131. package/src/types/index.ts +64 -0
  132. package/src/utils/README-ast-path-operations.md +390 -0
  133. package/src/utils/ast-path-operations.ts +581 -0
  134. package/src/utils/config.ts +64 -0
  135. package/src/utils/github-setup.ts +558 -0
  136. package/src/utils/idempotency.ts +215 -0
  137. package/src/utils/preflight.ts +306 -0
  138. package/src/utils/versioning.ts +244 -0
  139. package/tests/README.md +229 -0
  140. package/tests/TEST_STRUCTURE.md +256 -0
  141. package/tests/act/run-act-tests.sh +345 -0
  142. package/tests/debugging/debug-utils.ts +538 -0
  143. package/tests/debugging/debug-workflow.test.ts +339 -0
  144. package/tests/debugging/debug-workflows.sh +339 -0
  145. package/tests/debugging/iterative-debug.ts +652 -0
  146. package/tests/debugging/run-debug-tests.sh +431 -0
  147. package/tests/fixtures/basic-config.json +51 -0
  148. package/tests/fixtures/invalid-config.json +9 -0
  149. package/tests/fixtures/pipeline-generated.yml +235 -0
  150. package/tests/fixtures/pipeline-preserve-comments.yml +245 -0
  151. package/tests/fixtures/pipeline-user-modified.yml +245 -0
  152. package/tests/fixtures/test-config.json +58 -0
  153. package/tests/github-live/README.md +250 -0
  154. package/tests/github-local/JOB_WORKFLOW_TESTS.md +296 -0
  155. package/tests/github-local/README.md +240 -0
  156. package/tests/github-local/run-all-tests.sh +422 -0
  157. package/tests/github-local/test-job-workflows.sh +631 -0
  158. package/tests/github-local/test-pipeline-workflow.sh +440 -0
  159. package/tests/integration/generators.test.ts +578 -0
  160. package/tests/integration/path-based-template.test.ts +510 -0
  161. package/tests/integration/simple-path-based.test.ts +415 -0
  162. package/tests/setup.ts +56 -0
  163. package/tests/unit/ast-path-operations-extended.test.ts +302 -0
  164. package/tests/unit/cli.test.ts +541 -0
  165. package/tests/unit/config-extended.test.ts +412 -0
  166. package/tests/unit/config.test.ts +152 -0
  167. package/tests/unit/github-setup.test.ts +189 -0
  168. package/tests/unit/idempotency-isolated.test.ts +297 -0
  169. package/tests/unit/job-order.test.ts +157 -0
  170. package/tests/unit/pipeline-path-based.test.ts +511 -0
  171. package/tests/unit/validate-pipeline.test.ts +384 -0
  172. package/tests/unit/versioning-extended.test.ts +279 -0
  173. package/tests/unit/versioning.test.ts +241 -0
  174. package/tsconfig.debug.json +17 -0
  175. package/tsconfig.json +28 -0
  176. package/vitest.config.ts +51 -0
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test script to compare job order between original and generated pipelines
5
+ * This ensures that Pipecraft jobs maintain their original positions
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const yaml = require('yaml');
11
+
12
+ /**
13
+ * Extract job names in order from a YAML file
14
+ */
15
+ function getJobOrder(filePath) {
16
+ if (!fs.existsSync(filePath)) {
17
+ console.error(`āŒ File not found: ${filePath}`);
18
+ return [];
19
+ }
20
+
21
+ try {
22
+ const content = fs.readFileSync(filePath, 'utf8');
23
+ const doc = yaml.parseDocument(content);
24
+
25
+ if (!doc.contents || !doc.contents.get('jobs')) {
26
+ console.error(`āŒ No jobs section found in ${filePath}`);
27
+ return [];
28
+ }
29
+
30
+ const jobsNode = doc.contents.get('jobs');
31
+ if (!jobsNode.items) {
32
+ console.error(`āŒ No job items found in ${filePath}`);
33
+ return [];
34
+ }
35
+
36
+ return jobsNode.items.map(item => item.key.value);
37
+ } catch (error) {
38
+ console.error(`āŒ Error parsing ${filePath}:`, error.message);
39
+ return [];
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Compare two job orders and report differences
45
+ */
46
+ function compareJobOrders(original, generated, testName) {
47
+ console.log(`\nšŸ” Testing: ${testName}`);
48
+ console.log(`šŸ“‹ Original order: [${original.join(', ')}]`);
49
+ console.log(`šŸ“‹ Generated order: [${generated.join(', ')}]`);
50
+
51
+ if (original.length !== generated.length) {
52
+ console.log(`āŒ Length mismatch: original has ${original.length} jobs, generated has ${generated.length}`);
53
+ return false;
54
+ }
55
+
56
+ let matches = 0;
57
+ let differences = [];
58
+
59
+ for (let i = 0; i < original.length; i++) {
60
+ if (original[i] === generated[i]) {
61
+ matches++;
62
+ } else {
63
+ differences.push({
64
+ position: i,
65
+ original: original[i],
66
+ generated: generated[i]
67
+ });
68
+ }
69
+ }
70
+
71
+ if (matches === original.length) {
72
+ console.log(`āœ… Perfect match! All ${matches} jobs in correct order`);
73
+ return true;
74
+ } else {
75
+ console.log(`āŒ ${differences.length} position(s) differ:`);
76
+ differences.forEach(diff => {
77
+ console.log(` Position ${diff.position}: expected "${diff.original}", got "${diff.generated}"`);
78
+ });
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Main test function
85
+ */
86
+ function runJobOrderTests() {
87
+ console.log('🧪 Pipecraft Job Order Test Suite');
88
+ console.log('=====================================');
89
+
90
+ const testCases = [
91
+ {
92
+ name: 'User Modified Pipeline',
93
+ original: '.github/workflows/pipeline-user-modified.yml',
94
+ generated: '.github/workflows/pipeline-test-order-restored.yml'
95
+ },
96
+ {
97
+ name: 'Final Test Pipeline',
98
+ original: '.github/workflows/pipeline-user-modified.yml',
99
+ generated: '.github/workflows/pipeline-test-final.yml'
100
+ }
101
+ ];
102
+
103
+ let allPassed = true;
104
+
105
+ for (const testCase of testCases) {
106
+ const originalOrder = getJobOrder(testCase.original);
107
+ const generatedOrder = getJobOrder(testCase.generated);
108
+
109
+ if (originalOrder.length === 0 || generatedOrder.length === 0) {
110
+ console.log(`āŒ Skipping ${testCase.name} - failed to parse files`);
111
+ allPassed = false;
112
+ continue;
113
+ }
114
+
115
+ const passed = compareJobOrders(originalOrder, generatedOrder, testCase.name);
116
+ if (!passed) {
117
+ allPassed = false;
118
+ }
119
+ }
120
+
121
+ console.log('\nšŸ“Š Test Results');
122
+ console.log('================');
123
+ if (allPassed) {
124
+ console.log('āœ… All job order tests PASSED!');
125
+ console.log('šŸŽ‰ Pipecraft jobs are maintaining their original positions');
126
+ } else {
127
+ console.log('āŒ Some job order tests FAILED!');
128
+ console.log('šŸ”§ Job positions are not being preserved correctly');
129
+ }
130
+
131
+ return allPassed;
132
+ }
133
+
134
+ // Run the tests if this script is executed directly
135
+ if (require.main === module) {
136
+ const success = runJobOrderTests();
137
+ process.exit(success ? 0 : 1);
138
+ }
139
+
140
+ module.exports = { getJobOrder, compareJobOrders, runJobOrderTests };
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Pipeline Validation Script
5
+ *
6
+ * Validates GitHub Actions workflows and composite actions to catch common errors:
7
+ * - Missing git config in actions that perform git operations
8
+ * - Secrets usage in composite actions (should use inputs)
9
+ * - Local actions used without checkout
10
+ * - Missing token inputs for actions that need authentication
11
+ * - Version format issues
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const yaml = require('yaml');
17
+
18
+ // ANSI color codes for pretty output
19
+ const colors = {
20
+ reset: '\x1b[0m',
21
+ red: '\x1b[31m',
22
+ green: '\x1b[32m',
23
+ yellow: '\x1b[33m',
24
+ blue: '\x1b[34m',
25
+ cyan: '\x1b[36m',
26
+ };
27
+
28
+ function log(message, color = colors.reset) {
29
+ console.log(`${color}${message}${colors.reset}`);
30
+ }
31
+
32
+ function error(message) {
33
+ log(`āŒ ${message}`, colors.red);
34
+ }
35
+
36
+ function warning(message) {
37
+ log(`āš ļø ${message}`, colors.yellow);
38
+ }
39
+
40
+ function success(message) {
41
+ log(`āœ… ${message}`, colors.green);
42
+ }
43
+
44
+ function info(message) {
45
+ log(`ā„¹ļø ${message}`, colors.cyan);
46
+ }
47
+
48
+ /**
49
+ * Validate that git operations have git config set
50
+ */
51
+ function validateGitConfig(actionPath) {
52
+ const content = fs.readFileSync(actionPath, 'utf8');
53
+ // Only check for write operations that require git config
54
+ // Exclude read-only operations like 'git tag --points-at'
55
+ const gitWriteOps = [
56
+ /git commit(?!\s+--points-at)/,
57
+ /git tag(?!\s+(--points-at|--list))/,
58
+ /git push/,
59
+ /git merge/
60
+ ];
61
+
62
+ const hasGitWriteOps = gitWriteOps.some(op => op.test(content));
63
+ const hasGitConfig = content.includes('git config user.name') ||
64
+ content.includes('git config --global user.name');
65
+
66
+ if (hasGitWriteOps && !hasGitConfig) {
67
+ return `Git operations without git config (user.name/email)`;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Validate that composite actions don't access secrets directly
74
+ */
75
+ function validateTokenUsage(actionPath) {
76
+ const content = fs.readFileSync(actionPath, 'utf8');
77
+
78
+ try {
79
+ const action = yaml.parse(content);
80
+
81
+ // Check if composite action tries to use secrets
82
+ if (action.runs?.using === 'composite' && content.includes('secrets.')) {
83
+ return `Composite action accessing secrets directly (use inputs instead)`;
84
+ }
85
+
86
+ // Check if action uses gh/hub CLI without token input
87
+ if (content.includes('gh ') || content.includes('hub ') || content.includes('GITHUB_TOKEN')) {
88
+ const hasTokenInput = action.inputs?.token !== undefined;
89
+ if (!hasTokenInput && content.includes('secrets.GITHUB_TOKEN')) {
90
+ return `Uses GITHUB_TOKEN via secrets instead of input parameter`;
91
+ }
92
+ }
93
+ } catch (err) {
94
+ return `Failed to parse YAML: ${err.message}`;
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Validate version format handling
102
+ */
103
+ function validateVersionFormat(actionPath) {
104
+ const content = fs.readFileSync(actionPath, 'utf8');
105
+
106
+ // Check if action uses version input
107
+ if (content.includes('${{ inputs.version }}') || content.includes('${VERSION}')) {
108
+ // Check if it handles both v-prefixed and non-prefixed versions
109
+ const hasStripPrefix = content.includes('VERSION="${VERSION#v}"') ||
110
+ content.includes('strip v prefix') ||
111
+ content.includes('VERSION#v');
112
+
113
+ // Only warn if it looks like it's validating version format without stripping
114
+ const hasVersionValidation = content.includes('=~ ^[0-9]+') ||
115
+ content.includes('version format');
116
+
117
+ if (hasVersionValidation && !hasStripPrefix) {
118
+ return `Validates version format but doesn't strip 'v' prefix (may reject v1.0.0)`;
119
+ }
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Validate error handling in scripts
127
+ */
128
+ function validateErrorHandling(actionPath) {
129
+ const content = fs.readFileSync(actionPath, 'utf8');
130
+ const warnings = [];
131
+
132
+ // Check for suppressed error output (2>/dev/null) with critical commands
133
+ const criticalCommands = ['gh pr create', 'gh pr merge', 'gh release create', 'git push', 'git tag'];
134
+
135
+ for (const cmd of criticalCommands) {
136
+ // Match command followed by 2>/dev/null
137
+ const regex = new RegExp(`${cmd.replace(/\s+/g, '\\s+')}[^\\n]*2>/dev/null`, 'g');
138
+ if (regex.test(content)) {
139
+ warnings.push(`Suppresses errors for '${cmd}' (2>/dev/null hides useful error messages)`);
140
+ }
141
+ }
142
+
143
+ return warnings.length > 0 ? warnings.join('; ') : null;
144
+ }
145
+
146
+ /**
147
+ * Validate GitHub CLI usage has GH_TOKEN set
148
+ * Note: For composite actions, GH_TOKEN should be set at the workflow job level,
149
+ * not in the action itself. This validator just warns if gh CLI is used.
150
+ */
151
+ function validateGhToken(actionPath) {
152
+ const content = fs.readFileSync(actionPath, 'utf8');
153
+
154
+ // Check if action uses gh CLI commands
155
+ const ghCommands = ['gh pr', 'gh release', 'gh issue', 'gh workflow'];
156
+ const usesGhCli = ghCommands.some(cmd => content.includes(cmd));
157
+
158
+ if (!usesGhCli) {
159
+ return null;
160
+ }
161
+
162
+ // For composite actions, this is just a warning reminder
163
+ // The GH_TOKEN should be set at the job level in the workflow
164
+ return null; // No error - workflow should handle this
165
+ }
166
+
167
+ /**
168
+ * Validate workflow jobs
169
+ */
170
+ function validateWorkflow(workflowPath) {
171
+ const errors = [];
172
+ const warnings = [];
173
+
174
+ try {
175
+ const content = fs.readFileSync(workflowPath, 'utf8');
176
+ const workflow = yaml.parse(content);
177
+
178
+ // Check for local actions without checkout
179
+ for (const [jobName, job] of Object.entries(workflow.jobs || {})) {
180
+ const steps = job.steps || [];
181
+ let hasCheckout = false;
182
+
183
+ for (let i = 0; i < steps.length; i++) {
184
+ const step = steps[i];
185
+
186
+ if (step.uses === 'actions/checkout@v4' || step.uses?.startsWith('actions/checkout@')) {
187
+ hasCheckout = true;
188
+ }
189
+
190
+ // Check for local action usage
191
+ if (step.uses && step.uses.startsWith('./')) {
192
+ if (!hasCheckout) {
193
+ // Check if the action itself has a checkout step
194
+ const actionPath = path.join('.github', 'actions', path.basename(step.uses), 'action.yml');
195
+ let actionHasCheckout = false;
196
+
197
+ try {
198
+ if (fs.existsSync(actionPath)) {
199
+ const actionContent = fs.readFileSync(actionPath, 'utf8');
200
+ actionHasCheckout = actionContent.includes('actions/checkout');
201
+ }
202
+ } catch (e) {
203
+ // Ignore errors reading action file
204
+ }
205
+
206
+ if (!actionHasCheckout) {
207
+ errors.push(`Job '${jobName}' step ${i + 1}: Local action '${step.uses}' used before checkout`);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // Check for pnpm cache before pnpm install
215
+ for (const [jobName, job] of Object.entries(workflow.jobs || {})) {
216
+ const steps = job.steps || [];
217
+ let setupNodeWithPnpmCache = false;
218
+ let pnpmInstalled = false;
219
+
220
+ for (const step of steps) {
221
+ if (step.uses?.includes('actions/setup-node') && step.with?.cache === 'pnpm') {
222
+ setupNodeWithPnpmCache = true;
223
+ if (!pnpmInstalled) {
224
+ warnings.push(`Job '${jobName}': setup-node with pnpm cache before pnpm is installed`);
225
+ }
226
+ }
227
+
228
+ if (step.uses?.includes('pnpm/action-setup')) {
229
+ pnpmInstalled = true;
230
+ }
231
+ }
232
+ }
233
+
234
+ } catch (err) {
235
+ errors.push(`Failed to parse workflow: ${err.message}`);
236
+ }
237
+
238
+ return { errors, warnings };
239
+ }
240
+
241
+ /**
242
+ * Validate composite action
243
+ */
244
+ function validateCompositeAction(actionPath) {
245
+ const errors = [];
246
+ const warnings = [];
247
+ const fileName = path.basename(path.dirname(actionPath));
248
+
249
+ // Git config validation
250
+ const gitConfigError = validateGitConfig(actionPath);
251
+ if (gitConfigError) {
252
+ errors.push(`${fileName}: ${gitConfigError}`);
253
+ }
254
+
255
+ // Token usage validation
256
+ const tokenError = validateTokenUsage(actionPath);
257
+ if (tokenError) {
258
+ errors.push(`${fileName}: ${tokenError}`);
259
+ }
260
+
261
+ // Version format validation
262
+ const versionWarning = validateVersionFormat(actionPath);
263
+ if (versionWarning) {
264
+ warnings.push(`${fileName}: ${versionWarning}`);
265
+ }
266
+
267
+ // Error handling validation
268
+ const errorHandlingWarning = validateErrorHandling(actionPath);
269
+ if (errorHandlingWarning) {
270
+ warnings.push(`${fileName}: ${errorHandlingWarning}`);
271
+ }
272
+
273
+ // GH_TOKEN validation
274
+ const ghTokenError = validateGhToken(actionPath);
275
+ if (ghTokenError) {
276
+ errors.push(`${fileName}: ${ghTokenError}`);
277
+ }
278
+
279
+ return { errors, warnings };
280
+ }
281
+
282
+ /**
283
+ * Main validation function
284
+ */
285
+ function main() {
286
+ info('šŸ” Validating PipeCraft Pipeline...\n');
287
+
288
+ let allErrors = [];
289
+ let allWarnings = [];
290
+
291
+ // Validate workflows
292
+ const workflowsDir = path.join(process.cwd(), '.github/workflows');
293
+ if (fs.existsSync(workflowsDir)) {
294
+ const workflowFiles = fs.readdirSync(workflowsDir)
295
+ .filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
296
+ .map(f => path.join(workflowsDir, f));
297
+
298
+ log(`šŸ“„ Validating ${workflowFiles.length} workflow(s)...`, colors.blue);
299
+
300
+ for (const workflowPath of workflowFiles) {
301
+ const fileName = path.basename(workflowPath);
302
+ const { errors, warnings } = validateWorkflow(workflowPath);
303
+
304
+ if (errors.length === 0 && warnings.length === 0) {
305
+ success(` ${fileName}`);
306
+ } else {
307
+ if (errors.length > 0) {
308
+ error(` ${fileName}`);
309
+ errors.forEach(err => error(` ${err}`));
310
+ }
311
+ if (warnings.length > 0) {
312
+ warning(` ${fileName}`);
313
+ warnings.forEach(warn => warning(` ${warn}`));
314
+ }
315
+ }
316
+
317
+ allErrors = allErrors.concat(errors);
318
+ allWarnings = allWarnings.concat(warnings);
319
+ }
320
+ }
321
+
322
+ console.log();
323
+
324
+ // Validate composite actions
325
+ const actionsDir = path.join(process.cwd(), '.github/actions');
326
+ if (fs.existsSync(actionsDir)) {
327
+ const actionDirs = fs.readdirSync(actionsDir)
328
+ .map(dir => path.join(actionsDir, dir, 'action.yml'))
329
+ .filter(p => fs.existsSync(p));
330
+
331
+ log(`šŸŽ¬ Validating ${actionDirs.length} action(s)...`, colors.blue);
332
+
333
+ for (const actionPath of actionDirs) {
334
+ const actionName = path.basename(path.dirname(actionPath));
335
+ const { errors, warnings } = validateCompositeAction(actionPath);
336
+
337
+ if (errors.length === 0 && warnings.length === 0) {
338
+ success(` ${actionName}`);
339
+ } else {
340
+ if (errors.length > 0) {
341
+ error(` ${actionName}`);
342
+ errors.forEach(err => error(` ${err}`));
343
+ }
344
+ if (warnings.length > 0) {
345
+ warning(` ${actionName}`);
346
+ warnings.forEach(warn => warning(` ${warn}`));
347
+ }
348
+ }
349
+
350
+ allErrors = allErrors.concat(errors);
351
+ allWarnings = allWarnings.concat(warnings);
352
+ }
353
+ }
354
+
355
+ console.log();
356
+
357
+ // Summary
358
+ if (allErrors.length === 0 && allWarnings.length === 0) {
359
+ success('✨ Pipeline validation passed! No issues found.');
360
+ return 0;
361
+ }
362
+
363
+ if (allErrors.length > 0) {
364
+ error(`\nāŒ Found ${allErrors.length} error(s)`);
365
+ return 1;
366
+ }
367
+
368
+ if (allWarnings.length > 0) {
369
+ warning(`\nāš ļø Found ${allWarnings.length} warning(s)`);
370
+ warning('Warnings do not fail validation but should be reviewed.');
371
+ return 0;
372
+ }
373
+
374
+ return 0;
375
+ }
376
+
377
+ // Run validation
378
+ const exitCode = main();
379
+ process.exit(exitCode);
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+
3
+ # Quick job order verification script
4
+ # Usage: ./scripts/verify-job-order.sh <original-file> <generated-file>
5
+
6
+ set -e
7
+
8
+ ORIGINAL_FILE=${1:-".github/workflows/pipeline-user-modified.yml"}
9
+ GENERATED_FILE=${2:-".github/workflows/pipeline-test-order-restored.yml"}
10
+
11
+ echo "šŸ” Verifying job order preservation..."
12
+ echo "šŸ“‹ Original: $ORIGINAL_FILE"
13
+ echo "šŸ“‹ Generated: $GENERATED_FILE"
14
+
15
+ # Extract job names from both files (only from jobs section)
16
+ ORIGINAL_JOBS=$(sed -n '/^jobs:/,/^[^ ]/p' "$ORIGINAL_FILE" | grep "^ [a-zA-Z]" | sed 's/://' | tr '\n' ' ')
17
+ GENERATED_JOBS=$(sed -n '/^jobs:/,/^[^ ]/p' "$GENERATED_FILE" | grep "^ [a-zA-Z]" | sed 's/://' | tr '\n' ' ')
18
+
19
+ echo "šŸ“‹ Original jobs: $ORIGINAL_JOBS"
20
+ echo "šŸ“‹ Generated jobs: $GENERATED_JOBS"
21
+
22
+ # Compare the job orders
23
+ if [ "$ORIGINAL_JOBS" = "$GENERATED_JOBS" ]; then
24
+ echo "āœ… Job order preserved correctly!"
25
+ exit 0
26
+ else
27
+ echo "āŒ Job order differs!"
28
+ echo "šŸ”§ Pipecraft jobs may have moved to incorrect positions"
29
+ exit 1
30
+ fi