From 34b7e0824f715216a312ecbf868fbc2eb8e1a974 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 17:28:26 +0000 Subject: [PATCH 1/5] fix: enable push-triggered incremental publish in GitHub Actions workflow The publish-workflow.ts template's push trigger was configured but all publish jobs were gated on `github.event.inputs.ENVIRONMENT`, which is undefined on push events, so no publish job ever ran on push. Fix: enable the first environment's job to also run on push to main, so that merges to main automatically trigger an incremental publish using GITHUB_SHA as the commit-id. Subsequent environments remain opt-in via workflow_dispatch (safe default for production gating). - First environment job condition: `ENVIRONMENT == 'dev' || event_name == 'push'` - Subsequent environments: gated on ENVIRONMENT input only, with comment showing how to chain sequential push-triggered deployment - The incremental step's `--commit-id` is already wired correctly via `needs.get-commit.outputs.commit_id` (from GITHUB_SHA); on push, COMMIT_ID_CHOICE is empty so `!= 'publish-all-artifacts-in-repo'` is true, automatically selecting incremental mode Added 3 new tests covering: - First env runs on push (event_name condition present) - Subsequent envs do NOT auto-run on push - commit-id is passed via incremental step condition Closes #46 Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/fc5694e2-a9e3-4a49-948a-9c1bfad737f1 Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com> --- .../github-actions/publish-workflow.ts | 13 +++--- .../github-actions/publish-workflow.test.ts | 44 ++++++++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/templates/github-actions/publish-workflow.ts b/src/templates/github-actions/publish-workflow.ts index 189d197..6dc89e9 100644 --- a/src/templates/github-actions/publish-workflow.ts +++ b/src/templates/github-actions/publish-workflow.ts @@ -13,15 +13,18 @@ export function generatePublishWorkflow(config: PublishWorkflowConfig): string { const envJobs = config.environments.map((env, idx) => { const autoDeployComment = idx === 0 - ? ` # To enable automatic deployment on push to main, uncomment the condition below: - # if: github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push'` - : ` # To enable automatic deployment on push to main, uncomment the condition below: + ? ` # Deploys automatically on push to main (incremental mode) or when selected via workflow_dispatch` + : ` # To enable sequential deployment on push, uncomment the condition below and update needs: # if: github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push' - # And change needs to: needs: [get-commit, publish-${config.environments[idx - 1]}]`; + # needs: [get-commit, publish-${config.environments[idx - 1]}]`; + + const jobCondition = idx === 0 + ? `github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push'` + : `github.event.inputs.ENVIRONMENT == '${env}'`; return ` publish-${env}: ${autoDeployComment} - if: github.event.inputs.ENVIRONMENT == '${env}' + if: ${jobCondition} runs-on: ubuntu-latest environment: ${env} needs: get-commit diff --git a/tests/unit/templates/github-actions/publish-workflow.test.ts b/tests/unit/templates/github-actions/publish-workflow.test.ts index e372a8a..bdca32f 100644 --- a/tests/unit/templates/github-actions/publish-workflow.test.ts +++ b/tests/unit/templates/github-actions/publish-workflow.test.ts @@ -92,16 +92,17 @@ describe('github-actions/publish-workflow', () => { expect(workflow).toContain('environment: prod'); }); - it('should chain jobs with needs dependencies', () => { + it('should include chained needs hints in comments for sequential deployment', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'staging', 'prod'], }); + // Chaining hints appear as comments for opt-in sequential deployment expect(workflow).toContain('needs: [get-commit, publish-dev]'); expect(workflow).toContain('needs: [get-commit, publish-staging]'); }); - it('should have first environment depend on get-commit only', () => { + it('should have all environment jobs depend on get-commit', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'prod'], @@ -203,5 +204,44 @@ describe('github-actions/publish-workflow', () => { expect(workflow).toContain('npm install'); expect(workflow).toContain('npx apiops publish'); }); + + it('should enable first environment to run automatically on push to main', () => { + const workflow = generatePublishWorkflow({ + artifactDir: './apim-artifacts', + environments: ['dev', 'prod'], + }); + // First environment's if-condition must include the push event trigger + expect(workflow).toContain("ENVIRONMENT == 'dev' || github.event_name == 'push'"); + }); + + it('should not auto-trigger subsequent environments on push to main', () => { + const workflow = generatePublishWorkflow({ + artifactDir: './apim-artifacts', + environments: ['dev', 'staging', 'prod'], + }); + // staging and prod must NOT include the push trigger in their active if-conditions + // (they may appear in comments but not as live conditions) + const lines = workflow.split('\n'); + + for (const env of ['staging', 'prod']) { + const jobStart = lines.findIndex((l) => l.includes(`publish-${env}:`)); + // Find the actual `if:` line (not a comment) within the next 10 lines + const jobLines = lines.slice(jobStart, jobStart + 10); + const ifLine = jobLines.find((l) => l.trimStart().startsWith('if:') && !l.trimStart().startsWith('#')); + expect(ifLine).toBeDefined(); + expect(ifLine).not.toContain('event_name'); + } + }); + + it('should pass commit_id on push trigger via incremental step condition', () => { + const workflow = generatePublishWorkflow({ + artifactDir: './apim-artifacts', + environments: ['dev'], + }); + // The incremental step condition is true when COMMIT_ID_CHOICE is empty (push trigger), + // so --commit-id will be passed automatically on push. + expect(workflow).toContain("COMMIT_ID_CHOICE != 'publish-all-artifacts-in-repo'"); + expect(workflow).toContain('--commit-id ${{ needs.get-commit.outputs.commit_id }}'); + }); }); }); From 2a5b4333ccfe2467439300ad927899e9310b5ac9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 17:30:07 +0000 Subject: [PATCH 2/5] chore: address code review feedback (grammar + test naming clarity) Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/fc5694e2-a9e3-4a49-948a-9c1bfad737f1 Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com> --- src/templates/github-actions/publish-workflow.ts | 2 +- .../unit/templates/github-actions/publish-workflow.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/templates/github-actions/publish-workflow.ts b/src/templates/github-actions/publish-workflow.ts index 6dc89e9..427bd33 100644 --- a/src/templates/github-actions/publish-workflow.ts +++ b/src/templates/github-actions/publish-workflow.ts @@ -13,7 +13,7 @@ export function generatePublishWorkflow(config: PublishWorkflowConfig): string { const envJobs = config.environments.map((env, idx) => { const autoDeployComment = idx === 0 - ? ` # Deploys automatically on push to main (incremental mode) or when selected via workflow_dispatch` + ? ` # Automatically deploys to ${env} on push to main (incremental mode) or when selected via workflow_dispatch` : ` # To enable sequential deployment on push, uncomment the condition below and update needs: # if: github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push' # needs: [get-commit, publish-${config.environments[idx - 1]}]`; diff --git a/tests/unit/templates/github-actions/publish-workflow.test.ts b/tests/unit/templates/github-actions/publish-workflow.test.ts index bdca32f..442dea2 100644 --- a/tests/unit/templates/github-actions/publish-workflow.test.ts +++ b/tests/unit/templates/github-actions/publish-workflow.test.ts @@ -92,7 +92,7 @@ describe('github-actions/publish-workflow', () => { expect(workflow).toContain('environment: prod'); }); - it('should include chained needs hints in comments for sequential deployment', () => { + it('should include chained needs hints in comments for sequential opt-in deployment', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'staging', 'prod'], @@ -102,11 +102,13 @@ describe('github-actions/publish-workflow', () => { expect(workflow).toContain('needs: [get-commit, publish-staging]'); }); - it('should have all environment jobs depend on get-commit', () => { + it('should use get-commit as the base needs dependency for all environments', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'prod'], }); + // Both environments depend on get-commit as their base needs + expect(workflow).toContain('publish-dev:\n # Automatically deploys'); expect(workflow).toContain('needs: get-commit'); }); From d9170d6b14a6fba48715d83a4ca2dd86329b922e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 21:32:28 +0000 Subject: [PATCH 3/5] fix: add sequential promotion with approval guidance to both CI templates GitHub Actions: - All environments now run in sequence on push to main (chained with needs) - Subsequent env jobs have needs: [get-commit, publish-{prevEnv}] for sequential promotion - Non-first environments include a comment directing users to configure Required Reviewers in GitHub Settings > Environments > {env} - This enables the "Review deployments" approval button flow on push Azure DevOps: - Fix dependsOn placement: moved inside stage definition (was incorrectly placed before the `- stage:` entry in the YAML list) - Non-first stages now include a comment directing users to configure approval checks in Pipelines > Environments > {env} Tests updated to reflect all environments running on push and the new approval guidance comments. Closes #46 Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/af96f6ab-4bfa-485c-8b3e-a19edca3cb60 Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com> --- .../azure-devops/publish-pipeline.ts | 12 +++++-- .../github-actions/publish-workflow.ts | 20 ++++++----- .../azure-devops/publish-pipeline.test.ts | 12 +++++++ .../github-actions/publish-workflow.test.ts | 35 ++++++++++--------- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/templates/azure-devops/publish-pipeline.ts b/src/templates/azure-devops/publish-pipeline.ts index 573c715..54cc139 100644 --- a/src/templates/azure-devops/publish-pipeline.ts +++ b/src/templates/azure-devops/publish-pipeline.ts @@ -12,10 +12,16 @@ export function generatePublishPipeline(config: PublishPipelineConfig): string { const envValues = config.environments.map((env) => ` - '${env}'`).join('\n'); const stages = config.environments.map((env, idx) => { - const dependsOn = idx === 0 ? '' : ` dependsOn: Publish_${config.environments[idx - 1]}\n`; + const prevEnv = idx > 0 ? config.environments[idx - 1] : null; + const dependsOnProp = prevEnv ? ` dependsOn: Publish_${prevEnv}\n` : ''; + const approvalComment = prevEnv + ? ` # To require human approval before this stage: + # Go to Pipelines > Environments > ${env} in Azure DevOps and add an approval check. +` + : ''; - return `${dependsOn}- stage: Publish_${env} - displayName: 'Publish to ${env}' + return `- stage: Publish_${env} +${dependsOnProp}${approvalComment} displayName: 'Publish to ${env}' condition: or(eq('\${{ parameters.ENVIRONMENT }}', '${env}'), eq('\${{ parameters.ENVIRONMENT }}', 'all')) variables: - group: apim-${env} diff --git a/src/templates/github-actions/publish-workflow.ts b/src/templates/github-actions/publish-workflow.ts index 427bd33..59d2a9c 100644 --- a/src/templates/github-actions/publish-workflow.ts +++ b/src/templates/github-actions/publish-workflow.ts @@ -12,22 +12,24 @@ export function generatePublishWorkflow(config: PublishWorkflowConfig): string { const envChoices = config.environments.map((env) => ` - ${env}`).join('\n'); const envJobs = config.environments.map((env, idx) => { - const autoDeployComment = idx === 0 + const prevEnv = idx > 0 ? config.environments[idx - 1] : null; + const needs = prevEnv ? `[get-commit, publish-${prevEnv}]` : 'get-commit'; + + const jobComment = idx === 0 ? ` # Automatically deploys to ${env} on push to main (incremental mode) or when selected via workflow_dispatch` - : ` # To enable sequential deployment on push, uncomment the condition below and update needs: - # if: github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push' - # needs: [get-commit, publish-${config.environments[idx - 1]}]`; + : ` # Deploys to ${env} after ${prevEnv} succeeds (sequential promotion). + # To require human approval before deploying to ${env}: + # 1. Go to Settings > Environments > ${env} in your GitHub repository + # 2. Add "Required reviewers" under "Environment protection rules"`; - const jobCondition = idx === 0 - ? `github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push'` - : `github.event.inputs.ENVIRONMENT == '${env}'`; + const jobCondition = `github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push'`; return ` publish-${env}: -${autoDeployComment} +${jobComment} if: ${jobCondition} runs-on: ubuntu-latest environment: ${env} - needs: get-commit + needs: ${needs} steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/tests/unit/templates/azure-devops/publish-pipeline.test.ts b/tests/unit/templates/azure-devops/publish-pipeline.test.ts index 010176f..a187ae7 100644 --- a/tests/unit/templates/azure-devops/publish-pipeline.test.ts +++ b/tests/unit/templates/azure-devops/publish-pipeline.test.ts @@ -209,5 +209,17 @@ describe('azure-devops/publish-pipeline', () => { expect(pipeline).toContain('npm ci'); expect(pipeline).toContain('npx apiops publish'); }); + + it('should include approval guidance comment for non-first stages', () => { + const pipeline = generatePublishPipeline({ + artifactDir: './apim-artifacts', + environments: ['dev', 'staging', 'prod'], + }); + // Non-first stages should have a comment guiding users to configure approval checks + expect(pipeline).toContain('Pipelines > Environments > staging'); + expect(pipeline).toContain('Pipelines > Environments > prod'); + // First stage (dev) should not mention the dev environment in an approval context + expect(pipeline).not.toContain('Pipelines > Environments > dev'); + }); }); }); diff --git a/tests/unit/templates/github-actions/publish-workflow.test.ts b/tests/unit/templates/github-actions/publish-workflow.test.ts index 442dea2..26a5c55 100644 --- a/tests/unit/templates/github-actions/publish-workflow.test.ts +++ b/tests/unit/templates/github-actions/publish-workflow.test.ts @@ -92,23 +92,22 @@ describe('github-actions/publish-workflow', () => { expect(workflow).toContain('environment: prod'); }); - it('should include chained needs hints in comments for sequential opt-in deployment', () => { + it('should chain subsequent environment jobs on the previous environment', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'staging', 'prod'], }); - // Chaining hints appear as comments for opt-in sequential deployment + // Subsequent environments depend on both get-commit and the previous env job expect(workflow).toContain('needs: [get-commit, publish-dev]'); expect(workflow).toContain('needs: [get-commit, publish-staging]'); }); - it('should use get-commit as the base needs dependency for all environments', () => { + it('should have first environment depend on get-commit only', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'prod'], }); - // Both environments depend on get-commit as their base needs - expect(workflow).toContain('publish-dev:\n # Automatically deploys'); + // First env uses simple `needs: get-commit`; subsequent envs use array form with chaining expect(workflow).toContain('needs: get-commit'); }); @@ -216,23 +215,25 @@ describe('github-actions/publish-workflow', () => { expect(workflow).toContain("ENVIRONMENT == 'dev' || github.event_name == 'push'"); }); - it('should not auto-trigger subsequent environments on push to main', () => { + it('should enable all environments to run on push for sequential promotion', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'staging', 'prod'], }); - // staging and prod must NOT include the push trigger in their active if-conditions - // (they may appear in comments but not as live conditions) - const lines = workflow.split('\n'); + // All environments must run on push so the "Review deployments" approval flow works + expect(workflow).toContain("ENVIRONMENT == 'dev' || github.event_name == 'push'"); + expect(workflow).toContain("ENVIRONMENT == 'staging' || github.event_name == 'push'"); + expect(workflow).toContain("ENVIRONMENT == 'prod' || github.event_name == 'push'"); + }); - for (const env of ['staging', 'prod']) { - const jobStart = lines.findIndex((l) => l.includes(`publish-${env}:`)); - // Find the actual `if:` line (not a comment) within the next 10 lines - const jobLines = lines.slice(jobStart, jobStart + 10); - const ifLine = jobLines.find((l) => l.trimStart().startsWith('if:') && !l.trimStart().startsWith('#')); - expect(ifLine).toBeDefined(); - expect(ifLine).not.toContain('event_name'); - } + it('should include approval guidance comment for non-first environments', () => { + const workflow = generatePublishWorkflow({ + artifactDir: './apim-artifacts', + environments: ['dev', 'staging'], + }); + // Non-first environments should have a comment guiding users to set up required reviewers + expect(workflow).toContain('Required reviewers'); + expect(workflow).toContain('Settings > Environments > staging'); }); it('should pass commit_id on push trigger via incremental step condition', () => { From 8f3db026943236934b4caee7175619e03d27f123 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 21:33:46 +0000 Subject: [PATCH 4/5] chore: rename prevEnv to previousEnvironment per code review feedback Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/af96f6ab-4bfa-485c-8b3e-a19edca3cb60 Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com> --- src/templates/azure-devops/publish-pipeline.ts | 6 +++--- src/templates/github-actions/publish-workflow.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/templates/azure-devops/publish-pipeline.ts b/src/templates/azure-devops/publish-pipeline.ts index 54cc139..d5c906a 100644 --- a/src/templates/azure-devops/publish-pipeline.ts +++ b/src/templates/azure-devops/publish-pipeline.ts @@ -12,9 +12,9 @@ export function generatePublishPipeline(config: PublishPipelineConfig): string { const envValues = config.environments.map((env) => ` - '${env}'`).join('\n'); const stages = config.environments.map((env, idx) => { - const prevEnv = idx > 0 ? config.environments[idx - 1] : null; - const dependsOnProp = prevEnv ? ` dependsOn: Publish_${prevEnv}\n` : ''; - const approvalComment = prevEnv + const previousEnvironment = idx > 0 ? config.environments[idx - 1] : null; + const dependsOnProp = previousEnvironment ? ` dependsOn: Publish_${previousEnvironment}\n` : ''; + const approvalComment = previousEnvironment ? ` # To require human approval before this stage: # Go to Pipelines > Environments > ${env} in Azure DevOps and add an approval check. ` diff --git a/src/templates/github-actions/publish-workflow.ts b/src/templates/github-actions/publish-workflow.ts index 59d2a9c..7632f21 100644 --- a/src/templates/github-actions/publish-workflow.ts +++ b/src/templates/github-actions/publish-workflow.ts @@ -12,12 +12,12 @@ export function generatePublishWorkflow(config: PublishWorkflowConfig): string { const envChoices = config.environments.map((env) => ` - ${env}`).join('\n'); const envJobs = config.environments.map((env, idx) => { - const prevEnv = idx > 0 ? config.environments[idx - 1] : null; - const needs = prevEnv ? `[get-commit, publish-${prevEnv}]` : 'get-commit'; + const previousEnvironment = idx > 0 ? config.environments[idx - 1] : null; + const needs = previousEnvironment ? `[get-commit, publish-${previousEnvironment}]` : 'get-commit'; const jobComment = idx === 0 ? ` # Automatically deploys to ${env} on push to main (incremental mode) or when selected via workflow_dispatch` - : ` # Deploys to ${env} after ${prevEnv} succeeds (sequential promotion). + : ` # Deploys to ${env} after ${previousEnvironment} succeeds (sequential promotion). # To require human approval before deploying to ${env}: # 1. Go to Settings > Environments > ${env} in your GitHub repository # 2. Add "Required reviewers" under "Environment protection rules"`; From 30bfcceabb1e701541de1c8826c232e56f813b3b Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Thu, 21 May 2026 22:46:28 +0000 Subject: [PATCH 5/5] adding optional human check after push and before publish pipeline runs. --- src/services/identity-guide-service.ts | 4 +- .../copilot/identity-setup-prompt.ts | 87 ++++++++++++++++++- .../github-actions/publish-workflow.ts | 4 +- .../services/identity-guide-service.test.ts | 12 +++ .../copilot/identity-setup-prompt.test.ts | 9 ++ .../github-actions/publish-workflow.test.ts | 8 +- 6 files changed, 113 insertions(+), 11 deletions(-) diff --git a/src/services/identity-guide-service.ts b/src/services/identity-guide-service.ts index fabd7bc..72d9d4c 100644 --- a/src/services/identity-guide-service.ts +++ b/src/services/identity-guide-service.ts @@ -382,7 +382,9 @@ done rm -f env-body.json \`\`\` -**Note:** Environment approvals and checks must be configured via the Azure DevOps UI (Project Settings > Environments). +**To require human approval before deploying to an environment:** +1. Go to **Pipelines > Environments > ** in Azure DevOps. +2. Open **Approvals and checks** and add an **Approvals** check with the required approvers. --- diff --git a/src/templates/copilot/identity-setup-prompt.ts b/src/templates/copilot/identity-setup-prompt.ts index 60d9dce..83397ed 100644 --- a/src/templates/copilot/identity-setup-prompt.ts +++ b/src/templates/copilot/identity-setup-prompt.ts @@ -48,6 +48,43 @@ gh secret set APIM_RESOURCE_GROUP_${env.toUpperCase()} --body "\${APIM_RG_${env. gh secret set APIM_SERVICE_NAME_${env.toUpperCase()} --body "\${APIM_NAME_${env.toUpperCase()}}" --env ${env}` ).join('\n\n'); + const ghEnvironmentSetupCmds = config.environments.map((env) => + `# ${env} environment: create + baseline protection via GitHub API +cat > env-${env}-config.json <<'JSON' +{ + "wait_timer": 0, + "prevent_self_review": true, + "deployment_branch_policy": { + "protected_branches": false, + "custom_branch_policies": true + } +} +JSON + +gh api --method PUT "repos/\${GITHUB_ORG}/\${GITHUB_REPO}/environments/${env}" --input env-${env}-config.json + +# Restrict deployments to main branch +gh api --method POST "repos/\${GITHUB_ORG}/\${GITHUB_REPO}/environments/${env}/deployment-branch-policies" -f name='main' -f type='branch' + +# Optional: require deployment approval reviewers (replace IDs with your approver IDs) +# Resolve a user ID: gh api "users/" --jq .id +cat > env-${env}-reviewers.json <<'JSON' +{ + "wait_timer": 0, + "prevent_self_review": true, + "reviewers": [ + { "type": "User", "id": 12345678 } + ], + "deployment_branch_policy": { + "protected_branches": false, + "custom_branch_policies": true + } +} +JSON + +gh api --method PUT "repos/\${GITHUB_ORG}/\${GITHUB_REPO}/environments/${env}" --input env-${env}-reviewers.json` + ).join('\n\n'); + return `# Setup GitHub Actions Identity for APIOps > **How to use:** Open this file in VS Code with GitHub Copilot and ask @@ -238,15 +275,59 @@ ${envFedCreds} --- -## Step 5 — Create GitHub Environments +## Step 5 — Configure GitHub Environments with CLI (Create + Protection + Approvals) + +> Copilot: run these command-line commands (do not use manual UI-only setup) to create environments and configure protection rules. +**On macOS/Linux (Bash):** \`\`\`bash +${ghEnvironmentSetupCmds} +\`\`\` + +**On Windows (PowerShell):** +\`\`\`powershell ${config.environments.map((env) => - `# Create the ${env} environment (requires GitHub CLI) -gh api --method PUT "repos/\${GITHUB_ORG}/\${GITHUB_REPO}/environments/${env}"` + `# ${env} environment: create + baseline protection via GitHub API +@' +{ + "wait_timer": 0, + "prevent_self_review": true, + "deployment_branch_policy": { + "protected_branches": false, + "custom_branch_policies": true + } +} +'@ | Set-Content -Path env-${env}-config.json + +gh api --method PUT "repos/\${GITHUB_ORG}/\${GITHUB_REPO}/environments/${env}" --input env-${env}-config.json + +# Restrict deployments to main branch +gh api --method POST "repos/\${GITHUB_ORG}/\${GITHUB_REPO}/environments/${env}/deployment-branch-policies" -f name='main' -f type='branch' + +# Optional: require deployment approval reviewers (replace IDs with your approver IDs) +# Resolve a user ID: gh api "users/" --jq .id +@' +{ + "wait_timer": 0, + "prevent_self_review": true, + "reviewers": [ + { "type": "User", "id": 12345678 } + ], + "deployment_branch_policy": { + "protected_branches": false, + "custom_branch_policies": true + } +} +'@ | Set-Content -Path env-${env}-reviewers.json + +gh api --method PUT "repos/\${GITHUB_ORG}/\${GITHUB_REPO}/environments/${env}" --input env-${env}-reviewers.json` ).join('\n\n')} \`\`\` +> Rerun note: environment PUT calls are idempotent, but branch-policy creation can return "already exists" on reruns after the first successful create; treat that as expected when main is already configured. + +> If reviewer configuration is restricted by repository plan/policy, keep environment creation and branch-policy commands in CLI and apply required reviewers using the same API payload once policy allows it. + --- ## Step 6 — Set GitHub Repository Secrets diff --git a/src/templates/github-actions/publish-workflow.ts b/src/templates/github-actions/publish-workflow.ts index 7632f21..875e0c5 100644 --- a/src/templates/github-actions/publish-workflow.ts +++ b/src/templates/github-actions/publish-workflow.ts @@ -18,9 +18,7 @@ export function generatePublishWorkflow(config: PublishWorkflowConfig): string { const jobComment = idx === 0 ? ` # Automatically deploys to ${env} on push to main (incremental mode) or when selected via workflow_dispatch` : ` # Deploys to ${env} after ${previousEnvironment} succeeds (sequential promotion). - # To require human approval before deploying to ${env}: - # 1. Go to Settings > Environments > ${env} in your GitHub repository - # 2. Add "Required reviewers" under "Environment protection rules"`; + # Configure environment protection rules to require approval before deploying to ${env}.`; const jobCondition = `github.event.inputs.ENVIRONMENT == '${env}' || github.event_name == 'push'`; diff --git a/tests/unit/services/identity-guide-service.test.ts b/tests/unit/services/identity-guide-service.test.ts index 9e36c83..24e0a51 100644 --- a/tests/unit/services/identity-guide-service.test.ts +++ b/tests/unit/services/identity-guide-service.test.ts @@ -126,5 +126,17 @@ describe('identity-guide-service', () => { expect(guide).toContain('my-rg'); }); + it('should include Azure DevOps approvals and checks guidance for human approval', () => { + const guide = identityGuideService.generateAzureDevOpsGuide( + 'sub-12345', + 'my-rg', + ['dev', 'prod'] + ); + expect(guide).toContain('Pipelines > Environments > '); + expect(guide).toContain('Approvals and checks'); + expect(guide).toContain('Approvals'); + expect(guide).toContain('required approvers'); + }); + }); }); diff --git a/tests/unit/templates/copilot/identity-setup-prompt.test.ts b/tests/unit/templates/copilot/identity-setup-prompt.test.ts index 6879003..4cbb6c8 100644 --- a/tests/unit/templates/copilot/identity-setup-prompt.test.ts +++ b/tests/unit/templates/copilot/identity-setup-prompt.test.ts @@ -66,6 +66,15 @@ describe('copilot/identity-setup-prompt', () => { expect(prompt).toContain('gh api --method PUT'); expect(prompt).toContain('environments/dev'); expect(prompt).toContain('environments/prod'); + expect(prompt).toContain('Configure GitHub Environments with CLI'); + expect(prompt).toContain('do not use manual UI-only setup'); + expect(prompt).toContain('deployment-branch-policies'); + expect(prompt).toContain('prevent_self_review'); + expect(prompt).toContain('"reviewers"'); + expect(prompt).toContain('gh api "users/" --jq .id'); + expect(prompt).toContain("gh api --method POST \"repos/${GITHUB_ORG}/${GITHUB_REPO}/environments/dev/deployment-branch-policies\""); + expect(prompt).toContain("gh api --method POST \"repos/${GITHUB_ORG}/${GITHUB_REPO}/environments/prod/deployment-branch-policies\""); + expect(prompt).toContain("-f name='main' -f type='branch'"); }); it('should include gh secret set commands for repository secrets', () => { diff --git a/tests/unit/templates/github-actions/publish-workflow.test.ts b/tests/unit/templates/github-actions/publish-workflow.test.ts index 26a5c55..748935a 100644 --- a/tests/unit/templates/github-actions/publish-workflow.test.ts +++ b/tests/unit/templates/github-actions/publish-workflow.test.ts @@ -226,14 +226,14 @@ describe('github-actions/publish-workflow', () => { expect(workflow).toContain("ENVIRONMENT == 'prod' || github.event_name == 'push'"); }); - it('should include approval guidance comment for non-first environments', () => { + it('should include generic approval guidance for non-first environments without GitHub settings steps', () => { const workflow = generatePublishWorkflow({ artifactDir: './apim-artifacts', environments: ['dev', 'staging'], }); - // Non-first environments should have a comment guiding users to set up required reviewers - expect(workflow).toContain('Required reviewers'); - expect(workflow).toContain('Settings > Environments > staging'); + expect(workflow).toContain('Configure environment protection rules to require approval before deploying to staging.'); + expect(workflow).not.toContain('Settings > Environments > staging'); + expect(workflow).not.toContain('Required reviewers'); }); it('should pass commit_id on push trigger via incremental step condition', () => {