Skip to content

Commit 20c50df

Browse files
Merge pull request #102 from NHSDigital/DTOSS-12784-tag-driven-release-pipeline
[DTOSS-12784] - Tag-driven release pipeline with smoke tests
2 parents f633eb1 + a86ebd0 commit 20c50df

6 files changed

Lines changed: 465 additions & 74 deletions

File tree

.github/workflows/cicd-2-main-branch.yaml

Lines changed: 123 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@ on:
44
push:
55
branches:
66
- main
7-
tags:
8-
- 'v*'
97
workflow_dispatch:
8+
inputs:
9+
environment:
10+
description: "Target environment for infra + app deployment"
11+
required: true
12+
type: choice
13+
default: dev
14+
options:
15+
- dev
16+
- preprod
17+
- review
1018

1119
concurrency: cicd-${{ github.ref }}
1220

1321
permissions:
14-
contents: write
22+
contents: read
1523
id-token: write
16-
attestations: write
1724
security-events: write
1825

1926
jobs:
@@ -26,101 +33,144 @@ jobs:
2633
uses: ./.github/workflows/stage-2-test.yaml
2734
secrets: inherit
2835

29-
release-stage:
36+
# Resolve the latest release tag once so downstream jobs can consume it.
37+
resolve:
38+
name: Resolve deployment targets
3039
needs: test-stage
31-
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
3240
runs-on: ubuntu-latest
3341
outputs:
34-
new_release_published: ${{ steps.release.outputs.new_release_published }}
35-
new_release_version: ${{ steps.release.outputs.new_release_version }}
42+
release_tag: ${{ steps.tag.outputs.release_tag }}
43+
deploy_dev: ${{ steps.envs.outputs.deploy_dev }}
44+
deploy_preprod: ${{ steps.envs.outputs.deploy_preprod }}
45+
deploy_review: ${{ steps.envs.outputs.deploy_review }}
46+
env:
47+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3648
steps:
37-
- name: Checkout code
38-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
39-
with:
40-
fetch-depth: 0
41-
token: ${{ secrets.GITHUB_TOKEN }}
42-
43-
- name: Read tool versions
44-
id: tool-versions
49+
- name: Compute target environments
50+
id: envs
4551
run: |
46-
echo "python=$(awk '/^python / {print $2}' .tool-versions)" >> "$GITHUB_OUTPUT"
47-
echo "uv=$(awk '/^uv / {print $2}' .tool-versions)" >> "$GITHUB_OUTPUT"
48-
49-
- name: Set up Python
50-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
51-
with:
52-
python-version: ${{ steps.tool-versions.outputs.python }}
53-
54-
- name: Install uv
55-
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
56-
with:
57-
version: ${{ steps.tool-versions.outputs.uv }}
58-
enable-cache: true
59-
60-
- name: Run semantic-release
61-
id: release
62-
env:
63-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
INPUT="${{ inputs.environment }}"
53+
case "$INPUT" in
54+
dev)
55+
echo "deploy_dev=true" >> "$GITHUB_OUTPUT"
56+
echo "deploy_preprod=false" >> "$GITHUB_OUTPUT"
57+
echo "deploy_review=false" >> "$GITHUB_OUTPUT"
58+
;;
59+
preprod)
60+
echo "deploy_dev=false" >> "$GITHUB_OUTPUT"
61+
echo "deploy_preprod=true" >> "$GITHUB_OUTPUT"
62+
echo "deploy_review=false" >> "$GITHUB_OUTPUT"
63+
;;
64+
review)
65+
echo "deploy_dev=false" >> "$GITHUB_OUTPUT"
66+
echo "deploy_preprod=false" >> "$GITHUB_OUTPUT"
67+
echo "deploy_review=true" >> "$GITHUB_OUTPUT"
68+
;;
69+
*)
70+
# push-to-main (empty input): deploy to both dev and preprod
71+
echo "deploy_dev=true" >> "$GITHUB_OUTPUT"
72+
echo "deploy_preprod=true" >> "$GITHUB_OUTPUT"
73+
echo "deploy_review=false" >> "$GITHUB_OUTPUT"
74+
;;
75+
esac
76+
77+
- name: Resolve latest release tag
78+
id: tag
6479
run: |
65-
uv pip install python-semantic-release --system
80+
LATEST_TAG=$(gh release list \
81+
--repo "${{ github.repository }}" \
82+
--limit 1 \
83+
--json tagName --jq '.[0].tagName' 2>/dev/null || echo "")
84+
if [[ -z "$LATEST_TAG" ]]; then
85+
echo "::error::No GitHub Release exists. Push a v* tag to create the first release."
86+
exit 1
87+
fi
88+
echo "release_tag=${LATEST_TAG}" >> "$GITHUB_OUTPUT"
89+
echo "Latest release: ${LATEST_TAG}"
6690
67-
# Detect next version (--print exits without side effects)
68-
VERSION=$(semantic-release version --print 2>/dev/null) || true
91+
# ---- dev ------------------------------------------------------------------
6992

