diff --git a/.github/workflows/base-deploy.yml b/.github/workflows/base-deploy.yml new file mode 100644 index 000000000..086272309 --- /dev/null +++ b/.github/workflows/base-deploy.yml @@ -0,0 +1,259 @@ +name: Base Deploy + +on: + workflow_call: + inputs: + environment: + description: "Target environment (preprod | prod)" + required: true + type: string + ref: + description: "Git ref to deploy (branch/tag/SHA). For prod, supply the RC tag to promote." + required: true + type: string + release_type: + description: "Version bump for base version (preprod only: patch|minor|major)" + required: false + default: "patch" + type: string + secrets: {} + +jobs: + metadata: + name: "Set CI/CD metadata" + runs-on: ubuntu-latest + timeout-minutes: 2 + outputs: + build_datetime: ${{ steps.variables.outputs.build_datetime }} + build_timestamp: ${{ steps.variables.outputs.build_timestamp }} + build_epoch: ${{ steps.variables.outputs.build_epoch }} + nodejs_version: ${{ steps.variables.outputs.nodejs_version }} + python_version: ${{ steps.variables.outputs.python_version }} + terraform_version: ${{ steps.variables.outputs.terraform_version }} + ref: ${{ steps.variables.outputs.ref }} + environment: ${{ steps.variables.outputs.environment }} + steps: + - name: "Checkout ref" + uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 # get full history + tags + + - name: "Set CI/CD variables" + id: variables + shell: bash + run: | + set -euo pipefail + datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') + echo "build_datetime=$datetime" >> $GITHUB_OUTPUT + echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT + echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT + echo "nodejs_version=$(grep -E '^nodejs' .tool-versions 2>/dev/null | cut -d' ' -f2 | head -n1)" >> $GITHUB_OUTPUT + echo "python_version=$(grep -E '^python' .tool-versions 2>/dev/null | cut -d' ' -f2 | head -n1)" >> $GITHUB_OUTPUT + echo "terraform_version=$(grep -E '^terraform' .tool-versions 2>/dev/null | cut -d' ' -f2 | head -n1)" >> $GITHUB_OUTPUT + echo "ref=${{ inputs.ref }}" >> $GITHUB_OUTPUT + echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT + + - name: "List variables" + shell: bash + run: | + export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}" + export BUILD_TIMESTAMP="${{ steps.variables.outputs.build_timestamp }}" + export BUILD_EPOCH="${{ steps.variables.outputs.build_epoch }}" + export NODEJS_VERSION="${{ steps.variables.outputs.nodejs_version }}" + export PYTHON_VERSION="${{ steps.variables.outputs.python_version }}" + export TERRAFORM_VERSION="${{ steps.variables.outputs.terraform_version }}" + export REF="${{ steps.variables.outputs.ref }}" + export ENVIRONMENT="${{ steps.variables.outputs.environment }}" + echo "build_datetime=$BUILD_DATETIME" + echo "build_timestamp=$BUILD_TIMESTAMP" + echo "build_epoch=$BUILD_EPOCH" + echo "nodejs_version=$NODEJS_VERSION" + echo "python_version=$PYTHON_VERSION" + echo "terraform_version=$TERRAFORM_VERSION" + echo "ref=$REF" + echo "environment=$ENVIRONMENT" + + deploy: + name: "Deploy to ${{ needs.metadata.outputs.environment }}" + runs-on: ubuntu-latest + needs: [metadata] + timeout-minutes: 45 + permissions: + id-token: write + contents: write + environment: ${{ needs.metadata.outputs.environment }} + steps: + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.outputs.terraform_version }} + + - name: "Set up Python" + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: "Checkout repository at ref" + uses: actions/checkout@v5 + with: + ref: ${{ needs.metadata.outputs.ref }} + fetch-depth: 0 + + - name: "Build lambda artefact" + shell: bash + run: | + make dependencies install-python + make build + + - name: "Upload lambda artefact" + uses: actions/upload-artifact@v4 + with: + name: lambda + path: dist/lambda.zip + + - name: "Download Built Lambdas" + uses: actions/download-artifact@v5 + with: + name: lambda + path: ./build + + - name: "Configure AWS Credentials" + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + aws-region: eu-west-2 + + - name: "Terraform Apply" + env: + ENVIRONMENT: ${{ needs.metadata.outputs.environment }} + WORKSPACE: "default" + TF_VAR_API_CA_CERT: ${{ secrets.API_CA_CERT }} + TF_VAR_API_CLIENT_CERT: ${{ secrets.API_CLIENT_CERT }} + TF_VAR_API_PRIVATE_KEY_CERT: ${{ secrets.API_PRIVATE_KEY_CERT }} + working-directory: ./infrastructure + shell: bash + run: | + set -euo pipefail + mkdir -p ./build + echo "Running: make terraform env=$ENVIRONMENT workspace=$WORKSPACE stack=networking tf-command=apply" + make terraform env=$ENVIRONMENT stack=networking tf-command=apply workspace=$WORKSPACE + echo "Running: make terraform env=$ENVIRONMENT workspace=$WORKSPACE stack=api-layer tf-command=apply" + make terraform env=$ENVIRONMENT stack=api-layer tf-command=apply workspace=$WORKSPACE + + # ---------- Preprod path: create RC tag + pre-release ---------- + - name: "Create/Push RC tag for preprod" + if: ${{ needs.metadata.outputs.environment == 'preprod' }} + id: rc_tag + shell: bash + run: | + set -euo pipefail + git fetch --tags + + # Helper: get latest final and latest RC (across all bases) + latest_final="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1 || true)" + latest_any_rc="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*-rc.*' \ + | sort -V | tail -n1 || true)" + + # Determine the base version (vX.Y.Z) we will use for the next RC. + # If release_type=rc and we already have RCs, keep the SAME base as the latest RC. + # Otherwise, derive base from latest FINAL and bump per release_type. + if [[ "${{ inputs.release_type }}" == "rc" && -n "${latest_any_rc}" ]]; then + base="${latest_any_rc%-rc.*}" # strip '-rc.N' → vX.Y.Z + else + # Start from latest FINAL (or 0.0.0 if none) + if [[ -z "${latest_final}" ]]; then + base_major=0; base_minor=0; base_patch=0 + else + IFS='.' read -r base_major base_minor base_patch <<< "${latest_final#v}" + fi + + case "${{ inputs.release_type }}" in + major) base_major=$((base_major+1)); base_minor=0; base_patch=0 ;; + minor) base_minor=$((base_minor+1)); base_patch=0 ;; + patch|rc|*) base_patch=$((base_patch+1)) ;; # 'rc' with no prior RCs → default to patch bump + esac + + base="v${base_major}.${base_minor}.${base_patch}" + fi + + # Compute next RC number for this base + last_rc_for_base="$(git tag -l "${base}-rc.*" | sort -V | tail -n1 || true)" + if [[ -z "${last_rc_for_base}" ]]; then + next_rc="${base}-rc.1" + else + n="${last_rc_for_base##*-rc.}" + next_rc="${base}-rc.$((n+1))" + fi + + # Tag current commit (whatever ref was checked out) + sha="$(git rev-parse HEAD)" + echo "Tagging ${sha} as ${next_rc}" + git tag -a "${next_rc}" "${sha}" -m "Release candidate ${next_rc}" + git push origin "${next_rc}" + + echo "rc=${next_rc}" >> "$GITHUB_OUTPUT" + + - name: "Create GitHub Pre-release (preprod)" + if: ${{ needs.metadata.outputs.environment == 'preprod' }} + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.rc_tag.outputs.rc }} + release_name: "Pre-release ${{ steps.rc_tag.outputs.rc }}" + body: | + Auto pre-release created during preprod deployment. + draft: false + prerelease: true + + # ---------- Prod path: promote RC to final ---------- + - name: "Validate input is an RC tag (prod)" + if: ${{ needs.metadata.outputs.environment == 'prod' }} + shell: bash + run: | + set -euo pipefail + ref="${{ needs.metadata.outputs.ref }}" + if [[ ! "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then + echo "ERROR: For prod, 'ref' must be an RC tag like v1.4.0-rc.2 (got: $ref)" + exit 1 + fi + git fetch --tags --quiet + if ! git rev-parse -q --verify "refs/tags/$ref" >/dev/null; then + echo "ERROR: Tag '$ref' does not exist on origin." + exit 1 + fi + + - name: "Create final tag from RC (prod)" + if: ${{ needs.metadata.outputs.environment == 'prod' }} + id: final_tag + shell: bash + run: | + set -euo pipefail + rc="${{ needs.metadata.outputs.ref }}" + final="${rc%-rc.*}" # strip '-rc.N' + sha=$(git rev-list -n 1 "$rc") + + if git rev-parse -q --verify "refs/tags/${final}" >/dev/null; then + echo "ERROR: Final tag ${final} already exists." + exit 1 + fi + + echo "Promoting $rc ($sha) to final $final" + git tag -a "${final}" "${sha}" -m "Release ${final}" + git push origin "${final}" + echo "final=${final}" >> $GITHUB_OUTPUT + + - name: "Create GitHub Release (prod)" + if: ${{ needs.metadata.outputs.environment == 'prod' }} + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.final_tag.outputs.final }} + release_name: "Release ${{ steps.final_tag.outputs.final }}" + body: | + Auto-release created during production deployment. + draft: false + prerelease: false diff --git a/.github/workflows/cicd-3-deploy.yaml b/.github/workflows/cicd-3-deploy.yaml deleted file mode 100644 index 96e2fe61c..000000000 --- a/.github/workflows/cicd-3-deploy.yaml +++ /dev/null @@ -1,204 +0,0 @@ -# Deploys a given tag to a given environment and tags for semantic versioning -# creates semantic release - -name: "CI/CD deploy" - -concurrency: - group: terraform-deploy-${{ github.event.inputs.environment }} - cancel-in-progress: false - -on: - workflow_dispatch: - inputs: - tag: - description: "This is the tag that is going to be deployed" - required: true - default: "latest" - environment: - description: "Target environment (e.g., test, preprod or prod)" - required: true - type: choice - options: - - preprod - - prod - release_type: - description: "Version bump type (patch, minor, major)" - required: false - default: "patch" - type: choice - options: - - patch - - minor - - major - -jobs: - metadata: - name: "Set CI/CD metadata" - runs-on: ubuntu-latest - timeout-minutes: 1 - outputs: - build_datetime: ${{ steps.variables.outputs.build_datetime }} - build_timestamp: ${{ steps.variables.outputs.build_timestamp }} - build_epoch: ${{ steps.variables.outputs.build_epoch }} - nodejs_version: ${{ steps.variables.outputs.nodejs_version }} - python_version: ${{ steps.variables.outputs.python_version }} - terraform_version: ${{ steps.variables.outputs.terraform_version }} - version: ${{ steps.variables.outputs.version }} - tag: ${{ steps.variables.outputs.tag }} - steps: - - name: "Checkout tag" - uses: actions/checkout@v5 - with: - ref: ${{ github.event.inputs.tag }} - - - name: "Set CI/CD variables" - id: variables - run: | - datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') - echo "build_datetime=$datetime" >> $GITHUB_OUTPUT - echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT - echo "nodejs_version=$(grep "^nodejs" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "python_version=$(grep "^nodejs" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "terraform_version=$(grep "^terraform" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - # TODO: Get the version, but it may not be the .version file as this should come from the CI/CD Pull Request Workflow - echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT - echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT - - name: "List variables" - run: | - export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}" - export BUILD_TIMESTAMP="${{ steps.variables.outputs.build_timestamp }}" - export BUILD_EPOCH="${{ steps.variables.outputs.build_epoch }}" - export NODEJS_VERSION="${{ steps.variables.outputs.nodejs_version }}" - export PYTHON_VERSION="${{ steps.variables.outputs.python_version }}" - export TERRAFORM_VERSION="${{ steps.variables.outputs.terraform_version }}" - export VERSION="${{ steps.variables.outputs.version }}" - export TAG="${{ steps.variables.outputs.tag }}" - make list-variables - deploy: - name: "Deploy to an environment" - runs-on: ubuntu-latest - needs: [metadata] - environment: ${{ inputs.environment }} - timeout-minutes: 30 - permissions: - id-token: write - contents: write - steps: - - name: "Setup Terraform" - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: ${{ needs.metadata.outputs.terraform_version }} - - - name: "Set up Python" - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: "Checkout Repository" - uses: actions/checkout@v5 - - - name: "Build lambda artefact" - run: | - make dependencies install-python - make build - - - name: "Upload lambda artefact" - uses: actions/upload-artifact@v4 - with: - name: lambda - path: dist/lambda.zip - - - name: "Download Built Lambdas" - uses: actions/download-artifact@v5 - with: - name: lambda - path: ./build - - - name: "Configure AWS Credentials" - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role - aws-region: eu-west-2 - - - name: "Terraform Apply" - env: - ENVIRONMENT: ${{ inputs.environment }} - WORKSPACE: "default" - TF_VAR_API_CA_CERT: ${{ secrets.API_CA_CERT }} - TF_VAR_API_CLIENT_CERT: ${{ secrets.API_CLIENT_CERT }} - TF_VAR_API_PRIVATE_KEY_CERT: ${{ secrets.API_PRIVATE_KEY_CERT }} - - # just planning for now for safety and until review - run: | - mkdir -p ./build - echo "Running: make terraform env=$ENVIRONMENT workspace=$WORKSPACE stack=networking tf-command=apply" - make terraform env=$ENVIRONMENT stack=networking tf-command=apply workspace=$WORKSPACE - echo "Running: make terraform env=$ENVIRONMENT workspace=$WORKSPACE stack=api-layer tf-command=apply" - make terraform env=$ENVIRONMENT stack=api-layer tf-command=apply workspace=$WORKSPACE - working-directory: ./infrastructure - - - name: "Tag the deployment using incremental semantic versioning" - id: next_tag - run: | - # Fetch all tags and sort them semantically - git fetch --tags - latest_tag=$(git tag --list 'v*' | sort -V | tail -n 1) - echo "Latest tag: $latest_tag" - - if [[ -z "$latest_tag" ]]; then - next_tag="v0.1.0" - else - # Extract the version numbers - IFS='.' read -r major minor patch <<< "${latest_tag#v}" - case "${{ github.event.inputs.release_type }}" in - major) - major=$((major + 1)) - minor=0 - patch=0 - ;; - minor) - minor=$((minor + 1)) - patch=0 - ;; - patch|*) - patch=$((patch + 1)) - ;; - esac - - next_tag="v${major}.${minor}.${patch}" - fi - - echo "Next tag: $next_tag" - echo "tag=$next_tag" >> $GITHUB_OUTPUT - - - name: "Create GitHub Release" - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.next_tag.outputs.tag }} - release_name: Release ${{ steps.next_tag.outputs.tag }} - body: | - Auto-release created during deployment. - draft: false - prerelease: ${{ inputs.environment == 'preprod' }} - - # TODO: complete notify step - # success: - # name: "Success notification" - # runs-on: ubuntu-latest - # needs: [deploy] - # steps: - # - name: "Check prerequisites for notification" - # id: check - # run: echo "secret_exist=${{ secrets.TEAMS_NOTIFICATION_WEBHOOK_URL != '' }}" >> $GITHUB_OUTPUT - # - name: "Notify on deployment to an environment" - # if: steps.check.outputs.secret_exist == 'true' - # uses: nhs-england-tools/notify-msteams-action@v0.0.4 - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} - # teams-webhook-url: ${{ secrets.TEAMS_NOTIFICATION_WEBHOOK_URL }} - # message-title: "Notification title" - # message-text: "This is a notification body" - # link: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/cicd-4-test.yaml b/.github/workflows/cicd-3-test.yaml similarity index 100% rename from .github/workflows/cicd-4-test.yaml rename to .github/workflows/cicd-3-test.yaml diff --git a/.github/workflows/cicd-4-preprod-deploy.yml b/.github/workflows/cicd-4-preprod-deploy.yml new file mode 100644 index 000000000..bbb9ba082 --- /dev/null +++ b/.github/workflows/cicd-4-preprod-deploy.yml @@ -0,0 +1,32 @@ +name: Preprod Deploy + +concurrency: + group: terraform-deploy-preprod + cancel-in-progress: false + +on: + workflow_dispatch: + inputs: + ref: + description: "Branch/Tag/SHA to deploy to preprod (dev tag)" + required: true + default: "main" + release_type: + description: "Version bump type (use 'rc' to keep the same base and just increment RC)" + required: true + default: "rc" + type: choice + options: + - rc + - patch + - minor + - major + +jobs: + call: + uses: ./.github/workflows/base-deploy.yml + with: + environment: preprod + ref: ${{ inputs.ref }} + release_type: ${{ inputs.release_type }} + secrets: inherit diff --git a/.github/workflows/cicd-5-prod-deploy.yml b/.github/workflows/cicd-5-prod-deploy.yml new file mode 100644 index 000000000..90edf55e4 --- /dev/null +++ b/.github/workflows/cicd-5-prod-deploy.yml @@ -0,0 +1,20 @@ +name: Prod Promote + +concurrency: + group: terraform-deploy-prod + cancel-in-progress: false + +on: + workflow_dispatch: + inputs: + ref: + description: "RC tag to promote (e.g. v1.4.0-rc.2)" + required: true + +jobs: + call: + uses: ./.github/workflows/base-deploy.yml + with: + environment: prod + ref: ${{ inputs.ref }} + secrets: inherit diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 000000000..a410a0023 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,179 @@ +# Deployment & Release Process + +This repo uses GitHub Actions to deploy through four environments: + +- **Dev** – continuous integration deploys on push to `main`. Creates a `Dev-` tag. +- **test** – manual deploy of an existing tag (usually a `Dev-` tag). No releases or SemVer tags. +- **Preprod** – manual deploy that **cuts/bumps a Release Candidate (RC)** tag (`vX.Y.Z-rc.N`) and creates a **GitHub pre-release**. +- **prod** – manual promotion of a specific RC to a **final SemVer tag** (`vX.Y.Z`) and a **GitHub Release**. + +Releases are immutable and auditable: + +- **RC tags** live only in Preprod. +- **Final tags** (`vX.Y.Z`) live only in prod and point to the exact commit that shipped. + +--- + +## Workflow Map + +| Stage | Workflow file | Trigger | What it does | Tags / Releases | +|------------------|-------------------------------------------------------------------------|---------------------------------------|-----------------------------------------------------------------|---------------------------------------------| +| **Pull Request** | `.github/workflows/cicd-1-pull-request.yml` | `pull_request` (opened/sync/reopened) | Commit/Test/Build/Acceptance stages | No tags/releases | +| **Dev** | `.github/workflows/cicd-2-publish.yml` | `push` to `main` | Builds & deploys to Dev | Creates and pushes `Dev-YYYYMMDDHHMMSS` tag | +| **Test** | `.github/workflows/cicd-3-test.yml` | Manual (`workflow_dispatch`) | Deploys the chosen tag to test | No tags, no releases | +| **Preprod** | `.github/workflows/cicd-4-Preprod-deploy.yml` → calls `base-deploy.yml` | Manual (`workflow_dispatch`) | Deploys chosen ref and **creates/bumps an RC tag**; pre-release | `vX.Y.Z-rc.N` + GitHub **pre-release** | +| **Prod** | `.github/workflows/cicd-5-prod-deploy.yml` → calls `base-deploy.yml` | Manual (`workflow_dispatch`) | Promotes a specific RC to final | `vX.Y.Z` + GitHub **Release** | + +> **Note:** The Preprod/prod entry workflows are thin wrappers around a **reusable** workflow (`base-deploy.yml`). + +--- + +## Pull Request Workflow (CI) + +**File:** `CI/CD pull request` +**Trigger:** PR events (not drafts) + +- Runs stages: **commit → test → build → acceptance** via reusable stage files: + - `stage-1-commit.yaml` + - `stage-2-test.yaml` + - `stage-3-build.yaml` + - `stage-4-acceptance.yaml` +- Does not deploy or create tags/releases. + +--- + +## Dev Deployment (continuous on main) + +**File:** `CI/CD publish` +**Trigger:** push to `main` + +- Builds & deploys to **Dev**. +- Creates a timestamped **Dev tag**: `Dev-YYYYMMDDHHMMSS` +- No SemVer, no GitHub Release. + +**Why:** fast feedback and a stable pointer (the Dev tag) you can later promote to **test** or use as the **Preprod ref**. + +--- + +## Test Deployment (manual, by tag) + +**File:** `CI/CD deploy to TEST` +**Trigger:** manual (`workflow_dispatch`) + +### Inputs + +- `tag`: the ref to deploy (e.g., a **Dev** tag created by the Dev workflow). +- `environment`: fixed to `test`. + +### Behavior + +- Checks out the provided tag, builds, and deploys to **test**. +- **No new tags** created. **No GitHub Releases** created. + +### Recommended usage + +- Deploy the **same commit** that was verified in Dev by supplying the `Dev-` tag here. + +--- + +## Preprod (Release Candidates) + +**Entry workflow:** `cicd-4-Preprod-deploy.yml` → calls `base-deploy.yml` +**Trigger:** manual (`workflow_dispatch`) + +### Inputs + +- **`ref`**: branch/tag/SHA to deploy (`Dev-` tag). +- **`release_type`**: one of: + - `patch` – start a new **patch** series → `vX.Y.(Z+1)-rc.1` + - `minor` – start a new **minor** series → `vX.(Y+1).0-rc.1` + - `major` – start a new **major** series → `v(X+1).0.0-rc.1` + - `rc` – **keep the same base** version and cut the **next RC** (e.g. `-rc.1` → `-rc.2`) + +### Behavior + +- Tags the **checked-out commit** as the next **RC** (`vX.Y.Z-rc.N`) and pushes it. +- Deploys to **Preprod**. +- Creates a **GitHub pre-release** for that RC. + +### When to use which `release_type` + +- Use **`patch`/`minor`/`major`** when starting a **new base version** (first RC becomes `-rc.1`). +- Use **`rc`** when you need another candidate for the **same base** (`-rc.N+1`). + +--- + +## Prod (Final Releases) + +**Entry workflow:** `cicd-5-prod-deploy.yml` → calls `base-deploy.yml` +**Trigger:** manual (`workflow_dispatch`) + +### Inputs + +- **`ref`**: the **RC tag** to promote (e.g. `v1.4.0-rc.2`). + +### Behavior + +- Validates the RC tag exists. +- Creates the corresponding **final tag** (e.g. `v1.4.0`) at the **same commit**. +- Deploys to **prod**. +- Creates a **GitHub Release**. + +--- + +## Decision Guide (what to pick, when) + +| Situation | Workflow | Input: `ref` | Input: `release_type` | Result | +|-------------------------------------------------|----------------------|------------------------------|-----------------------|----------------------------------------------| +| Deploy automatically after merge to main | **Dev** (auto) | `main` (implicit) | n/a | Deploys to Dev, creates `Dev-YYYYMMDDHHMMSS` | +| Deploy an existing build to test | **Test** (manual) | a tag (e.g. `Dev-20250817…`) | n/a | Deploys to test (no tags/releases) | +| Start a **new patch release** into Preprod | **Preprod** (manual) | branch/tag/SHA | `patch` | `vX.Y.(Z+1)-rc.1` + pre-release | +| Start a **new minor release** into Preprod | **Preprod** | branch/tag/SHA | `minor` | `vX.(Y+1).0-rc.1` + pre-release | +| Start a **new major release** into Preprod | **Preprod** | branch/tag/SHA | `major` | `v(X+1).0.0-rc.1` + pre-release | +| Cut **another candidate** for the **same base** | **Preprod** | branch/tag/SHA (same train) | `rc` | `vX.Y.Z-rc.N+1` + pre-release | +| Promote a **tested RC** to production | **Prod** (manual) | RC tag (e.g. `v1.4.0-rc.2`) | n/a | `v1.4.0` + GitHub Release | + +--- + +## Versioning Rules + +- **Dev tags**: `Dev-YYYYMMDDHHMMSS` (automation convenience; never promoted to prod directly). +- **RC tags**: `vX.Y.Z-rc.N` (Preprod only; immutable; one per candidate). +- **Final tags**: `vX.Y.Z` (prod only; immutable; exactly what shipped). + +### Promotion path + +- Choose a commit (often via a **Dev tag**) → cut RC(s) in **Preprod** → promote the selected RC to **prod**. + +--- + +## Common Q&A + +**Can I use a Dev tag as the Preprod `ref`?** +Yes. `ref` can be any branch/tag/SHA. Using a `Dev-` tag guarantees you deploy the **same commit** previously built on Dev. + +**What does `rc` mean?** +**Release Candidate**: a build that could become the final version if it passes testing. Each new candidate for the same base version increments the suffix: `-rc.1`, `-rc.2`, … + +**What if I already have `v1.4.0-rc.1` and I need another Preprod build for the same release?** +Run **Preprod Deploy** with `release_type=rc` to get `v1.4.0-rc.2`. + +**What if I discover issues after Preprod?** +Fix, then cut a new RC (`-rc.N+1`). Only promote to prod when ready. + +--- + +## Quick Examples + +### New minor release + +1. Dev (auto) → creates `Dev-20250818…`. +2. Preprod (manual) → `ref=Dev-20250818…`, `release_type=minor` → `v1.4.0-rc.1`. +3. Preprod (manual) → `release_type=rc` → `v1.4.0-rc.2`. +4. Prod (manual) → `ref=v1.4.0-rc.2` → final `v1.4.0`. + +### Same release, more candidates + +- Already at `v1.3.3-rc.1`. +- Preprod → `release_type=rc` → `v1.3.3-rc.2`, test, repeat as needed. +- Prod → promote the stable RC to `v1.3.3`. diff --git a/README.md b/README.md index d27ef9560..6c6e3f546 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The software will only be used for signposting an individual to an appropriate s - [Prerequisites](#prerequisites) - [Configuration](#configuration) - [Environment variables - Local](#environment-variables---local) - - [Environment variables - DEV, PROD or PRE-PROD](#environment-variables---dev-prod-or-pre-prod) + - [Environment variables - Dev, Prod or Preprod](#environment-variables---dev-prod-or-preprod) - [Usage](#usage) - [Testing](#testing) - [Sandbox and Specification](#sandbox-and-specification) @@ -26,6 +26,7 @@ The software will only be used for signposting an individual to an appropriate s - [Design](#design) - [Diagrams](#diagrams) - [Contributing](#contributing) + - [Deployment](#deployment) - [Contacts](#contacts) - [Licence](#licence) @@ -81,7 +82,7 @@ The following software packages, or their equivalents, are expected to be instal | `LOG_LEVEL` | `WARNING` | Logging level. Must be one of `DEBUG`, `INFO`, `WARNING`, `ERROR` or `CRITICAL` as per [Logging Levels](https://docs.python.org/3/library/logging.html#logging-levels) | | `RULES_BUCKET_NAME` | `test-rules-bucket` | AWS S3 bucket from which to read rules. | -#### Environment variables - DEV, PROD or PRE-PROD +#### Environment variables - Dev, Prod or Preprod | Variable | Default | Description | Comments | |-------------------------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| @@ -254,6 +255,10 @@ Describe or link templates on how to raise an issue, feature request or make a c - Backlog, board, roadmap, ways of working - High-level requirements, guiding principles, decision records, etc. +## Deployment + +See [DEPLOYMENT.md](./DEPLOYMENT.md) for details on how to cut release candidates and promote them to production. + ## Contacts Please contact the team on [Slack](https://nhsdigitalcorporate.enterprise.slack.com/archives/C08ATG7TBDW) diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt index 7ed71edb8..603ea003b 100644 --- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt +++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt @@ -29,3 +29,7 @@ wireup Pydantic yanai Portman +repo +Preprod +Dev +auditable