From 6d69a410b2ceee0dfd625c99e2dcfd4d0672dce6 Mon Sep 17 00:00:00 2001 From: "E. Lynette Rayle" Date: Sat, 13 Apr 2024 18:56:36 -0400 Subject: [PATCH] create reusable workflows for primary apps deploy --- .github/workflows/app-build-and-deploy.yml | 112 +++++++++++++++++ .github/workflows/app-build-docker-image.yml | 86 +++++++++++++ .github/workflows/app-deploy-to-azure.yml | 122 +++++++++++++++++++ .github/workflows/app-is-deployable.yml | 104 ++++++++++++++++ 4 files changed, 424 insertions(+) create mode 100644 .github/workflows/app-build-and-deploy.yml create mode 100644 .github/workflows/app-build-docker-image.yml create mode 100644 .github/workflows/app-deploy-to-azure.yml create mode 100644 .github/workflows/app-is-deployable.yml diff --git a/.github/workflows/app-build-and-deploy.yml b/.github/workflows/app-build-and-deploy.yml new file mode 100644 index 0000000..6fd54d9 --- /dev/null +++ b/.github/workflows/app-build-and-deploy.yml @@ -0,0 +1,112 @@ +# This workflow will build a docker image, push it to ghcr.io, and deploy it to an Azure WebApp. +name: Build and Deploy to prod service app + +on: + workflow_call: + inputs: + deploy-env: + description: 'environment to deploy (i.e. dev | prod)' + required: true + type: string + application-type: + description: 'application type (i.e. api | worker | ui)' + required: true + type: string + azure-app-base-name: + description: 'Azure application name of webapp to deploy (i.e. clearlydefined-api | cdcrawler | clearlydefined)' + required: true + type: string + azure-app-name-postfix: + description: 'postfix to apply to the base name for the primary deploy site (e.g. -prod, -dev)' + required: true + type: string + secondary-azure-app-name-postfix: + description: 'postfix to apply to the base name for a secondary deploy site (e.g. -prod-europe, do not specify if no secondary site)' + type: string + default: '' + is-release: + description: 'whether this is a release deployment' + type: boolean + default: false + +# Secrets required for publish to ghcr and deploy to Azure. +# +# Passed Secrets: +# AZURE_WEBAPP_PUBLISH_PROFILE: publish profile for the Azure WebApp being deployed +# AZURE_SECONDARY_WEBAPP_PUBLISH_PROFILE: publish profile for a secondary Azure WebApp if being deployed +# +# Org Secrets: +# AZURE_CREDENTIALS: service principal that has access to the Azure apps (dev and prod) +# DEPLOY_TOKEN: token that is used to determine if the deployment is allowed (prod only) +# PRODUCTION_DEPLOYERS: name of the team that defines who can deploy to production (prod only) + +jobs: + get-version: + name: Get version from package-lock.json + runs-on: ubuntu-latest + outputs: + version: ${{ env.VERSION }} + steps: + - name: Get version from package-lock.json + id: get_version + shell: bash + run: | + version='v'$(jq -r '.version' package-lock.json) // e.g. v1.2.0 + if [[ ${{ inputs.deploy-env }} != 'prod' ]]; then + short_sha=$(echo "${{ github.sha }}" | cut -c 1-10) + version=$version'+dev:'$short_sha // e.g. v1.2.0+dev:1234567890 + fi + + // validate the version when triggered by a release + if [[ ${{ inputs.is-release }} == 'true' ]]; then + if [[ $version != 'v'${{ github.event.release.tag_name }} ]]; then + echo "Version in package-lock.json ($version) does not match the release tag (${{ github.event.release.tag_name }})" + exit 1 + fi + fi + echo "VERSION=$version" >> $GITHUB_ENV + + build-and-publish-image: + name: Build and publish Docker image + needs: get-version + uses: clearlydefined/operations/.github/workflows/app-build-docker-image.yml@elr/reusable-deploy-workflow + secrets: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + PRODUCTION_DEPLOYERS: ${{ secrets.PRODUCTION_DEPLOYERS }} + with: + deploy-env: ${{ inputs.deploy-env }} + application-type: ${{ inputs.application-type }} + image-tag: ${{ needs.get-version.outputs.version }} + + deploy-primary-app-to-azure: + name: Deploy to primary Azure app + needs: [get-version, build-and-publish-image] + uses: clearlydefined/operations/.github/workflows/app-deploy-to-azure.yml@elr/reusable-deploy-workflow + secrets: + AZURE_WEBAPP_PUBLISH_PROFILE: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + PRODUCTION_DEPLOYERS: ${{ secrets.PRODUCTION_DEPLOYERS }} + with: + deploy-env: ${{ inputs.deploy-env }} + application-type: ${{ inputs.application-type }} + azure-webapp-name: ${{ inputs.azure-app-base-name }}${{ inputs.azure-app-name-postfix }} + application-version: ${{ needs.get-version.outputs.version }} + image-name-with-tag: ${{ needs.build-and-publish-image.outputs.docker-image-name-with-tag }} + + deploy-secondary-app-to-azure: + name: Deploy to secondary Azure app + if: ${{ inputs.secondary-azure-app-name-postfix != '' }} + needs: [get-version, build-and-publish-image] + uses: clearlydefined/operations/.github/workflows/app-deploy-to-azure.yml@elr/reusable-deploy-workflow + secrets: + AZURE_WEBAPP_PUBLISH_PROFILE: ${{ secrets.AZURE_SECONDARY_WEBAPP_PUBLISH_PROFILE }} + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + PRODUCTION_DEPLOYERS: ${{ secrets.PRODUCTION_DEPLOYERS }} + with: + deploy-env: ${{ inputs.deploy-env }} + application-type: ${{ inputs.application-type }} + azure-webapp-name: ${{ inputs.azure-app-base-name }}${{ inputs.secondary-azure-app-name-postfix }} + application-version: ${{ needs.get-version.outputs.version }} + image-name-with-tag: ${{ needs.build-and-publish-image.outputs.docker-image-name-with-tag }} diff --git a/.github/workflows/app-build-docker-image.yml b/.github/workflows/app-build-docker-image.yml new file mode 100644 index 0000000..dabbc27 --- /dev/null +++ b/.github/workflows/app-build-docker-image.yml @@ -0,0 +1,86 @@ +# This workflow will build a docker image, push it to ghcr.io. It returns the docker image name and tag. +name: Build docker image for Azure app + +on: + workflow_call: + inputs: + deploy-env: + description: 'environment to deploy (i.e. dev | prod) - used as a label for the Docker image' + required: true + type: string + application-type: + description: 'application type (i.e. api | worker | ui) - used as a label for the Docker image' + required: true + type: string + image-tag: + description: 'the tag to use for the image (e.g. prod: v1.2.0, dev: v1.2.0+dev:1D3F567 -- number is first 7 char in sha)' + required: true + type: string + + outputs: + docker-image-name_with_tag: + value: ${{ jobs.determine-image-name.outputs.docker-image-name-with-tag }} + +# Secrets required for publish to ghcr. +# +# Org Secrets: +# DEPLOY_TOKEN: token that is used to determine if the deployment is allowed +# PRODUCTION_DEPLOYERS: name of the team that defines who can deploy to production +# +# Environment Variables: +# DOCKER_IMAGE_BASE_NAME: base name of the Docker image that is being built and pushed to ghcr.io. + +env: + DOCKER_IMAGE_BASE_NAME: ghcr.io/${{ github.repository }} # e.g. ghcr.io/clearlydefined/service + +jobs: + check-deployable: + uses: clearlydefined/operations/.github/workflows/app-is-deployable.yml@elr/reusable-deploy-workflow + with: + deploy-env: ${{ inputs.deploy-env }} + secrets: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + PRODUCTION_DEPLOYERS: ${{ secrets.PRODUCTION_DEPLOYERS }} + + determine-image-name: + name: Determine Image Name + runs-on: ubuntu-latest + outputs: + docker-image-name_with_tag: ${{ env.DOCKER_IMAGE_NAME_WITH_TAG }} + steps: + - name: Determine Docker Image Name + id: determine_image_name + run: | + if [[ env.DEPLOY_ENV == "prod" ]] ; then + echo "DOCKER_IMAGE_NAME_WITH_TAG=${{ env.DOCKER_IMAGE_BASE_NAME }}:${{ inputs.image-tag }}" >> $GITHUB_ENV + else if [[ env.DEPLOY_ENV == "dev" ]] ; then + echo "DOCKER_IMAGE_NAME_WITH_TAG=${{ env.DOCKER_IMAGE_BASE_NAME }}-dev:${{ inputs.image-tag }}" >> $GITHUB_ENV + else + echo "DOCKER_IMAGE_NAME_WITH_TAG=${{ env.DOCKER_IMAGE_BASE_NAME }}-unknown:${{ inputs.image-tag }}" >> $GITHUB_ENV + fi + + build-docker-image: + name: Build Image + runs-on: ubuntu-latest + needs: [check-deployable, determine-image-name] + steps: + - uses: actions/checkout@v4.1.1 + + - name: Log into ghcr registry + uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} # user that kicked off the action + password: ${{ secrets.GITHUB_TOKEN }} # token created when the action launched (short lived) + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v5.2.0 + with: + context: . + push: true + file: Dockerfile + tags: ${{ needs.determine-image-name.outputs.docker-image-name_with_tag }} + labels: | + env=${{ inputs.deploy-env }} + type=${{ inputs.application-type }} diff --git a/.github/workflows/app-deploy-to-azure.yml b/.github/workflows/app-deploy-to-azure.yml new file mode 100644 index 0000000..cae2402 --- /dev/null +++ b/.github/workflows/app-deploy-to-azure.yml @@ -0,0 +1,122 @@ +# This workflow will deploy a Docker image in ghcr.io to an Azure WebApp. +name: Deploy docker image to Azure WebApp + +on: + workflow_call: + inputs: + deploy-env: + description: 'environment to deploy (i.e. dev | prod)' + required: true + type: string + application-type: + description: 'application type (i.e. api | worker | ui)' + required: true + type: string + azure-webapp-name: + description: 'Azure application name of application to deploy (i.e. clearlydefined-api | cdcrawler | clearlydefined)' + required: true + type: string + application-version: + description: 'application version (e.g. sha for dev env, version for prod env in the format: v1.2.0)' + required: true + type: string + image-name-with-tag: + description: 'Docker image name with the tag (e.g. ghcr.io/clearlydefined/clearlydefined-api:v1.2.0)' + required: true + type: string + +# Secrets required for deploy to Azure. +# +# Passed Secrets: +# AZURE_WEBAPP_PUBLISH_PROFILE: publish profile for the Azure WebApp being deployed +# +# Org Secrets: +# AZURE_CREDENTIALS: service principal that has access to the Azure app being deployed +# DEPLOY_TOKEN: token that is used to determine if the deployment is allowed +# PRODUCTION_DEPLOYERS: name of the team that defines who can deploy to production + +jobs: + check-deployable: + uses: clearlydefined/operations/.github/workflows/app-is-deployable.yml@elr/reusable-deploy-workflow + with: + deploy-env: ${{ inputs.deploy-env }} + secrets: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + PRODUCTION_DEPLOYERS: ${{ secrets.PRODUCTION_DEPLOYERS }} + + verify-secrets: + name: Secrets Verification + runs-on: ubuntu-latest + steps: + - name: Check secrets + shell: bash + run: | + missing=false + + secret_value=$(echo '${{ secrets.AZURE_CREDENTIALS }}') + single_line_value=$(echo -n "$secret_value" | tr -d '\n') + len=${#single_line_value} + if [[ ${len} -le 0 ]]; then + echo "Secret AZURE_CREDENTIALS does not have a value" + missing=true + fi + + secret_value=$(echo '${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}') + single_line_value=$(echo -n "$secret_value" | tr -d '\n') + len=${#single_line_value} + if [[ ${len} -le 0 ]]; then + echo "Secret AZURE_WEBAPP_PUBLISH_PROFILE does not have a value" + missing=true + fi + + if [[ $missing == true ]]; then + exit 1 + fi + echo "Required secrets all have values" + + deploy: + name: Deploy to Azure WebApp + runs-on: ubuntu-latest + needs: [ check-deployable, verify-secrets ] + steps: + - name: Login for Azure cli commands + uses: azure/login@v2.0.0 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set DOCKER configs in Azure web app + uses: azure/appservice-settings@v1.1.1 + with: + app-name: ${{ inputs.azure-webapp-name }} + app-settings-json: | + [ + { + "name": "DOCKER_CUSTOM_IMAGE_NAME", + "value": "${{ inputs.image-name-with-tag }}, + "slotSetting": false + }, + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "https://ghcr.io", + "slotSetting": false + }, + { + "name": "APP_VERSION", + "value": "${{ inputs.application-version }}", + "slotSetting": false + }, + { + "name": "BUILD_SHA", + "value": "${{ github.sha }}", + "slotSetting": false + } + ] + + # v3.0.1 passes when AZURE_WEBAPP_PUBLISH_PROFILE_PROD isn't set, but should fail. + # Added secret check above to ensure it is set. + - name: Deploy to Azure WebApp + uses: azure/webapps-deploy@v3.0.1 + with: + app-name: ${{ inputs.azure-webapp-name }} + publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} + images: '${{ inputs.image-name-with-tag }}' diff --git a/.github/workflows/app-is-deployable.yml b/.github/workflows/app-is-deployable.yml new file mode 100644 index 0000000..04d14e9 --- /dev/null +++ b/.github/workflows/app-is-deployable.yml @@ -0,0 +1,104 @@ +name: Deployable + +on: + workflow_call: + inputs: + deploy-env: + description: 'environment to deploy to - one of dev, prod' + required: true + type: string + +# Org Secrets: +# DEPLOY_TOKEN: token with permissions needed to determine if github.actor can deploy to production +# PRODUCTION_DEPLOYERS: name of team identifying users that can deploy to production + +env: + DEPLOY_ENVIRONMENT: ${{ inputs.deploy-env }} + +jobs: + verify-secrets: + name: Secrets Verification + runs-on: ubuntu-latest + steps: + - name: Check secrets + run: | + missing=false + + secret_value=$(echo '${{ secrets.PRODUCTION_DEPLOYERS }}') + single_line_value=$(echo -n "$secret_value" | tr -d '\n') + len=${#single_line_value} + if [[ ${len} -le 0 ]]; then + echo "Secret PRODUCTION_DEPLOYERS does not have a value" + missing=true + fi + + secret_value=$(echo '${{ secrets.DEPLOY_TOKEN }}') + single_line_value=$(echo -n "$secret_value" | tr -d '\n') + len=${#single_line_value} + if [[ ${len} -le 0 ]]; then + echo "Secret DEPLOY_TOKEN does not have a value" + missing=true + fi + + if [[ $missing == true ]]; then + exit 1 + fi + echo "Deployable: Required secrets all have values" + + confirm-dev: + runs-on: ubuntu-latest + outputs: + isdev: ${{ steps.confirm-dev.outputs.isdev }} + steps: + # erring on the side of caution by assuming everything is prod unless it is identified as dev and ends in -dev + - id: confirm-dev + shell: bash + run: | + isdev=false + if [[ "${{ env.DEPLOY_ENVIRONMENT }}" == 'dev' ]] && [[ "${{ env.AZURE_WEBAPP_NAME }}" == "*-dev" ]]; then + isdev=true + echo "Deploying to dev environment" + else + echo "Deploying to prod or UNKNOWN environment" + fi + echo "::set-output name=isdev::$isdev" + + deployable: + runs-on: ubuntu-latest + needs: [ verify-secrets, confirm-dev ] + # run deployable check for anything that is NOT dev (most conservative approach) + if: ${{ needs.confirm-dev.outputs.isdev != 'true' }} + steps: + - name: Get organization ID + run: | + org_name=${{ github.repository_owner }} + org_info=$(curl \ + -H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/orgs/$org_name) + org_id=$(echo "$org_info" | jq .id) + echo "ORG_ID=$org_id" >> $GITHUB_ENV + + - name: Check team membership + run: | + user="${{ github.actor }}" + org_id=${{ env.ORG_ID }} + org_name=${{ github.repository_owner }} + + team_info=$(curl \ + -H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/orgs/$org_name/teams) + team_id=$(echo "$team_info" | jq '.[] | select(.name=="${{ secrets.PRODUCTION_DEPLOYERS }}") | .id') + + membership=$(curl \ + -H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/orgs/$org_id/team/$team_id/memberships/$user) + + if [[ $membership == *"active"* ]]; then + echo "$user is a member of the team" + else + echo "$user does not have permissions to deploy" + exit 1 + fi