70-
if [[ -n "$VERSION" ]] && ! git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then
71-
echo "Next version detected: $VERSION"
93+
deploy-infra-dev:
94+
name: Deploy infra (dev)
95+
needs: resolve
96+
if: needs.resolve.outputs.deploy_dev == 'true'
97+
permissions:
98+
id-token: write
99+
uses: ./.github/workflows/stage-4-deploy.yaml
100+
with:
101+
environments: '["dev"]'
102+
commit_sha: ${{ github.sha }}
103+
secrets: inherit
72104

73-
# Create and push the tag (no commit needed, tag push is not blocked by branch protection)
74-
git tag "v$VERSION"
75-
git push origin "v$VERSION"
105+
deploy-app-dev:
106+
name: Deploy app (dev)
107+
needs: [resolve, deploy-infra-dev]
108+
if: needs.resolve.outputs.deploy_dev == 'true'
109+
permissions:
110+
id-token: write
111+
uses: ./.github/workflows/stage-4-deploy-app.yaml
112+
with:
113+
environments: '["dev"]'
114+
release_tag: ${{ needs.resolve.outputs.release_tag }}
115+
commit_sha: ${{ github.sha }}
116+
secrets: inherit
76117

77-
echo "new_release_published=true" >> "$GITHUB_OUTPUT"
78-
echo "new_release_version=v$VERSION" >> "$GITHUB_OUTPUT"
79-
else
80-
echo "No new release needed (version: ${VERSION:-none}, tag may already exist)"
81-
echo "new_release_published=false" >> "$GITHUB_OUTPUT"
82-
echo "new_release_version=" >> "$GITHUB_OUTPUT"
83-
fi
118+
# ---- preprod --------------------------------------------------------------
119+
120+
deploy-infra-preprod:
121+
name: Deploy infra (preprod)
122+
needs: [resolve, deploy-app-dev]
123+
if: |
124+
always() &&
125+
needs.resolve.outputs.deploy_preprod == 'true' &&
126+
(needs.deploy-app-dev.result == 'success' || needs.deploy-app-dev.result == 'skipped')
127+
permissions:
128+
id-token: write
129+
uses: ./.github/workflows/stage-4-deploy.yaml
130+
with:
131+
environments: '["preprod"]'
132+
commit_sha: ${{ github.sha }}
133+
secrets: inherit
84134

85-
build-stage:
86-
needs: [test-stage, release-stage]
135+
deploy-app-preprod:
136+
name: Deploy app (preprod)
137+
needs: [resolve, deploy-infra-preprod]
87138
if: |
88139
always() &&
89-
needs.test-stage.result == 'success' &&
90-
(
91-
github.ref_type == 'tag' ||
92-
needs.release-stage.outputs.new_release_published == 'true'
93-
)
94-
uses: ./.github/workflows/stage-3-build.yaml
140+
needs.resolve.outputs.deploy_preprod == 'true' &&
141+
needs.deploy-infra-preprod.result == 'success'
142+
permissions:
143+
id-token: write
144+
uses: ./.github/workflows/stage-4-deploy-app.yaml
95145
with:
96-
version: ${{ needs.release-stage.outputs.new_release_version }}
97-
new_release_published: ${{ needs.release-stage.outputs.new_release_published == 'true' }}
146+
environments: '["preprod"]'
147+
release_tag: ${{ needs.resolve.outputs.release_tag }}
148+
commit_sha: ${{ github.sha }}
98149
secrets: inherit
99150

100-
deploy-stage:
101-
name: Deploy stage
102-
needs: [commit-stage, test-stage]
151+
# ---- review (manual only) -------------------------------------------------
152+
153+
deploy-infra-review:
154+
name: Deploy infra (review)
155+
needs: resolve
156+
if: needs.resolve.outputs.deploy_review == 'true'
103157
permissions:
104158
id-token: write
105159
uses: ./.github/workflows/stage-4-deploy.yaml
106160
with:
107-
environments: '["review", "dev", "preprod"]'
161+
environments: '["review"]'
108162
commit_sha: ${{ github.sha }}
109163
secrets: inherit
110164

