Skip to content

Commit fa94b13

Browse files
committed
update azdo pipeline to publish one env at a time.
1 parent fea2ccc commit fa94b13

2 files changed

Lines changed: 70 additions & 81 deletions

File tree

src/templates/azure-devops/publish-pipeline.ts

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ export function generatePublishPipeline(config: PublishPipelineConfig): string {
1414
const defaultEnvironment = config.environments[0] ?? 'dev';
1515
const envValues = config.environments.map((env) => ` - '${env}'`).join('\n');
1616

17-
const stages = config.environments.map((env) => {
18-
const envUpper = env.toUpperCase();
19-
20-
return `- stage: Publish_${env}
21-
displayName: 'Publish to ${env}'
22-
condition: eq('\${{ parameters.ENVIRONMENT }}', '${env}')
17+
// A single parameterized stage drives all environments. The ENVIRONMENT
18+
// parameter is resolved at template (compile) time, so it can select the
19+
// variable group, deployment environment, config file, and env-suffixed
20+
// variable names (via upper()) without duplicating the stage per environment.
21+
const stage = `- stage: Publish
22+
displayName: 'Publish to \${{ parameters.ENVIRONMENT }}'
2323
variables:
24-
- group: apim-${env}
24+
- group: apim-\${{ parameters.ENVIRONMENT }}
2525
jobs:
2626
- deployment: Deploy
27-
displayName: 'Deploy to ${env}'
28-
environment: ${env}
27+
displayName: 'Deploy to \${{ parameters.ENVIRONMENT }}'
28+
environment: \${{ parameters.ENVIRONMENT }}
2929
pool:
3030
vmImage: 'ubuntu-latest'
3131
strategy:
@@ -54,88 +54,87 @@ export function generatePublishPipeline(config: PublishPipelineConfig): string {
5454
displayName: 'Install dependencies'
5555
5656
- task: replacetokens@6
57-
displayName: 'Substitute tokens in configuration.${env}.yaml'
57+
displayName: 'Substitute tokens in configuration.\${{ parameters.ENVIRONMENT }}.yaml'
5858
inputs:
59-
sources: 'configuration.${env}.yaml'
59+
sources: 'configuration.\${{ parameters.ENVIRONMENT }}.yaml'
6060
tokenPattern: 'custom'
6161
tokenPrefix: '{#['
6262
tokenSuffix: ']#}'
6363
missingVarAction: 'keep'
6464
missingVarLog: 'error'
6565
6666
- script: |
67-
if grep -q '{#\\[' configuration.${env}.yaml; then
68-
echo "Unresolved tokens remain in configuration.${env}.yaml"
69-
grep -o '{#\\[[^]]*\\]#}' configuration.${env}.yaml | sort -u
67+
if grep -q '{#\\[' configuration.\${{ parameters.ENVIRONMENT }}.yaml; then
68+
echo "Unresolved tokens remain in configuration.\${{ parameters.ENVIRONMENT }}.yaml"
69+
grep -o '{#\\[[^]]*\\]#}' configuration.\${{ parameters.ENVIRONMENT }}.yaml | sort -u
7070
exit 1
7171
fi
72-
displayName: 'Validate token substitution (${env})'
72+
displayName: 'Validate token substitution'
7373
7474
- task: AzureCLI@2
75-
displayName: 'Dry-run validation (${env}, incremental)'
75+
displayName: 'Dry-run validation (incremental)'
7676
condition: and(succeeded(), ne('\${{ parameters.COMMIT_ID_CHOICE }}', 'publish-all-artifacts-in-repo'))
7777
inputs:
78-
azureSubscription: 'AZURE_SERVICE_CONNECTION_${envUpper}'
78+
azureSubscription: 'AZURE_SERVICE_CONNECTION_\${{ upper(parameters.ENVIRONMENT) }}'
7979
scriptType: 'bash'
8080
scriptLocation: 'inlineScript'
8181
inlineScript: |
8282
npx apiops publish \\
83-
--resource-group $(APIM_RESOURCE_GROUP_${envUpper}) \\
84-
--service-name $(APIM_SERVICE_NAME_${envUpper}) \\
83+
--resource-group $(APIM_RESOURCE_GROUP_\${{ upper(parameters.ENVIRONMENT) }}) \\
84+
--service-name $(APIM_SERVICE_NAME_\${{ upper(parameters.ENVIRONMENT) }}) \\
8585
--source ${config.artifactDir} \\
86-
--overrides configuration.${env}.yaml \\
86+
--overrides configuration.\${{ parameters.ENVIRONMENT }}.yaml \\
8787
--commit-id $(Build.SourceVersion) \\
8888
--subscription-id $(AZURE_SUBSCRIPTION_ID) \\
8989
--dry-run
9090
9191
- task: AzureCLI@2
92-
displayName: 'Dry-run validation (${env}, all artifacts)'
92+
displayName: 'Dry-run validation (all artifacts)'
9393
condition: and(succeeded(), eq('\${{ parameters.COMMIT_ID_CHOICE }}', 'publish-all-artifacts-in-repo'))
9494
inputs:
95-
azureSubscription: 'AZURE_SERVICE_CONNECTION_${envUpper}'
95+
azureSubscription: 'AZURE_SERVICE_CONNECTION_\${{ upper(parameters.ENVIRONMENT) }}'
9696
scriptType: 'bash'
9797
scriptLocation: 'inlineScript'
9898
inlineScript: |
9999
npx apiops publish \\
100-
--resource-group $(APIM_RESOURCE_GROUP_${envUpper}) \\
101-
--service-name $(APIM_SERVICE_NAME_${envUpper}) \\
100+
--resource-group $(APIM_RESOURCE_GROUP_\${{ upper(parameters.ENVIRONMENT) }}) \\
101+
--service-name $(APIM_SERVICE_NAME_\${{ upper(parameters.ENVIRONMENT) }}) \\
102102
--source ${config.artifactDir} \\
103-
--overrides configuration.${env}.yaml \\
103+
--overrides configuration.\${{ parameters.ENVIRONMENT }}.yaml \\
104104
--subscription-id $(AZURE_SUBSCRIPTION_ID) \\
105105
--dry-run
106106
107107
- task: AzureCLI@2
108-
displayName: 'Publish to ${env} (incremental - last commit only)'
108+
displayName: 'Publish (incremental - last commit only)'
109109
condition: and(succeeded(), ne('\${{ parameters.COMMIT_ID_CHOICE }}', 'publish-all-artifacts-in-repo'))
110110
inputs:
111-
azureSubscription: 'AZURE_SERVICE_CONNECTION_${envUpper}'
111+
azureSubscription: 'AZURE_SERVICE_CONNECTION_\${{ upper(parameters.ENVIRONMENT) }}'
112112
scriptType: 'bash'
113113
scriptLocation: 'inlineScript'
114114
inlineScript: |
115115
npx apiops publish \\
116-
--resource-group $(APIM_RESOURCE_GROUP_${envUpper}) \\
117-
--service-name $(APIM_SERVICE_NAME_${envUpper}) \\
116+
--resource-group $(APIM_RESOURCE_GROUP_\${{ upper(parameters.ENVIRONMENT) }}) \\
117+
--service-name $(APIM_SERVICE_NAME_\${{ upper(parameters.ENVIRONMENT) }}) \\
118118
--source ${config.artifactDir} \\
119-
--overrides configuration.${env}.yaml \\
119+
--overrides configuration.\${{ parameters.ENVIRONMENT }}.yaml \\
120120
--commit-id $(Build.SourceVersion) \\
121121
--subscription-id $(AZURE_SUBSCRIPTION_ID)
122122
123123
- task: AzureCLI@2
124-
displayName: 'Publish to ${env} (all artifacts)'
124+
displayName: 'Publish (all artifacts)'
125125
condition: and(succeeded(), eq('\${{ parameters.COMMIT_ID_CHOICE }}', 'publish-all-artifacts-in-repo'))
126126
inputs:
127-
azureSubscription: 'AZURE_SERVICE_CONNECTION_${envUpper}'
127+
azureSubscription: 'AZURE_SERVICE_CONNECTION_\${{ upper(parameters.ENVIRONMENT) }}'
128128
scriptType: 'bash'
129129
scriptLocation: 'inlineScript'
130130
inlineScript: |
131131
npx apiops publish \\
132-
--resource-group $(APIM_RESOURCE_GROUP_${envUpper}) \\
133-
--service-name $(APIM_SERVICE_NAME_${envUpper}) \\
132+
--resource-group $(APIM_RESOURCE_GROUP_\${{ upper(parameters.ENVIRONMENT) }}) \\
133+
--service-name $(APIM_SERVICE_NAME_\${{ upper(parameters.ENVIRONMENT) }}) \\
134134
--source ${config.artifactDir} \\
135-
--overrides configuration.${env}.yaml \\
135+
--overrides configuration.\${{ parameters.ENVIRONMENT }}.yaml \\
136136
--subscription-id $(AZURE_SUBSCRIPTION_ID)
137137
`;
138-
}).join('\n');
139138

140139
return `# Azure DevOps Pipeline: Run APIM Publisher
141140
@@ -166,6 +165,6 @@ parameters:
166165
${envValues}
167166
168167
stages:
169-
${stages}
168+
${stage}
170169
`;
171170
}

tests/unit/templates/azure-devops/publish-pipeline.test.ts

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -75,41 +75,40 @@ describe('azure-devops/publish-pipeline', () => {
7575
expect(pipeline).toContain("- 'prod'");
7676
});
7777

78-
it('should create stage for each environment', () => {
78+
it('should create a single parameterized publish stage', () => {
7979
const pipeline = generatePublishPipeline({
8080
artifactDir: './apim-artifacts',
8181
environments: ['dev', 'staging', 'prod'],
8282
});
83-
expect(pipeline).toContain('stage: Publish_dev');
84-
expect(pipeline).toContain('stage: Publish_staging');
85-
expect(pipeline).toContain('stage: Publish_prod');
83+
expect(pipeline).toContain('stage: Publish');
84+
expect(pipeline).not.toContain('stage: Publish_dev');
85+
expect(pipeline).not.toContain('stage: Publish_staging');
86+
expect(pipeline).not.toContain('stage: Publish_prod');
8687
});
8788

8889
it('should not chain stages with dependsOn', () => {
8990
const pipeline = generatePublishPipeline({
9091
artifactDir: './apim-artifacts',
9192
environments: ['dev', 'staging', 'prod'],
9293
});
93-
expect(pipeline).not.toContain('dependsOn: Publish_');
94+
expect(pipeline).not.toContain('dependsOn: Publish');
9495
});
9596

96-
it('should filter stages by ENVIRONMENT parameter', () => {
97+
it('should drive the stage from the ENVIRONMENT parameter', () => {
9798
const pipeline = generatePublishPipeline({
9899
artifactDir: './apim-artifacts',
99100
environments: ['dev', 'prod'],
100101
});
101-
expect(pipeline).toContain("eq('${{ parameters.ENVIRONMENT }}', 'dev')");
102-
expect(pipeline).toContain("eq('${{ parameters.ENVIRONMENT }}', 'prod')");
103-
expect(pipeline).not.toContain("eq('${{ parameters.ENVIRONMENT }}', 'all')");
102+
expect(pipeline).toContain("displayName: 'Publish to ${{ parameters.ENVIRONMENT }}'");
103+
expect(pipeline).not.toContain("eq('${{ parameters.ENVIRONMENT }}', 'dev')");
104104
});
105105

106-
it('should use environment-specific variable groups', () => {
106+
it('should select the variable group from the ENVIRONMENT parameter', () => {
107107
const pipeline = generatePublishPipeline({
108108
artifactDir: './apim-artifacts',
109109
environments: ['dev', 'prod'],
110110
});
111-
expect(pipeline).toContain('- group: apim-dev');
112-
expect(pipeline).toContain('- group: apim-prod');
111+
expect(pipeline).toContain('- group: apim-${{ parameters.ENVIRONMENT }}');
113112
});
114113

115114
it('should use deployment job with environment', () => {
@@ -118,7 +117,7 @@ describe('azure-devops/publish-pipeline', () => {
118117
environments: ['dev'],
119118
});
120119
expect(pipeline).toContain('deployment: Deploy');
121-
expect(pipeline).toContain('environment: dev');
120+
expect(pipeline).toContain('environment: ${{ parameters.ENVIRONMENT }}');
122121
});
123122

124123
it('should use runOnce deployment strategy', () => {
@@ -165,41 +164,37 @@ describe('azure-devops/publish-pipeline', () => {
165164
environments: ['dev'],
166165
});
167166
const lines = pipeline.split('\n');
168-
const allArtStart = lines.findIndex((l) => l.includes("Publish to dev (all artifacts)"));
167+
const allArtStart = lines.findIndex((l) => l.includes("Publish (all artifacts)"));
169168
// Find the next step or stage boundary after the all-artifacts step
170169
const sectionEnd = lines.findIndex((l, i) => i > allArtStart + 1 && (l.includes('- task:') || l.includes('- stage:')));
171170
const end = sectionEnd === -1 ? lines.length : sectionEnd;
172171
const allArtSection = lines.slice(allArtStart, end).join('\n');
173172
expect(allArtSection).not.toContain('--commit-id');
174173
});
175174

176-
it('should use environment-specific service connection', () => {
175+
it('should select the service connection from the ENVIRONMENT parameter', () => {
177176
const pipeline = generatePublishPipeline({
178177
artifactDir: './apim-artifacts',
179178
environments: ['dev', 'prod'],
180179
});
181-
expect(pipeline).toContain("azureSubscription: 'AZURE_SERVICE_CONNECTION_DEV'");
182-
expect(pipeline).toContain("azureSubscription: 'AZURE_SERVICE_CONNECTION_PROD'");
180+
expect(pipeline).toContain("azureSubscription: 'AZURE_SERVICE_CONNECTION_${{ upper(parameters.ENVIRONMENT) }}'");
183181
});
184182

185-
it('should use environment-specific resource group and service name', () => {
183+
it('should reference env-suffixed resource group and service name via parameter', () => {
186184
const pipeline = generatePublishPipeline({
187185
artifactDir: './apim-artifacts',
188186
environments: ['dev', 'prod'],
189187
});
190-
expect(pipeline).toContain('$(APIM_RESOURCE_GROUP_DEV)');
191-
expect(pipeline).toContain('$(APIM_SERVICE_NAME_DEV)');
192-
expect(pipeline).toContain('$(APIM_RESOURCE_GROUP_PROD)');
193-
expect(pipeline).toContain('$(APIM_SERVICE_NAME_PROD)');
188+
expect(pipeline).toContain('$(APIM_RESOURCE_GROUP_${{ upper(parameters.ENVIRONMENT) }})');
189+
expect(pipeline).toContain('$(APIM_SERVICE_NAME_${{ upper(parameters.ENVIRONMENT) }})');
194190
});
195191

196-
it('should use environment-specific override files', () => {
192+
it('should reference the env-specific override file via parameter', () => {
197193
const pipeline = generatePublishPipeline({
198194
artifactDir: './apim-artifacts',
199195
environments: ['dev', 'prod'],
200196
});
201-
expect(pipeline).toContain('--overrides configuration.dev.yaml');
202-
expect(pipeline).toContain('--overrides configuration.prod.yaml');
197+
expect(pipeline).toContain('--overrides configuration.${{ parameters.ENVIRONMENT }}.yaml');
203198
});
204199

205200
it('should use lockfile-aware dependency install', () => {
@@ -231,19 +226,16 @@ describe('azure-devops/publish-pipeline', () => {
231226
artifactDir: './apim-artifacts',
232227
environments: ['dev', 'prod'],
233228
});
234-
expect(pipeline).toContain('Validate token substitution (dev)');
235-
expect(pipeline).toContain('Validate token substitution (prod)');
236-
expect(pipeline).toContain("grep -q '{#\\[' configuration.dev.yaml");
237-
expect(pipeline).toContain("grep -q '{#\\[' configuration.prod.yaml");
229+
expect(pipeline).toContain('Validate token substitution');
230+
expect(pipeline).toContain("grep -q '{#\\[' configuration.${{ parameters.ENVIRONMENT }}.yaml");
238231
});
239232

240-
it('should target environment-specific configuration file for token substitution', () => {
233+
it('should target the parameterized configuration file for token substitution', () => {
241234
const pipeline = generatePublishPipeline({
242235
artifactDir: './apim-artifacts',
243236
environments: ['dev', 'prod'],
244237
});
245-
expect(pipeline).toContain("sources: 'configuration.dev.yaml'");
246-
expect(pipeline).toContain("sources: 'configuration.prod.yaml'");
238+
expect(pipeline).toContain("sources: 'configuration.${{ parameters.ENVIRONMENT }}.yaml'");
247239
});
248240

249241
it('should place token substitution step before publish steps', () => {
@@ -262,8 +254,8 @@ describe('azure-devops/publish-pipeline', () => {
262254
artifactDir: './apim-artifacts',
263255
environments: ['dev'],
264256
});
265-
expect(pipeline).toContain('Dry-run validation (dev, incremental)');
266-
expect(pipeline).toContain('Dry-run validation (dev, all artifacts)');
257+
expect(pipeline).toContain('Dry-run validation (incremental)');
258+
expect(pipeline).toContain('Dry-run validation (all artifacts)');
267259
});
268260

269261
it('should include --dry-run flag in dry-run validation steps', () => {
@@ -272,7 +264,7 @@ describe('azure-devops/publish-pipeline', () => {
272264
environments: ['dev'],
273265
});
274266
const lines = pipeline.split('\n');
275-
const dryRunIncrIdx = lines.findIndex((l) => l.includes('Dry-run validation (dev, incremental)'));
267+
const dryRunIncrIdx = lines.findIndex((l) => l.includes('Dry-run validation (incremental)'));
276268
const dryRunSection = lines.slice(dryRunIncrIdx, dryRunIncrIdx + 20).join('\n');
277269
expect(dryRunSection).toContain('--dry-run');
278270
});
@@ -282,21 +274,19 @@ describe('azure-devops/publish-pipeline', () => {
282274
artifactDir: './apim-artifacts',
283275
environments: ['dev'],
284276
});
285-
const dryRunIdx = pipeline.indexOf('Dry-run validation (dev, incremental)');
286-
const publishIdx = pipeline.indexOf("Publish to dev (incremental");
277+
const dryRunIdx = pipeline.indexOf('Dry-run validation (incremental)');
278+
const publishIdx = pipeline.indexOf("Publish (incremental");
287279
expect(dryRunIdx).toBeGreaterThan(0);
288280
expect(dryRunIdx).toBeLessThan(publishIdx);
289281
});
290282

291-
it('should include dry-run validation for each environment', () => {
283+
it('should include dry-run validation steps for the parameterized environment', () => {
292284
const pipeline = generatePublishPipeline({
293285
artifactDir: './apim-artifacts',
294286
environments: ['dev', 'prod'],
295287
});
296-
expect(pipeline).toContain('Dry-run validation (dev, incremental)');
297-
expect(pipeline).toContain('Dry-run validation (dev, all artifacts)');
298-
expect(pipeline).toContain('Dry-run validation (prod, incremental)');
299-
expect(pipeline).toContain('Dry-run validation (prod, all artifacts)');
288+
expect(pipeline).toContain('Dry-run validation (incremental)');
289+
expect(pipeline).toContain('Dry-run validation (all artifacts)');
300290
});
301291

302292
it('should pass commit-id in incremental dry-run step', () => {
@@ -305,7 +295,7 @@ describe('azure-devops/publish-pipeline', () => {
305295
environments: ['dev'],
306296
});
307297
const lines = pipeline.split('\n');
308-
const dryRunIncrIdx = lines.findIndex((l) => l.includes('Dry-run validation (dev, incremental)'));
298+
const dryRunIncrIdx = lines.findIndex((l) => l.includes('Dry-run validation (incremental)'));
309299
const nextTaskIdx = lines.findIndex((l, i) => i > dryRunIncrIdx + 1 && l.includes("- task:"));
310300
const dryRunSection = lines.slice(dryRunIncrIdx, nextTaskIdx).join('\n');
311301
expect(dryRunSection).toContain('--commit-id');
@@ -318,7 +308,7 @@ describe('azure-devops/publish-pipeline', () => {
318308
environments: ['dev'],
319309
});
320310
const lines = pipeline.split('\n');
321-
const dryRunAllIdx = lines.findIndex((l) => l.includes('Dry-run validation (dev, all artifacts)'));
311+
const dryRunAllIdx = lines.findIndex((l) => l.includes('Dry-run validation (all artifacts)'));
322312
const nextTaskIdx = lines.findIndex((l, i) => i > dryRunAllIdx + 1 && l.includes("- task:"));
323313
const dryRunSection = lines.slice(dryRunAllIdx, nextTaskIdx).join('\n');
324314
expect(dryRunSection).not.toContain('--commit-id');

0 commit comments

Comments
 (0)