diff --git a/.github/workflows/build-and-deploy-to-eks.yaml b/.github/workflows/build-and-deploy-to-eks.yaml new file mode 100644 index 0000000..37814a9 --- /dev/null +++ b/.github/workflows/build-and-deploy-to-eks.yaml @@ -0,0 +1,229 @@ +name: Kubernetes +on: + workflow_call: + inputs: + artifactName: + required: false + description: Downloads a previously uploaded artifact (has to be in the same workflow). Both artifactPath and artifactName have to be passed. + default: "" + type: string + artifactPath: + required: false + description: Downloads a previously uploaded artifact (has to be in the same workflow). Both artifactPath and artifactName have to be passed. + default: "" + type: string + appName: + required: false + type: string + default: "" + environment: + required: false + type: string + default: "" + description: + required: false + type: string + deploymentRepoPath: + required: false + description: Path to the values.yaml file in the deployment repository (e.g. .chart/staging) + type: string + deploymentRepoURL: + required: false + description: URL of the deployment repository + type: string + createGitHubDeployment: + required: false + default: false + type: boolean + enableContainerScan: + required: false + default: true + type: boolean + env: + required: true + type: string + imageTargets: + required: false + description: Sets targets for as many image builds as targets specified in Containerfile + default: "" + type: string + ref: + required: true + type: string + runner: + required: false + default: ubuntu-latest + type: string + sentryOrg: + required: false + type: string + sentryProject: + required: false + type: string + sentryEnvironment: + required: false + type: string + sentryUrl: + required: false + type: string + slackChannelId: + required: false + type: string + tagPath: + required: false + type: string + context: + required: false + type: string + default: "." + dockerfile: + required: false + type: string + default: "Containerfile" + pushToEnvTag: + required: false + type: boolean + default: false + secrets: + slackBotToken: + required: false + description: The Slack bot token to write messages in the desired channels (required if slack channel ids are provided) + sentryAuthToken: + required: false + REPO_ACCESS_TOKEN: + required: false + AWS_ROLE_TO_ASSUME: + required: true + description: AWS OIDC role for GitHub to assume + +jobs: + init: + runs-on: ${{ inputs.runner }} + outputs: + version: ${{ steps.vars.outputs.version }} + steps: + - name: Load deployment variables + id: vars + run: | + REF="${{ inputs.ref }}" + SHA="${{ github.sha }}" + if [[ "${{ inputs.env }}" == 'prod' ]] + then + # shellcheck disable=SC2086 + echo "version=${REF##*/}" >> $GITHUB_OUTPUT + else + # shellcheck disable=SC2086 + echo "version=${SHA:0:7}" >> $GITHUB_OUTPUT + fi + + build: + needs: init + permissions: + contents: read + id-token: write + uses: ./.github/workflows/build-image.yaml + with: + artifactName: ${{ inputs.artifactName }} + artifactPath: ${{ inputs.artifactPath }} + imageTargets: ${{ inputs.imageTargets }} + enableContainerScan: ${{ inputs.enableContainerScan }} + runner: ${{ inputs.runner }} + version: ${{ needs.init.outputs.version }} + appName: ${{ inputs.appName }} + environment: ${{ inputs.environment }} + context: ${{ inputs.context }} + dockerfile: ${{ inputs.dockerfile }} + secrets: inherit + + commit: + needs: build + concurrency: commit-${{ inputs.deploymentRepoURL }}-${{ github.sha }} + runs-on: ${{ inputs.runner }} + steps: + - name: Checkout current git repository + uses: actions/checkout@v6 + - name: Deploy ${{ github.sha }} to ${{ inputs.env }} values + uses: mikefarah/yq@v4.30.8 + with: + cmd: yq '(.${{ inputs.tagPath }} = "${{ needs.init.outputs.version }}")' -i ${{ inputs.deploymentRepoPath }}/values.yaml + - name: Commit and push new tag + env: + env: ${{ inputs.env }} + version: ${{ needs.init.outputs.version }} + run: | + set -euxo pipefail + git config user.email "dev.bot@parcellab.com" + git config user.name "parcellab-dev-bot" + git add . + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + git commit -m "chore(deploy): set $env image tag to $version" + if [ "${{ inputs.pushToEnvTag }}" = "true" ]; then + echo "Set new image tag to $env" + NEW_SHA=$(git rev-parse HEAD) + git tag -fa "$env" -m "$env deploy $version ($NEW_SHA)" "$NEW_SHA" + git push origin -f "refs/tags/$env" + else + git push origin HEAD:main + fi + + post-deploy: + needs: [init, commit] + runs-on: ${{ inputs.runner }} + steps: + - if: inputs.slackChannelId + name: Send out Slack notification + continue-on-error: true + uses: darioblanco/slack-deployment@main + env: + SLACK_BOT_TOKEN: ${{ secrets.slackBotToken }} + with: + channel_id: ${{ inputs.slackChannelId }} + deployment_description: "No description" + deployment_name: ${{ inputs.artifactName != '' && inputs.artifactName || 'unknown' }} + environment: ${{ inputs.env }} + owner: ${{ github.actor }} + package: ${{ inputs.artifactName != '' && inputs.artifactName || 'unknown' }} + ref: ${{ inputs.ref }} + repo: ${{ github.repository }} + sha: ${{ github.sha }} + status_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + version: ${{ needs.init.outputs.version }} + - if: inputs.sentryOrg != '' && inputs.sentryProject != '' + name: Create Sentry release + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.sentryAuthToken }} + SENTRY_ORG: ${{ inputs.sentryOrg }} + SENTRY_PROJECT: ${{ inputs.sentryProject }} + SENTRY_URL: ${{ inputs.sentryUrl }} + with: + environment: ${{ inputs.sentryEnvironment != '' && inputs.sentryEnvironment || inputs.env }} + set_commits: skip + version: ${{ needs.init.outputs.version }} + continue-on-error: true + - if: inputs.createGitHubDeployment + name: Create GitHub Deployment + uses: chrnorm/deployment-action@v2 + id: deployment + with: + token: ${{ secrets.REPO_ACCESS_TOKEN }} + ref: ${{ inputs.ref }} + environment: ${{ inputs.env }} + description: ${{ inputs.description != '' && inputs.description || format('Manual deployment {0}', github.sha) }} + auto-merge: false + payload: | + {"env":${{ toJSON(inputs.env) }},"name":"product-api","author":${{ toJSON(github.actor) }},"description":${{ toJSON(inputs.description) }},"kubernetes":{"versionKey":"monolith.image.tag"}} + - if: inputs.createGitHubDeployment + name: Set GitHub Deployment status to successful + uses: chrnorm/deployment-status@v2 + with: + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + environment-url: ${{ steps.deployment.outputs.environment_url }} + environment: ${{ inputs.env }} + state: "success" + token: ${{ github.token }} + \ No newline at end of file diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 2edb895..13821b4 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -17,11 +17,6 @@ on: description: If provided, sets targets for as many image builds as targets specified default: "" type: string - preScript: - required: false - description: If provided, runs a script after repo checkout and before the docker image is built. Useful in case that you need to build a package outside of the docker image (and load the artifacts via copy). - default: "" - type: string enableContainerScan: required: false description: Apply the container scan @@ -35,6 +30,20 @@ on: version: required: true type: string + appName: + required: false + type: string + environment: + required: false + type: string + context: + required: false + type: string + default: "." + dockerfile: + required: false + type: string + default: "Containerfile" env: IMAGE_SCAN_SEVERITY: LOW @@ -48,8 +57,11 @@ jobs: permissions: id-token: write contents: read - environment: ${{ github.event.deployment.payload.env }} + environment: ${{ inputs.environment != '' && inputs.environment || github.event.deployment.payload.env }} runs-on: ${{ inputs.runner }} + env: + APP_NAME: ${{ inputs.appName != '' && inputs.appName || inputs.artifactName != '' && inputs.artifactName || github.event.deployment.payload.name }} + ENVIRONMENT: ${{ inputs.environment != '' && inputs.environment || github.event.deployment.payload.env }} steps: - name: Checkout current git repository uses: actions/checkout@v4 @@ -75,12 +87,12 @@ jobs: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - name: Create ECR repository if it doesn't exist run: | - if ! aws ecr describe-repositories --repository-names ${{ github.event.deployment.payload.name }} 2>/dev/null; then - echo "Repository ${{ github.event.deployment.payload.name }} does not exist, creating it..." - aws ecr create-repository --repository-name ${{ github.event.deployment.payload.name }} + if ! aws ecr describe-repositories --repository-names ${{ env.APP_NAME }} 2>/dev/null; then + echo "Repository ${{ env.APP_NAME }} does not exist, creating it..." + aws ecr create-repository --repository-name ${{ env.APP_NAME }} echo "Setting lifecycle policy..." else - echo "Repository ${{ github.event.deployment.payload.name }} already exists, skipping creation" + echo "Repository ${{ env.APP_NAME }} already exists, skipping creation" fi echo "Applying lifecycle policies" @@ -89,7 +101,7 @@ jobs: {"rulePriority":2,"description":"Preserve production images","selection":{"tagStatus":"tagged","tagPatternList":["v*"],"countType":"imageCountMoreThan","countNumber":50},"action":{"type":"expire"}}, {"rulePriority":3,"description":"Remove untagged images","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countUnit":"days","countNumber":7},"action":{"type":"expire"}} ]}' - aws ecr put-lifecycle-policy --repository-name ${{ github.event.deployment.payload.name }} --lifecycle-policy-text "$LIFECYCLE_POLICY" + aws ecr put-lifecycle-policy --repository-name ${{ env.APP_NAME }} --lifecycle-policy-text "$LIFECYCLE_POLICY" - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 @@ -99,25 +111,25 @@ jobs: build-args: | GITHUB_SHA=${{ github.sha }} VERSION=${{ inputs.version }} - APP_NAME=${{ github.event.deployment.payload.name }} - ENVIRONMENT=${{ github.event.deployment.payload.env }} + APP_NAME=${{ env.APP_NAME }} + ENVIRONMENT=${{ env.ENVIRONMENT }} NPM_GITHUB_TOKEN=${{ secrets.npmGithubReadToken }} - cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:cache - cache-to: mode=max,image-manifest=true,oci-mediatypes=true,type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:cache - context: ${{ github.event.deployment.payload.container.context }} + cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:cache + cache-to: mode=max,image-manifest=true,oci-mediatypes=true,type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:cache + context: ${{ inputs.context != '' && inputs.context || github.event.deployment.payload.container.context }} load: true - file: ${{ github.event.deployment.payload.container.file }} + file: ${{ inputs.dockerfile != '' && inputs.dockerfile || github.event.deployment.payload.container.file }} platforms: linux/amd64 tags: | - ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:latest - ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:${{ inputs.version }} - ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:latest + ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:${{ inputs.version }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:${{ github.sha }} - name: Scan for vulnerabilities if: inputs.enableContainerScan uses: crazy-max/ghaction-container-scan@v3 with: - image: ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:latest - dockerfile: Containerfile + image: ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:latest + dockerfile: ${{ inputs.dockerfile }} severity: ${{ env.IMAGE_SCAN_SEVERITY }} severity_threshold: ${{ env.IMAGE_SCAN_SEVERITY_THRESHOLD }} annotations: ${{ env.IMAGE_SCAN_ANNOTATIONS }} @@ -125,18 +137,21 @@ jobs: TRIVY_TIMEOUT: ${{ env.IMAGE_SCAN_TRIVY_TIMEOUT }} - name: Push image to ECR run: | - docker push -a ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }} + docker push -a ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }} build-ecr-matrix: if: inputs.imageTargets != '' permissions: id-token: write contents: read - environment: ${{ github.event.deployment.payload.env }} + environment: ${{ inputs.environment != '' && inputs.environment || github.event.deployment.payload.env }} runs-on: ${{ inputs.runner }} strategy: matrix: containerfile_targets: ${{ fromJson(inputs.imageTargets) }} + env: + APP_NAME: ${{ inputs.appName != '' && inputs.appName || inputs.artifactName != '' && inputs.artifactName || github.event.deployment.payload.name }} + ENVIRONMENT: ${{ inputs.environment != '' && inputs.environment || github.event.deployment.payload.env }} steps: - name: Checkout current git repository uses: actions/checkout@v4 @@ -162,12 +177,12 @@ jobs: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - name: Create ${{ matrix.containerfile_targets }} ECR repository if it doesn't exist run: | - if ! aws ecr describe-repositories --repository-names ${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }} 2>/dev/null; then - echo "Repository ${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }} does not exist, creating it..." - aws ecr create-repository --repository-name ${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }} + if ! aws ecr describe-repositories --repository-names ${{ env.APP_NAME }}-${{ matrix.containerfile_targets }} 2>/dev/null; then + echo "Repository ${{ env.APP_NAME }}-${{ matrix.containerfile_targets }} does not exist, creating it..." + aws ecr create-repository --repository-name ${{ env.APP_NAME }}-${{ matrix.containerfile_targets }} echo "Setting lifecycle policy..." else - echo "Repository ${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }} already exists, skipping creation" + echo "Repository ${{ env.APP_NAME }}-${{ matrix.containerfile_targets }} already exists, skipping creation" fi echo "Applying lifecycle policies" @@ -176,7 +191,7 @@ jobs: {"rulePriority":2,"description":"Preserve production images","selection":{"tagStatus":"tagged","tagPatternList":["v*"],"countType":"imageCountMoreThan","countNumber":50},"action":{"type":"expire"}}, {"rulePriority":3,"description":"Remove untagged images","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countUnit":"days","countNumber":7},"action":{"type":"expire"}} ]}' - aws ecr put-lifecycle-policy --repository-name ${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }} --lifecycle-policy-text "$LIFECYCLE_POLICY" + aws ecr put-lifecycle-policy --repository-name ${{ env.APP_NAME }}-${{ matrix.containerfile_targets }} --lifecycle-policy-text "$LIFECYCLE_POLICY" - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 @@ -186,26 +201,26 @@ jobs: build-args: | GITHUB_SHA=${{ github.sha }} VERSION=${{ inputs.version }} - APP_NAME=${{ github.event.deployment.payload.name }} - ENVIRONMENT=${{ github.event.deployment.payload.env }} + APP_NAME=${{ env.APP_NAME }} + ENVIRONMENT=${{ env.ENVIRONMENT }} NPM_GITHUB_TOKEN=${{ secrets.npmGithubReadToken }} - cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:cache - cache-to: mode=max,image-manifest=true,oci-mediatypes=true,type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}:cache - context: ${{ github.event.deployment.payload.container.context }} + cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:cache + cache-to: mode=max,image-manifest=true,oci-mediatypes=true,type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}:cache + context: ${{ inputs.context != '' && inputs.context || github.event.deployment.payload.container.context }} load: true - file: ${{ github.event.deployment.payload.container.file }} + file: ${{ inputs.dockerfile != '' && inputs.dockerfile || github.event.deployment.payload.container.file }} platforms: linux/amd64 tags: | - ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }}:latest - ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }}:${{ inputs.version }} - ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}-${{ matrix.containerfile_targets }}:latest + ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}-${{ matrix.containerfile_targets }}:${{ inputs.version }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}-${{ matrix.containerfile_targets }}:${{ github.sha }} target: ${{ matrix.containerfile_targets }} - name: Scan for vulnerabilities if: inputs.enableContainerScan uses: crazy-max/ghaction-container-scan@v3 with: - image: ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }}:latest - dockerfile: Containerfile + image: ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}-${{ matrix.containerfile_targets }}:latest + dockerfile: ${{ inputs.dockerfile }} severity: ${{ env.IMAGE_SCAN_SEVERITY }} severity_threshold: ${{ env.IMAGE_SCAN_SEVERITY_THRESHOLD }} annotations: ${{ env.IMAGE_SCAN_ANNOTATIONS }} @@ -213,4 +228,4 @@ jobs: TRIVY_TIMEOUT: ${{ env.IMAGE_SCAN_TRIVY_TIMEOUT }} - name: Push ${{ matrix.containerfile_targets }} image to ECR run: | - docker push -a ${{ steps.login-ecr.outputs.registry }}/${{ github.event.deployment.payload.name }}-${{ matrix.containerfile_targets }} + docker push -a ${{ steps.login-ecr.outputs.registry }}/${{ env.APP_NAME }}-${{ matrix.containerfile_targets }} diff --git a/.github/workflows/commit-and-push.yaml b/.github/workflows/commit-and-push.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/kubernetes.yaml b/.github/workflows/kubernetes.yaml index 761c0e4..c8ce5eb 100644 --- a/.github/workflows/kubernetes.yaml +++ b/.github/workflows/kubernetes.yaml @@ -204,7 +204,7 @@ jobs: repository: ${{ inputs.deploymentRepoURL }} directory: remote github_token: ${{ secrets.repoAccessToken }} - branch: main + branch: ${{ input.ref }} - if: success() name: Successful ${{ github.event.deployment.payload.name }} deployment uses: chrnorm/deployment-status@v2