111-
deploy-app-stage:
112-
name: Deploy app stage
113-
needs: [build-stage, release-stage]
114-
# Only deploy when a new release was published — no release means no deployable artifact.
115-
if: |
116-
always() &&
117-
needs.build-stage.result == 'success' &&
118-
needs.release-stage.outputs.new_release_published == 'true'
165+
deploy-app-review:
166+
name: Deploy app (review)
167+
needs: [resolve, deploy-infra-review]
168+
if: needs.resolve.outputs.deploy_review == 'true'
119169
permissions:
120170
id-token: write
121171
uses: ./.github/workflows/stage-4-deploy-app.yaml
122172
with:
123-
environments: '["dev", "preprod"]'
124-
release_tag: ${{ needs.release-stage.outputs.new_release_version }}
173+
environments: '["review"]'
174+
release_tag: ${{ needs.resolve.outputs.release_tag }}
125175
commit_sha: ${{ github.sha }}
126176
secrets: inherit
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: "CI/CD: Release"
2+
3+
# Prod requires a manual approval — configure required reviewers in GitHub
4+
# Settings → Environments → prod before provisioning the prod environment.
5+
6+
on:
7+
push:
8+
tags:
9+
- 'v*'
10+
11+
concurrency: release-${{ github.ref }}
12+
13+
permissions:
14+
contents: write
15+
id-token: write
16+
attestations: write
17+
security-events: write
18+
19+
jobs:
20+
commit-stage:
21+
uses: ./.github/workflows/stage-1-commit.yaml
22+
secrets: inherit
23+
24+
test-stage:
25+
needs: commit-stage
26+
uses: ./.github/workflows/stage-2-test.yaml
27+
secrets: inherit
28+
29+
build-stage:
30+
needs: test-stage
31+
uses: ./.github/workflows/stage-3-build.yaml
32+
with:
33+
version: ${{ github.ref_name }}
34+
new_release_published: true
35+
secrets: inherit
36+
37+
deploy-dev:
38+
name: Deploy (dev)
39+
needs: build-stage
40+
uses: ./.github/workflows/stage-4-deploy-env.yaml
41+
with:
42+
environment: dev
43+
release_tag: ${{ github.ref_name }}
44+
commit_sha: ${{ github.sha }}
45+
secrets: inherit
46+
47+
deploy-preprod:
48+
name: Deploy (preprod)
49+
needs: deploy-dev
50+
uses: ./.github/workflows/stage-4-deploy-env.yaml
51+
with:
52+
environment: preprod
53+
release_tag: ${{ github.ref_name }}
54+
commit_sha: ${{ github.sha }}
55+
secrets: inherit
56+
57+
prod-approval:
58+
name: Awaiting prod approval
59+
needs: deploy-preprod
60+
runs-on: ubuntu-latest
61+
environment: prod
62+
steps:
63+
- name: Approved
64+
run: echo "Prod deployment approved - proceeding"
65+
66+
deploy-prod:
67+
name: Deploy (prod)
68+
needs: prod-approval
69+
uses: ./.github/workflows/stage-4-deploy-env.yaml
70+
with:
71+
environment: prod
72+
release_tag: ${{ github.ref_name }}
73+
commit_sha: ${{ github.sha }}
74+
secrets: inherit
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: "Stage 4: Deploy to environment"
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
environment:
7+
description: Target environment (dev / preprod / prod)
8+
required: true
9+
type: string
10+
release_tag:
11+
description: GitHub release tag to deploy (e.g. v1.2.3)
12+
required: true
13+
type: string
14+
commit_sha:
15+
description: Commit SHA used to trigger the ADO pipeline
16+
required: true
17+
type: string
18+
19+
jobs:
20+
deploy-infra:
21+
name: Deploy infra
22+
permissions:
23+
id-token: write
24+
uses: ./.github/workflows/stage-4-deploy.yaml
25+
with:
26+
environments: '["${{ inputs.environment }}"]'
27+
commit_sha: ${{ inputs.commit_sha }}
28+
secrets: inherit
29+
30+
deploy-app:
31+
name: Deploy app
32+
needs: deploy-infra
33+
permissions:
34+
id-token: write
35+
uses: ./.github/workflows/stage-4-deploy-app.yaml
36+
with:
37+
environments: '["${{ inputs.environment }}"]'
38+
release_tag: ${{ inputs.release_tag }}
39+
commit_sha: ${{ inputs.commit_sha }}
40+
secrets: inherit
41+
42+
smoke-test:
43+
name: Smoke test
44+
needs: deploy-app
45+
runs-on: ubuntu-latest
46+
environment: ${{ inputs.environment }}
47+
permissions:
48+
id-token: write
49+
steps:
50+
- name: Checkout code
51+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
52+
53+
- name: Azure login
54+
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
55+
with:
56+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
57+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
58+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
59+
60+
- name: Run smoke test
61+
run: bash scripts/bash/smoke_test.sh ${{ inputs.environment }}

0 commit comments

Comments
 (0)