Skip to content

Commit 40778fc

Browse files
authored
Merge pull request #305 from NHSDigital/feature/te-prerelease-handling
switching to pre-release style handling
2 parents 0b70b86 + 06b8d38 commit 40778fc

File tree

8 files changed

+501
-206
lines changed

8 files changed

+501
-206
lines changed

.github/workflows/base-deploy.yml

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
name: Base Deploy
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
environment:
7+
description: "Target environment (preprod | prod)"
8+
required: true
9+
type: string
10+
ref:
11+
description: "Git ref to deploy (branch/tag/SHA). For prod, supply the RC tag to promote."
12+
required: true
13+
type: string
14+
release_type:
15+
description: "Version bump for base version (preprod only: patch|minor|major)"
16+
required: false
17+
default: "patch"
18+
type: string
19+
secrets: {}
20+
21+
jobs:
22+
metadata:
23+
name: "Set CI/CD metadata"
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 2
26+
outputs:
27+
build_datetime: ${{ steps.variables.outputs.build_datetime }}
28+
build_timestamp: ${{ steps.variables.outputs.build_timestamp }}
29+
build_epoch: ${{ steps.variables.outputs.build_epoch }}
30+
nodejs_version: ${{ steps.variables.outputs.nodejs_version }}
31+
python_version: ${{ steps.variables.outputs.python_version }}
32+
terraform_version: ${{ steps.variables.outputs.terraform_version }}
33+
ref: ${{ steps.variables.outputs.ref }}
34+
environment: ${{ steps.variables.outputs.environment }}
35+
steps:
36+
- name: "Checkout ref"
37+
uses: actions/checkout@v5
38+
with:
39+
ref: ${{ inputs.ref }}
40+
fetch-depth: 0 # get full history + tags
41+
42+
- name: "Set CI/CD variables"
43+
id: variables
44+
shell: bash
45+
run: |
46+
set -euo pipefail
47+
datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z')
48+
echo "build_datetime=$datetime" >> $GITHUB_OUTPUT
49+
echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
50+
echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT
51+
echo "nodejs_version=$(grep -E '^nodejs' .tool-versions 2>/dev/null | cut -d' ' -f2 | head -n1)" >> $GITHUB_OUTPUT
52+
echo "python_version=$(grep -E '^python' .tool-versions 2>/dev/null | cut -d' ' -f2 | head -n1)" >> $GITHUB_OUTPUT
53+
echo "terraform_version=$(grep -E '^terraform' .tool-versions 2>/dev/null | cut -d' ' -f2 | head -n1)" >> $GITHUB_OUTPUT
54+
echo "ref=${{ inputs.ref }}" >> $GITHUB_OUTPUT
55+
echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT
56+
57+
- name: "List variables"
58+
shell: bash
59+
run: |
60+
export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}"
61+
export BUILD_TIMESTAMP="${{ steps.variables.outputs.build_timestamp }}"
62+
export BUILD_EPOCH="${{ steps.variables.outputs.build_epoch }}"
63+
export NODEJS_VERSION="${{ steps.variables.outputs.nodejs_version }}"
64+
export PYTHON_VERSION="${{ steps.variables.outputs.python_version }}"
65+
export TERRAFORM_VERSION="${{ steps.variables.outputs.terraform_version }}"
66+
export REF="${{ steps.variables.outputs.ref }}"
67+
export ENVIRONMENT="${{ steps.variables.outputs.environment }}"
68+
echo "build_datetime=$BUILD_DATETIME"
69+
echo "build_timestamp=$BUILD_TIMESTAMP"
70+
echo "build_epoch=$BUILD_EPOCH"
71+
echo "nodejs_version=$NODEJS_VERSION"
72+
echo "python_version=$PYTHON_VERSION"
73+
echo "terraform_version=$TERRAFORM_VERSION"
74+
echo "ref=$REF"
75+
echo "environment=$ENVIRONMENT"
76+
77+
deploy:
78+
name: "Deploy to ${{ needs.metadata.outputs.environment }}"
79+
runs-on: ubuntu-latest
80+
needs: [metadata]
81+
timeout-minutes: 45
82+
permissions:
83+
id-token: write
84+
contents: write
85+
environment: ${{ needs.metadata.outputs.environment }}
86+
steps:
87+
- name: "Setup Terraform"
88+
uses: hashicorp/setup-terraform@v3
89+
with:
90+
terraform_version: ${{ needs.metadata.outputs.terraform_version }}
91+
92+
- name: "Set up Python"
93+
uses: actions/setup-python@v5
94+
with:
95+
python-version: "3.13"
96+
97+
- name: "Checkout repository at ref"
98+
uses: actions/checkout@v5
99+
with:
100+
ref: ${{ needs.metadata.outputs.ref }}
101+
fetch-depth: 0
102+
103+
- name: "Build lambda artefact"
104+
shell: bash
105+
run: |
106+
make dependencies install-python
107+
make build
108+
109+
- name: "Upload lambda artefact"
110+
uses: actions/upload-artifact@v4
111+
with:
112+
name: lambda
113+
path: dist/lambda.zip
114+
115+
- name: "Download Built Lambdas"
116+
uses: actions/download-artifact@v5
117+
with:
118+
name: lambda
119+
path: ./build
120+
121+
- name: "Configure AWS Credentials"
122+
uses: aws-actions/configure-aws-credentials@v4
123+
with:
124+
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role
125+
aws-region: eu-west-2
126+
127+
- name: "Terraform Apply"
128+
env:
129+
ENVIRONMENT: ${{ needs.metadata.outputs.environment }}
130+
WORKSPACE: "default"
131+
TF_VAR_API_CA_CERT: ${{ secrets.API_CA_CERT }}
132+
TF_VAR_API_CLIENT_CERT: ${{ secrets.API_CLIENT_CERT }}
133+
TF_VAR_API_PRIVATE_KEY_CERT: ${{ secrets.API_PRIVATE_KEY_CERT }}
134+
working-directory: ./infrastructure
135+
shell: bash
136+
run: |
137+
set -euo pipefail
138+
mkdir -p ./build
139+
echo "Running: make terraform env=$ENVIRONMENT workspace=$WORKSPACE stack=networking tf-command=apply"
140+
make terraform env=$ENVIRONMENT stack=networking tf-command=apply workspace=$WORKSPACE
141+
echo "Running: make terraform env=$ENVIRONMENT workspace=$WORKSPACE stack=api-layer tf-command=apply"
142+
make terraform env=$ENVIRONMENT stack=api-layer tf-command=apply workspace=$WORKSPACE
143+
144+
# ---------- Preprod path: create RC tag + pre-release ----------
145+
- name: "Create/Push RC tag for preprod"
146+
if: ${{ needs.metadata.outputs.environment == 'preprod' }}
147+
id: rc_tag
148+
shell: bash
149+
run: |
150+
set -euo pipefail
151+
git fetch --tags
152+
153+
# Helper: get latest final and latest RC (across all bases)
154+
latest_final="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' \
155+
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1 || true)"
156+
latest_any_rc="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*-rc.*' \
157+
| sort -V | tail -n1 || true)"
158+
159+
# Determine the base version (vX.Y.Z) we will use for the next RC.
160+
# If release_type=rc and we already have RCs, keep the SAME base as the latest RC.
161+
# Otherwise, derive base from latest FINAL and bump per release_type.
162+
if [[ "${{ inputs.release_type }}" == "rc" && -n "${latest_any_rc}" ]]; then
163+
base="${latest_any_rc%-rc.*}" # strip '-rc.N' → vX.Y.Z
164+
else
165+
# Start from latest FINAL (or 0.0.0 if none)
166+
if [[ -z "${latest_final}" ]]; then
167+
base_major=0; base_minor=0; base_patch=0
168+
else
169+
IFS='.' read -r base_major base_minor base_patch <<< "${latest_final#v}"
170+
fi
171+
172+
case "${{ inputs.release_type }}" in
173+
major) base_major=$((base_major+1)); base_minor=0; base_patch=0 ;;
174+
minor) base_minor=$((base_minor+1)); base_patch=0 ;;
175+
patch|rc|*) base_patch=$((base_patch+1)) ;; # 'rc' with no prior RCs → default to patch bump
176+
esac
177+
178+
base="v${base_major}.${base_minor}.${base_patch}"
179+
fi
180+
181+
# Compute next RC number for this base
182+
last_rc_for_base="$(git tag -l "${base}-rc.*" | sort -V | tail -n1 || true)"
183+
if [[ -z "${last_rc_for_base}" ]]; then
184+
next_rc="${base}-rc.1"
185+
else
186+
n="${last_rc_for_base##*-rc.}"
187+
next_rc="${base}-rc.$((n+1))"
188+
fi
189+
190+
# Tag current commit (whatever ref was checked out)
191+
sha="$(git rev-parse HEAD)"
192+
echo "Tagging ${sha} as ${next_rc}"
193+
git tag -a "${next_rc}" "${sha}" -m "Release candidate ${next_rc}"
194+
git push origin "${next_rc}"
195+
196+
echo "rc=${next_rc}" >> "$GITHUB_OUTPUT"
197+
198+
- name: "Create GitHub Pre-release (preprod)"
199+
if: ${{ needs.metadata.outputs.environment == 'preprod' }}
200+
uses: actions/create-release@v1
201+
env:
202+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
203+
with:
204+
tag_name: ${{ steps.rc_tag.outputs.rc }}
205+
release_name: "Pre-release ${{ steps.rc_tag.outputs.rc }}"
206+
body: |
207+
Auto pre-release created during preprod deployment.
208+
draft: false
209+
prerelease: true
210+
211+
# ---------- Prod path: promote RC to final ----------
212+
- name: "Validate input is an RC tag (prod)"
213+
if: ${{ needs.metadata.outputs.environment == 'prod' }}
214+
shell: bash
215+
run: |
216+
set -euo pipefail
217+
ref="${{ needs.metadata.outputs.ref }}"
218+
if [[ ! "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
219+
echo "ERROR: For prod, 'ref' must be an RC tag like v1.4.0-rc.2 (got: $ref)"
220+
exit 1
221+
fi
222+
git fetch --tags --quiet
223+
if ! git rev-parse -q --verify "refs/tags/$ref" >/dev/null; then
224+
echo "ERROR: Tag '$ref' does not exist on origin."
225+
exit 1
226+
fi
227+
228+
- name: "Create final tag from RC (prod)"
229+
if: ${{ needs.metadata.outputs.environment == 'prod' }}
230+
id: final_tag
231+
shell: bash
232+
run: |
233+
set -euo pipefail
234+
rc="${{ needs.metadata.outputs.ref }}"
235+
final="${rc%-rc.*}" # strip '-rc.N'
236+
sha=$(git rev-list -n 1 "$rc")
237+
238+
if git rev-parse -q --verify "refs/tags/${final}" >/dev/null; then
239+
echo "ERROR: Final tag ${final} already exists."
240+
exit 1
241+
fi
242+
243+
echo "Promoting $rc ($sha) to final $final"
244+
git tag -a "${final}" "${sha}" -m "Release ${final}"
245+
git push origin "${final}"
246+
echo "final=${final}" >> $GITHUB_OUTPUT
247+
248+
- name: "Create GitHub Release (prod)"
249+
if: ${{ needs.metadata.outputs.environment == 'prod' }}
250+
uses: actions/create-release@v1
251+
env:
252+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
253+
with:
254+
tag_name: ${{ steps.final_tag.outputs.final }}
255+
release_name: "Release ${{ steps.final_tag.outputs.final }}"
256+
body: |
257+
Auto-release created during production deployment.
258+
draft: false
259+
prerelease: false

0 commit comments

Comments
 (0)