From c17d72b6c0a0bf78dfe0ea314cf6c84057b3d1d4 Mon Sep 17 00:00:00 2001 From: Kris Szlapa Date: Thu, 30 Nov 2023 20:59:02 +0000 Subject: [PATCH] Add GitHub actions --- .github/dependabot.yml | 30 ++ .github/pull_request_template.md | 30 ++ .../workflows/adhoc_create_release_notes.yml | 21 ++ .github/workflows/combine-dependabot-prs.yml | 151 ++++++++++ .../create_confluence_release_notes.yml | 128 ++++++++ .../delete_old_cloudformation_stacks.yml | 54 ++++ .../dependabot_auto_approve_and_merge.yml | 44 +++ .github/workflows/pr-link.yml | 29 ++ .github/workflows/pull_request.yml | 95 ++++++ .github/workflows/quality_checks.yml | 58 ++++ .github/workflows/release.yml | 274 ++++++++++++++++++ .github/workflows/sam_package_code.yml | 81 ++++++ .github/workflows/sam_release_code.yml | 98 +++++++ 13 files changed, 1093 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/adhoc_create_release_notes.yml create mode 100644 .github/workflows/combine-dependabot-prs.yml create mode 100644 .github/workflows/create_confluence_release_notes.yml create mode 100644 .github/workflows/delete_old_cloudformation_stacks.yml create mode 100644 .github/workflows/dependabot_auto_approve_and_merge.yml create mode 100644 .github/workflows/pr-link.yml create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/quality_checks.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/sam_package_code.yml create mode 100644 .github/workflows/sam_release_code.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1df53e63 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +######################################################################### +# Dependabot configuration file +######################################################################### + +version: 2 +updates: + - package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "daily" + + ################################### + # NPM workspace ################## + ################################### + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + versioning-strategy: increase + + ################################### + # Poetry ######################### + ################################### + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + versioning-strategy: increase diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..4b00a00d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ +## Summary + +**Remove items from this list if they are not relevant. Remove this line once this has been done** + +- Routine Change +- :exclamation: Breaking Change +- :robot: Operational or Infrastructure Change +- :sparkles: New Feature +- :warning: Potential issues that might be caused by this change + +### Details + +Add any summary information of what is in the change. **Remove this line if you have nothing to add.** + +## Reviews Required + +**Check who should review this. Remove this line once this has been done** + +- [x] Dev +- [ ] Test +- [ ] Tech Author +- [ ] Product Owner + +## Review Checklist + +:information_source: This section is to be filled in by the **reviewer**. + +- [ ] I have reviewed the changes in this PR and they fill all or part of the acceptance criteria of the ticket, and the code is in a mergeable state. +- [ ] If there were infrastructure, operational, or build changes, I have made sure there is sufficient evidence that the changes will work. +- [ ] I have ensured the jira ticket has been updated with the github pull request link diff --git a/.github/workflows/adhoc_create_release_notes.yml b/.github/workflows/adhoc_create_release_notes.yml new file mode 100644 index 00000000..875bc80f --- /dev/null +++ b/.github/workflows/adhoc_create_release_notes.yml @@ -0,0 +1,21 @@ +name: Adhoc create confluence release notes + +on: + workflow_dispatch: + +jobs: + create_aws_int_release_notes: + uses: ./.github/workflows/create_confluence_release_notes.yml + with: + TARGET_ENVIRONMENT: int + secrets: + DEV_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.INT_CLOUD_FORMATION_DEPLOY_ROLE }} + + create_aws_prod_release_notes: + uses: ./.github/workflows/create_confluence_release_notes.yml + with: + TARGET_ENVIRONMENT: prod + secrets: + DEV_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_DEPLOY_ROLE }} diff --git a/.github/workflows/combine-dependabot-prs.yml b/.github/workflows/combine-dependabot-prs.yml new file mode 100644 index 00000000..b14f445a --- /dev/null +++ b/.github/workflows/combine-dependabot-prs.yml @@ -0,0 +1,151 @@ +name: 'Combine PRs' + +# Controls when the action will run - in this case triggered manually +on: + workflow_dispatch: + inputs: + branchPrefix: + description: 'Branch prefix to find combinable PRs based on' + required: true + default: 'dependabot' + mustBeGreen: + description: 'Only combine PRs that are green (status is success)' + required: true + default: "true" + combineBranchName: + description: 'Name of the branch to combine PRs into' + required: true + default: 'combine-dependabot-PRs' + ignoreLabel: + description: 'Exclude PRs with this label' + required: true + default: 'nocombine' + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "combine-prs" + combine-prs: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/github-script@v7 + id: create-combined-pr + name: Create Combined PR + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', { + owner: context.repo.owner, + repo: context.repo.repo + }); + let branchesAndPRStrings = []; + let baseBranch = null; + let baseBranchSHA = null; + for (const pull of pulls) { + const branch = pull['head']['ref']; + console.log('Pull for branch: ' + branch); + if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) { + console.log('Branch matched prefix: ' + branch); + let statusOK = true; + if(${{ github.event.inputs.mustBeGreen }}) { + console.log('Checking green status: ' + branch); + const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number:$pull_number) { + commits(last: 1) { + nodes { + commit { + statusCheckRollup { + state + } + } + } + } + } + } + }` + const vars = { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pull['number'] + }; + const result = await github.graphql(stateQuery, vars); + const [{ commit }] = result.repository.pullRequest.commits.nodes; + const state = commit.statusCheckRollup.state + console.log('Validating status: ' + state); + if(state != 'SUCCESS') { + console.log('Discarding ' + branch + ' with status ' + state); + statusOK = false; + } + } + console.log('Checking labels: ' + branch); + const labels = pull['labels']; + for(const label of labels) { + const labelName = label['name']; + console.log('Checking label: ' + labelName); + if(labelName == '${{ github.event.inputs.ignoreLabel }}') { + console.log('Discarding ' + branch + ' with label ' + labelName); + statusOK = false; + } + } + if (statusOK) { + console.log('Adding branch to array: ' + branch); + const prString = '#' + pull['number'] + ' ' + pull['title']; + branchesAndPRStrings.push({ branch, prString }); + baseBranch = pull['base']['ref']; + baseBranchSHA = pull['base']['sha']; + } + } + } + if (branchesAndPRStrings.length == 0) { + core.setFailed('No PRs/branches matched criteria'); + return; + } + try { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}', + sha: baseBranchSHA + }); + } catch (error) { + console.log(error); + core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?'); + return; + } + + let combinedPRs = []; + let mergeFailedPRs = []; + for(const { branch, prString } of branchesAndPRStrings) { + try { + await github.rest.repos.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + base: '${{ github.event.inputs.combineBranchName }}', + head: branch, + }); + console.log('Merged branch ' + branch); + combinedPRs.push(prString); + } catch (error) { + console.log('Failed to merge branch ' + branch); + mergeFailedPRs.push(prString); + } + } + + console.log('Creating combined PR'); + const combinedPRsString = combinedPRs.join('\n'); + let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString; + if(mergeFailedPRs.length > 0) { + const mergeFailedPRsString = mergeFailedPRs.join('\n'); + body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString + } + await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Combined PR', + head: '${{ github.event.inputs.combineBranchName }}', + base: baseBranch, + body: body + }); diff --git a/.github/workflows/create_confluence_release_notes.yml b/.github/workflows/create_confluence_release_notes.yml new file mode 100644 index 00000000..3e608433 --- /dev/null +++ b/.github/workflows/create_confluence_release_notes.yml @@ -0,0 +1,128 @@ +name: 'Create confluence release notes' + +on: + workflow_call: + inputs: + TARGET_ENVIRONMENT: + required: true + type: string + secrets: + DEV_CLOUD_FORMATION_DEPLOY_ROLE: + required: true + TARGET_CLOUD_FORMATION_DEPLOY_ROLE: + required: true + +jobs: + create_confluence_release_notes: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ env.BRANCH_NAME }} + + # using git commit sha for version of action to ensure we have stable version + - name: Install asdf + uses: asdf-vm/actions/setup@4f8f7939dd917fc656bb7c3575969a5988c28364 + with: + asdf_branch: v0.11.3 + + - name: Cache asdf + uses: actions/cache@v3 + with: + path: | + ~/.asdf + key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} + restore-keys: | + ${{ runner.os }}-asdf- + + - name: Install asdf dependencies in .tool-versions + uses: asdf-vm/actions/install@4f8f7939dd917fc656bb7c3575969a5988c28364 + with: + asdf_branch: v0.11.3 + env: + PYTHON_CONFIGURE_OPTS: --enable-shared + + - name: make install + run: | + make install + + - name: Configure target AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.TARGET_CLOUD_FORMATION_DEPLOY_ROLE }} + role-session-name: github-actions + + - shell: bash + name: get target tag + run: | + target_tag=$(aws cloudformation describe-stacks --stack-name ${{ inputs.TARGET_ENVIRONMENT }}-ci --query "Stacks[0].Tags[?Key=='version'].Value" --output text) + export target_tag + echo "target_tag=${target_tag}" >> "$GITHUB_ENV" + + - name: Configure dev AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + role-session-name: github-actions + + - shell: bash + name: get dev tag + run: | + dev_tag=$(aws cloudformation describe-stacks --stack-name dev-ci --query "Stacks[0].Tags[?Key=='version'].Value" --output text) + export dev_tag + echo "dev_tag=${dev_tag}" >> "$GITHUB_ENV" + + - shell: bash + name: create int release notes + if: inputs.TARGET_ENVIRONMENT == 'int' + run: | + ENV_VAR=release-notes:CreateReleaseNotesLambdaName + RELEASE_NOTES_LAMBDA=$(aws cloudformation list-exports \ + --query "Exports[?Name=='$ENV_VAR'].Value" \ + --output text) + cat < payload.json + { + "currentTag": "$target_tag", + "targetTag": "$dev_tag", + "repoName": "electronic-prescription-service-clinical-prescription-tracker", + "targetEnvironment": "INT", + "productName": "Prescritpions for Patients AWS layer", + "releaseNotesPageId": "693750027", + "releaseNotesPageTitle": "Current PfP AWS layer release notes - INT" + } + EOF + cat payload.json + aws lambda invoke --function-name "${RELEASE_NOTES_LAMBDA}" \ + --cli-binary-format raw-in-base64-out \ + --payload file://payload.json out.txt + + - shell: bash + name: create prod release notes + if: inputs.TARGET_ENVIRONMENT == 'prod' + run: | + ENV_VAR=release-notes:CreateReleaseNotesLambdaName + RELEASE_NOTES_LAMBDA=$(aws cloudformation list-exports \ + --query "Exports[?Name=='$ENV_VAR'].Value" \ + --output text) + cat < payload.json + { + "currentTag": "$target_tag", + "targetTag": "$dev_tag", + "repoName": "electronic-prescription-service-clinical-prescription-tracker", + "targetEnvironment": "PROD", + "productName": "Prescritpions for Patients AWS layer", + "releaseNotesPageId": "693750029", + "releaseNotesPageTitle": "Current PfP AWS layer release notes - PROD" + } + EOF + cat payload.json + aws lambda invoke --function-name "${RELEASE_NOTES_LAMBDA}" \ + --cli-binary-format raw-in-base64-out \ + --payload file://payload.json out.txt + diff --git a/.github/workflows/delete_old_cloudformation_stacks.yml b/.github/workflows/delete_old_cloudformation_stacks.yml new file mode 100644 index 00000000..c91fe83f --- /dev/null +++ b/.github/workflows/delete_old_cloudformation_stacks.yml @@ -0,0 +1,54 @@ +name: 'Delete old cloudformation stacks' + +# Controls when the action will run - in this case triggered manually +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +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-latest + permissions: + id-token: write + contents: read + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + role-session-name: github-actions + + - name: delete stacks + shell: bash + run: | + ACTIVE_STACKS=$(aws cloudformation list-stacks | + jq -r '.StackSummaries[] | + select ( .StackStatus != "DELETE_COMPLETE" ) | + select( .StackName | capture("^pr-(sandbox-)?(\\d+)$") ) + | .StackName ') + + ACTIVE_STACKS_ARRAY=( $ACTIVE_STACKS ) + + for i in "${ACTIVE_STACKS_ARRAY[@]}" + do + echo "Checking if stack $i has open pull request" + PULL_REQUEST=${i//pr-/} + PULL_REQUEST=${PULL_REQUEST//sandbox-/} + echo "Checking pull request id ${PULL_REQUEST}" + URL="https://api.github.com/repos/NHSDigital/electronic-prescription-service-clinical-prescription-tracker/pulls/${PULL_REQUEST}" + RESPONSE=$(curl ${URL} 2>/dev/null) + STATE=$(echo ${RESPONSE} | jq -r .state) + if [ "$STATE" == "closed" ]; then + echo "** going to delete stack $i as state is ${STATE} **" + aws cloudformation delete-stack --stack-name ${i} + else + echo "not going to delete stack $i as state is ${STATE}" + fi + done diff --git a/.github/workflows/dependabot_auto_approve_and_merge.yml b/.github/workflows/dependabot_auto_approve_and_merge.yml new file mode 100644 index 00000000..5f68a880 --- /dev/null +++ b/.github/workflows/dependabot_auto_approve_and_merge.yml @@ -0,0 +1,44 @@ +name: Dependabot auto-approve +on: pull_request + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Dependabot metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@v1 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Approve patch and minor updates + if: ${{steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' || steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr review $PR_URL --approve -b "I'm **approving** this pull request because **it includes a patch or minor update**" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Approve major updates of development dependencies + if: ${{steps.dependabot-metadata.outputs.update-type == 'version-update:semver-major' && steps.dependabot-metadata.outputs.dependency-type == 'direct:development'}} + run: gh pr review $PR_URL --approve -b "I'm **approving** this pull request because **it includes a major update of a dependency used only in development**" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Comment on major updates of non-development dependencies + if: ${{steps.dependabot-metadata.outputs.update-type == 'version-update:semver-major' && steps.dependabot-metadata.outputs.dependency-type == 'direct:production'}} + run: | + gh pr comment $PR_URL --body "I'm **not approving** this PR because **it includes a major update of a dependency used in production**" + gh pr edit $PR_URL --add-label "requires-manual-qa" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + # enable auto merge on all dependabot prs + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.AUTOMERGE_PAT}} + diff --git a/.github/workflows/pr-link.yml b/.github/workflows/pr-link.yml new file mode 100644 index 00000000..ef9284cb --- /dev/null +++ b/.github/workflows/pr-link.yml @@ -0,0 +1,29 @@ +name: PR Link ticket +on: + pull_request: + types: [opened] +jobs: + link-ticket: + runs-on: ubuntu-latest + steps: + - name: Check ticket name conforms to requirements + run: echo ${{ github.event.pull_request.head.ref }} | grep -i -E -q "(aea-[0-9]+)|(apm-[0-9]+)|(apmspii-[0-9]+)|(adz-[0-9]+)|(amb-[0-9]+)|(dependabot\/)" + continue-on-error: true + + - name: Grab ticket name + if: contains(github.event.pull_request.head.ref, 'aea-') || contains(github.event.pull_request.head.ref, 'AEA-') || contains(github.event.pull_request.head.ref, 'apm-') || contains(github.event.pull_request.head.ref, 'APM-') || contains(github.event.pull_request.head.ref, 'apmspii-') || contains(github.event.pull_request.head.ref, 'APMSPII-') || contains(github.event.pull_request.head.ref, 'adz-') || contains(github.event.pull_request.head.ref, 'ADZ-') || contains(github.event.pull_request.head.ref, 'amb-') || contains(github.event.pull_request.head.ref, 'AMB-') + run: echo ::set-env name=TICKET_NAME::$(echo ${{ github.event.pull_request.head.ref }} | grep -i -o '\(aea-[0-9]\+\)\|\(apm-[0-9]\+\)\|\(apmspii-[0-9]\+\)\|\(adz-[0-9]\+\)|\(amb-[0-9]\+\)' | tr '[:lower:]' '[:upper:]') + continue-on-error: true + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + + - name: Comment on PR with link to JIRA ticket + if: contains(github.event.pull_request.head.ref, 'aea-') || contains(github.event.pull_request.head.ref, 'AEA-') || contains(github.event.pull_request.head.ref, 'apm-') || contains(github.event.pull_request.head.ref, 'APM-') || contains(github.event.pull_request.head.ref, 'apmspii-') || contains(github.event.pull_request.head.ref, 'APMSPII-') || contains(github.event.pull_request.head.ref, 'adz-') || contains(github.event.pull_request.head.ref, 'ADZ-') || contains(github.event.pull_request.head.ref, 'amb-') || contains(github.event.pull_request.head.ref, 'AMB-') + continue-on-error: true + uses: unsplash/comment-on-pr@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + msg: | + This branch is work on a ticket in the NHS Digital APM JIRA Project. Here's a handy link to the ticket: + # [${{ env.TICKET_NAME }}](https://nhsd-jira.digital.nhs.uk/browse/${{ env.TICKET_NAME }}) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..8b2fba6e --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,95 @@ +name: deploy_pr + +on: + pull_request: + branches: [main] + +env: + BRANCH_NAME: ${{ github.event.pull_request.head.ref }} + +jobs: + quality_checks: + uses: ./.github/workflows/quality_checks.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + get_issue_number: + runs-on: ubuntu-latest + needs: quality_checks + outputs: + issue_number: ${{steps.get_issue_number.outputs.result}} + + steps: + - uses: actions/github-script@v7 + 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 + + get_commit_id: + runs-on: ubuntu-latest + outputs: + commit_id: ${{ steps.commit_id.outputs.commit_id }} + steps: + - name: Get Commit ID + id: commit_id + run: | + echo "commit_id=${{ github.sha }}" >> "$GITHUB_OUTPUT" + + package_code: + needs: get_issue_number + uses: ./.github/workflows/sam_package_code.yml + + release_code: + needs: [get_issue_number, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + STACK_NAME: pr-${{needs.get_issue_number.outputs.issue_number}} + ARTIFACT_BUCKET_PREFIX: PR-${{needs.get_issue_number.outputs.issue_number}} + TARGET_ENVIRONMENT: dev-pr + ENABLE_MUTUAL_TLS: false + DEPLOY_SANDBOX: false + BUILD_ARTIFACT: packaged_code + TRUSTSTORE_FILE: truststore.pem + VERSION_NUMBER: PR-${{ needs.get_issue_number.outputs.issue_number }} + COMMIT_ID: ${{ needs.get_commit_id.outputs.commit_id }} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.DEV_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: ${{ secrets.DEV_TARGET_SPINE_SERVER }} + + release_sandbox_code: + needs: [get_issue_number, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + STACK_NAME: pr-sandbox-${{needs.get_issue_number.outputs.issue_number}} + ARTIFACT_BUCKET_PREFIX: PR-sandbox-${{needs.get_issue_number.outputs.issue_number}} + TARGET_ENVIRONMENT: dev-pr + ENABLE_MUTUAL_TLS: false + DEPLOY_SANDBOX: true + BUILD_ARTIFACT: packaged_sandbox_code + TRUSTSTORE_FILE: sandbox-truststore.pem + VERSION_NUMBER: PR-${{ needs.get_issue_number.outputs.issue_number }} + COMMIT_ID: ${{ needs.get_commit_id.outputs.commit_id }} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.DEV_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: sandbox diff --git a/.github/workflows/quality_checks.yml b/.github/workflows/quality_checks.yml new file mode 100644 index 00000000..2c432ebf --- /dev/null +++ b/.github/workflows/quality_checks.yml @@ -0,0 +1,58 @@ +name: quality checks + +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true + +jobs: + quality_checks: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ env.BRANCH_NAME }} + fetch-depth: 0 + + # using git commit sha for version of action to ensure we have stable version + - name: Install asdf + uses: asdf-vm/actions/setup@05e0d2ed97b598bfce82fd30daf324ae0c4570e6 + with: + asdf_branch: v0.11.3 + + - name: Cache asdf + uses: actions/cache@v3 + with: + path: | + ~/.asdf + key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} + restore-keys: | + ${{ runner.os }}-asdf- + + - name: Install asdf dependencies in .tool-versions + uses: asdf-vm/actions/install@05e0d2ed97b598bfce82fd30daf324ae0c4570e6 + with: + asdf_branch: v0.11.3 + env: + PYTHON_CONFIGURE_OPTS: --enable-shared + + - name: make install + run: | + make install + + - name: run check-licenses + run: make check-licenses + + - name: run lint + run: make lint + + - name: run unit tests + run: make test + + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1c9a1ce8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,274 @@ +name: release workflow + +on: + push: + branches: [main] + tags: [v**] + +env: + BRANCH_NAME: ${{ github.event.ref.BRANCH_NAME }} + +jobs: + quality_checks: + uses: ./.github/workflows/quality_checks.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + get_commit_id: + runs-on: ubuntu-latest + outputs: + commit_id: ${{ steps.commit_id.outputs.commit_id }} + steps: + - name: Get Commit ID + id: commit_id + run: | + echo "commit_id=${{ github.sha }}" >> "$GITHUB_OUTPUT" + + tag_release: + needs: quality_checks + runs-on: ubuntu-latest + outputs: + spec_version: ${{steps.output_spec_version.outputs.SPEC_VERSION}} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ env.BRANCH_NAME }} + fetch-depth: 0 + + # using git commit sha for version of action to ensure we have stable version + - name: Install asdf + uses: asdf-vm/actions/setup@05e0d2ed97b598bfce82fd30daf324ae0c4570e6 + with: + asdf_branch: v0.11.3 + + - name: Cache asdf + uses: actions/cache@v3 + with: + path: | + ~/.asdf + key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} + restore-keys: | + ${{ runner.os }}-asdf- + + - name: Install asdf dependencies in .tool-versions + uses: asdf-vm/actions/install@05e0d2ed97b598bfce82fd30daf324ae0c4570e6 + with: + asdf_branch: v0.11.3 + env: + PYTHON_CONFIGURE_OPTS: --enable-shared + + - name: Install python packages + run: | + make install-python + + - name: Set SPEC_VERSION env var for merges to main + run: echo "SPEC_VERSION=$(poetry run python scripts/calculate_version.py)" >> $GITHUB_ENV + if: github.ref == 'refs/heads/main' + + - name: Set SPEC_VERSION env var for tags + run: echo "SPEC_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + if: github.ref != 'refs/heads/main' + + - name: Create release (tags and main) + id: create-release + # using commit hash for version v1.13.0 + uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag: ${{ env.SPEC_VERSION }} + commit: ${{ github.sha }} + body: | + ## Commit message + ${{ github.event.head_commit.message }} + ## Info + [See code diff](${{ github.event.compare }}) + [Release workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + It was initialized by [${{ github.event.sender.login }}](${{ github.event.sender.html_url }}) + + - name: output SPEC_VERSION + id: output_spec_version + run: echo SPEC_VERSION=${{ env.SPEC_VERSION }} >> $GITHUB_OUTPUT + + package_code: + needs: tag_release + uses: ./.github/workflows/sam_package_code.yml + + release_dev: + needs: [tag_release, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + ARTIFACT_BUCKET_PREFIX: ${{needs.tag_release.outputs.spec_version}} + STACK_NAME: dev-ci + TARGET_ENVIRONMENT: dev + ENABLE_MUTUAL_TLS: true + DEPLOY_SANDBOX: false + BUILD_ARTIFACT: packaged_code + TRUSTSTORE_FILE: truststore.pem + VERSION_NUMBER: ${{needs.tag_release.outputs.spec_version}} + COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.DEV_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: ${{ secrets.DEV_TARGET_SPINE_SERVER }} + + create_int_release_notes_post_dev: + needs: [release_dev] + uses: ./.github/workflows/create_confluence_release_notes.yml + with: + TARGET_ENVIRONMENT: int + secrets: + DEV_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.INT_CLOUD_FORMATION_DEPLOY_ROLE }} + + create_prod_release_notes_post_dev: + needs: [release_dev] + uses: ./.github/workflows/create_confluence_release_notes.yml + with: + TARGET_ENVIRONMENT: prod + secrets: + DEV_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_DEPLOY_ROLE }} + + release_dev_sandbox: + needs: [tag_release, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + ARTIFACT_BUCKET_PREFIX: ${{needs.tag_release.outputs.spec_version}} + STACK_NAME: dev-sandbox + TARGET_ENVIRONMENT: dev + ENABLE_MUTUAL_TLS: true + DEPLOY_SANDBOX: true + BUILD_ARTIFACT: packaged_sandbox_code + TRUSTSTORE_FILE: sandbox-truststore.pem + VERSION_NUMBER: ${{needs.tag_release.outputs.spec_version}} + COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.DEV_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: sandbox + + release_ref: + needs: [tag_release, release_dev, release_dev_sandbox, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + ARTIFACT_BUCKET_PREFIX: ${{needs.tag_release.outputs.spec_version}} + STACK_NAME: ref-ci + TARGET_ENVIRONMENT: ref + ENABLE_MUTUAL_TLS: true + DEPLOY_SANDBOX: false + BUILD_ARTIFACT: packaged_code + TRUSTSTORE_FILE: truststore.pem + VERSION_NUMBER: ${{needs.tag_release.outputs.spec_version}} + COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.REF_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.REF_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: ${{ secrets.REF_TARGET_SPINE_SERVER }} + + release_qa: + needs: [tag_release, release_dev, release_dev_sandbox, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + ARTIFACT_BUCKET_PREFIX: ${{needs.tag_release.outputs.spec_version}} + STACK_NAME: qa-ci + TARGET_ENVIRONMENT: qa + ENABLE_MUTUAL_TLS: true + DEPLOY_SANDBOX: false + BUILD_ARTIFACT: packaged_code + TRUSTSTORE_FILE: truststore.pem + VERSION_NUMBER: ${{needs.tag_release.outputs.spec_version}} + COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.QA_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.QA_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: ${{ secrets.QA_TARGET_SPINE_SERVER }} + + release_int: + needs: [tag_release, release_qa, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + ARTIFACT_BUCKET_PREFIX: ${{needs.tag_release.outputs.spec_version}} + STACK_NAME: int-ci + TARGET_ENVIRONMENT: int + ENABLE_MUTUAL_TLS: true + DEPLOY_SANDBOX: false + BUILD_ARTIFACT: packaged_code + TRUSTSTORE_FILE: truststore.pem + VERSION_NUMBER: ${{needs.tag_release.outputs.spec_version}} + COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.INT_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.INT_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: ${{ secrets.INT_TARGET_SPINE_SERVER }} + + create_int_release_notes_post_int: + needs: [release_int] + uses: ./.github/workflows/create_confluence_release_notes.yml + with: + TARGET_ENVIRONMENT: int + secrets: + DEV_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.INT_CLOUD_FORMATION_DEPLOY_ROLE }} + + release_sandbox: + needs: [tag_release, release_qa, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + ARTIFACT_BUCKET_PREFIX: ${{needs.tag_release.outputs.spec_version}} + STACK_NAME: int-sandox + TARGET_ENVIRONMENT: int + ENABLE_MUTUAL_TLS: true + DEPLOY_SANDBOX: true + BUILD_ARTIFACT: packaged_sandbox_code + TRUSTSTORE_FILE: sandbox-truststore.pem + VERSION_NUMBER: ${{needs.tag_release.outputs.spec_version}} + COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.INT_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.INT_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: sandbox + + release_prod: + needs: [tag_release, release_int, release_sandbox, package_code, get_commit_id] + uses: ./.github/workflows/sam_release_code.yml + with: + ARTIFACT_BUCKET_PREFIX: ${{needs.tag_release.outputs.spec_version}} + STACK_NAME: prod-ci + TARGET_ENVIRONMENT: prod + ENABLE_MUTUAL_TLS: true + DEPLOY_SANDBOX: false + BUILD_ARTIFACT: packaged_code + TRUSTSTORE_FILE: truststore.pem + VERSION_NUMBER: ${{needs.tag_release.outputs.spec_version}} + COMMIT_ID: ${{needs.get_commit_id.outputs.commit_id}} + LOG_LEVEL: INFO + LOG_RETENTION_DAYS: 731 + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_DEPLOY_ROLE }} + SPLUNK_HEC_TOKEN: ${{ secrets.PROD_SPLUNK_HEC_TOKEN }} + TARGET_SPINE_SERVER: ${{ secrets.PROD_TARGET_SPINE_SERVER }} + + create_prod_release_notes_post_prod: + needs: [release_prod] + uses: ./.github/workflows/create_confluence_release_notes.yml + with: + TARGET_ENVIRONMENT: prod + secrets: + DEV_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_DEPLOY_ROLE }} diff --git a/.github/workflows/sam_package_code.yml b/.github/workflows/sam_package_code.yml new file mode 100644 index 00000000..a6ccef84 --- /dev/null +++ b/.github/workflows/sam_package_code.yml @@ -0,0 +1,81 @@ +name: sam package code + +on: + workflow_call: + +jobs: + sam_package_code: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ env.BRANCH_NAME }} + + # using git commit sha for version of action to ensure we have stable version + - name: Install asdf + uses: asdf-vm/actions/setup@05e0d2ed97b598bfce82fd30daf324ae0c4570e6 + with: + asdf_branch: v0.11.3 + + - name: Cache asdf + uses: actions/cache@v3 + with: + path: | + ~/.asdf + key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }} + restore-keys: | + ${{ runner.os }}-asdf- + + - name: Install asdf dependencies in .tool-versions + uses: asdf-vm/actions/install@05e0d2ed97b598bfce82fd30daf324ae0c4570e6 + with: + asdf_branch: v0.11.3 + env: + PYTHON_CONFIGURE_OPTS: --enable-shared + + - name: make install + run: | + make install + + - shell: bash + name: package code + run: | + cp .tool-versions ~/ + rm -rf .aws-sam + export PATH=$PATH:$PWD/node_modules/.bin + make sam-build + make compile-go + cp Makefile .aws-sam/build/ + cp samconfig_package_and_deploy.toml .aws-sam/build/ + + - uses: actions/upload-artifact@v3 + name: upload build artifact + with: + name: packaged_code + path: | + .aws-sam/build + packages/getSecretLayer/lib/get-secrets-layer.zip + + - shell: bash + name: package sandbox code + run: | + cp .tool-versions ~/ + rm -rf .aws-sam + export PATH=$PATH:$PWD/node_modules/.bin + make sam-build-sandbox + make compile-go + cp Makefile .aws-sam/build/ + cp samconfig_package_and_deploy.toml .aws-sam/build/ + + - uses: actions/upload-artifact@v3 + name: upload sandbox build artifact + with: + name: packaged_sandbox_code + path: | + .aws-sam/build + packages/getSecretLayer/lib/get-secrets-layer.zip + diff --git a/.github/workflows/sam_release_code.yml b/.github/workflows/sam_release_code.yml new file mode 100644 index 00000000..b9d374e3 --- /dev/null +++ b/.github/workflows/sam_release_code.yml @@ -0,0 +1,98 @@ +name: sam release code + +on: + workflow_call: + inputs: + STACK_NAME: + required: true + type: string + ARTIFACT_BUCKET_PREFIX: + required: true + type: string + TARGET_ENVIRONMENT: + required: true + type: string + ENABLE_MUTUAL_TLS: + required: true + type: string + BUILD_ARTIFACT: + required: true + type: string + DEPLOY_SANDBOX: + required: true + type: string + TRUSTSTORE_FILE: + required: true + type: string + VERSION_NUMBER: + required: true + type: string + COMMIT_ID: + required: true + type: string + LOG_LEVEL: + required: true + type: string + LOG_RETENTION_DAYS: + required: true + type: string + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: + required: true + SPLUNK_HEC_TOKEN: + required: true + TARGET_SPINE_SERVER: + required: true + +jobs: + sam_release_code: + runs-on: ubuntu-latest + environment: ${{ inputs.TARGET_ENVIRONMENT }} + permissions: + id-token: write + contents: read + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.CLOUD_FORMATION_DEPLOY_ROLE }} + role-session-name: github-actions + + - uses: actions/download-artifact@v3 + name: download build artifact + with: + name: ${{ inputs.BUILD_ARTIFACT }} + path: . + + - shell: bash + name: release code + run: | + echo ${{ inputs.COMMIT_ID }} + export artifact_bucket=$(aws cloudformation list-exports --output json | \ + jq -r '.Exports[] | select(.Name == "account-resources:ArtifactsBucket") | .Value' |grep -o '[^:]*$') + export artifact_bucket_prefix=prescriptions_for_patients/${{ inputs.ARTIFACT_BUCKET_PREFIX }} + export template_file=template.yaml + export stack_name=${{ inputs.STACK_NAME }} + export SPLUNK_HEC_ENDPOINT=${{ vars.SPLUNK_STRUCTURED_ENDPOINT }} + export SPLUNK_HEC_TOKEN=${{ secrets.SPLUNK_HEC_TOKEN }} + export target_spine_server=${{ secrets.TARGET_SPINE_SERVER }} + export VERSION_NUMBER=${{ inputs.VERSION_NUMBER }} + export COMMIT_ID=${{ inputs.COMMIT_ID }} + export cloud_formation_execution_role=$(aws cloudformation list-exports --output json | \ + jq -r '.Exports[] | select(.Name == "ci-resources:CloudFormationExecutionRole") | .Value' ) + TRUSTSTORE_BUCKET_ARN=$(aws cloudformation describe-stacks \ + --stack-name account-resources \ + --query 'Stacks[0].Outputs[?OutputKey==`TrustStoreBucket`].OutputValue' --output text) + TRUSTSTORE_BUCKET_NAME=$(echo ${TRUSTSTORE_BUCKET_ARN} | cut -d ":" -f 6) + export LATEST_TRUSTSTORE_VERSION=$(aws s3api list-object-versions \ + --bucket ${TRUSTSTORE_BUCKET_NAME} \ + --prefix ${{ inputs.TRUSTSTORE_FILE }} \ + --query 'Versions[?IsLatest].[VersionId]' --output text) + export enable_mutual_tls=${{ inputs.ENABLE_MUTUAL_TLS }} + export LOG_LEVEL=${{inputs.LOG_LEVEL}} + export deploy_sandbox=${{ inputs.DEPLOY_SANDBOX }} + export LOG_RETENTION_DAYS=${{inputs.LOG_RETENTION_DAYS}} + export TARGET_ENVIRONMENT=${{inputs.TARGET_ENVIRONMENT}} + cd .aws-sam/build + make sam-deploy-package