pipecraft 0.0.0-releaseit → 0.26.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 +126 -108
- package/dist/cli/index.d.ts +69 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +94 -22
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/init.tpl.d.ts +70 -0
- package/dist/generators/init.tpl.d.ts.map +1 -1
- package/dist/generators/init.tpl.js +100 -8
- package/dist/generators/init.tpl.js.map +1 -1
- package/dist/generators/workflows.tpl.d.ts +86 -0
- package/dist/generators/workflows.tpl.d.ts.map +1 -1
- package/dist/generators/workflows.tpl.js +101 -9
- package/dist/generators/workflows.tpl.js.map +1 -1
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts +47 -0
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts.map +1 -1
- package/dist/templates/actions/calculate-version.yml.tpl.js +72 -3
- package/dist/templates/actions/calculate-version.yml.tpl.js.map +1 -1
- package/dist/templates/actions/create-pr.yml.tpl.d.ts +14 -0
- package/dist/templates/actions/create-pr.yml.tpl.d.ts.map +1 -1
- package/dist/templates/actions/create-pr.yml.tpl.js +22 -2
- package/dist/templates/actions/create-pr.yml.tpl.js.map +1 -1
- package/dist/templates/actions/create-release.yml.tpl.d.ts +18 -0
- package/dist/templates/actions/create-release.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/create-release.yml.tpl.js +141 -0
- package/dist/templates/actions/create-release.yml.tpl.js.map +1 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts +14 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts.map +1 -1
- package/dist/templates/actions/create-tag.yml.tpl.js +22 -2
- package/dist/templates/actions/create-tag.yml.tpl.js.map +1 -1
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts +70 -0
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts.map +1 -1
- package/dist/templates/actions/detect-changes.yml.tpl.js +98 -2
- package/dist/templates/actions/detect-changes.yml.tpl.js.map +1 -1
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts +14 -0
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts.map +1 -1
- package/dist/templates/actions/manage-branch.yml.tpl.js +22 -2
- package/dist/templates/actions/manage-branch.yml.tpl.js.map +1 -1
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts +14 -0
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts.map +1 -1
- package/dist/templates/actions/promote-branch.yml.tpl.js +50 -7
- package/dist/templates/actions/promote-branch.yml.tpl.js.map +1 -1
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts +114 -1
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts.map +1 -1
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js +479 -74
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js.map +1 -1
- package/dist/types/index.d.ts +231 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/ast-path-operations.d.ts.map +1 -1
- package/dist/utils/ast-path-operations.js +24 -11
- package/dist/utils/ast-path-operations.js.map +1 -1
- package/dist/utils/config.d.ts +63 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +71 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/github-setup.d.ts +323 -2
- package/dist/utils/github-setup.d.ts.map +1 -1
- package/dist/utils/github-setup.js +551 -23
- package/dist/utils/github-setup.js.map +1 -1
- package/dist/utils/idempotency.d.ts +147 -7
- package/dist/utils/idempotency.d.ts.map +1 -1
- package/dist/utils/idempotency.js +143 -15
- package/dist/utils/idempotency.js.map +1 -1
- package/dist/utils/logger.d.ts +134 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +153 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/preflight.d.ts +223 -12
- package/dist/utils/preflight.d.ts.map +1 -1
- package/dist/utils/preflight.js +216 -16
- package/dist/utils/preflight.js.map +1 -1
- package/dist/utils/versioning.d.ts +160 -8
- package/dist/utils/versioning.d.ts.map +1 -1
- package/dist/utils/versioning.js +179 -15
- package/dist/utils/versioning.js.map +1 -1
- package/package.json +26 -5
- package/.claude/settings.local.json +0 -35
- package/.github/actions/calculate-version/action.yml +0 -106
- package/.github/actions/create-pr/action.yml +0 -122
- package/.github/actions/create-release/action.yml +0 -74
- package/.github/actions/create-tag/action.yml +0 -94
- package/.github/actions/detect-changes/action.yml +0 -62
- package/.github/actions/manage-branch/action.yml +0 -113
- package/.github/actions/promote-branch/action.yml +0 -352
- package/.github/example/workflows/job.analyze.code.yml +0 -26
- package/.github/example/workflows/job.analyze.docker.yml +0 -32
- package/.github/example/workflows/job.app.api.deploy.yml +0 -127
- package/.github/example/workflows/job.app.api.test.yml +0 -102
- package/.github/example/workflows/job.app.docs.deploy.yml +0 -40
- package/.github/example/workflows/job.app.docs.test.yml +0 -50
- package/.github/example/workflows/job.app.web.deploy.yml +0 -96
- package/.github/example/workflows/job.app.web.test.yml +0 -49
- package/.github/example/workflows/job.changes.yml +0 -82
- package/.github/example/workflows/job.create-pr.yml +0 -96
- package/.github/example/workflows/job.env-check.yml +0 -43
- package/.github/example/workflows/job.fast-forward.yml +0 -103
- package/.github/example/workflows/job.lint.yml +0 -64
- package/.github/example/workflows/job.pr-name.yml +0 -70
- package/.github/example/workflows/job.pr-squash-name.yml +0 -27
- package/.github/example/workflows/job.tag.yml +0 -38
- package/.github/example/workflows/job.version.yml +0 -99
- package/.github/example/workflows/lib.plugin-base.publish.yml +0 -59
- package/.github/example/workflows/pipe.yml +0 -178
- package/.github/example/workflows/scripts/fastforward.sh +0 -51
- package/.github/example/workflows/scripts/pipe-cleaner.sh +0 -70
- package/.github/scripts/fastforward.sh +0 -51
- package/.github/scripts/pipe-cleaner.sh +0 -70
- package/.github/workflows/pipeline.yml +0 -220
- package/.github/workflows/pr-title-check.yml +0 -70
- package/.github/workflows/publish.yml +0 -39
- package/.pipecraftrc.json +0 -78
- package/.release-it.cjs +0 -71
- package/PIPELINE_TESTING_PLAN.md +0 -499
- package/TRUNK_FLOW_PLAN.md +0 -401
- package/assets/logo_banner.png +0 -0
- package/assets/logo_banner.webp +0 -0
- package/docs/USER_JOURNEY_ERRORS.md +0 -352
- package/eslint.config.js +0 -77
- package/examples/basic-config.json +0 -41
- package/examples/monorepo-config.json +0 -49
- package/examples/usage.md +0 -289
- package/scripts/debug-workflows.sh +0 -339
- package/scripts/parse-pipeline.js +0 -29
- package/scripts/test-job-order.cjs +0 -140
- package/scripts/validate-pipeline.cjs +0 -379
- package/scripts/verify-job-order.sh +0 -30
- package/src/cli/index.ts +0 -414
- package/src/generators/init.tpl.ts +0 -126
- package/src/generators/workflows.tpl.ts +0 -80
- package/src/templates/actions/calculate-version.yml.tpl.ts +0 -124
- package/src/templates/actions/create-pr.yml.tpl.ts +0 -147
- package/src/templates/actions/create-tag.yml.tpl.ts +0 -112
- package/src/templates/actions/detect-changes.yml.tpl.ts +0 -87
- package/src/templates/actions/manage-branch.yml.tpl.ts +0 -132
- package/src/templates/actions/promote-branch.yml.tpl.ts +0 -364
- package/src/templates/workflows/pipeline-path-based.yml.tpl.ts +0 -706
- package/src/types/index.ts +0 -64
- package/src/utils/README-ast-path-operations.md +0 -390
- package/src/utils/ast-path-operations.ts +0 -581
- package/src/utils/config.ts +0 -64
- package/src/utils/github-setup.ts +0 -558
- package/src/utils/idempotency.ts +0 -215
- package/src/utils/preflight.ts +0 -306
- package/src/utils/versioning.ts +0 -244
- package/tests/README.md +0 -229
- package/tests/TEST_STRUCTURE.md +0 -256
- package/tests/act/run-act-tests.sh +0 -345
- package/tests/debugging/debug-utils.ts +0 -538
- package/tests/debugging/debug-workflow.test.ts +0 -339
- package/tests/debugging/debug-workflows.sh +0 -339
- package/tests/debugging/iterative-debug.ts +0 -652
- package/tests/debugging/run-debug-tests.sh +0 -431
- package/tests/fixtures/basic-config.json +0 -51
- package/tests/fixtures/invalid-config.json +0 -9
- package/tests/fixtures/pipeline-generated.yml +0 -235
- package/tests/fixtures/pipeline-preserve-comments.yml +0 -245
- package/tests/fixtures/pipeline-user-modified.yml +0 -245
- package/tests/fixtures/test-config.json +0 -58
- package/tests/github-live/README.md +0 -250
- package/tests/github-local/JOB_WORKFLOW_TESTS.md +0 -296
- package/tests/github-local/README.md +0 -240
- package/tests/github-local/run-all-tests.sh +0 -422
- package/tests/github-local/test-job-workflows.sh +0 -631
- package/tests/github-local/test-pipeline-workflow.sh +0 -440
- package/tests/integration/generators.test.ts +0 -578
- package/tests/integration/path-based-template.test.ts +0 -510
- package/tests/integration/simple-path-based.test.ts +0 -415
- package/tests/setup.ts +0 -56
- package/tests/unit/ast-path-operations-extended.test.ts +0 -302
- package/tests/unit/cli.test.ts +0 -541
- package/tests/unit/config-extended.test.ts +0 -412
- package/tests/unit/config.test.ts +0 -152
- package/tests/unit/github-setup.test.ts +0 -189
- package/tests/unit/idempotency-isolated.test.ts +0 -297
- package/tests/unit/job-order.test.ts +0 -157
- package/tests/unit/pipeline-path-based.test.ts +0 -511
- package/tests/unit/validate-pipeline.test.ts +0 -384
- package/tests/unit/versioning-extended.test.ts +0 -279
- package/tests/unit/versioning.test.ts +0 -241
- package/tsconfig.debug.json +0 -17
- package/tsconfig.json +0 -28
- package/vitest.config.ts +0 -51
|
@@ -1,16 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-Based Pipeline Template Generator
|
|
3
|
+
*
|
|
4
|
+
* The core template that generates the main CI/CD pipeline workflow for PipeCraft.
|
|
5
|
+
* This is the most complex template in the system, responsible for creating a
|
|
6
|
+
* GitHub Actions workflow that orchestrates the entire trunk-based development flow.
|
|
7
|
+
*
|
|
8
|
+
* ## Key Responsibilities
|
|
9
|
+
*
|
|
10
|
+
* 1. **Change Detection**: Generates jobs to detect which domains (api, web, libs, etc.) changed
|
|
11
|
+
* 2. **Test Execution**: Creates domain-specific test jobs based on changes
|
|
12
|
+
* 3. **Version Management**: Integrates semantic versioning for staging/production branches
|
|
13
|
+
* 4. **Branch Promotion**: Auto-promotes code through branch flow (develop → staging → main)
|
|
14
|
+
* 5. **User Job Preservation**: Maintains user-added custom jobs during regeneration
|
|
15
|
+
* 6. **Comment Preservation**: Retains user comments when updating workflows
|
|
16
|
+
*
|
|
17
|
+
* ## Intelligent Merging
|
|
18
|
+
*
|
|
19
|
+
* The generator distinguishes between:
|
|
20
|
+
* - **Pipecraft-owned jobs**: `changes`, `version`, `tag`, `promote`, `release`, `test-*`, `deploy-*`
|
|
21
|
+
* - **User jobs**: Any jobs not owned by Pipecraft
|
|
22
|
+
*
|
|
23
|
+
* During regeneration:
|
|
24
|
+
* - Pipecraft jobs are completely replaced with template versions
|
|
25
|
+
* - User jobs are preserved exactly as-is
|
|
26
|
+
* - User comments are maintained
|
|
27
|
+
* - Job order is intelligently managed (Pipecraft jobs first, then user jobs)
|
|
28
|
+
*
|
|
29
|
+
* ## Architecture
|
|
30
|
+
*
|
|
31
|
+
* Uses AST-based path operations for surgical YAML manipulation:
|
|
32
|
+
* - Parse existing workflow into AST
|
|
33
|
+
* - Apply precise path-based operations
|
|
34
|
+
* - Preserve formatting and comments
|
|
35
|
+
* - Rebuild YAML maintaining structure
|
|
36
|
+
*
|
|
37
|
+
* @module templates/workflows/pipeline-path-based.yml.tpl
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { generate } from './templates/workflows/pipeline-path-based.yml.tpl.js'
|
|
42
|
+
*
|
|
43
|
+
* // Initial generation
|
|
44
|
+
* await generate({
|
|
45
|
+
* cwd: '/path/to/project',
|
|
46
|
+
* branchFlow: ['develop', 'staging', 'main'],
|
|
47
|
+
* domains: {
|
|
48
|
+
* api: { paths: ['src/api/**'], test: true },
|
|
49
|
+
* web: { paths: ['src/web/**'], test: true }
|
|
50
|
+
* }
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* // Incremental update (preserves user jobs)
|
|
54
|
+
* await generate({
|
|
55
|
+
* cwd: '/path/to/project',
|
|
56
|
+
* existingPipeline: parsedYAML,
|
|
57
|
+
* existingPipelineContent: rawYAMLString,
|
|
58
|
+
* branchFlow: ['develop', 'staging', 'main'],
|
|
59
|
+
* domains: { ... }
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @see {@link module:utils/ast-path-operations} for YAML manipulation details
|
|
64
|
+
*/
|
|
1
65
|
import { toFile, renderTemplate } from '@featherscloud/pinion';
|
|
2
66
|
import { parseDocument, stringify, Scalar } from 'yaml';
|
|
3
67
|
import fs from 'fs';
|
|
4
68
|
import { applyPathOperations, createValueFromString } from '../../utils/ast-path-operations.js';
|
|
5
69
|
import dedent from 'dedent';
|
|
6
|
-
|
|
7
|
-
* Path-based pipeline generator
|
|
8
|
-
*
|
|
9
|
-
* Uses precise AST path operations to:
|
|
10
|
-
* - Ensure required paths exist
|
|
11
|
-
* - Set/merge/overwrite specific values
|
|
12
|
-
* - Preserve user customizations while ensuring template requirements
|
|
13
|
-
*/
|
|
70
|
+
import { logger } from '../../utils/logger.js';
|
|
14
71
|
/**
|
|
15
72
|
* Get minimal base template - just enough structure to be parsed
|
|
16
73
|
* All actual content is defined via operations list
|
|
@@ -30,7 +87,7 @@ const getPipecraftOwnedJobs = (branchFlow, domains = {}) => {
|
|
|
30
87
|
'changes',
|
|
31
88
|
'version',
|
|
32
89
|
'tag',
|
|
33
|
-
'promote', //
|
|
90
|
+
'promote', // Promotion job - triggers workflow on next branch
|
|
34
91
|
'release' // GitHub release creation on final branch
|
|
35
92
|
]);
|
|
36
93
|
// Add domain-based jobs (test-*, deploy-*, remote-test-*) based on flags
|
|
@@ -56,51 +113,95 @@ const isPipecraftJob = (jobName, branchFlow) => {
|
|
|
56
113
|
*/
|
|
57
114
|
export const createPathBasedPipeline = (ctx) => {
|
|
58
115
|
const branchFlow = ctx.branchFlow || ['develop', 'staging', 'main'];
|
|
59
|
-
|
|
60
|
-
|
|
116
|
+
logger.debug('🔍 Branch flow from context:', branchFlow);
|
|
117
|
+
logger.debug('🔍 Context keys:', Object.keys(ctx));
|
|
61
118
|
// Use existing pipeline from context or start with base template
|
|
62
119
|
let doc;
|
|
63
120
|
let hasExistingPipeline = false;
|
|
64
121
|
if (ctx.existingPipelineContent) {
|
|
65
|
-
// Parse the original YAML content
|
|
66
|
-
// This prevents old comments from being preserved when we rebuild
|
|
122
|
+
// Parse the original YAML content to preserve structure and comments
|
|
67
123
|
doc = parseDocument(ctx.existingPipelineContent);
|
|
124
|
+
// Note: We save document-level comments later and selectively restore them
|
|
125
|
+
// Don't clear doc.commentBefore here - we'll handle it after saving
|
|
68
126
|
hasExistingPipeline = true;
|
|
69
|
-
|
|
127
|
+
logger.verbose('🔄 Merging with existing pipeline');
|
|
70
128
|
}
|
|
71
129
|
else if (ctx.existingPipeline) {
|
|
72
130
|
// Convert existing pipeline object to YAML string first
|
|
73
131
|
const existingYaml = stringify(ctx.existingPipeline);
|
|
74
132
|
doc = parseDocument(existingYaml);
|
|
75
133
|
hasExistingPipeline = true;
|
|
76
|
-
|
|
134
|
+
logger.verbose('🔄 Merging with existing pipeline');
|
|
77
135
|
}
|
|
78
136
|
else {
|
|
79
137
|
doc = parseDocument(getBaseTemplate(ctx));
|
|
80
|
-
|
|
138
|
+
logger.verbose('📝 Creating new pipeline');
|
|
81
139
|
}
|
|
82
140
|
if (!doc.contents) {
|
|
83
141
|
throw new Error('Failed to parse pipeline document');
|
|
84
142
|
}
|
|
85
143
|
// Apply path-based operations
|
|
86
144
|
const operations = [
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// WORKFLOW HEADER COMMENT
|
|
147
|
+
// Note: This comment should be preserved if user has custom comments at
|
|
148
|
+
// document level, only job-level managed headers should replace user comments
|
|
149
|
+
// =============================================================================
|
|
150
|
+
{
|
|
151
|
+
path: 'name',
|
|
152
|
+
operation: 'preserve',
|
|
153
|
+
value: (() => {
|
|
154
|
+
const nameScalar = new Scalar('Pipeline');
|
|
155
|
+
nameScalar.type = Scalar.QUOTE_DOUBLE;
|
|
156
|
+
return nameScalar;
|
|
157
|
+
})(),
|
|
158
|
+
required: true
|
|
159
|
+
// Note: No commentBefore here - we'll add it only if no user comment exists
|
|
160
|
+
},
|
|
87
161
|
// =============================================================================
|
|
88
162
|
// WORKFLOW METADATA - Name and run identification
|
|
89
163
|
// =============================================================================
|
|
90
164
|
{
|
|
91
165
|
path: 'run-name',
|
|
92
|
-
operation: '
|
|
93
|
-
value:
|
|
94
|
-
|
|
166
|
+
operation: 'preserve',
|
|
167
|
+
value: (() => {
|
|
168
|
+
const branchList = branchFlow.join(',');
|
|
169
|
+
const runNameScalar = new Scalar(`\${{ github.event_name == 'pull_request' && !contains('${branchList}', github.head_ref) && github.event.pull_request.title || github.ref_name }} #\${{ inputs.run_number || github.run_number }}\${{ inputs.version && format(' - {0}', inputs.version) || '' }}`);
|
|
170
|
+
runNameScalar.type = Scalar.QUOTE_DOUBLE;
|
|
171
|
+
return runNameScalar;
|
|
172
|
+
})(),
|
|
173
|
+
required: true,
|
|
174
|
+
spaceBefore: true
|
|
95
175
|
},
|
|
96
176
|
// =============================================================================
|
|
97
177
|
// WORKFLOW TRIGGERS - Define when the pipeline runs
|
|
98
178
|
// =============================================================================
|
|
99
|
-
// The pipeline
|
|
100
|
-
// 1.
|
|
101
|
-
//
|
|
179
|
+
// The pipeline runs on:
|
|
180
|
+
// 1. pull_request (opened/synchronize/reopened) targeting initial branch only
|
|
181
|
+
// - Excludes 'closed' type to avoid duplicate runs when PR is merged
|
|
182
|
+
// - Only targets initial branch (e.g., develop) to avoid duplicates
|
|
183
|
+
// - Automated PRs (develop→staging, staging→main) don't trigger (wrong target)
|
|
184
|
+
// - Only runs changes detection + tests (no versioning/tagging/promotion)
|
|
185
|
+
// 2. push to branch flow branches - Runs full pipeline after PR merge
|
|
186
|
+
// - Includes versioning, tagging, PR creation to next branch, and promotion
|
|
187
|
+
// 3. workflow_dispatch - Manual trigger with full pipeline
|
|
188
|
+
// 4. workflow_call - Can be called from other workflows
|
|
102
189
|
//
|
|
103
|
-
//
|
|
190
|
+
// Flow example:
|
|
191
|
+
// feature/xyz → PR to develop → Tests run (targets develop ✓)
|
|
192
|
+
// PR merged → Push to develop → Full pipeline (version + tag + createpr)
|
|
193
|
+
// Pipecraft creates PR: develop → staging (targets staging, skipped ✓)
|
|
194
|
+
// Auto-merge → Push to staging → Full pipeline continues
|
|
195
|
+
// Pipecraft creates PR: staging → main (targets main, skipped ✓)
|
|
196
|
+
// Auto-merge → Push to main → Full pipeline completes
|
|
197
|
+
// Ensure 'on' key exists with proper spacing (nested operations below will populate it)
|
|
198
|
+
{
|
|
199
|
+
path: 'on',
|
|
200
|
+
operation: 'set',
|
|
201
|
+
value: {},
|
|
202
|
+
required: true,
|
|
203
|
+
spaceBefore: true
|
|
204
|
+
},
|
|
104
205
|
{
|
|
105
206
|
path: 'on.workflow_dispatch.inputs.version',
|
|
106
207
|
operation: 'set',
|
|
@@ -131,18 +232,88 @@ export const createPathBasedPipeline = (ctx) => {
|
|
|
131
232
|
},
|
|
132
233
|
required: true
|
|
133
234
|
},
|
|
235
|
+
{
|
|
236
|
+
path: 'on.workflow_dispatch.inputs.commitSha',
|
|
237
|
+
operation: 'set',
|
|
238
|
+
value: {
|
|
239
|
+
description: 'The exact commit SHA to checkout and test',
|
|
240
|
+
required: false,
|
|
241
|
+
type: 'string'
|
|
242
|
+
},
|
|
243
|
+
required: true
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
path: 'on.workflow_call.inputs.version',
|
|
247
|
+
operation: 'set',
|
|
248
|
+
value: {
|
|
249
|
+
description: 'The version to deploy',
|
|
250
|
+
required: false,
|
|
251
|
+
type: 'string'
|
|
252
|
+
},
|
|
253
|
+
required: true
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
path: 'on.workflow_call.inputs.baseRef',
|
|
257
|
+
operation: 'set',
|
|
258
|
+
value: {
|
|
259
|
+
description: 'The base reference for comparison',
|
|
260
|
+
required: false,
|
|
261
|
+
type: 'string'
|
|
262
|
+
},
|
|
263
|
+
required: true
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
path: 'on.workflow_call.inputs.run_number',
|
|
267
|
+
operation: 'set',
|
|
268
|
+
value: {
|
|
269
|
+
description: 'The original run number from develop branch',
|
|
270
|
+
required: false,
|
|
271
|
+
type: 'string'
|
|
272
|
+
},
|
|
273
|
+
required: true
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
path: 'on.workflow_call.inputs.commitSha',
|
|
277
|
+
operation: 'set',
|
|
278
|
+
value: {
|
|
279
|
+
description: 'The exact commit SHA to checkout and test',
|
|
280
|
+
required: false,
|
|
281
|
+
type: 'string'
|
|
282
|
+
},
|
|
283
|
+
required: true
|
|
284
|
+
},
|
|
134
285
|
{
|
|
135
286
|
path: 'on.push.branches',
|
|
136
287
|
operation: 'set',
|
|
137
288
|
value: branchFlow,
|
|
138
289
|
required: true
|
|
139
290
|
},
|
|
291
|
+
{
|
|
292
|
+
path: 'on.pull_request.types',
|
|
293
|
+
operation: 'set',
|
|
294
|
+
value: ['opened', 'synchronize', 'reopened'],
|
|
295
|
+
required: true
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
path: 'on.pull_request.branches',
|
|
299
|
+
operation: 'set',
|
|
300
|
+
value: [ctx.initialBranch || branchFlow[0]],
|
|
301
|
+
required: true
|
|
302
|
+
},
|
|
140
303
|
// =============================================================================
|
|
141
304
|
// CORE PIPECRAFT JOBS - Template-managed jobs that get updates
|
|
142
305
|
// =============================================================================
|
|
143
306
|
// These are the core Pipecraft jobs that should always use the latest template
|
|
144
307
|
// version. Using 'overwrite' operation ensures users get bug fixes and improvements.
|
|
145
308
|
// These jobs are essential for Pipecraft functionality and should not be customized.
|
|
309
|
+
// Ensure 'jobs' key exists with proper spacing (nested operations below will populate it)
|
|
310
|
+
{
|
|
311
|
+
path: 'jobs',
|
|
312
|
+
operation: 'set',
|
|
313
|
+
value: {},
|
|
314
|
+
required: true,
|
|
315
|
+
spaceBefore: true
|
|
316
|
+
},
|
|
146
317
|
{
|
|
147
318
|
path: 'jobs.changes',
|
|
148
319
|
operation: 'overwrite',
|
|
@@ -150,6 +321,8 @@ export const createPathBasedPipeline = (ctx) => {
|
|
|
150
321
|
runs-on: ubuntu-latest
|
|
151
322
|
steps:
|
|
152
323
|
- uses: actions/checkout@v4
|
|
324
|
+
with:
|
|
325
|
+
ref: \${{ inputs.commitSha || github.sha }}
|
|
153
326
|
- uses: ./.github/actions/detect-changes
|
|
154
327
|
id: detect
|
|
155
328
|
with:
|
|
@@ -159,8 +332,9 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
159
332
|
`, ctx),
|
|
160
333
|
commentBefore: dedent `
|
|
161
334
|
=============================================================================
|
|
162
|
-
CHANGES DETECTION
|
|
335
|
+
CHANGES DETECTION (⚠️ Managed by Pipecraft - do not modify)
|
|
163
336
|
=============================================================================
|
|
337
|
+
This job detects which domains have changed and sets outputs for downstream jobs.
|
|
164
338
|
`,
|
|
165
339
|
required: true
|
|
166
340
|
},
|
|
@@ -190,8 +364,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
190
364
|
|
|
191
365
|
|
|
192
366
|
=============================================================================
|
|
193
|
-
TESTING JOBS
|
|
367
|
+
TESTING JOBS (✅ Customize these with your test logic)
|
|
194
368
|
=============================================================================
|
|
369
|
+
These jobs run tests for each domain when changes are detected.
|
|
370
|
+
Replace the TODO comments with your actual test commands.
|
|
195
371
|
` : undefined,
|
|
196
372
|
required: true
|
|
197
373
|
})),
|
|
@@ -200,19 +376,24 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
200
376
|
operation: 'overwrite',
|
|
201
377
|
commentBefore: dedent `
|
|
202
378
|
=============================================================================
|
|
203
|
-
VERSIONING
|
|
379
|
+
VERSIONING (⚠️ Managed by Pipecraft - do not modify)
|
|
204
380
|
=============================================================================
|
|
381
|
+
Calculates the next version based on conventional commits and semver rules.
|
|
382
|
+
Only runs on push events (skipped on pull requests).
|
|
205
383
|
`,
|
|
206
384
|
value: createValueFromString(`
|
|
207
|
-
if: \${{ always() && (${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result == 'success'`).join(' || ')}) && ${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result != 'failure'`).join(' && ')} }}
|
|
385
|
+
if: \${{ always() && github.event_name != 'pull_request' && (${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result == 'success'`).join(' || ')}) && ${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `needs.test-${domain}.result != 'failure'`).join(' && ')} }}
|
|
208
386
|
needs: [ changes, ${Object.keys(ctx.domains || {}).sort().filter((domain) => ctx.domains[domain].test !== false).map((domain) => `test-${domain}`).join(', ')} ]
|
|
209
387
|
runs-on: ubuntu-latest
|
|
210
388
|
steps:
|
|
211
389
|
- uses: actions/checkout@v4
|
|
390
|
+
with:
|
|
391
|
+
ref: \${{ inputs.commitSha || github.sha }}
|
|
212
392
|
- uses: ./.github/actions/calculate-version
|
|
213
393
|
id: version
|
|
214
394
|
with:
|
|
215
395
|
baseRef: \${{ inputs.baseRef || '${ctx.finalBranch || "main"}' }}
|
|
396
|
+
commitSha: \${{ inputs.commitSha || github.sha }}
|
|
216
397
|
outputs:
|
|
217
398
|
version: \${{ steps.version.outputs.version }}
|
|
218
399
|
`, ctx),
|
|
@@ -225,8 +406,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
225
406
|
operation: 'overwrite',
|
|
226
407
|
commentBefore: index === 0 ? dedent `
|
|
227
408
|
=============================================================================
|
|
228
|
-
DEPLOYMENT JOBS
|
|
409
|
+
DEPLOYMENT JOBS (✅ Customize these with your deploy logic)
|
|
229
410
|
=============================================================================
|
|
411
|
+
These jobs deploy each domain when changes are detected and tests pass.
|
|
412
|
+
Replace the TODO comments with your actual deployment commands.
|
|
230
413
|
` : undefined,
|
|
231
414
|
spaceBefore: index === 0 ? true : undefined,
|
|
232
415
|
value: createValueFromString(`
|
|
@@ -248,8 +431,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
248
431
|
operation: 'overwrite',
|
|
249
432
|
commentBefore: index === 0 ? dedent `
|
|
250
433
|
=============================================================================
|
|
251
|
-
REMOTE TESTING JOBS
|
|
434
|
+
REMOTE TESTING JOBS (✅ Customize these with your remote test logic)
|
|
252
435
|
=============================================================================
|
|
436
|
+
These jobs test deployed services remotely after deployment succeeds.
|
|
437
|
+
Replace the TODO comments with your actual remote testing commands.
|
|
253
438
|
` : undefined,
|
|
254
439
|
spaceBefore: index === 0 ? true : undefined,
|
|
255
440
|
value: createValueFromString(`
|
|
@@ -289,8 +474,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
289
474
|
needs: [ ${needsArray.join(', ')} ]
|
|
290
475
|
if: \${{
|
|
291
476
|
always() &&
|
|
477
|
+
github.event_name != 'pull_request' &&
|
|
292
478
|
github.ref_name == '${ctx.initialBranch || branchFlow[0]}' &&
|
|
293
479
|
needs.version.result == 'success' &&
|
|
480
|
+
needs.version.outputs.version != '' &&
|
|
294
481
|
(
|
|
295
482
|
${noFailures}
|
|
296
483
|
) &&
|
|
@@ -301,6 +488,8 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
301
488
|
runs-on: ubuntu-latest
|
|
302
489
|
steps:
|
|
303
490
|
- uses: actions/checkout@v4
|
|
491
|
+
with:
|
|
492
|
+
ref: \${{ inputs.commitSha || github.sha }}
|
|
304
493
|
- uses: ./.github/actions/create-tag
|
|
305
494
|
with:
|
|
306
495
|
version: \${{ needs.version.outputs.version }}
|
|
@@ -309,8 +498,10 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
309
498
|
spaceBefore: true,
|
|
310
499
|
commentBefore: dedent `
|
|
311
500
|
=============================================================================
|
|
312
|
-
TAG & PROMOTE
|
|
501
|
+
TAG & PROMOTE (⚠️ Managed by Pipecraft - do not modify)
|
|
313
502
|
=============================================================================
|
|
503
|
+
Creates a git tag with the calculated version on the initial branch.
|
|
504
|
+
Only runs on push events after successful tests and deployments.
|
|
314
505
|
`,
|
|
315
506
|
required: true
|
|
316
507
|
},
|
|
@@ -320,12 +511,14 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
320
511
|
operation: 'overwrite',
|
|
321
512
|
value: createValueFromString(`
|
|
322
513
|
# Only runs on push or manual workflow_dispatch events to branches that can promote
|
|
323
|
-
#
|
|
514
|
+
# Requires version to succeed (which means tests passed)
|
|
515
|
+
# Requires a version to have been calculated (skip promotion for non-versioned commits)
|
|
324
516
|
# Needs all deploy and/or remote test jobs to succeed
|
|
325
517
|
if: \${{
|
|
326
518
|
always() &&
|
|
327
519
|
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
|
|
328
|
-
|
|
520
|
+
needs.version.result == 'success' &&
|
|
521
|
+
needs.version.outputs.version != '' &&
|
|
329
522
|
(needs.tag.result == 'success' || needs.tag.result == 'skipped') &&
|
|
330
523
|
(
|
|
331
524
|
${branchFlow.slice(0, -1).map((branch) => `github.ref_name == '${branch}'`).join(' || \n ')}
|
|
@@ -335,6 +528,8 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
335
528
|
runs-on: ubuntu-latest
|
|
336
529
|
steps:
|
|
337
530
|
- uses: actions/checkout@v4
|
|
531
|
+
with:
|
|
532
|
+
ref: \${{ inputs.commitSha || github.sha }}
|
|
338
533
|
- uses: ./.github/actions/promote-branch
|
|
339
534
|
with:
|
|
340
535
|
sourceBranch: \${{ github.ref_name }}
|
|
@@ -343,11 +538,11 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
343
538
|
token: \${{ secrets.GITHUB_TOKEN }}
|
|
344
539
|
`, ctx),
|
|
345
540
|
spaceBefore: true,
|
|
346
|
-
commentBefore:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
541
|
+
commentBefore: `=============================================================================
|
|
542
|
+
PROMOTION JOB (⚠️ Managed by Pipecraft - do not modify)
|
|
543
|
+
=============================================================================
|
|
544
|
+
Triggers the next branch's workflow after successful versioning and tagging.
|
|
545
|
+
Passes version and run_number to maintain traceability across branches.`,
|
|
351
546
|
required: true
|
|
352
547
|
},
|
|
353
548
|
// Generate release job for final branch (main)
|
|
@@ -361,23 +556,26 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
361
556
|
github.ref_name == '${ctx.finalBranch || branchFlow[branchFlow.length - 1]}' &&
|
|
362
557
|
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
|
|
363
558
|
needs.version.result == 'success' &&
|
|
559
|
+
needs.version.outputs.version != '' &&
|
|
364
560
|
(needs.tag.result == 'success' || needs.tag.result == 'skipped')
|
|
365
561
|
}}
|
|
366
562
|
needs: [ version, tag ]
|
|
367
563
|
runs-on: ubuntu-latest
|
|
368
564
|
steps:
|
|
369
565
|
- uses: actions/checkout@v4
|
|
566
|
+
with:
|
|
567
|
+
ref: \${{ inputs.commitSha || github.sha }}
|
|
370
568
|
- uses: ./.github/actions/create-release
|
|
371
569
|
with:
|
|
372
570
|
version: \${{ needs.version.outputs.version }}
|
|
373
571
|
token: \${{ secrets.GITHUB_TOKEN }}
|
|
374
572
|
`, ctx),
|
|
375
573
|
spaceBefore: true,
|
|
376
|
-
commentBefore:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
574
|
+
commentBefore: `=============================================================================
|
|
575
|
+
RELEASE JOB (⚠️ Managed by Pipecraft - do not modify)
|
|
576
|
+
=============================================================================
|
|
577
|
+
Creates a GitHub release on the final branch with release notes.
|
|
578
|
+
Only runs after successful versioning and tagging on the final branch.`,
|
|
381
579
|
required: true
|
|
382
580
|
},
|
|
383
581
|
];
|
|
@@ -401,44 +599,178 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
401
599
|
const jobsNode = doc.contents.get('jobs');
|
|
402
600
|
if (jobsNode && jobsNode.items) {
|
|
403
601
|
originalJobOrder = getJobKeysInOrder(jobsNode);
|
|
404
|
-
|
|
602
|
+
logger.debug('📋 Original job order:', originalJobOrder);
|
|
405
603
|
}
|
|
406
604
|
}
|
|
605
|
+
// Deprecated jobs that should be removed (old promotion strategy)
|
|
606
|
+
const DEPRECATED_JOBS = new Set(['createpr', 'branch']);
|
|
407
607
|
// Collect user jobs (non-Pipecraft jobs) to preserve them
|
|
608
|
+
// Save the entire item (key, value, and comments) not just the value
|
|
408
609
|
const userJobs = new Map();
|
|
409
610
|
if (doc.contents.get('jobs')) {
|
|
410
611
|
const jobsNode = doc.contents.get('jobs');
|
|
411
612
|
if (jobsNode && jobsNode.items) {
|
|
412
613
|
for (const item of jobsNode.items) {
|
|
413
614
|
const jobName = item.key?.toString() || item.key?.value;
|
|
414
|
-
|
|
415
|
-
|
|
615
|
+
// Skip deprecated jobs - don't preserve them
|
|
616
|
+
if (jobName && !PIPECRAFT_OWNED_JOBS.has(jobName) && !DEPRECATED_JOBS.has(jobName)) {
|
|
617
|
+
// Save the entire item to preserve comments on keys
|
|
618
|
+
userJobs.set(jobName, item);
|
|
416
619
|
}
|
|
417
620
|
}
|
|
418
621
|
if (userJobs.size > 0) {
|
|
419
|
-
|
|
622
|
+
logger.verbose(`📋 Preserving ${userJobs.size} user jobs: ${Array.from(userJobs.keys()).join(', ')}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// To ensure proper key order (name, run-name, on, jobs), we need to:
|
|
627
|
+
// 1. Extract all values we care about
|
|
628
|
+
// 2. Clear the document
|
|
629
|
+
// 3. Re-add them in the correct order
|
|
630
|
+
// Save existing values and comments that should be preserved
|
|
631
|
+
const existingName = doc.contents.get('name');
|
|
632
|
+
const existingRunName = doc.contents.get('run-name');
|
|
633
|
+
const existingOn = doc.contents.get('on');
|
|
634
|
+
const existingJobs = doc.contents.get('jobs');
|
|
635
|
+
// Save jobs node comments (before we rebuild)
|
|
636
|
+
const jobsNodeCommentBefore = existingJobs ? existingJobs.commentBefore : undefined;
|
|
637
|
+
const jobsNodeComment = existingJobs ? existingJobs.comment : undefined;
|
|
638
|
+
// Save document-level comments (user comments at the top)
|
|
639
|
+
// Note: YAML stores document-level comments on doc.commentBefore, not doc.contents.commentBefore
|
|
640
|
+
const docCommentBefore = doc.commentBefore;
|
|
641
|
+
const docComment = doc.comment;
|
|
642
|
+
// Save user comments on keys (but not Pipecraft-managed comments)
|
|
643
|
+
const savedComments = new Map();
|
|
644
|
+
const isPipecraftComment = (comment) => {
|
|
645
|
+
if (!comment)
|
|
646
|
+
return false;
|
|
647
|
+
const lowerComment = comment.toLowerCase();
|
|
648
|
+
return lowerComment.includes('pipecraft') ||
|
|
649
|
+
lowerComment.includes('managed by pipecraft') ||
|
|
650
|
+
lowerComment.includes('do not modify');
|
|
651
|
+
};
|
|
652
|
+
for (const item of doc.contents.items) {
|
|
653
|
+
const keyName = typeof item.key === 'string' ? item.key : item.key?.value;
|
|
654
|
+
if (keyName) {
|
|
655
|
+
const keyCommentBefore = item.key?.commentBefore;
|
|
656
|
+
const keyComment = item.key?.comment;
|
|
657
|
+
// Only save non-Pipecraft comments
|
|
658
|
+
if ((keyCommentBefore && !isPipecraftComment(keyCommentBefore)) ||
|
|
659
|
+
(keyComment && !isPipecraftComment(keyComment))) {
|
|
660
|
+
savedComments.set(keyName, {
|
|
661
|
+
commentBefore: keyCommentBefore && !isPipecraftComment(keyCommentBefore) ? keyCommentBefore : undefined,
|
|
662
|
+
comment: keyComment && !isPipecraftComment(keyComment) ? keyComment : undefined
|
|
663
|
+
});
|
|
420
664
|
}
|
|
421
665
|
}
|
|
422
666
|
}
|
|
423
|
-
// Clear the
|
|
667
|
+
// Clear the document to rebuild with correct key order
|
|
668
|
+
doc.contents.items = [];
|
|
669
|
+
// Apply root-level operations to set name, run-name, on
|
|
670
|
+
const rootOperations = operations.filter(op => !op.path.startsWith('jobs.'));
|
|
671
|
+
applyPathOperations(doc.contents, rootOperations, doc);
|
|
672
|
+
// Restore document-level comments (user comments at the top of file)
|
|
673
|
+
// Only restore if they're not Pipecraft comments
|
|
674
|
+
const hasUserDocComment = docCommentBefore && !isPipecraftComment(docCommentBefore);
|
|
675
|
+
if (hasUserDocComment) {
|
|
676
|
+
;
|
|
677
|
+
doc.commentBefore = docCommentBefore;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
// No user comment at document level, add Pipecraft workflow header
|
|
681
|
+
const pipecraftHeader = `=============================================================================
|
|
682
|
+
PIPECRAFT MANAGED WORKFLOW
|
|
683
|
+
=============================================================================
|
|
684
|
+
|
|
685
|
+
✅ YOU CAN CUSTOMIZE:
|
|
686
|
+
- test-*** jobs for each domain
|
|
687
|
+
- deploy-*** jobs for each domain
|
|
688
|
+
- remote-test-*** jobs for each domain
|
|
689
|
+
- Workflow name
|
|
690
|
+
|
|
691
|
+
⚠️ PIPECRAFT MANAGES (do not modify):
|
|
692
|
+
- Workflow triggers, job dependencies, and conditionals
|
|
693
|
+
- Changes detection, version calculation, and tag creation
|
|
694
|
+
- CreatePR, branch management, promote, and release jobs
|
|
695
|
+
|
|
696
|
+
📌 VERSION PROMOTION BEHAVIOR:
|
|
697
|
+
- Only commits that trigger a version bump will promote to staging/main
|
|
698
|
+
- Non-versioned commits (test, build, etc.) remain on develop
|
|
699
|
+
- This keeps staging/main aligned with tagged releases
|
|
700
|
+
|
|
701
|
+
Running 'pipecraft generate' updates managed sections while preserving
|
|
702
|
+
your customizations in test/deploy/remote-test jobs.
|
|
703
|
+
|
|
704
|
+
📖 Learn more: https://docs.pipecraft.dev
|
|
705
|
+
=============================================================================`;
|
|
706
|
+
doc.commentBefore = pipecraftHeader;
|
|
707
|
+
}
|
|
708
|
+
if (docComment && !isPipecraftComment(docComment)) {
|
|
709
|
+
;
|
|
710
|
+
doc.comment = docComment;
|
|
711
|
+
}
|
|
712
|
+
// Replace values for preserve operations that had existing values
|
|
713
|
+
// This maintains the order and comments from operations, but uses the preserved values
|
|
714
|
+
// Only do this if there was an actual existing pipeline (not from base template)
|
|
715
|
+
if (hasExistingPipeline) {
|
|
716
|
+
if (existingName !== undefined && existingName !== null) {
|
|
717
|
+
// Find the 'name' key and replace its value
|
|
718
|
+
const nameIndex = doc.contents.items.findIndex((item) => {
|
|
719
|
+
const key = item.key;
|
|
720
|
+
if (typeof key === 'string')
|
|
721
|
+
return key === 'name';
|
|
722
|
+
if (key && typeof key.value === 'string')
|
|
723
|
+
return key.value === 'name';
|
|
724
|
+
return false;
|
|
725
|
+
});
|
|
726
|
+
if (nameIndex >= 0) {
|
|
727
|
+
doc.contents.items[nameIndex].value = existingName;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (existingRunName !== undefined && existingRunName !== null) {
|
|
731
|
+
// Find the 'run-name' key and replace its value
|
|
732
|
+
const runNameIndex = doc.contents.items.findIndex((item) => {
|
|
733
|
+
const key = item.key;
|
|
734
|
+
if (typeof key === 'string')
|
|
735
|
+
return key === 'run-name';
|
|
736
|
+
if (key && typeof key.value === 'string')
|
|
737
|
+
return key.value === 'run-name';
|
|
738
|
+
return false;
|
|
739
|
+
});
|
|
740
|
+
if (runNameIndex >= 0) {
|
|
741
|
+
doc.contents.items[runNameIndex].value = existingRunName;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// Restore user comments on keys (but don't overwrite Pipecraft comments)
|
|
746
|
+
for (const item of doc.contents.items) {
|
|
747
|
+
const keyName = typeof item.key === 'string' ? item.key : item.key?.value;
|
|
748
|
+
if (keyName && savedComments.has(keyName)) {
|
|
749
|
+
const saved = savedComments.get(keyName);
|
|
750
|
+
const currentKey = item.key;
|
|
751
|
+
// Only restore user comments if current comment is Pipecraft-managed or missing
|
|
752
|
+
const currentCommentBefore = currentKey?.commentBefore;
|
|
753
|
+
const currentComment = currentKey?.comment;
|
|
754
|
+
if (saved.commentBefore && (!currentCommentBefore || !isPipecraftComment(currentCommentBefore))) {
|
|
755
|
+
;
|
|
756
|
+
currentKey.commentBefore = saved.commentBefore;
|
|
757
|
+
}
|
|
758
|
+
if (saved.comment && (!currentComment || !isPipecraftComment(currentComment))) {
|
|
759
|
+
;
|
|
760
|
+
currentKey.comment = saved.comment;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Note: We don't restore 'on' or 'jobs' here because the operations already created them
|
|
765
|
+
// with proper spacing. Restoring them would overwrite the Scalar keys and lose spacing.
|
|
766
|
+
// Clear the jobs section to rebuild in correct order
|
|
424
767
|
const jobsNode = doc.contents.get('jobs');
|
|
425
768
|
if (jobsNode && jobsNode.items) {
|
|
426
769
|
jobsNode.items = [];
|
|
427
|
-
// Clear any orphaned comments that were attached to the jobs node
|
|
428
|
-
// When we parse YAML with comments and clear items, comments can become orphaned on the parent
|
|
429
|
-
delete jobsNode.commentBefore;
|
|
430
|
-
delete jobsNode.comment;
|
|
431
|
-
}
|
|
432
|
-
// Apply all operations in order - this creates/overwrites Pipecraft jobs
|
|
433
|
-
// The 'overwrite' operation handles both create and update cases automatically
|
|
434
|
-
applyPathOperations(doc.contents, operations, doc);
|
|
435
|
-
// Remove old trigger types that are no longer used (workflow_call, pull_request)
|
|
436
|
-
// We only want push and workflow_dispatch triggers
|
|
437
|
-
const onNode = doc.contents.get('on');
|
|
438
|
-
if (onNode && onNode.delete) {
|
|
439
|
-
onNode.delete('workflow_call');
|
|
440
|
-
onNode.delete('pull_request');
|
|
441
770
|
}
|
|
771
|
+
// Apply job operations
|
|
772
|
+
const jobOperations = operations.filter(op => op.path.startsWith('jobs.'));
|
|
773
|
+
applyPathOperations(doc.contents, jobOperations, doc);
|
|
442
774
|
// Now we need to reorder jobs to match the original order
|
|
443
775
|
// Collect all current jobs (Pipecraft jobs that were just created)
|
|
444
776
|
const currentJobs = new Map();
|
|
@@ -458,8 +790,14 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
458
790
|
// For each job in the original order:
|
|
459
791
|
// - If it's a Pipecraft job, use the newly created version from currentJobs
|
|
460
792
|
// - If it's a user job, use the preserved version from userJobs
|
|
793
|
+
// - Skip deprecated jobs
|
|
461
794
|
// IMPORTANT: Ensure all keys are Scalars (not strings) so we can add comments later
|
|
462
795
|
for (const jobName of originalJobOrder) {
|
|
796
|
+
// Skip deprecated jobs
|
|
797
|
+
if (DEPRECATED_JOBS.has(jobName)) {
|
|
798
|
+
logger.verbose(`🗑️ Removing deprecated job: ${jobName}`);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
463
801
|
if (PIPECRAFT_OWNED_JOBS.has(jobName)) {
|
|
464
802
|
// It's a Pipecraft job - use the newly created version
|
|
465
803
|
const item = currentJobs.get(jobName);
|
|
@@ -472,10 +810,14 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
472
810
|
}
|
|
473
811
|
}
|
|
474
812
|
else {
|
|
475
|
-
// It's a user job - re-insert
|
|
476
|
-
const
|
|
477
|
-
if (
|
|
478
|
-
|
|
813
|
+
// It's a user job - re-insert the entire item (with comments)
|
|
814
|
+
const jobItem = userJobs.get(jobName);
|
|
815
|
+
if (jobItem && jobsNode) {
|
|
816
|
+
// Ensure the key is a Scalar, not a string
|
|
817
|
+
if (typeof jobItem.key === 'string') {
|
|
818
|
+
jobItem.key = new Scalar(jobItem.key);
|
|
819
|
+
}
|
|
820
|
+
jobsNode.items.push(jobItem);
|
|
479
821
|
}
|
|
480
822
|
}
|
|
481
823
|
}
|
|
@@ -489,20 +831,34 @@ ${Object.keys(ctx.domains || {}).sort().map((domain) => ` ${domain}: \$
|
|
|
489
831
|
jobsNode.items.push(item);
|
|
490
832
|
}
|
|
491
833
|
}
|
|
492
|
-
//
|
|
834
|
+
// Get the final jobs node to restore comments
|
|
493
835
|
const finalJobsNode = doc.contents.get('jobs');
|
|
836
|
+
// Restore user comments on the jobs node (if they weren't Pipecraft comments)
|
|
837
|
+
if (jobsNodeCommentBefore && !isPipecraftComment(jobsNodeCommentBefore) && finalJobsNode) {
|
|
838
|
+
;
|
|
839
|
+
finalJobsNode.commentBefore = jobsNodeCommentBefore;
|
|
840
|
+
logger.debug('✅ Restored comment to jobs node');
|
|
841
|
+
}
|
|
842
|
+
if (jobsNodeComment && !isPipecraftComment(jobsNodeComment) && finalJobsNode) {
|
|
843
|
+
;
|
|
844
|
+
finalJobsNode.comment = jobsNodeComment;
|
|
845
|
+
}
|
|
846
|
+
// Log final job order (after operations are applied)
|
|
494
847
|
if (finalJobsNode && finalJobsNode.items && finalJobsNode.items.length > 0) {
|
|
495
848
|
const jobNames = getJobKeysInOrder(finalJobsNode);
|
|
496
849
|
if (jobNames.length > 0) {
|
|
497
|
-
|
|
850
|
+
logger.debug('📋 Final job order:', jobNames);
|
|
498
851
|
}
|
|
499
|
-
// Remove duplicate comment headers
|
|
500
|
-
// When we reuse existing job values, they may carry old comments from parsing
|
|
501
|
-
// We want to
|
|
852
|
+
// Remove duplicate Pipecraft comment headers from values
|
|
853
|
+
// When we reuse existing job values, they may carry old Pipecraft comments from parsing
|
|
854
|
+
// We want to clear Pipecraft comments but preserve user comments
|
|
502
855
|
for (const item of finalJobsNode.items) {
|
|
503
856
|
if (item.value && item.value.commentBefore) {
|
|
504
|
-
|
|
505
|
-
|
|
857
|
+
const valueComment = item.value.commentBefore;
|
|
858
|
+
// Only clear if it's a Pipecraft-managed comment
|
|
859
|
+
if (isPipecraftComment(valueComment)) {
|
|
860
|
+
delete item.value.commentBefore;
|
|
861
|
+
}
|
|
506
862
|
}
|
|
507
863
|
}
|
|
508
864
|
// Add section headers and spacing to domain-based jobs
|
|
@@ -630,7 +986,56 @@ const loadExistingPipeline = (filePath) => {
|
|
|
630
986
|
return fs.readFileSync(filePath, 'utf8');
|
|
631
987
|
};
|
|
632
988
|
/**
|
|
633
|
-
* Main
|
|
989
|
+
* Main pipeline generator entry point.
|
|
990
|
+
*
|
|
991
|
+
* Generates the complete GitHub Actions pipeline workflow with intelligent
|
|
992
|
+
* merging of existing user customizations.
|
|
993
|
+
*
|
|
994
|
+
* @param {PinionContext & { existingPipeline?: any, outputPipelinePath?: string }} ctx - Generator context
|
|
995
|
+
* @param {any} [ctx.existingPipeline] - Parsed existing pipeline YAML for merging
|
|
996
|
+
* @param {string} [ctx.existingPipelineContent] - Raw existing pipeline content for comment preservation
|
|
997
|
+
* @param {string} [ctx.outputPipelinePath] - Custom output path (default: .github/workflows/pipeline.yml)
|
|
998
|
+
* @param {string[]} ctx.branchFlow - Branch flow sequence (e.g., ['develop', 'staging', 'main'])
|
|
999
|
+
* @param {Record<string, DomainConfig>} ctx.domains - Domain configurations for change detection
|
|
1000
|
+
* @param {string} [ctx.ciProvider] - CI provider ('github' or 'gitlab')
|
|
1001
|
+
* @param {string} [ctx.mergeStrategy] - Merge strategy ('fast-forward' or 'merge')
|
|
1002
|
+
* @returns {Promise<PinionContext>} Updated context with generated YAML
|
|
1003
|
+
*
|
|
1004
|
+
* @throws {Error} If pipeline file cannot be written
|
|
1005
|
+
* @throws {Error} If existing pipeline cannot be parsed
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* ```typescript
|
|
1009
|
+
* // Generate new pipeline
|
|
1010
|
+
* await generate({
|
|
1011
|
+
* cwd: '/path/to/project',
|
|
1012
|
+
* branchFlow: ['develop', 'main'],
|
|
1013
|
+
* domains: {
|
|
1014
|
+
* api: { paths: ['src/api/**'], test: true }
|
|
1015
|
+
* }
|
|
1016
|
+
* })
|
|
1017
|
+
*
|
|
1018
|
+
* // Update existing pipeline (preserves user jobs)
|
|
1019
|
+
* const existing = parseDocument(readFileSync('pipeline.yml', 'utf8'))
|
|
1020
|
+
* await generate({
|
|
1021
|
+
* cwd: '/path/to/project',
|
|
1022
|
+
* existingPipeline: existing,
|
|
1023
|
+
* existingPipelineContent: readFileSync('pipeline.yml', 'utf8'),
|
|
1024
|
+
* branchFlow: ['develop', 'staging', 'main'],
|
|
1025
|
+
* domains: { ... }
|
|
1026
|
+
* })
|
|
1027
|
+
* ```
|
|
1028
|
+
*
|
|
1029
|
+
* @note The generator performs these steps:
|
|
1030
|
+
* 1. Calls `createPathBasedPipeline()` to build the workflow
|
|
1031
|
+
* 2. Logs merge status (new vs. merged)
|
|
1032
|
+
* 3. Writes the final YAML to the output path
|
|
1033
|
+
*
|
|
1034
|
+
* The heavy lifting is done by `createPathBasedPipeline()` which handles:
|
|
1035
|
+
* - Job generation based on domains and branch flow
|
|
1036
|
+
* - User job preservation and merging
|
|
1037
|
+
* - Comment preservation from existing pipeline
|
|
1038
|
+
* - Intelligent job ordering
|
|
634
1039
|
*/
|
|
635
1040
|
export const generate = (ctx) => Promise.resolve(ctx)
|
|
636
1041
|
.then((ctx) => {
|
|
@@ -645,7 +1050,7 @@ export const generate = (ctx) => Promise.resolve(ctx)
|
|
|
645
1050
|
// Provide user feedback about file operation
|
|
646
1051
|
const outputPath = ctx.outputPipelinePath || '.github/workflows/pipeline.yml';
|
|
647
1052
|
const status = ctx.mergeStatus === 'merged' ? '🔄 Merged with existing' : '📝 Created new';
|
|
648
|
-
|
|
1053
|
+
logger.verbose(`${status} ${outputPath}`);
|
|
649
1054
|
return ctx;
|
|
650
1055
|
})
|
|
651
1056
|
.then(renderTemplate((ctx) => ctx.yamlContent, toFile((ctx) => ctx.outputPipelinePath || '.github/workflows/pipeline.yml')));
|