Skip to content

Commit 496a5dd

Browse files
Merge branch 'main' into ELI-387-lambda-hardening
2 parents 90be688 + c7f4587 commit 496a5dd

File tree

10 files changed

+516
-212
lines changed

10 files changed

+516
-212
lines changed

.github/workflows/base-deploy.yml

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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+
- name: "Set up git identity"
145+
if: ${{ needs.metadata.outputs.environment == 'preprod' || needs.metadata.outputs.environment == 'prod' }}
146+
run: |
147+
git config user.name "github-actions"
148+
git config user.email "[email protected]"
149+
150+
# ---------- Preprod path: create RC tag + pre-release ----------
151+
- name: "Create/Push RC tag for preprod"
152+
if: ${{ needs.metadata.outputs.environment == 'preprod' }}
153+
id: rc_tag
154+
shell: bash
155+
run: |
156+
set -euo pipefail
157+
git fetch --tags
158+
159+
# Helper: get latest final and latest RC (across all bases)
160+
latest_final="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' \
161+
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1 || true)"
162+
latest_any_rc="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*-rc.*' \
163+
| sort -V | tail -n1 || true)"
164+
165+
# Determine the base version (vX.Y.Z) we will use for the next RC.
166+
# If release_type=rc and we already have RCs, keep the SAME base as the latest RC.
167+
# Otherwise, derive base from latest FINAL and bump per release_type.
168+
if [[ "${{ inputs.release_type }}" == "rc" && -n "${latest_any_rc}" ]]; then
169+
base="${latest_any_rc%-rc.*}" # strip '-rc.N' → vX.Y.Z
170+
else
171+
# Start from latest FINAL (or 0.0.0 if none)
172+
if [[ -z "${latest_final}" ]]; then
173+
base_major=0; base_minor=0; base_patch=0
174+
else
175+
IFS='.' read -r base_major base_minor base_patch <<< "${latest_final#v}"
176+
fi
177+
178+
case "${{ inputs.release_type }}" in
179+
major) base_major=$((base_major+1)); base_minor=0; base_patch=0 ;;
180+
minor) base_minor=$((base_minor+1)); base_patch=0 ;;
181+
patch|rc|*) base_patch=$((base_patch+1)) ;; # 'rc' with no prior RCs → default to patch bump
182+
esac
183+
184+
base="v${base_major}.${base_minor}.${base_patch}"
185+
fi
186+
187+
# Compute next RC number for this base
188+
last_rc_for_base="$(git tag -l "${base}-rc.*" | sort -V | tail -n1 || true)"
189+
if [[ -z "${last_rc_for_base}" ]]; then
190+
next_rc="${base}-rc.1"
191+
else
192+
n="${last_rc_for_base##*-rc.}"
193+
next_rc="${base}-rc.$((n+1))"
194+
fi
195+
196+
# Tag current commit (whatever ref was checked out)
197+
sha="$(git rev-parse HEAD)"
198+
echo "Tagging ${sha} as ${next_rc}"
199+
git tag -a "${next_rc}" "${sha}" -m "Release candidate ${next_rc}"
200+
git push origin "${next_rc}"
201+
202+
echo "rc=${next_rc}" >> "$GITHUB_OUTPUT"
203+
204+
- name: "Create GitHub Pre-release (preprod)"
205+
if: ${{ needs.metadata.outputs.environment == 'preprod' }}
206+
uses: actions/create-release@v1
207+
env:
208+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
209+
with:
210+
tag_name: ${{ steps.rc_tag.outputs.rc }}
211+
release_name: "Pre-release ${{ steps.rc_tag.outputs.rc }}"
212+
body: |
213+
Auto pre-release created during preprod deployment.
214+
draft: false
215+
prerelease: true
216+
217+
# ---------- Prod path: promote RC to final ----------
218+
- name: "Validate input is an RC tag (prod)"
219+
if: ${{ needs.metadata.outputs.environment == 'prod' }}
220+
shell: bash
221+
run: |
222+
set -euo pipefail
223+
ref="${{ needs.metadata.outputs.ref }}"
224+
if [[ ! "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
225+
echo "ERROR: For prod, 'ref' must be an RC tag like v1.4.0-rc.2 (got: $ref)"
226+
exit 1
227+
fi
228+
git fetch --tags --quiet
229+
if ! git rev-parse -q --verify "refs/tags/$ref" >/dev/null; then
230+
echo "ERROR: Tag '$ref' does not exist on origin."
231+
exit 1
232+
fi
233+
234+
- name: "Create final tag from RC (prod)"
235+
if: ${{ needs.metadata.outputs.environment == 'prod' }}
236+
id: final_tag
237+
shell: bash
238+
run: |
239+
set -euo pipefail
240+
rc="${{ needs.metadata.outputs.ref }}"
241+
final="${rc%-rc.*}" # strip '-rc.N'
242+
sha=$(git rev-list -n 1 "$rc")
243+
244+
if git rev-parse -q --verify "refs/tags/${final}" >/dev/null; then
245+
echo "ERROR: Final tag ${final} already exists."
246+
exit 1
247+
fi
248+
249+
echo "Promoting $rc ($sha) to final $final"
250+
git tag -a "${final}" "${sha}" -m "Release ${final}"
251+
git push origin "${final}"
252+
echo "final=${final}" >> $GITHUB_OUTPUT
253+
254+
- name: "Create GitHub Release (prod)"
255+
if: ${{ needs.metadata.outputs.environment == 'prod' }}
256+
uses: actions/create-release@v1
257+
env:
258+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
259+
with:
260+
tag_name: ${{ steps.final_tag.outputs.final }}
261+
release_name: "Release ${{ steps.final_tag.outputs.final }}"
262+
body: |
263+
Auto-release created during production deployment.
264+
draft: false
265+
prerelease: false

0 commit comments

Comments
 (0)