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.
- package/.claude/settings.local.json +35 -0
- package/.github/actions/calculate-version/action.yml +106 -0
- package/.github/actions/create-pr/action.yml +122 -0
- package/.github/actions/create-release/action.yml +74 -0
- package/.github/actions/create-tag/action.yml +94 -0
- package/.github/actions/detect-changes/action.yml +62 -0
- package/.github/actions/manage-branch/action.yml +113 -0
- package/.github/actions/promote-branch/action.yml +352 -0
- package/.github/example/workflows/job.analyze.code.yml +26 -0
- package/.github/example/workflows/job.analyze.docker.yml +32 -0
- package/.github/example/workflows/job.app.api.deploy.yml +127 -0
- package/.github/example/workflows/job.app.api.test.yml +102 -0
- package/.github/example/workflows/job.app.docs.deploy.yml +40 -0
- package/.github/example/workflows/job.app.docs.test.yml +50 -0
- package/.github/example/workflows/job.app.web.deploy.yml +96 -0
- package/.github/example/workflows/job.app.web.test.yml +49 -0
- package/.github/example/workflows/job.changes.yml +82 -0
- package/.github/example/workflows/job.create-pr.yml +96 -0
- package/.github/example/workflows/job.env-check.yml +43 -0
- package/.github/example/workflows/job.fast-forward.yml +103 -0
- package/.github/example/workflows/job.lint.yml +64 -0
- package/.github/example/workflows/job.pr-name.yml +70 -0
- package/.github/example/workflows/job.pr-squash-name.yml +27 -0
- package/.github/example/workflows/job.tag.yml +38 -0
- package/.github/example/workflows/job.version.yml +99 -0
- package/.github/example/workflows/lib.plugin-base.publish.yml +59 -0
- package/.github/example/workflows/pipe.yml +178 -0
- package/.github/example/workflows/scripts/fastforward.sh +51 -0
- package/.github/example/workflows/scripts/pipe-cleaner.sh +70 -0
- package/.github/scripts/fastforward.sh +51 -0
- package/.github/scripts/pipe-cleaner.sh +70 -0
- package/.github/workflows/pipeline.yml +220 -0
- package/.github/workflows/pr-title-check.yml +70 -0
- package/.github/workflows/publish.yml +39 -0
- package/.pipecraftrc.json +78 -0
- package/.release-it.cjs +71 -0
- package/PIPELINE_TESTING_PLAN.md +499 -0
- package/README.md +1101 -0
- package/TRUNK_FLOW_PLAN.md +401 -0
- package/assets/logo_banner.png +0 -0
- package/assets/logo_banner.webp +0 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +364 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/generators/init.tpl.d.ts +3 -0
- package/dist/generators/init.tpl.d.ts.map +1 -0
- package/dist/generators/init.tpl.js +117 -0
- package/dist/generators/init.tpl.js.map +1 -0
- package/dist/generators/workflows.tpl.d.ts +7 -0
- package/dist/generators/workflows.tpl.d.ts.map +1 -0
- package/dist/generators/workflows.tpl.js +73 -0
- package/dist/generators/workflows.tpl.js.map +1 -0
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/calculate-version.yml.tpl.js +122 -0
- package/dist/templates/actions/calculate-version.yml.tpl.js.map +1 -0
- package/dist/templates/actions/create-pr.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/create-pr.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/create-pr.yml.tpl.js +145 -0
- package/dist/templates/actions/create-pr.yml.tpl.js.map +1 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/create-tag.yml.tpl.js +111 -0
- package/dist/templates/actions/create-tag.yml.tpl.js.map +1 -0
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/detect-changes.yml.tpl.js +79 -0
- package/dist/templates/actions/detect-changes.yml.tpl.js.map +1 -0
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/manage-branch.yml.tpl.js +130 -0
- package/dist/templates/actions/manage-branch.yml.tpl.js.map +1 -0
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/promote-branch.yml.tpl.js +362 -0
- package/dist/templates/actions/promote-branch.yml.tpl.js.map +1 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts +16 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts.map +1 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js +652 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js.map +1 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ast-path-operations.d.ts +287 -0
- package/dist/utils/ast-path-operations.d.ts.map +1 -0
- package/dist/utils/ast-path-operations.js +419 -0
- package/dist/utils/ast-path-operations.js.map +1 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +49 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/github-setup.d.ts +74 -0
- package/dist/utils/github-setup.d.ts.map +1 -0
- package/dist/utils/github-setup.js +418 -0
- package/dist/utils/github-setup.js.map +1 -0
- package/dist/utils/idempotency.d.ts +47 -0
- package/dist/utils/idempotency.d.ts.map +1 -0
- package/dist/utils/idempotency.js +176 -0
- package/dist/utils/idempotency.js.map +1 -0
- package/dist/utils/preflight.d.ts +55 -0
- package/dist/utils/preflight.d.ts.map +1 -0
- package/dist/utils/preflight.js +261 -0
- package/dist/utils/preflight.js.map +1 -0
- package/dist/utils/versioning.d.ts +41 -0
- package/dist/utils/versioning.d.ts.map +1 -0
- package/dist/utils/versioning.js +219 -0
- package/dist/utils/versioning.js.map +1 -0
- package/docs/USER_JOURNEY_ERRORS.md +352 -0
- package/eslint.config.js +77 -0
- package/examples/basic-config.json +41 -0
- package/examples/monorepo-config.json +49 -0
- package/examples/usage.md +289 -0
- package/package.json +58 -0
- package/scripts/debug-workflows.sh +339 -0
- package/scripts/parse-pipeline.js +29 -0
- package/scripts/test-job-order.cjs +140 -0
- package/scripts/validate-pipeline.cjs +379 -0
- package/scripts/verify-job-order.sh +30 -0
- package/src/cli/index.ts +414 -0
- package/src/generators/init.tpl.ts +126 -0
- package/src/generators/workflows.tpl.ts +80 -0
- package/src/templates/actions/calculate-version.yml.tpl.ts +124 -0
- package/src/templates/actions/create-pr.yml.tpl.ts +147 -0
- package/src/templates/actions/create-tag.yml.tpl.ts +112 -0
- package/src/templates/actions/detect-changes.yml.tpl.ts +87 -0
- package/src/templates/actions/manage-branch.yml.tpl.ts +132 -0
- package/src/templates/actions/promote-branch.yml.tpl.ts +364 -0
- package/src/templates/workflows/pipeline-path-based.yml.tpl.ts +706 -0
- package/src/types/index.ts +64 -0
- package/src/utils/README-ast-path-operations.md +390 -0
- package/src/utils/ast-path-operations.ts +581 -0
- package/src/utils/config.ts +64 -0
- package/src/utils/github-setup.ts +558 -0
- package/src/utils/idempotency.ts +215 -0
- package/src/utils/preflight.ts +306 -0
- package/src/utils/versioning.ts +244 -0
- package/tests/README.md +229 -0
- package/tests/TEST_STRUCTURE.md +256 -0
- package/tests/act/run-act-tests.sh +345 -0
- package/tests/debugging/debug-utils.ts +538 -0
- package/tests/debugging/debug-workflow.test.ts +339 -0
- package/tests/debugging/debug-workflows.sh +339 -0
- package/tests/debugging/iterative-debug.ts +652 -0
- package/tests/debugging/run-debug-tests.sh +431 -0
- package/tests/fixtures/basic-config.json +51 -0
- package/tests/fixtures/invalid-config.json +9 -0
- package/tests/fixtures/pipeline-generated.yml +235 -0
- package/tests/fixtures/pipeline-preserve-comments.yml +245 -0
- package/tests/fixtures/pipeline-user-modified.yml +245 -0
- package/tests/fixtures/test-config.json +58 -0
- package/tests/github-live/README.md +250 -0
- package/tests/github-local/JOB_WORKFLOW_TESTS.md +296 -0
- package/tests/github-local/README.md +240 -0
- package/tests/github-local/run-all-tests.sh +422 -0
- package/tests/github-local/test-job-workflows.sh +631 -0
- package/tests/github-local/test-pipeline-workflow.sh +440 -0
- package/tests/integration/generators.test.ts +578 -0
- package/tests/integration/path-based-template.test.ts +510 -0
- package/tests/integration/simple-path-based.test.ts +415 -0
- package/tests/setup.ts +56 -0
- package/tests/unit/ast-path-operations-extended.test.ts +302 -0
- package/tests/unit/cli.test.ts +541 -0
- package/tests/unit/config-extended.test.ts +412 -0
- package/tests/unit/config.test.ts +152 -0
- package/tests/unit/github-setup.test.ts +189 -0
- package/tests/unit/idempotency-isolated.test.ts +297 -0
- package/tests/unit/job-order.test.ts +157 -0
- package/tests/unit/pipeline-path-based.test.ts +511 -0
- package/tests/unit/validate-pipeline.test.ts +384 -0
- package/tests/unit/versioning-extended.test.ts +279 -0
- package/tests/unit/versioning.test.ts +241 -0
- package/tsconfig.debug.json +17 -0
- package/tsconfig.json +28 -0
- package/vitest.config.ts +51 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { PinionContext, toFile, renderTemplate } from '@featherscloud/pinion'
|
|
2
|
+
import { parseDocument, stringify, Scalar } from 'yaml'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import {
|
|
5
|
+
applyPathOperations,
|
|
6
|
+
PathOperationConfig,
|
|
7
|
+
createValueFromString
|
|
8
|
+
} from '../../utils/ast-path-operations.js'
|
|
9
|
+
import dedent from 'dedent';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Path-based pipeline generator
|
|
13
|
+
*
|
|
14
|
+
* Uses precise AST path operations to:
|
|
15
|
+
* - Ensure required paths exist
|
|
16
|
+
* - Set/merge/overwrite specific values
|
|
17
|
+
* - Preserve user customizations while ensuring template requirements
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get minimal base template - just enough structure to be parsed
|
|
23
|
+
* All actual content is defined via operations list
|
|
24
|
+
*/
|
|
25
|
+
const getBaseTemplate = (ctx: any) => {
|
|
26
|
+
return dedent`
|
|
27
|
+
name: "Pipeline"
|
|
28
|
+
on:
|
|
29
|
+
jobs:
|
|
30
|
+
`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Define which jobs Pipecraft owns vs user jobs
|
|
35
|
+
*/
|
|
36
|
+
const getPipecraftOwnedJobs = (branchFlow: string[], domains: Record<string, any> = {}): Set<string> => {
|
|
37
|
+
const jobs = new Set([
|
|
38
|
+
'changes',
|
|
39
|
+
'version',
|
|
40
|
+
'tag',
|
|
41
|
+
'promote', // Single promote job instead of multiple promote-to-{target} jobs
|
|
42
|
+
'release' // GitHub release creation on final branch
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
// Add domain-based jobs (test-*, deploy-*, remote-test-*) based on flags
|
|
46
|
+
Object.keys(domains).forEach(domain => {
|
|
47
|
+
const domainConfig = domains[domain]
|
|
48
|
+
if (domainConfig.test !== false) jobs.add(`test-${domain}`)
|
|
49
|
+
if (domainConfig.deploy === true) jobs.add(`deploy-${domain}`)
|
|
50
|
+
if (domainConfig.remoteTest === true) jobs.add(`remote-test-${domain}`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return jobs
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a job is owned by Pipecraft
|
|
58
|
+
*/
|
|
59
|
+
const isPipecraftJob = (jobName: string, branchFlow: string[]): boolean => {
|
|
60
|
+
return getPipecraftOwnedJobs(branchFlow).has(jobName)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create path-based pipeline content
|
|
65
|
+
*/
|
|
66
|
+
export const createPathBasedPipeline = (ctx: any) => {
|
|
67
|
+
const branchFlow = ctx.branchFlow || ['develop', 'staging', 'main']
|
|
68
|
+
console.log('🔍 Branch flow from context:', branchFlow)
|
|
69
|
+
console.log('🔍 Context keys:', Object.keys(ctx))
|
|
70
|
+
|
|
71
|
+
// Use existing pipeline from context or start with base template
|
|
72
|
+
let doc: any
|
|
73
|
+
let hasExistingPipeline = false
|
|
74
|
+
|
|
75
|
+
if (ctx.existingPipelineContent) {
|
|
76
|
+
// Parse the original YAML content WITHOUT source tokens
|
|
77
|
+
// This prevents old comments from being preserved when we rebuild
|
|
78
|
+
doc = parseDocument(ctx.existingPipelineContent)
|
|
79
|
+
hasExistingPipeline = true
|
|
80
|
+
console.log('🔄 Merging with existing pipeline from existingPipelineContent')
|
|
81
|
+
} else if (ctx.existingPipeline) {
|
|
82
|
+
// Convert existing pipeline object to YAML string first
|
|
83
|
+
const existingYaml = stringify(ctx.existingPipeline)
|
|
84
|
+
doc = parseDocument(existingYaml)
|
|
85
|
+
hasExistingPipeline = true
|
|
86
|
+
console.log('🔄 Merging with existing pipeline from existingPipeline object')
|
|
87
|
+
} else {
|
|
88
|
+
doc = parseDocument(getBaseTemplate(ctx))
|
|
89
|
+
console.log('📝 Creating new pipeline')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!doc.contents) {
|
|
93
|
+
throw new Error('Failed to parse pipeline document')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Apply path-based operations
|
|
97
|
+
const operations: PathOperationConfig[] = [
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// WORKFLOW METADATA - Name and run identification
|
|
101
|
+
// =============================================================================
|
|
102
|
+
{
|
|
103
|
+
path: 'run-name',
|
|
104
|
+
operation: 'set',
|
|
105
|
+
value: `\${{ github.ref_name }} #\${{ inputs.run_number || github.run_number }}\${{ inputs.version && format(' - {0}', inputs.version) || '' }}`,
|
|
106
|
+
required: true
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// WORKFLOW TRIGGERS - Define when the pipeline runs
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// The pipeline should only run on:
|
|
113
|
+
// 1. Push to develop/staging/main (from PR merge or promotion)
|
|
114
|
+
// 2. Manual trigger via workflow_dispatch
|
|
115
|
+
//
|
|
116
|
+
// NOT on pull_request events - we only want to run after the PR is merged
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
path: 'on.workflow_dispatch.inputs.version',
|
|
120
|
+
operation: 'set',
|
|
121
|
+
value: {
|
|
122
|
+
description: 'The version to deploy',
|
|
123
|
+
required: false,
|
|
124
|
+
type: 'string'
|
|
125
|
+
},
|
|
126
|
+
required: true
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
path: 'on.workflow_dispatch.inputs.baseRef',
|
|
130
|
+
operation: 'set',
|
|
131
|
+
value: {
|
|
132
|
+
description: 'The base reference for comparison',
|
|
133
|
+
required: false,
|
|
134
|
+
type: 'string'
|
|
135
|
+
},
|
|
136
|
+
required: true
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
path: 'on.workflow_dispatch.inputs.run_number',
|
|
140
|
+
operation: 'set',
|
|
141
|
+
value: {
|
|
142
|
+
description: 'The original run number from develop branch',
|
|
143
|
+
required: false,
|
|
144
|
+
type: 'string'
|
|
145
|
+
},
|
|
146
|
+
required: true
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
path: 'on.push.branches',
|
|
150
|
+
operation: 'set',
|
|
151
|
+
value: branchFlow,
|
|
152
|
+
required: true
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// CORE PIPECRAFT JOBS - Template-managed jobs that get updates
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// These are the core Pipecraft jobs that should always use the latest template
|
|
159
|
+
// version. Using 'overwrite' operation ensures users get bug fixes and improvements.
|
|
160
|
+
// These jobs are essential for Pipecraft functionality and should not be customized.
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
path: 'jobs.changes',
|
|
164
|
+
operation: 'overwrite',
|
|
165
|
+
value: createValueFromString(`
|
|
166
|
+
runs-on: ubuntu-latest
|
|
167
|
+
steps:
|
|
168
|
+
- uses: actions/checkout@v4
|
|
169
|
+
- uses: ./.github/actions/detect-changes
|
|
170
|
+
id: detect
|
|
171
|
+
with:
|
|
172
|
+
baseRef: \${{ inputs.baseRef || '${ctx.finalBranch || "main"}' }}
|
|
173
|
+
outputs:
|
|
174
|
+
${Object.keys(ctx.domains || {}).sort().map((domain: string) => ` ${domain}: \${{ steps.detect.outputs.${domain} }}`).join('\n')}
|
|
175
|
+
`, ctx),
|
|
176
|
+
commentBefore: dedent`
|
|
177
|
+
=============================================================================
|
|
178
|
+
CHANGES DETECTION
|
|
179
|
+
=============================================================================
|
|
180
|
+
`,
|
|
181
|
+
required: true
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// USER-MANAGED SECTIONS - Preserve user customizations
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// These sections are designed for user customizations (testing, deployment).
|
|
188
|
+
// Using 'preserve' operation to keep any existing user jobs while providing
|
|
189
|
+
// template structure and examples for new users.
|
|
190
|
+
|
|
191
|
+
// Generate test jobs for each domain (only if test: true)
|
|
192
|
+
...Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].test !== false).map((domain: string) => ({
|
|
193
|
+
path: `jobs.test-${domain}`,
|
|
194
|
+
operation: 'preserve' as const,
|
|
195
|
+
value: createValueFromString(`
|
|
196
|
+
needs: changes
|
|
197
|
+
if: \${{ needs.changes.outputs.${domain} == 'true' }}
|
|
198
|
+
runs-on: ubuntu-latest
|
|
199
|
+
steps:
|
|
200
|
+
# TODO: Replace with your ${domain} test logic
|
|
201
|
+
- name: Run ${domain} tests
|
|
202
|
+
run: |
|
|
203
|
+
echo "Running tests for ${domain} domain"
|
|
204
|
+
echo "Replace this with your actual test commands"
|
|
205
|
+
# Example: npm test -- --testPathPattern=${domain}
|
|
206
|
+
`, ctx),
|
|
207
|
+
commentBefore: domain === Object.keys(ctx.domains || {}).sort()[0] ? dedent`
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
=============================================================================
|
|
211
|
+
TESTING JOBS
|
|
212
|
+
=============================================================================
|
|
213
|
+
` : undefined,
|
|
214
|
+
required: true
|
|
215
|
+
})),
|
|
216
|
+
|
|
217
|
+
{
|
|
218
|
+
path: 'jobs.version',
|
|
219
|
+
operation: 'overwrite',
|
|
220
|
+
commentBefore: dedent`
|
|
221
|
+
=============================================================================
|
|
222
|
+
VERSIONING
|
|
223
|
+
=============================================================================
|
|
224
|
+
`,
|
|
225
|
+
value: createValueFromString(`
|
|
226
|
+
if: \${{ always() && (${Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].test !== false).map((domain: string) => `needs.test-${domain}.result == 'success'`).join(' || ')}) && ${Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].test !== false).map((domain: string) => `needs.test-${domain}.result != 'failure'`).join(' && ')} }}
|
|
227
|
+
needs: [ changes, ${Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].test !== false).map((domain: string) => `test-${domain}`).join(', ')} ]
|
|
228
|
+
runs-on: ubuntu-latest
|
|
229
|
+
steps:
|
|
230
|
+
- uses: actions/checkout@v4
|
|
231
|
+
- uses: ./.github/actions/calculate-version
|
|
232
|
+
id: version
|
|
233
|
+
with:
|
|
234
|
+
baseRef: \${{ inputs.baseRef || '${ctx.finalBranch || "main"}' }}
|
|
235
|
+
outputs:
|
|
236
|
+
version: \${{ steps.version.outputs.version }}
|
|
237
|
+
`, ctx),
|
|
238
|
+
required: true,
|
|
239
|
+
spaceBefore: true,
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// Generate deployment jobs for each domain (only if deploy: true)
|
|
243
|
+
...Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].deploy === true).map((domain: string, index: number) => ({
|
|
244
|
+
path: `jobs.deploy-${domain}`,
|
|
245
|
+
operation: 'overwrite' as const,
|
|
246
|
+
commentBefore: index === 0 ? dedent`
|
|
247
|
+
=============================================================================
|
|
248
|
+
DEPLOYMENT JOBS
|
|
249
|
+
=============================================================================
|
|
250
|
+
` : undefined,
|
|
251
|
+
spaceBefore: index === 0 ? true : undefined,
|
|
252
|
+
value: createValueFromString(`
|
|
253
|
+
needs: [ version, changes ]
|
|
254
|
+
if: \${{ always() && needs.version.result == 'success' && needs.changes.outputs.${domain} == 'true' }}
|
|
255
|
+
runs-on: ubuntu-latest
|
|
256
|
+
steps:
|
|
257
|
+
- name: Deploy ${domain}
|
|
258
|
+
run: |
|
|
259
|
+
echo "Deploying ${domain}"
|
|
260
|
+
echo "Replace this with your actual deploy commands"
|
|
261
|
+
# Example: npm deploy -- --testPathPattern=${domain}
|
|
262
|
+
`, ctx),
|
|
263
|
+
required: true
|
|
264
|
+
})),
|
|
265
|
+
|
|
266
|
+
// Generate remote testing jobs for each domain (only if remoteTest: true)
|
|
267
|
+
...Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].remoteTest === true).map((domain: string, index: number) => ({
|
|
268
|
+
path: `jobs.remote-test-${domain}`,
|
|
269
|
+
operation: 'overwrite' as const,
|
|
270
|
+
commentBefore: index === 0 ? dedent`
|
|
271
|
+
=============================================================================
|
|
272
|
+
REMOTE TESTING JOBS
|
|
273
|
+
=============================================================================
|
|
274
|
+
` : undefined,
|
|
275
|
+
spaceBefore: index === 0 ? true : undefined,
|
|
276
|
+
value: createValueFromString(`
|
|
277
|
+
needs: [ deploy-${domain}, changes ]
|
|
278
|
+
if: \${{ always() }}
|
|
279
|
+
runs-on: ubuntu-latest
|
|
280
|
+
steps:
|
|
281
|
+
- name: Test ${domain}
|
|
282
|
+
if: \${{ needs.changes.outputs.${domain} == 'true' && needs.deploy-${domain}.result == 'success' }}
|
|
283
|
+
run: |
|
|
284
|
+
echo "Testing ${domain} remotely"
|
|
285
|
+
echo "Replace this with your actual test commands"
|
|
286
|
+
# Example: npm test -- --testPathPattern=${domain}
|
|
287
|
+
`, ctx),
|
|
288
|
+
required: true
|
|
289
|
+
})),
|
|
290
|
+
|
|
291
|
+
{
|
|
292
|
+
path: 'jobs.tag',
|
|
293
|
+
operation: 'overwrite',
|
|
294
|
+
value: (() => {
|
|
295
|
+
// Build list of deploy and remote-test jobs that tag depends on
|
|
296
|
+
const deployJobs = Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].deploy === true).map((domain: string) => `deploy-${domain}`)
|
|
297
|
+
const remoteTestJobs = Object.keys(ctx.domains || {}).sort().filter((domain: string) => ctx.domains[domain].remoteTest === true).map((domain: string) => `remote-test-${domain}`)
|
|
298
|
+
const allDeployTestJobs = [...deployJobs, ...remoteTestJobs]
|
|
299
|
+
|
|
300
|
+
// Build needs array (version + all deploy/remote-test jobs)
|
|
301
|
+
const needsArray = ['version', ...allDeployTestJobs]
|
|
302
|
+
|
|
303
|
+
// Build conditional: no failures AND at least one success
|
|
304
|
+
const noFailures = allDeployTestJobs.length > 0
|
|
305
|
+
? allDeployTestJobs.map((job: string) => `needs.${job}.result != 'failure'`).join(' && \n ')
|
|
306
|
+
: 'true'
|
|
307
|
+
const atLeastOneSuccess = allDeployTestJobs.length > 0
|
|
308
|
+
? allDeployTestJobs.map((job: string) => `needs.${job}.result == 'success'`).join(' || \n ')
|
|
309
|
+
: 'true'
|
|
310
|
+
|
|
311
|
+
return createValueFromString(`
|
|
312
|
+
# Needs all deploy and/or remote test jobs to succeed or be skipped
|
|
313
|
+
# Needs at least one domain to succeed
|
|
314
|
+
needs: [ ${needsArray.join(', ')} ]
|
|
315
|
+
if: \${{
|
|
316
|
+
always() &&
|
|
317
|
+
github.ref_name == '${ctx.initialBranch || branchFlow[0]}' &&
|
|
318
|
+
needs.version.result == 'success' &&
|
|
319
|
+
(
|
|
320
|
+
${noFailures}
|
|
321
|
+
) &&
|
|
322
|
+
(
|
|
323
|
+
${atLeastOneSuccess}
|
|
324
|
+
)
|
|
325
|
+
}}
|
|
326
|
+
runs-on: ubuntu-latest
|
|
327
|
+
steps:
|
|
328
|
+
- uses: actions/checkout@v4
|
|
329
|
+
- uses: ./.github/actions/create-tag
|
|
330
|
+
with:
|
|
331
|
+
version: \${{ needs.version.outputs.version }}
|
|
332
|
+
`, ctx)
|
|
333
|
+
})(),
|
|
334
|
+
spaceBefore: true,
|
|
335
|
+
commentBefore: dedent`
|
|
336
|
+
=============================================================================
|
|
337
|
+
TAG & PROMOTE
|
|
338
|
+
=============================================================================
|
|
339
|
+
`,
|
|
340
|
+
required: true
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// Generate single promotion job that handles all branch transitions dynamically
|
|
344
|
+
{
|
|
345
|
+
path: 'jobs.promote',
|
|
346
|
+
operation: 'overwrite' as const,
|
|
347
|
+
value: createValueFromString(`
|
|
348
|
+
# Only runs on push or manual workflow_dispatch events to branches that can promote
|
|
349
|
+
# Waits for version/tag if they run, but doesn't fail if they're skipped
|
|
350
|
+
# Needs all deploy and/or remote test jobs to succeed
|
|
351
|
+
if: \${{
|
|
352
|
+
always() &&
|
|
353
|
+
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
|
|
354
|
+
(needs.version.result == 'success' || needs.version.result == 'skipped') &&
|
|
355
|
+
(needs.tag.result == 'success' || needs.tag.result == 'skipped') &&
|
|
356
|
+
(
|
|
357
|
+
${branchFlow.slice(0, -1).map((branch: string) => `github.ref_name == '${branch}'`).join(' || \n ')}
|
|
358
|
+
)
|
|
359
|
+
}}
|
|
360
|
+
needs: [ version, tag ]
|
|
361
|
+
runs-on: ubuntu-latest
|
|
362
|
+
steps:
|
|
363
|
+
- uses: actions/checkout@v4
|
|
364
|
+
- uses: ./.github/actions/promote-branch
|
|
365
|
+
with:
|
|
366
|
+
sourceBranch: \${{ github.ref_name }}
|
|
367
|
+
version: \${{ needs.version.outputs.version }}
|
|
368
|
+
run_number: \${{ inputs.run_number || github.run_number }}
|
|
369
|
+
token: \${{ secrets.GITHUB_TOKEN }}
|
|
370
|
+
`, ctx),
|
|
371
|
+
spaceBefore: true,
|
|
372
|
+
commentBefore: dedent`
|
|
373
|
+
=============================================================================
|
|
374
|
+
PROMOTION JOB
|
|
375
|
+
=============================================================================
|
|
376
|
+
`,
|
|
377
|
+
required: true
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// Generate release job for final branch (main)
|
|
381
|
+
{
|
|
382
|
+
path: 'jobs.release',
|
|
383
|
+
operation: 'overwrite' as const,
|
|
384
|
+
value: createValueFromString(`
|
|
385
|
+
# Create GitHub release on main branch after successful tests and versioning
|
|
386
|
+
if: \${{
|
|
387
|
+
always() &&
|
|
388
|
+
github.ref_name == '${ctx.finalBranch || branchFlow[branchFlow.length - 1]}' &&
|
|
389
|
+
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
|
|
390
|
+
needs.version.result == 'success' &&
|
|
391
|
+
(needs.tag.result == 'success' || needs.tag.result == 'skipped')
|
|
392
|
+
}}
|
|
393
|
+
needs: [ version, tag ]
|
|
394
|
+
runs-on: ubuntu-latest
|
|
395
|
+
steps:
|
|
396
|
+
- uses: actions/checkout@v4
|
|
397
|
+
- uses: ./.github/actions/create-release
|
|
398
|
+
with:
|
|
399
|
+
version: \${{ needs.version.outputs.version }}
|
|
400
|
+
token: \${{ secrets.GITHUB_TOKEN }}
|
|
401
|
+
`, ctx),
|
|
402
|
+
spaceBefore: true,
|
|
403
|
+
commentBefore: dedent`
|
|
404
|
+
=============================================================================
|
|
405
|
+
RELEASE JOB (Main Branch Only)
|
|
406
|
+
=============================================================================
|
|
407
|
+
`,
|
|
408
|
+
required: true
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
// Helper function to get job keys in order
|
|
414
|
+
const getJobKeysInOrder = (jobsNode: any): string[] => {
|
|
415
|
+
if (!jobsNode || !jobsNode.items) return []
|
|
416
|
+
return jobsNode.items
|
|
417
|
+
.filter((item: any) => item.key)
|
|
418
|
+
.map((item: any) => item.key.toString())
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Unified approach: Use the operation system for all Pipecraft jobs
|
|
422
|
+
// The operation system already handles:
|
|
423
|
+
// - If key exists and is owned by Pipecraft → overwrite it
|
|
424
|
+
// - If key doesn't exist → create it
|
|
425
|
+
// - If key isn't part of Pipecraft → ignore it (via job filtering)
|
|
426
|
+
|
|
427
|
+
const PIPECRAFT_OWNED_JOBS = getPipecraftOwnedJobs(branchFlow, ctx.domains)
|
|
428
|
+
|
|
429
|
+
// Capture original job order before any modifications
|
|
430
|
+
let originalJobOrder: string[] = []
|
|
431
|
+
if (doc.contents.get('jobs')) {
|
|
432
|
+
const jobsNode = doc.contents.get('jobs')
|
|
433
|
+
if (jobsNode && jobsNode.items) {
|
|
434
|
+
originalJobOrder = getJobKeysInOrder(jobsNode)
|
|
435
|
+
console.log('📋 Original job order:', originalJobOrder)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Collect user jobs (non-Pipecraft jobs) to preserve them
|
|
440
|
+
const userJobs = new Map<string, any>()
|
|
441
|
+
if (doc.contents.get('jobs')) {
|
|
442
|
+
const jobsNode = doc.contents.get('jobs')
|
|
443
|
+
if (jobsNode && jobsNode.items) {
|
|
444
|
+
for (const item of jobsNode.items) {
|
|
445
|
+
const jobName = item.key?.toString() || item.key?.value
|
|
446
|
+
if (jobName && !PIPECRAFT_OWNED_JOBS.has(jobName)) {
|
|
447
|
+
userJobs.set(jobName, item.value)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (userJobs.size > 0) {
|
|
451
|
+
console.log(`📋 Preserving ${userJobs.size} user jobs: ${Array.from(userJobs.keys()).join(', ')}`)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Clear the entire jobs section to rebuild in correct order
|
|
457
|
+
const jobsNode = doc.contents.get('jobs')
|
|
458
|
+
if (jobsNode && jobsNode.items) {
|
|
459
|
+
jobsNode.items = []
|
|
460
|
+
// Clear any orphaned comments that were attached to the jobs node
|
|
461
|
+
// When we parse YAML with comments and clear items, comments can become orphaned on the parent
|
|
462
|
+
delete (jobsNode as any).commentBefore
|
|
463
|
+
delete (jobsNode as any).comment
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Apply all operations in order - this creates/overwrites Pipecraft jobs
|
|
467
|
+
// The 'overwrite' operation handles both create and update cases automatically
|
|
468
|
+
applyPathOperations(doc.contents, operations, doc)
|
|
469
|
+
|
|
470
|
+
// Remove old trigger types that are no longer used (workflow_call, pull_request)
|
|
471
|
+
// We only want push and workflow_dispatch triggers
|
|
472
|
+
const onNode = doc.contents.get('on')
|
|
473
|
+
if (onNode && onNode.delete) {
|
|
474
|
+
onNode.delete('workflow_call')
|
|
475
|
+
onNode.delete('pull_request')
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Now we need to reorder jobs to match the original order
|
|
479
|
+
// Collect all current jobs (Pipecraft jobs that were just created)
|
|
480
|
+
const currentJobs = new Map<string, any>()
|
|
481
|
+
if (jobsNode && jobsNode.items) {
|
|
482
|
+
for (const item of jobsNode.items) {
|
|
483
|
+
const jobName = item.key?.toString()
|
|
484
|
+
if (jobName) {
|
|
485
|
+
currentJobs.set(jobName, item)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Clear again to rebuild in correct order
|
|
491
|
+
if (jobsNode && jobsNode.items) {
|
|
492
|
+
jobsNode.items = []
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Rebuild jobs in original order
|
|
496
|
+
// For each job in the original order:
|
|
497
|
+
// - If it's a Pipecraft job, use the newly created version from currentJobs
|
|
498
|
+
// - If it's a user job, use the preserved version from userJobs
|
|
499
|
+
// IMPORTANT: Ensure all keys are Scalars (not strings) so we can add comments later
|
|
500
|
+
for (const jobName of originalJobOrder) {
|
|
501
|
+
if (PIPECRAFT_OWNED_JOBS.has(jobName)) {
|
|
502
|
+
// It's a Pipecraft job - use the newly created version
|
|
503
|
+
const item = currentJobs.get(jobName)
|
|
504
|
+
if (item && jobsNode) {
|
|
505
|
+
// Ensure the key is a Scalar, not a string
|
|
506
|
+
if (typeof item.key === 'string') {
|
|
507
|
+
item.key = new Scalar(item.key)
|
|
508
|
+
}
|
|
509
|
+
jobsNode.items.push(item)
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
// It's a user job - re-insert from preserved values
|
|
513
|
+
const jobValue = userJobs.get(jobName)
|
|
514
|
+
if (jobValue && jobsNode) {
|
|
515
|
+
jobsNode.set(jobName, jobValue)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Add any new Pipecraft jobs that weren't in the original order (at the end)
|
|
521
|
+
for (const [jobName, item] of currentJobs) {
|
|
522
|
+
if (!originalJobOrder.includes(jobName) && jobsNode) {
|
|
523
|
+
// Ensure the key is a Scalar, not a string
|
|
524
|
+
if (typeof item.key === 'string') {
|
|
525
|
+
item.key = new Scalar(item.key)
|
|
526
|
+
}
|
|
527
|
+
jobsNode.items.push(item)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Log final job order (after operations are applied)
|
|
532
|
+
const finalJobsNode = doc.contents.get('jobs')
|
|
533
|
+
if (finalJobsNode && finalJobsNode.items && finalJobsNode.items.length > 0) {
|
|
534
|
+
const jobNames = getJobKeysInOrder(finalJobsNode)
|
|
535
|
+
if (jobNames.length > 0) {
|
|
536
|
+
console.log('📋 Final job order:', jobNames)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Remove duplicate comment headers
|
|
540
|
+
// When we reuse existing job values, they may carry old comments from parsing
|
|
541
|
+
// We want to keep only the comments we explicitly set on keys
|
|
542
|
+
for (const item of finalJobsNode.items) {
|
|
543
|
+
if (item.value && (item.value as any).commentBefore) {
|
|
544
|
+
// Clear commentBefore from values - comments should only be on keys
|
|
545
|
+
delete (item.value as any).commentBefore
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Add section headers and spacing to domain-based jobs
|
|
550
|
+
// This ensures headers appear in the right place and jobs have proper spacing
|
|
551
|
+
const domainKeys = Object.keys(ctx.domains || {})
|
|
552
|
+
if (domainKeys.length > 0) {
|
|
553
|
+
let foundFirstTest = false
|
|
554
|
+
let foundFirstDeploy = false
|
|
555
|
+
let foundFirstRemoteTest = false
|
|
556
|
+
|
|
557
|
+
for (const item of finalJobsNode.items) {
|
|
558
|
+
const jobName = item.key?.toString()
|
|
559
|
+
if (!jobName) continue
|
|
560
|
+
|
|
561
|
+
// Skip if key is a string (can't add properties to primitive strings)
|
|
562
|
+
if (typeof item.key === 'string') continue
|
|
563
|
+
|
|
564
|
+
// Handle test-* jobs
|
|
565
|
+
if (jobName.startsWith('test-')) {
|
|
566
|
+
if (!foundFirstTest) {
|
|
567
|
+
// First test job gets the header
|
|
568
|
+
(item.key as any).commentBefore = `
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
=============================================================================
|
|
572
|
+
TESTING JOBS
|
|
573
|
+
=============================================================================`
|
|
574
|
+
;(item.key as any).spaceBefore = true
|
|
575
|
+
foundFirstTest = true
|
|
576
|
+
} else {
|
|
577
|
+
// Subsequent test jobs get a blank line
|
|
578
|
+
;(item.key as any).spaceBefore = true
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Handle deploy-* jobs
|
|
583
|
+
if (jobName.startsWith('deploy-')) {
|
|
584
|
+
if (!foundFirstDeploy) {
|
|
585
|
+
// First deploy job gets the header
|
|
586
|
+
;(item.key as any).commentBefore = `
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
=============================================================================
|
|
590
|
+
DEPLOYMENT JOBS
|
|
591
|
+
=============================================================================`
|
|
592
|
+
;(item.key as any).spaceBefore = true
|
|
593
|
+
foundFirstDeploy = true
|
|
594
|
+
} else {
|
|
595
|
+
// Subsequent deploy jobs get a blank line
|
|
596
|
+
;(item.key as any).spaceBefore = true
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Handle remote-test-* jobs
|
|
601
|
+
if (jobName.startsWith('remote-test-')) {
|
|
602
|
+
if (!foundFirstRemoteTest) {
|
|
603
|
+
// First remote-test job gets the header
|
|
604
|
+
;(item.key as any).commentBefore = `
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
=============================================================================
|
|
608
|
+
REMOTE TESTING JOBS
|
|
609
|
+
=============================================================================`
|
|
610
|
+
;(item.key as any).spaceBefore = true
|
|
611
|
+
foundFirstRemoteTest = true
|
|
612
|
+
} else {
|
|
613
|
+
// Subsequent remote-test jobs get a blank line
|
|
614
|
+
;(item.key as any).spaceBefore = true
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Generate final content with comment preservation
|
|
622
|
+
// Use lineWidth: 0 to prevent line wrapping of long expressions
|
|
623
|
+
// This keeps GitHub Actions expressions on a single line
|
|
624
|
+
let finalContent = stringify(doc, {
|
|
625
|
+
lineWidth: 0,
|
|
626
|
+
minContentWidth: 0
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
// Post-process: Format long GitHub Actions conditionals for better readability
|
|
630
|
+
// The YAML library doesn't handle newlines well in flow scalars, so we format after stringify
|
|
631
|
+
finalContent = finalContent.replace(
|
|
632
|
+
/if: \$\{\{([^}]+)\}\}/g,
|
|
633
|
+
(match, condition) => {
|
|
634
|
+
// Only format if the condition is long enough to benefit from formatting
|
|
635
|
+
if (condition.length < 100) return match
|
|
636
|
+
|
|
637
|
+
let formatted = condition.trim()
|
|
638
|
+
|
|
639
|
+
// Step 1: Protect function calls like always() by replacing with placeholders
|
|
640
|
+
const functionCalls: string[] = []
|
|
641
|
+
formatted = formatted.replace(/(\w+)\(\)/g, (match: string) => {
|
|
642
|
+
const placeholder = `__FUNC_${functionCalls.length}__`
|
|
643
|
+
functionCalls.push(match)
|
|
644
|
+
return placeholder
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
// Step 2: Add line breaks for logical operators
|
|
648
|
+
formatted = formatted.replace(/\s+&&\s+/g, ' &&\n ')
|
|
649
|
+
formatted = formatted.replace(/\s+\|\|\s+/g, ' ||\n ')
|
|
650
|
+
|
|
651
|
+
// Step 3: Format grouping parentheses (now that function calls are protected)
|
|
652
|
+
formatted = formatted.replace(/\(\s*/g, '(\n ')
|
|
653
|
+
formatted = formatted.replace(/\s*\)\s*(&&|\|\|)/g, '\n ) $1')
|
|
654
|
+
formatted = formatted.replace(/\s*\)(\s*)$/g, '\n )')
|
|
655
|
+
|
|
656
|
+
// Step 4: Restore function calls
|
|
657
|
+
functionCalls.forEach((funcCall, index) => {
|
|
658
|
+
formatted = formatted.replace(`__FUNC_${index}__`, funcCall)
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
return `if: $\{{\n ${formatted}\n }}`
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
yamlContent: finalContent,
|
|
667
|
+
mergeStatus: hasExistingPipeline ? 'merged' : 'overwritten'
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Load existing pipeline file
|
|
675
|
+
*/
|
|
676
|
+
const loadExistingPipeline = (filePath: string): string | null => {
|
|
677
|
+
if (!fs.existsSync(filePath)) {
|
|
678
|
+
return null
|
|
679
|
+
}
|
|
680
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Main export - path-based pipeline generator
|
|
685
|
+
*/
|
|
686
|
+
export const generate = (ctx: PinionContext & { existingPipeline?: any, outputPipelinePath?: string }) =>
|
|
687
|
+
Promise.resolve(ctx)
|
|
688
|
+
.then((ctx) => {
|
|
689
|
+
const result = createPathBasedPipeline(ctx)
|
|
690
|
+
return {
|
|
691
|
+
...ctx,
|
|
692
|
+
yamlContent: result.yamlContent,
|
|
693
|
+
mergeStatus: result.mergeStatus
|
|
694
|
+
}
|
|
695
|
+
})
|
|
696
|
+
.then((ctx) => {
|
|
697
|
+
// Provide user feedback about file operation
|
|
698
|
+
const outputPath = ctx.outputPipelinePath || '.github/workflows/pipeline.yml'
|
|
699
|
+
const status = ctx.mergeStatus === 'merged' ? '🔄 Merged with existing' : '📝 Created new'
|
|
700
|
+
console.log(`${status} ${outputPath}`)
|
|
701
|
+
return ctx
|
|
702
|
+
})
|
|
703
|
+
.then(renderTemplate(
|
|
704
|
+
(ctx: any) => ctx.yamlContent,
|
|
705
|
+
toFile((ctx: any) => ctx.outputPipelinePath || '.github/workflows/pipeline.yml')
|
|
706
|
+
))
|