diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 56d556bbdd..87d66f9af0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "args": { "DOCKER_GID": "${env:DOCKER_GID:}", "IMAGE_NAME": "node_24_python_3_14", - "IMAGE_VERSION": "v1.2.0", + "IMAGE_VERSION": "v1.4.4", "USER_UID": "${localEnv:USER_ID:}", "USER_GID": "${localEnv:GROUP_ID:}" } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..0492a66516 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# restrict access to approving workflow changes +.github/workflows/ @NHSDigital/eps-admins diff --git a/.github/actions/mark_jira_released/action.yml b/.github/actions/mark_jira_released/action.yml deleted file mode 100644 index 8318b4916e..0000000000 --- a/.github/actions/mark_jira_released/action.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Create confluence release notes" -description: "Do release note actions in confluence and jira" -inputs: - RELEASE_TAG: - required: false - description: "The tag we are marking as released in jira" - DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: - required: true - description: "The role to assume to execute the release notes lambda" - -runs: - using: "composite" - steps: - - name: connect to dev account - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 - with: - aws-region: eu-west-2 - role-to-assume: ${{ inputs.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }} - role-session-name: prescription-clinical-tracker-ui-release-notes-run-lambda - - - name: call markJiraReleased lambda - shell: bash - working-directory: .github/scripts - env: - RELEASE_TAG: ${{ inputs.RELEASE_TAG }} - run: ./call_mark_jira_released.sh diff --git a/.github/actions/update_confluence_jira/action.yml b/.github/actions/update_confluence_jira/action.yml deleted file mode 100644 index 90bff965b6..0000000000 --- a/.github/actions/update_confluence_jira/action.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: "Create confluence release notes" -description: "Do release note actions in confluence and jira" -inputs: - TARGET_ENVIRONMENT: - required: true - description: "Target Environment" - RELEASE_TAG: - required: false - description: "The tag we are releasing - only used for create_rc_release_notes" - CONFLUENCE_PAGE_ID: - required: true - description: "The id of confluence page to update or create under" - CREATE_RC_RELEASE_NOTES: - required: true - description: "whether to create rc release notes page instead of normal release notes" - default: "false" - DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: - required: true - description: "The role to assume to execute the release notes lambda" - DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE: - required: true - description: "The dev cloud formation deploy role" - TARGET_CLOUD_FORMATION_CHECK_VERSION_ROLE: - required: true - description: "The target cloud formation deploy role" - -runs: - using: "composite" - steps: - - name: connect to target account - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 - with: - aws-region: eu-west-2 - role-to-assume: ${{ inputs.TARGET_CLOUD_FORMATION_CHECK_VERSION_ROLE }} - role-session-name: prescription-clinical-tracker-ui-release-notes-target - - - name: Get deployed tag on target - shell: bash - working-directory: .github/scripts - env: - TARGET_ENVIRONMENT: ${{ inputs.TARGET_ENVIRONMENT }} - run: ./get_target_deployed_tag.sh - - - name: connect to dev account - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 - with: - aws-region: eu-west-2 - role-to-assume: ${{ inputs.DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE }} - role-session-name: prescription-clinical-tracker-ui-release-notes-dev - - - name: get current dev tag - shell: bash - working-directory: .github/scripts - run: ./get_current_dev_tag.sh - - - name: connect to dev account to run release notes lambda - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 - with: - aws-region: eu-west-2 - role-to-assume: ${{ inputs.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }} - role-session-name: prescription-clinical-tracker-ui-release-notes-run-lambda - unset-current-credentials: true - - - name: create int release notes - shell: bash - working-directory: .github/scripts - if: inputs.TARGET_ENVIRONMENT == 'int' && inputs.CREATE_RC_RELEASE_NOTES == 'false' - env: - ENV: INT - PAGE_ID: ${{ inputs.CONFLUENCE_PAGE_ID }} - run: ./create_env_release_notes.sh - - - name: create int rc release notes - shell: bash - working-directory: .github/scripts - if: inputs.TARGET_ENVIRONMENT == 'int' && inputs.CREATE_RC_RELEASE_NOTES == 'true' - env: - RELEASE_TAG: ${{ inputs.RELEASE_TAG }} - PAGE_ID: ${{ inputs.CONFLUENCE_PAGE_ID }} - run: ./create_int_rc_release_notes.sh - - - name: create prod release notes - shell: bash - working-directory: .github/scripts - if: inputs.TARGET_ENVIRONMENT == 'prod' - env: - ENV: PROD - PAGE_ID: ${{ inputs.CONFLUENCE_PAGE_ID }} - run: ./create_env_release_notes.sh diff --git a/.github/workflows/cdk_package_code.yml b/.github/workflows/cdk_package_code.yml index 69d4d8db45..6d89354686 100644 --- a/.github/workflows/cdk_package_code.yml +++ b/.github/workflows/cdk_package_code.yml @@ -13,6 +13,7 @@ on: required: true type: string +permissions: {} jobs: package_code: runs-on: ubuntu-22.04 @@ -33,7 +34,7 @@ jobs: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - ref: ${{ env.BRANCH_NAME }} + persist-credentials: false - name: make install run: | make install diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e6975a433..a697d22cef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,19 +4,26 @@ on: push: branches: [main] -env: - BRANCH_NAME: ${{ github.event.ref.BRANCH_NAME }} +permissions: {} jobs: get_config_values: - uses: NHSDigital/eps-common-workflows/.github/workflows/get-repo-config.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/get-repo-config.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 with: verify_published_from_main_image: true + permissions: + attestations: read + contents: read + packages: read quality_checks: - uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 needs: [get_config_values] with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + permissions: + contents: read + id-token: write + packages: read secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -32,20 +39,24 @@ jobs: tag_release: needs: [quality_checks, get_commit_id, get_config_values] - uses: NHSDigital/eps-common-workflows/.github/workflows/tag-release-devcontainer.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/tag-release-devcontainer.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 permissions: id-token: write contents: write + packages: write with: dry_run: true pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} branch_name: main tag_format: ${{ needs.get_config_values.outputs.tag_format }} - secrets: inherit package_code: needs: [tag_release, get_commit_id, get_config_values] uses: ./.github/workflows/cdk_package_code.yml + permissions: + contents: read + packages: read + id-token: write with: VERSION_NUMBER: ${{needs.tag_release.outputs.version_tag}} COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} @@ -54,6 +65,9 @@ jobs: release_dev: needs: [tag_release, package_code, get_commit_id, get_config_values] uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui @@ -93,6 +107,9 @@ jobs: release_qa: needs: [tag_release, release_dev, package_code, get_commit_id, get_config_values] uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui diff --git a/.github/workflows/delete_old_cloudformation_stacks.yml b/.github/workflows/delete_old_cloudformation_stacks.yml index 367f618435..12e41a81c8 100644 --- a/.github/workflows/delete_old_cloudformation_stacks.yml +++ b/.github/workflows/delete_old_cloudformation_stacks.yml @@ -6,24 +6,21 @@ on: schedule: - cron: "20 * * * *" -# A workflow run is made up of one or more jobs that can run sequentially or in parallel +permissions: {} jobs: - # This workflow contains a single job called "combine-prs" delete-old-cloudformation-stacks: - # The type of runner that the job will run on runs-on: ubuntu-22.04 permissions: id-token: write contents: read - # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Checkout local github scripts uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - ref: ${{ env.BRANCH_NAME }} sparse-checkout: | .github/scripts + persist-credentials: false - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 diff --git a/.github/workflows/link_dev_website.yml b/.github/workflows/link_dev_website.yml deleted file mode 100644 index b80092d4b0..0000000000 --- a/.github/workflows/link_dev_website.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Link Dev Website -on: - pull_request: - types: [opened] -jobs: - get_issue_number: - runs-on: ubuntu-22.04 - outputs: - issue_number: ${{steps.get_issue_number.outputs.result}} - - steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - name: get issue number - id: get_issue_number - with: - script: | - if (context.issue.number) { - // Return issue number if present - return context.issue.number; - } else { - // Otherwise return issue number from commit - return ( - await github.rest.repos.listPullRequestsAssociatedWithCommit({ - commit_sha: context.sha, - owner: context.repo.owner, - repo: context.repo.repo, - }) - ).data[0].number; - } - result-encoding: string - - link-dev-website: - needs: [get_issue_number] - runs-on: ubuntu-22.04 - continue-on-error: true - steps: - - name: Link Dev Website - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: unsplash/comment-on-pr@b5610c6125a7197eaec80072ea35ef53e1fc6035 - with: - msg: | - Deployed URL: https://cpt-ui-pr-${{ needs.get_issue_number.outputs.issue_number }}.dev.eps.national.nhs.uk/site diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cc5bec54fd..5ec1cee49b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -4,30 +4,39 @@ on: pull_request: branches: [main] -env: - BRANCH_NAME: ${{ github.event.pull_request.head.ref }} +permissions: {} jobs: get_config_values: - uses: NHSDigital/eps-common-workflows/.github/workflows/get-repo-config.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/get-repo-config.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 with: verify_published_from_main_image: false + permissions: + attestations: read + contents: read + packages: read dependabot-auto-approve-and-merge: needs: quality_checks - uses: NHSDigital/eps-common-workflows/.github/workflows/dependabot-auto-approve-and-merge.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/dependabot-auto-approve-and-merge.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 + permissions: + contents: write + pull-requests: write secrets: AUTOMERGE_APP_ID: ${{ secrets.AUTOMERGE_APP_ID }} AUTOMERGE_PEM: ${{ secrets.AUTOMERGE_PEM }} get_commit_message: runs-on: ubuntu-22.04 + permissions: + contents: read outputs: commit_message: ${{ steps.commit_message.outputs.commit_message }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - ref: ${{ env.BRANCH_NAME }} - fetch-depth: 0 + persist-credentials: false + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} - name: Get Commit message id: commit_message run: | @@ -36,9 +45,13 @@ jobs: quality_checks: # always run, but only block in the non-skip case needs: [get_commit_message, get_config_values] - uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + permissions: + contents: read + id-token: write + packages: read secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -63,42 +76,63 @@ jobs: const maxRetries = 120; // 20 minutes at 10 seconds each let attempts = 0; - async function fetchQCJob() { + async function fetchQCJobs() { const { data } = await github.rest.actions.listJobsForWorkflowRun({ owner, repo, run_id: runId }); - return data.jobs.find(job => job.name === 'quality_checks / quality_checks'); + return data.jobs.filter(job => job.name.startsWith('quality_checks')); } - let qc = await fetchQCJob(); - while ((!qc || qc.status !== 'completed') && attempts < maxRetries) { + let qcJobs = await fetchQCJobs(); + while (attempts < maxRetries) { + const allCompleted = qcJobs.length > 0 && qcJobs.every(qc => qc.status === 'completed'); + if (allCompleted) { + break; + } + attempts++; - console.log(`Attempt #${attempts}: ` + - (qc - ? `found job “${qc.name}” with status=${qc.status}` - : 'no matching quality_checks job yet')); + + if (qcJobs.length === 0) { + console.log(`Attempt #${attempts}: no matching quality_checks jobs yet`); + } else { + const incompleteJobs = qcJobs + .filter(qc => qc.status !== 'completed') + .map(qc => `“${qc.name}” status=${qc.status}`) + .join(', '); + console.log(`Attempt #${attempts}: waiting for quality_checks jobs to complete: ${incompleteJobs}`); + } + + if (attempts >= maxRetries) { + break; + } + await new Promise(r => setTimeout(r, pollTime)); - qc = await fetchQCJob(); + qcJobs = await fetchQCJobs(); } - if (!qc) { + if (qcJobs.length === 0) { core.setFailed( `Timed out waiting for a “quality_checks” job (after ${attempts} polls).` ); return; } - if (qc.status !== 'completed') { - core.setFailed( - `Quality checks job never completed (last status=${qc.status}).` - ); - return; + for (const qc of qcJobs) { + if (qc.status !== 'completed') { + core.setFailed( + `Quality checks job ${qc.name} never completed (last status=${qc.status})` + ); + return; + } } - if (qc.conclusion !== 'success') { - core.setFailed( - `Quality checks failed (conclusion=${qc.conclusion}).` - ); + for (const qc of qcJobs) { + if (qc.conclusion !== 'success' && qc.conclusion !== 'skipped') { + core.setFailed( + `Quality checks job ${qc.name} failed (conclusion=${qc.conclusion}).` + ); + return; + } } - name: Bypass QC gate @@ -106,7 +140,9 @@ jobs: run: echo "Skipping QC gate per commit message." pr_title_format_check: - uses: NHSDigital/eps-common-workflows/.github/workflows/pr_title_check.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/pr_title_check.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 + permissions: + pull-requests: write get_issue_number: runs-on: ubuntu-22.04 @@ -136,16 +172,16 @@ jobs: tag_release: needs: [get_config_values] - uses: NHSDigital/eps-common-workflows/.github/workflows/tag-release-devcontainer.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/tag-release-devcontainer.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 permissions: id-token: write contents: write + packages: write with: dry_run: true pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} branch_name: ${{ github.event.pull_request.head.ref }} tag_format: ${{ needs.get_config_values.outputs.tag_format }} - secrets: inherit get_commit_id: runs-on: ubuntu-22.04 @@ -159,6 +195,10 @@ jobs: package_code: needs: [get_issue_number, get_commit_id, quality_gate, get_config_values] + permissions: + contents: read + packages: read + id-token: write if: | always() && ! contains(needs.*.result, 'failure') && @@ -169,6 +209,7 @@ jobs: COMMIT_ID: ${{ needs.get_commit_id.outputs.commit_id }} pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + release_code: needs: [get_issue_number, package_code, get_commit_id, get_config_values] if: | @@ -176,6 +217,9 @@ jobs: ! contains(needs.*.result, 'failure') && ! contains(needs.*.result, 'cancelled') uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui-pr-${{needs.get_issue_number.outputs.issue_number}} @@ -222,4 +266,6 @@ jobs: steps: - name: Report Deployed URL run: | - echo "Deployed URL: https://cpt-ui-pr-${{ needs.get_issue_number.outputs.issue_number }}.dev.eps.national.nhs.uk" >> "$GITHUB_STEP_SUMMARY" + echo "Deployed URL: https://cpt-ui-pr-${ISSUE_NUMBER}.dev.eps.national.nhs.uk" >> "$GITHUB_STEP_SUMMARY" + env: + ISSUE_NUMBER: ${{ needs.get_issue_number.outputs.issue_number }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae3026efa8..4b38d9862f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,19 +3,26 @@ name: deploy to environments on: workflow_dispatch: -env: - BRANCH_NAME: ${{ github.event.ref.BRANCH_NAME }} +permissions: {} jobs: get_config_values: - uses: NHSDigital/eps-common-workflows/.github/workflows/get-repo-config.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/get-repo-config.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 with: verify_published_from_main_image: true + permissions: + attestations: read + contents: read + packages: read quality_checks: - uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 needs: [get_config_values] with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + permissions: + contents: read + id-token: write + packages: read secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -31,10 +38,11 @@ jobs: tag_release: needs: [quality_checks, get_commit_id, get_config_values] - uses: NHSDigital/eps-common-workflows/.github/workflows/tag-release-devcontainer.yml@5ac2707dd9cd60ad127275179495b9c890d74711 + uses: NHSDigital/eps-common-workflows/.github/workflows/tag-release-devcontainer.yml@c8f899f30a6a726859b0277faa73cd9ff7f4de20 permissions: id-token: write contents: write + packages: write with: dry_run: false pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} @@ -48,6 +56,10 @@ jobs: package_code: needs: [tag_release, get_commit_id, get_config_values] uses: ./.github/workflows/cdk_package_code.yml + permissions: + contents: read + packages: read + id-token: write with: VERSION_NUMBER: ${{needs.tag_release.outputs.version_tag}} COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} @@ -56,6 +68,9 @@ jobs: release_dev: needs: [tag_release, package_code, get_commit_id, get_config_values] uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui @@ -95,6 +110,9 @@ jobs: release_ref: needs: [tag_release, package_code, get_commit_id, release_dev, get_config_values] uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui @@ -134,6 +152,9 @@ jobs: release_qa: needs: [tag_release, package_code, get_commit_id, release_dev, get_config_values] uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui @@ -172,6 +193,9 @@ jobs: release_int: needs: [tag_release, package_code, get_commit_id, release_qa, get_config_values] uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui @@ -206,6 +230,9 @@ jobs: release_prod: needs: [tag_release, package_code, get_commit_id, release_int, get_config_values] uses: ./.github/workflows/release_all_stacks.yml + permissions: + contents: write + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} SERVICE_NAME: cpt-ui diff --git a/.github/workflows/release_all_stacks.yml b/.github/workflows/release_all_stacks.yml index 1e3afa130b..39c60f3f22 100644 --- a/.github/workflows/release_all_stacks.yml +++ b/.github/workflows/release_all_stacks.yml @@ -84,6 +84,7 @@ on: pinned_image: required: true type: string +permissions: {} jobs: release_all_code: runs-on: ubuntu-22.04 @@ -106,7 +107,7 @@ jobs: - name: Checkout local github actions uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - ref: ${{ env.BRANCH_NAME }} + persist-credentials: false fetch-depth: 0 sparse-checkout: | .github @@ -136,19 +137,19 @@ jobs: CF_LONDON_EXPORTS=$(aws cloudformation list-exports --region eu-west-2 --output json) CLOUDFRONT_DISTRIBUTION_ID=$(echo "$CF_LONDON_EXPORTS" | \ jq \ - --arg EXPORT_NAME "${{ inputs.SERVICE_NAME }}-stateless-resources:cloudfrontDistribution:Id" \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateless-resources:cloudfrontDistribution:Id" \ -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') CLOUDFRONT_DISTRIBUTION_ARN=$(echo "$CF_LONDON_EXPORTS" | \ jq \ - --arg EXPORT_NAME "${{ inputs.SERVICE_NAME }}-stateless-resources:cloudfrontDistribution:Arn" \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateless-resources:cloudfrontDistribution:Arn" \ -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') RUM_LOG_GROUP_ARN=$(echo "$CF_LONDON_EXPORTS" | \ jq \ - --arg EXPORT_NAME "${{ inputs.SERVICE_NAME }}-stateful-resources:rum:logGroup:arn" \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateful-resources:rum:logGroup:arn" \ -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') RUM_APP_NAME=$(echo "$CF_LONDON_EXPORTS" | \ jq \ - --arg EXPORT_NAME "${{ inputs.SERVICE_NAME }}-stateful-resources:rum:rumApp:Name" \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateful-resources:rum:rumApp:Name" \ -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') REDEPLOY_STATEFUL_STACK="false" @@ -175,6 +176,8 @@ jobs: echo "REDEPLOY_STATEFUL_STACK=$REDEPLOY_STATEFUL_STACK" >> "$GITHUB_OUTPUT" shell: bash + env: + SERVICE_NAME: ${{ inputs.SERVICE_NAME }} - name: fix cdk.json for deployment stateful stack run: | @@ -236,7 +239,6 @@ jobs: run: | CF_LONDON_EXPORTS=$(aws cloudformation list-exports --region eu-west-2 --output json) CF_US_EXPORTS=$(aws cloudformation list-exports --region us-east-1 --output json) - SERVICE_NAME='${{ inputs.SERVICE_NAME }}' hostedLoginDomain=$(echo "$CF_US_EXPORTS" | jq --arg EXPORT_NAME "${SERVICE_NAME}-us-certs:fullCognitoDomain:Name" -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') userPoolClientId=$(echo "$CF_LONDON_EXPORTS" | jq --arg EXPORT_NAME "${SERVICE_NAME}-stateful-resources:userPoolClient:userPoolClientId" -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') userPoolId=$(echo "$CF_LONDON_EXPORTS" | jq --arg EXPORT_NAME "${SERVICE_NAME}-stateful-resources:userPool:Id" -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') @@ -262,6 +264,8 @@ jobs: echo "rumSessionSampleRate=${rumSessionSampleRate}" echo "rumTelemetries=${rumTelemetries}" } >> "$GITHUB_ENV" + env: + SERVICE_NAME: ${{ inputs.SERVICE_NAME }} - name: build react app run: | @@ -271,9 +275,9 @@ jobs: export VITE_redirectSignIn="https://${fullCloudfrontDomain}/site/select-your-role" export VITE_redirectSignOut="https://${fullCloudfrontDomain}/site/logout" export VITE_redirectSessionSignOut="https://${fullCloudfrontDomain}/site/session-logged-out" - export VITE_COMMIT_ID=${{ inputs.COMMIT_ID }} - export VITE_VERSION_NUMBER=${{ inputs.VERSION_NUMBER }} - export VITE_TARGET_ENVIRONMENT=${{ inputs.TARGET_ENVIRONMENT }} + export VITE_COMMIT_ID=${COMMIT_ID} + export VITE_VERSION_NUMBER=${VERSION_NUMBER} + export VITE_TARGET_ENVIRONMENT=${TARGET_ENVIRONMENT} export VITE_RUM_GUEST_ROLE_ARN=${rumUnauthenticatedRumRoleArn} export VITE_RUM_IDENTITY_POOL_ID=${rumIdentityPoolId} export VITE_RUM_APPLICATION_ID=${rumAppId} @@ -281,20 +285,30 @@ jobs: export VITE_RUM_ENABLE_XRAY=${rumEnableXRay} export VITE_RUM_SESSION_SAMPLE_RATE=${rumSessionSampleRate} export VITE_RUM_TELEMETRIES=${rumTelemetries} - export VITE_REACT_LOG_LEVEL=${{ inputs.REACT_LOG_LEVEL }} + export VITE_REACT_LOG_LEVEL=${REACT_LOG_LEVEL} cd .build make react-build + env: + REACT_LOG_LEVEL: ${{ inputs.REACT_LOG_LEVEL }} + COMMIT_ID: ${{ inputs.COMMIT_ID }} + VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }} + TARGET_ENVIRONMENT: ${{ inputs.TARGET_ENVIRONMENT }} - name: deploy website run: | - staticBucketName=$(aws cloudformation list-exports --query "Exports[?Name=='${{ inputs.SERVICE_NAME }}-stateful-resources:StaticContentBucket:Name'].Value" --output text) + staticBucketName=$(aws cloudformation list-exports --query "Exports[?Name=='${SERVICE_NAME}-stateful-resources:StaticContentBucket:Name'].Value" --output text) aws s3 cp ".build/packages/staticContent/404.html" "s3://${staticBucketName}/404.html" aws s3 cp ".build/packages/staticContent/404.css" "s3://${staticBucketName}/404.css" aws s3 cp ".build/packages/staticContent/500.html" "s3://${staticBucketName}/500.html" - aws s3 cp ".build/packages/staticContent/jwks/${{ inputs.TARGET_ENVIRONMENT }}/jwks.json" "s3://${staticBucketName}/jwks.json" - aws s3 cp --recursive ".build/packages/cpt-ui/dist/" "s3://${staticBucketName}/${{ inputs.VERSION_NUMBER }}/" - aws s3 cp ".build/packages/cpt-ui/dist/assets/" "s3://${staticBucketName}/source_maps/${{ inputs.COMMIT_ID }}/site/assets/" --exclude "*" --include "*.map" --recursive + aws s3 cp ".build/packages/staticContent/jwks/${TARGET_ENVIRONMENT}/jwks.json" "s3://${staticBucketName}/jwks.json" + aws s3 cp --recursive ".build/packages/cpt-ui/dist/" "s3://${staticBucketName}/${VERSION_NUMBER}/" + aws s3 cp ".build/packages/cpt-ui/dist/assets/" "s3://${staticBucketName}/source_maps/${COMMIT_ID}/site/assets/" --exclude "*" --include "*.map" --recursive + env: + SERVICE_NAME: ${{ inputs.SERVICE_NAME }} + TARGET_ENVIRONMENT: ${{ inputs.TARGET_ENVIRONMENT }} + VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }} + COMMIT_ID: ${{ inputs.COMMIT_ID }} - name: fix cdk.json for deployment for stateless stack run: | @@ -354,16 +368,25 @@ jobs: id: update_cloudfront_kvs shell: bash run: | - # shellcheck disable=SC2140 - keyValueStore=$(aws cloudformation list-exports --region eu-west-2 --query "Exports[?Name=='"${{ inputs.SERVICE_NAME }}-stateless-resources:KeyValueStore:Arn"'].Value" --output text) - # shellcheck disable=SC2140 - cloudfrontDistributionId=$(aws cloudformation list-exports --region eu-west-2 --query "Exports[?Name=='"${{ inputs.SERVICE_NAME }}-stateless-resources:cloudfrontDistribution:Id"'].Value" --output text) - # shellcheck disable=SC2140 - primaryJwtPrivateKeyArn=$(aws cloudformation list-exports --region eu-west-2 --query "Exports[?Name=='"${{ inputs.SERVICE_NAME }}-stateless-resources:primaryJwtPrivateKey:Arn"'].Value" --output text) - # shellcheck disable=SC2140 - mockJwtPrivateKeyArn=$(aws cloudformation list-exports --region eu-west-2 --query "Exports[?Name=='"${{ inputs.SERVICE_NAME }}-stateless-resources:mockJwtPrivateKey:Arn"'].Value" --output text) + CF_LONDON_EXPORTS=$(aws cloudformation list-exports --region eu-west-2 --output json) + keyValueStore=$(echo "$CF_LONDON_EXPORTS" | \ + jq \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateless-resources:KeyValueStore:Arn" \ + -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') + cloudfrontDistributionId=$(echo "$CF_LONDON_EXPORTS" | \ + jq \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateless-resources:cloudfrontDistribution:Id" \ + -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') + primaryJwtPrivateKeyArn=$(echo "$CF_LONDON_EXPORTS" | \ + jq \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateless-resources:primaryJwtPrivateKey:Arn" \ + -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') + mockJwtPrivateKeyArn=$(echo "$CF_LONDON_EXPORTS" | \ + jq \ + --arg EXPORT_NAME "${SERVICE_NAME}-stateless-resources:mockJwtPrivateKey:Arn" \ + -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') - newVersion="${{ inputs.VERSION_NUMBER }}" + newVersion="${VERSION_NUMBER}" ETag=$(aws cloudfront-keyvaluestore describe-key-value-store \ --kvs-arn="$keyValueStore" \ @@ -396,14 +419,18 @@ jobs: if [ -z "${primaryJwtPrivateKeyArn}" ]; then echo "primaryJwtPrivateKeyArn is unset or set to the empty string" else - aws secretsmanager put-secret-value --secret-id "${primaryJwtPrivateKeyArn}" --secret-string "${{ secrets.JWT_PRIVATE_KEY }}" + aws secretsmanager put-secret-value --secret-id "${primaryJwtPrivateKeyArn}" --secret-string "${JWT_PRIVATE_KEY}" fi # set the mockJwtPrivateKeyArn secret if [ -z "${mockJwtPrivateKeyArn}" ]; then echo "mockJwtPrivateKeyArn is unset or set to the empty string" else - aws secretsmanager put-secret-value --secret-id "${mockJwtPrivateKeyArn}" --secret-string "${{ secrets.JWT_PRIVATE_KEY }}" + aws secretsmanager put-secret-value --secret-id "${mockJwtPrivateKeyArn}" --secret-string "${JWT_PRIVATE_KEY}" fi + env: + VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }} + SERVICE_NAME: ${{ inputs.SERVICE_NAME }} + JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }} - name: fix cdk.json for deployment for stateful stack redeployment if: ${{ steps.check_redeploy_stateful_stack.outputs.REDEPLOY_STATEFUL_STACK == 'true' }} @@ -476,27 +503,35 @@ jobs: with: ref: gh-pages path: gh-pages + persist-credentials: true - name: update release tag in github pages if: ${{ inputs.IS_PULL_REQUEST == false }} run: | cd gh-pages NOW=$(date +'%Y-%m-%dT%H:%M:%S') - echo "tag,release_datetime" > _data/${{ inputs.TARGET_ENVIRONMENT }}_latest.csv - echo "${{ inputs.VERSION_NUMBER }},${NOW}" >> _data/${{ inputs.TARGET_ENVIRONMENT }}_latest.csv - echo "${{ inputs.VERSION_NUMBER }},${NOW}" >> _data/${{ inputs.TARGET_ENVIRONMENT }}_deployments.csv + echo "tag,release_datetime" > "_data/${TARGET_ENVIRONMENT}_latest.csv" + echo "${VERSION_NUMBER},${NOW}" >> "_data/${TARGET_ENVIRONMENT}_latest.csv" + echo "${VERSION_NUMBER},${NOW}" >> "_data/${TARGET_ENVIRONMENT}_deployments.csv" git config user.name github-actions git config user.email github-actions@github.com - git add _data/${{ inputs.TARGET_ENVIRONMENT }}_latest.csv - git add _data/${{ inputs.TARGET_ENVIRONMENT }}_deployments.csv - git commit -m 'update releases for ${{ inputs.TARGET_ENVIRONMENT }}' + git add "_data/${TARGET_ENVIRONMENT}_latest.csv" + git add "_data/${TARGET_ENVIRONMENT}_deployments.csv" + git commit -m "update releases for ${TARGET_ENVIRONMENT}" parallel --retries 10 --delay 3 ::: "git pull --rebase && git push" + env: + TARGET_ENVIRONMENT: ${{ inputs.TARGET_ENVIRONMENT }} + VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }} regression_tests: name: Regression Tests uses: ./.github/workflows/run_regression_tests.yml if: ${{ always() && !failure() && !cancelled() && inputs.RUN_REGRESSION_TESTS == true }} needs: [release_all_code] + permissions: + contents: write + id-token: write + with: ENVIRONMENT: ${{ inputs.TARGET_ENVIRONMENT }} VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }} diff --git a/.github/workflows/run_regression_tests.yml b/.github/workflows/run_regression_tests.yml index 476a2e2918..a3998d36e8 100644 --- a/.github/workflows/run_regression_tests.yml +++ b/.github/workflows/run_regression_tests.yml @@ -18,6 +18,8 @@ on: REGRESSION_TESTS_PEM: required: true +permissions: {} + jobs: run_regression_tests: runs-on: ubuntu-22.04 @@ -38,7 +40,7 @@ jobs: - name: Checkout local github actions uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - ref: ${{ env.BRANCH_NAME }} + persist-credentials: false fetch-depth: 0 - name: Generate a token to authenticate regression testing @@ -54,7 +56,7 @@ jobs: env: TARGET_ENVIRONMENT: ${{ inputs.ENVIRONMENT }} VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }} - GITHUB-TOKEN: ${{ steps.generate-token.outputs.token }} + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} run: | if [[ "$TARGET_ENVIRONMENT" != "prod" && "$TARGET_ENVIRONMENT" != "ref" ]]; then REGRESSION_TEST_REPO_TAG="v3.12.12" # This is the tag or branch of the regression test code to run, usually a version tag like v3.1.0 or a branch name @@ -85,7 +87,7 @@ jobs: poetry run python -u run_regression_tests.py \ --env="$TARGET_ENVIRONMENT" \ --pr_label="$VERSION_NUMBER" \ - --token=${{ steps.generate-token.outputs.token }} \ + --token="${GITHUB_TOKEN}" \ --is_called_from_github=true \ --product=CPTS-UI \ --regression_test_repo_tag "${REGRESSION_TEST_REPO_TAG}" \ diff --git a/.github/workflows/sync_copilot.yml b/.github/workflows/sync_copilot.yml index 72b62eb1d7..1bb9174306 100644 --- a/.github/workflows/sync_copilot.yml +++ b/.github/workflows/sync_copilot.yml @@ -5,6 +5,8 @@ on: schedule: - cron: '0 6 * * 1' +permissions: {} + jobs: sync-copilot-instructions: runs-on: ubuntu-22.04 diff --git a/.github/workflows/update_dev_container_version.yml b/.github/workflows/update_dev_container_version.yml index ef79d062c5..04e985f175 100644 --- a/.github/workflows/update_dev_container_version.yml +++ b/.github/workflows/update_dev_container_version.yml @@ -4,6 +4,7 @@ on: workflow_dispatch: schedule: - cron: '0 6 * * 4' + permissions: {} jobs: diff --git a/.gitignore b/.gitignore index 0fbc257ad7..0c4ba9b77d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ packages/cpt-ui/dist packages/auth_demo/build/ .build/ .local_config/ +.sbom/ diff --git a/.grype.yaml b/.grype.yaml new file mode 100644 index 0000000000..f20b2ec8f6 --- /dev/null +++ b/.grype.yaml @@ -0,0 +1,16 @@ +ignore: + # rollup + - vulnerability: GHSA-mw96-cpmx-2vgc + # node-forge + - vulnerability: GHSA-5m6q-g25r-mvwx + - vulnerability: GHSA-q67f-28xg-22rw + - vulnerability: GHSA-ppp5-5v6c-4jwp + - vulnerability: GHSA-2328-f5f3-gj25 + # path-to-regexp + - vulnerability: GHSA-j3q9-mxjg-w52f + # lodash + - vulnerability: GHSA-r5fr-rjxr-66jc + # vite - only affects dev server - needs vitest and @vitejs/plugin-react-swc to be updated + - vulnerability: GHSA-p9ff-h696-f583 + # vite - only affects server.fs.deny which we don't use + - vulnerability: GHSA-v2wj-q39q-566r diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2b85a72a3..2d392429bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,14 @@ repos: - repo: local hooks: + - id: grype-scan-local + name: Grype scan local changes + entry: make + args: ["grype-scan-local"] + language: system + pass_filenames: false + always_run: true + - id: check-commit-signing name: Check commit signing description: Ensures that commits are GPG signed diff --git a/.trivyignore.yaml b/.trivyignore.yaml deleted file mode 100644 index a8adbba059..0000000000 --- a/.trivyignore.yaml +++ /dev/null @@ -1,62 +0,0 @@ -vulnerabilities: - - id: CVE-2025-12816 - paths: - - "package-lock.json" - statement: downstream dependency just used in build stage - expired_at: 2026-06-01 - - id: CVE-2025-66031 - paths: - - "package-lock.json" - statement: downstream dependency just used in build stage - expired_at: 2026-06-01 - - id: CVE-2026-25128 - paths: - - "package-lock.json" - statement: downstream dependency of fast-xml-parser - expired_at: 2026-06-01 - - id: CVE-2026-26278 - statement: fast-xml-parser vulnerability accepted as risk - dependency of aws-cdk/client-dynamodb - expired_at: 2026-06-01 - - id: CVE-2026-26960 - statement: tar vulnerability accepted as risk - expired_at: 2026-06-01 - - id: CVE-2026-26996 - paths: - - "package-lock.json" - statement: downstream dependency - vulnerability is not relevant - we don't allow user input for anything regex. - - id: CVE-2026-27606 - paths: - - "package-lock.json" - statement: Vulnerability in AWS dependency accepted as risk - expired_at: 2026-06-01 - - id: CVE-2026-27903 - paths: - - "package-lock.json" - statement: Vulnerability in minimatch - waiting for depencency updates - expired_at: 2026-06-01 - - id: CVE-2026-27904 - paths: - - "package-lock.json" - statement: Vulnerability in minimatch - waiting for depencency updates - expired_at: 2026-06-01 - - id: CVE-2026-29063 - statement: fixed - waiting for dependency updates - expired_at: 2026-06-01 - - id: CVE-2026-32141 - statement: Transitive dependency vulnerability in flatted - expired_at: 2026-06-01 - - id: CVE-2026-1526 - statement: Transitive dependency vulnerability in undici of npm - expired_at: 2026-06-01 - - id: CVE-2026-1528 - statement: Transitive dependency vulnerability in undici of npm - expired_at: 2026-06-01 - - id: CVE-2026-2229 - statement: Transitive dependency vulnerability in undici of npm - expired_at: 2026-06-01 - - id: CVE-2026-32597 - statement: Transitive dependency vulnerability in pyjwt - expired_at: 2026-06-01 - - id: CVE-2026-33036 - statement: fast-xml-parser vulnerability accepted as risk - dependency of aws-cdk/client-dynamodb - expired_at: 2026-06-01 diff --git a/Makefile b/Makefile index c24df66311..6f8e7be31c 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ install-python: poetry install install-node: - npm ci + npm ci --ignore-scripts install-hooks: install-python poetry run pre-commit install --install-hooks --overwrite diff --git a/trivy.yaml b/trivy.yaml deleted file mode 100644 index eb2433758b..0000000000 --- a/trivy.yaml +++ /dev/null @@ -1 +0,0 @@ -ignorefile: ".trivyignore.yaml" diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000000..fcefc33b95 --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,24 @@ +rules: + unpinned-images: + # these workflows use unpinned images because they are using a full image passed in that contains the tag + ignore: + - cdk_package_code.yml:21:7 + - release_all_stacks.yml:93:7 + - run_regression_tests.yml:27:7 + - pull_request.yml:223:7 + secrets-outside-env: + # these are ignored because they are using known secrets + ignore: + - delete_old_cloudformation_stacks.yml:29:31 + - delete_old_cloudformation_stacks.yml:18:9 + - run_regression_tests.yml:51:28 + secrets-inherit: + ignore: + - pull_request.yml:219:11 + - release.yml:232:11 + - release.yml:195:11 + - release.yml:154:11 + - release.yml:112:11 + - release.yml:70:11 + - ci.yml:109:11 + - ci.yml:67:11