From 2c23cf9bda692b4d1b5b5292b7917e01bbdefbdd Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 15:10:00 +0200 Subject: [PATCH] build(Github): Refactors main workflow and adds Dry Run mode and environment override --- .../build-and-deploy-cloud-run/action.yml | 85 +++++++++++ .github/workflows/{deploy.yml => main.yml} | 142 +++++++++--------- 2 files changed, 156 insertions(+), 71 deletions(-) create mode 100644 .github/actions/build-and-deploy-cloud-run/action.yml rename .github/workflows/{deploy.yml => main.yml} (66%) diff --git a/.github/actions/build-and-deploy-cloud-run/action.yml b/.github/actions/build-and-deploy-cloud-run/action.yml new file mode 100644 index 0000000..af57282 --- /dev/null +++ b/.github/actions/build-and-deploy-cloud-run/action.yml @@ -0,0 +1,85 @@ +name: Build And Deploy to Cloud Run +description: Build And Deploy to Cloud Run +inputs: + GCP_SA_KEY: + description: "Service Account Key to log into GCP" + required: true + REPOSITORY: + description: "Repository" + required: true + SERVICE: + description: "Service" + required: true + PROJECT_ID: + description: "GCP Project Id" + required: true + GAR_LOCATION: + description: "GCP Artifact Registry url" + required: true + ENVIRONMENT_NAME: + description: "Environment name (PRODUCTION, STAGING)" + required: true + REGION: + description: "GCP Region" + required: true + COMPONENT_PATH: + description: "Component path of the component to be deployed (./cms, ./client ...)" + required: true + DRY_RUN: + description: "Makes the action work in Dry Run Mode" + required: false + type: boolean + default: false + +#NOTE Actions needs to specify the shell to use on every action https://stackoverflow.com/questions/71041836/github-actions-required-property-is-missing-shell + +outputs: + url: + description: url + value: ${{steps.deploy.output}} + +runs: + using: "composite" + steps: + - name: Google Auth + id: auth + uses: 'google-github-actions/auth@v1' + with: + credentials_json: "${{ inputs.GCP_SA_KEY }}" + token_format: 'access_token' + + # Authenticate Docker to Google Cloud Artifact Registry via credentials json + - name: Docker Auth + id: docker-auth + uses: 'docker/login-action@v3' + with: + registry: ${{ inputs.GAR_LOCATION }}-docker.pkg.dev + username: _json_key + password: ${{ inputs.GCP_SA_KEY }} + + - name: Build Container + shell: bash + run: |- + docker build -f ${{ inputs.COMPONENT_PATH }}/Dockerfile.prod -t "${{ inputs.GAR_LOCATION }}-docker.pkg.dev/${{ inputs.PROJECT_ID }}/${{ inputs.REPOSITORY }}/${{ inputs.SERVICE }}:${{ github.sha }}" ${{ inputs.COMPONENT_PATH }} + + - name: Push Container + if: ${{ ! inputs.DRY_RUN }} + shell: bash + run: |- + docker push "${{ inputs.GAR_LOCATION }}-docker.pkg.dev/${{ inputs.PROJECT_ID }}/${{ inputs.REPOSITORY }}/${{ inputs.SERVICE }}:${{ github.sha }}" + # tag as "latest" + docker tag "${{ inputs.GAR_LOCATION }}-docker.pkg.dev/${{ inputs.PROJECT_ID }}/${{ inputs.REPOSITORY }}/${{ inputs.SERVICE }}:${{ github.sha }}" "${{ inputs.GAR_LOCATION }}-docker.pkg.dev/${{ inputs.PROJECT_ID }}/${{ inputs.REPOSITORY }}/${{ inputs.SERVICE }}:latest" + docker push "${{ inputs.GAR_LOCATION }}-docker.pkg.dev/${{ inputs.PROJECT_ID }}/${{ inputs.REPOSITORY }}/${{ inputs.SERVICE }}:latest" + + - name: Deploy to Cloud Run + if: ${{ ! inputs.DRY_RUN }} + id: deploy + uses: google-github-actions/deploy-cloudrun@v1 + with: + service: ${{ inputs.SERVICE }} + region: ${{ inputs.REGION }} + image: ${{ inputs.GAR_LOCATION }}-docker.pkg.dev/${{ inputs.PROJECT_ID }}/${{ inputs.REPOSITORY }}/${{ inputs.SERVICE }}:${{ github.sha }} + # NOTE: You can also set env variables here: + # env_vars: | + # NODE_ENV=production + # TOKEN_EXPIRE=6400 diff --git a/.github/workflows/deploy.yml b/.github/workflows/main.yml similarity index 66% rename from .github/workflows/deploy.yml rename to .github/workflows/main.yml index ec3b4f9..0f0c5f7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,17 @@ name: Run deploy to GCP on: workflow_dispatch: + inputs: + ENVIRONMENT_NAME_OVERRIDE: + description: "Environment name to override the environment used in the workflow" + required: false + type: string + dry_run: + description: "Makes the workflow trigger in dry run mode (no components will be published or deployed to the actual environment infrastructure)" + required: false + default: false + type: boolean + push: branches: - main @@ -47,8 +58,16 @@ jobs: client: - 'client/**' - '.github/workflows/**' + + - name: Applicable check + id: applicable_check + run: | + { + echo "flag=${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }}" + } >> $GITHUB_OUTPUT + - name: Extract branch name - if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} run: | { branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} @@ -58,16 +77,15 @@ jobs: id: extract_branch - name: Set environment name - if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} id: environment_name run: | { - echo "ENVIRONMENT=${{ steps.extract_branch.outputs.branch == 'main' && 'PRODUCTION' || steps.extract_branch.outputs.branch_upper }}" - } >> $GITHUB_ENV - + echo "ENVIRONMENT=${{ inputs.ENVIRONMENT_NAME_OVERRIDE || steps.extract_branch.outputs.branch == 'main' && 'PRODUCTION' || steps.extract_branch.outputs.branch_upper }}" + } >> $GITHUB_OUTPUT - name: Output secrets and vars as JSON - if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} # Use GH Actions toJSON function to convert secrets and vars to JSON; in case no values present, output null (otherwise jq will fail) run: | { @@ -81,7 +99,7 @@ jobs: id: env_json - name: Output secrets and vars as key=value entries - if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} # 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 @@ -95,7 +113,7 @@ jobs: id: env_entries_all - name: Filter secrets and vars for inclusion in .env file by environment and application prefixes - if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} # Use grep to filter client secrets & vars and save .env file (names starting with (TF_)((PRODUCTION|STAGING|SOMEBRANCH)_)[CLIENT_ENV|CMS_ENV]_ run: | { @@ -106,7 +124,7 @@ jobs: 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' }} + if: ${{ steps.applicable_check.outputs.flag }} # Use sed to strip environment and application prefixes from secret and var names run: | { @@ -117,23 +135,22 @@ jobs: id: env_entries_stripped - name: Save .env file - if: ${{ github.event_name == 'workflow_dispatch' || steps.client-changes.outputs.client == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} 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' }} + if: ${{ steps.applicable_check.outputs.flag }} id: auth - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' 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' }} + if: ${{ steps.applicable_check.outputs.flag }} id: docker-auth uses: 'docker/login-action@v3' with: @@ -141,20 +158,27 @@ jobs: 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' }} + - name: Build Container + if: ${{ steps.applicable_check.outputs.flag }} 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 + + - name: Push Container + if: ${{ steps.applicable_check.outputs.flag && !inputs.dry_run }} + 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 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' }} + if: ${{ steps.applicable_check.outputs.flag && !inputs.dry_run }} 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)] }} @@ -171,7 +195,7 @@ jobs: # If required, use the Cloud Run url output in later steps - name: Show Output - run: echo ${{ steps.deploy.outputs.url }} + run: echo ${{ steps.deploy.outputs.url || 'No URL generated' }} deploy_cms: # Add 'id-token' with the intended permissions for workload identity federation @@ -206,16 +230,23 @@ jobs: } >> $GITHUB_OUTPUT id: extract_branch + - name: Applicable check + id: applicable_check + run: | + { + echo "flag=${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }}" + } >> $GITHUB_OUTPUT + - name: Set environment name - if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} id: environment_name run: | { - echo "ENVIRONMENT=${{ steps.extract_branch.outputs.branch == 'main' && 'PRODUCTION' || steps.extract_branch.outputs.branch_upper }}" - } >> $GITHUB_ENV + echo "ENVIRONMENT=${{ inputs.ENVIRONMENT_NAME_OVERRIDE ||steps.extract_branch.outputs.branch == 'main' && 'PRODUCTION' || steps.extract_branch.outputs.branch_upper }}" + } >> $GITHUB_OUTPUT - name: Output secrets and vars as JSON - if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} # Use GH Actions toJSON function to convert secrets and vars to JSON; in case no values present, output null (otherwise jq will fail) run: | { @@ -229,7 +260,7 @@ jobs: id: env_json - name: Output secrets and vars as key=value entries - if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} # 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 @@ -243,7 +274,7 @@ jobs: id: env_entries_all - name: Filter secrets and vars for inclusion in .env file by environment and application prefixes - if: ${{ github.event_name == 'workflow_dispatch' || steps.cms-changes.outputs.cms == 'true' }} + if: ${{ steps.applicable_check.outputs.flag }} # Use grep to filter client secrets & vars and save .env file (names starting with (TF_)((PRODUCTION|STAGING|SOMEBRANCH)_)[CLIENT_ENV|CMS_ENV]_ run: | { @@ -254,69 +285,38 @@ jobs: 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' }} + if: ${{ steps.applicable_check.outputs.flag }} # 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' }} + if: ${{ steps.applicable_check.outputs.flag }} 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' + - name: Deploy and Push to Docker + if: ${{ steps.applicable_check.outputs.flag }} + id: deploy_and_push + uses: ./.github/actions/build-and-deploy-cloud-run 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: + GCP_SA_KEY: ${{ secrets[format('TF_{0}_GCP_SA_KEY', steps.environment_name.outputs.ENVIRONMENT)] }} + COMPONENT_PATH: "./cms" + ENVIRONMENT_NAME: ${{ steps.environment_name.outputs.ENVIRONMENT }} + GAR_LOCATION: ${{ env.GAR_LOCATION }} + PROJECT_ID: ${{ env.PROJECT_ID }} + REGION: ${{ env.REGION }} 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 + DRY_RUN: ${{ inputs.dry_run }} # 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 }} + if: ${{ steps.applicable_check.outputs.flag }} + run: echo ${{ steps.deploy_and_push.outputs.url || 'No URL generated' }}