From 26a58ea758f59ec3d7848fb827b75930c71aeb0f Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 14 May 2024 17:25:51 +0200 Subject: [PATCH] build(Github): Adds main Github Action workflow file --- .github/workflows/deploy.yml | 322 +++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..ec3b4f9 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,322 @@ +# The workflow processes GH secrets and variables managed by Terraform or manually, some of them for general usage in +# the github jobs, and some which component-relevant (cms, client, etc), used to build the .env files for the containers. +# These follow the naming convention: +# - TF_[PRODUCTION|]_[CLIENT_ENV|CMS_ENV|]_* - managed by Terraform +# - [PRODUCTION|]_[CLIENT_ENV|CMS_ENV|]_* - managed manually + +name: Run deploy to GCP + +on: + workflow_dispatch: + push: + branches: + - main + - staging + + paths: + - 'client/**' + - 'cms/**' + - '.github/workflows/*' + - 'infrastructure/**' + +env: + PROJECT_ID: ${{ secrets.TF_GCP_PROJECT_ID }} + GAR_LOCATION: ${{ secrets.TF_GCP_REGION }} + REGION: ${{ secrets.TF_GCP_REGION }} + +jobs: + deploy_client: + # Add 'id-token' with the intended permissions for workload identity federation + permissions: + contents: 'read' + id-token: 'write' + + runs-on: ubuntu-latest + env: + APP_ENV_PREFIX: CLIENT_ENV + APP_ENV_PATH: client/.env.local + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: client-changes + with: + filters: | + client: + - 'client/**' + - '.github/workflows/**' + - name: Extract branch name + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + run: | + { + branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + echo "branch=${branch}" + echo "branch_upper=${branch^^}" + } >> $GITHUB_OUTPUT + id: extract_branch + + - name: Set environment name + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + id: environment_name + run: | + { + echo "ENVIRONMENT=${{ steps.extract_branch.outputs.branch == 'main' && 'PRODUCTION' || steps.extract_branch.outputs.branch_upper }}" + } >> $GITHUB_ENV + + + - name: Output secrets and vars as JSON + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + # Use GH Actions toJSON function to convert secrets and vars to JSON; in case no values present, output null (otherwise jq will fail) + run: | + { + echo 'secrets<> $GITHUB_OUTPUT + id: env_json + + - name: Output secrets and vars as key=value entries + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + # Use jq to convert JSON to key=value entries + # 1. to_entries converts JSON to array of key/value pairs + # 2. map(.key + "=" + .value) converts each key/value pair to key=value + # 3. .[] flattens array to key=value entries + run: | + { + echo 'entries_all<> $GITHUB_OUTPUT + id: env_entries_filtered + + - name: Strip environment and application prefixes from secret and var names + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + # Use sed to strip environment and application prefixes from secret and var names + run: | + { + echo 'entries_stripped<> $GITHUB_OUTPUT + id: env_entries_stripped + + - name: Save .env file + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + run: | + echo '${{ steps.env_entries_stripped.outputs.entries_stripped }}' >> $APP_ENV_PATH + cat $APP_ENV_PATH + + #- name: Google Auth authentication via credentials json + - name: Google Auth + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + id: auth + uses: 'google-github-actions/auth@v1' + with: + credentials_json: "${{ secrets[format('TF_{0}_GCP_SA_KEY', steps.environment_name.outputs.ENVIRONMENT)] }}" + token_format: 'access_token' + + # Authenticate Docker to Google Cloud Artifact Registry via credentials json + - name: Docker Auth + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + id: docker-auth + uses: 'docker/login-action@v3' + with: + registry: ${{ env.GAR_LOCATION }}-docker.pkg.dev + username: _json_key + password: ${{ secrets[format('TF_{0}_GCP_SA_KEY', steps.environment_name.outputs.ENVIRONMENT)] }} + + - name: Build and Push Container + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + env: + REPOSITORY: ${{ secrets[format('TF_{0}_CLIENT_REPOSITORY', steps.environment_name.outputs.ENVIRONMENT)] }} + SERVICE: ${{ secrets[format('TF_{0}_CLIENT_SERVICE', steps.environment_name.outputs.ENVIRONMENT)] }} + run: |- + docker build -f client/Dockerfile.prod -t "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" ./client + docker push "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" + # tag as "latest" + docker tag "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:latest" + docker push "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:latest" + + - name: Deploy to Cloud Run + if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + env: + REPOSITORY: ${{ secrets[format('TF_{0}_CLIENT_REPOSITORY', steps.environment_name.outputs.ENVIRONMENT)] }} + SERVICE: ${{ secrets[format('TF_{0}_CLIENT_SERVICE', steps.environment_name.outputs.ENVIRONMENT)] }} + id: deploy + uses: google-github-actions/deploy-cloudrun@v1 + with: + service: ${{ env.SERVICE }} + region: ${{ env.REGION }} + image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }} + # NOTE: You can also set env variables here: + # env_vars: | + # NODE_ENV=production + # TOKEN_EXPIRE=6400 + + # If required, use the Cloud Run url output in later steps + - name: Show Output + run: echo ${{ steps.deploy.outputs.url }} + + deploy_cms: + # Add 'id-token' with the intended permissions for workload identity federation + permissions: + contents: 'read' + id-token: 'write' + + runs-on: ubuntu-latest + env: + APP_ENV_PREFIX: CMS_ENV + APP_ENV_PATH: cms/.env + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: cms-changes + with: + filters: | + cms: + - 'cms/**' + - '.github/workflows/**' + + - name: Extract branch name + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + run: | + { + branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + echo "branch=${branch}" + echo "branch_upper=${branch^^}" + } >> $GITHUB_OUTPUT + id: extract_branch + + - name: Set environment name + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + id: environment_name + run: | + { + echo "ENVIRONMENT=${{ steps.extract_branch.outputs.branch == 'main' && 'PRODUCTION' || steps.extract_branch.outputs.branch_upper }}" + } >> $GITHUB_ENV + + - name: Output secrets and vars as JSON + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + # Use GH Actions toJSON function to convert secrets and vars to JSON; in case no values present, output null (otherwise jq will fail) + run: | + { + echo 'secrets<> $GITHUB_OUTPUT + id: env_json + + - name: Output secrets and vars as key=value entries + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + # Use jq to convert JSON to key=value entries + # 1. to_entries converts JSON to array of key/value pairs + # 2. map(.key + "=" + .value) converts each key/value pair to key=value + # 3. .[] flattens array to key=value entries + run: | + { + echo 'entries_all<> $GITHUB_OUTPUT + id: env_entries_filtered + + - name: Strip environment and application prefixes from secret and var names + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + # Use sed to strip environment and application prefixes from secret and var names + run: | + { + echo 'entries_stripped<> $GITHUB_OUTPUT + id: env_entries_stripped + + - name: Save .env file + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + run: | + echo '${{ steps.env_entries_stripped.outputs.entries_stripped }}' >> $APP_ENV_PATH + cat $APP_ENV_PATH + + - name: Google Auth + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + id: auth + uses: 'google-github-actions/auth@v1' + with: + credentials_json: "${{ secrets[format('TF_{0}_GCP_SA_KEY', steps.environment_name.outputs.ENVIRONMENT)] }}" + token_format: 'access_token' + + # Authenticate Docker to Google Cloud Artifact Registry via credentials json + - name: Docker Auth + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + id: docker-auth + uses: 'docker/login-action@v3' + with: + registry: ${{ env.GAR_LOCATION }}-docker.pkg.dev + username: _json_key + password: ${{ secrets[format('TF_{0}_GCP_SA_KEY', steps.environment_name.outputs.ENVIRONMENT)] }} + + - name: Build and Push Container + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + env: + REPOSITORY: ${{ secrets[format('TF_{0}_CMS_REPOSITORY', steps.environment_name.outputs.ENVIRONMENT)] }} + SERVICE: ${{ secrets[format('TF_{0}_CMS_SERVICE', steps.environment_name.outputs.ENVIRONMENT)] }} + run: |- + docker build -f cms/Dockerfile.prod -t "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" ./cms + docker push "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" + # tag as "latest" + docker tag "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:latest" + docker push "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:latest" + + - name: Deploy to Cloud Run + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + env: + REPOSITORY: ${{ secrets[format('TF_{0}_CMS_REPOSITORY', steps.environment_name.outputs.ENVIRONMENT)] }} + SERVICE: ${{ secrets[format('TF_{0}_CMS_SERVICE', steps.environment_name.outputs.ENVIRONMENT)] }} + id: deploy + uses: google-github-actions/deploy-cloudrun@v1 + with: + service: ${{ env.SERVICE }} + region: ${{ env.REGION }} + image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }} + # NOTE: You can also set env variables here: + # env_vars: | + # NODE_ENV=production + # TOKEN_EXPIRE=6400 + + # If required, use the Cloud Run url output in later steps + - name: Show Output + if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + run: echo ${{ steps.deploy.outputs.url }}