What are Github Actions? - Part 4

Last week I alluded to how our first job’s output dictates whether or not downstream workflow jobs even need to be ran. Declaring this dependency is faily simple in the GitHub Action syntax. Similar to my previous post, I am going to walk us through the interesting bits of the second jobs entry. See below for the snippet we’ll be working through today.

  prefect_deploy:
    needs: changes
    if: needs.changes.outputs.prefect_flows_changed == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up Python 3.10
        uses: actions/setup-python@v4
        with:
          python-version: '3.10.4'
          architecture: 'x64'

      - name: Python dependencies
        working-directory: ${{ env.WORKINGDIR }}
        run: |
          pip install .

      - name: Prefect Cloud login
        run: |
          prefect config set PREFECT_API_KEY=${{ secrets.PREFECT_API_KEY }}
          prefect config set PREFECT_API_URL=https://FAKE_URL.COM

      - name: Deploy Blocks to Prefect Cloud
        id: build-blocks
        working-directory: ${{ env.WORKINGDIR }}
        run: |
          cat <<EOF > gcp_cred_block.py
          from prefect_gcp import GcpCredentials

          service_account_info = ${{ secrets.GCP_CREDENTIALS }}
          gcp_creds = GcpCredentials(service_account_info=service_account_info)
          gcp_creds.save("my-srvc-usr", overwrite=True)
          EOF
          python gcp_cred_block.py
          python blocks.py --env $ENVIRONMENT

      - name: Deploy Flows to Prefect Cloud
        id: build-deployments
        working-directory: ${{ env.WORKINGDIR }}
        run: |
          python deployments.py --deployments-ver $GITHUB_SHA --env $ENVIRONMENT

      - name: Upload YAML deployment manifest as artifact
        uses: actions/upload-artifact@v3
        with:
          name: Deployment YAML manifests
          path: ./ints/**/*.yaml

needs

With needs you are able to quickly list any jobs that need to be successfully completed prior to this job kicking off. Potential values here can either be a string (if you only depend on a single job) or an array of stings (to list multiple jobs that need to successfully complete).

if

🥹 Our dear friend the if-statement! This works exactly how you probably are already imagining. The conditional will only allow a job to run if the logic in the if evaluates to True.

steps

Our first three steps here can be grouped together and probably don’t require that much of a detailed explanation. These can be thought of as setting up our ubuntu runner. As a matter of fact, you could probably glean all you needed to know from just reading the name of each of the three steps. If our changes job successfully runs, then with our first three steps of the prefect_deploy our workflow runner gather’s the state of our repo at the time of the workflow trigger, ensures that python is available for us to use, and lastly we perform a good ole pip install because we are going to need some of our python modules as we’ll soon see. Notice our use of the previously covered env when we set the working directory we want to be in when we run our pip install.

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up Python 3.10
        uses: actions/setup-python@v4
        with:
          python-version: '3.10.4'
          architecture: 'x64'

      - name: Python dependencies
        working-directory: ${{ env.WORKINGDIR }}
        run: |
          pip install .

The remaining steps in this prefect_deploy job directly relate to deploying new or modified files that need to be synced to my Prefect Cloud workspace. Some of these steps may not necessarily make the most sense (especially if you are not familiar with Prefect), but they are still good examples of how you can use your workflow steps to perform some simple cli commands to aid with your deployments.

      - name: Prefect Cloud login
        run: |
          prefect config set PREFECT_API_KEY=${{ secrets.PREFECT_API_KEY }}
          prefect config set PREFECT_API_URL=https://FAKE_URL.COM

      - name: Deploy Blocks to Prefect Cloud
        id: build-blocks
        working-directory: ${{ env.WORKINGDIR }}
        run: |
          cat <<EOF > gcp_cred_block.py
          from prefect_gcp import GcpCredentials

          service_account_info = ${{ secrets.GCP_CREDENTIALS }}
          gcp_creds = GcpCredentials(service_account_info=service_account_info)
          gcp_creds.save("my-srvc-usr", overwrite=True)
          EOF
          python gcp_cred_block.py
          python blocks.py --env $ENVIRONMENT

      - name: Deploy Flows to Prefect Cloud
        id: build-deployments
        working-directory: ${{ env.WORKINGDIR }}
        run: |
          python deployments.py --deployments-ver $GITHUB_SHA --env $ENVIRONMENT

Lastly, remember that if syntax at the start of this job?

if: needs.changes.outputs.prefect_flows_changed == 'true'

I configured this job to only run if files that match the flows filter in my changes job are either added or modified (see below for a quick refresher).

jobs:
  changes:
    name: Code & dependency changes
    runs-on: ubuntu-latest
    outputs:
      prefect_flows: ${{ steps.filter.outputs.flows_files }}
      prefect_flows_changed: ${{ steps.filter.outputs.flows }}
      code_dependencies_changed: ${{ steps.filter.outputs.docker_code }}

    ...

      - uses: dorny/paths-filter@v2
        id: filter
        with:
          list-files: json
          filters: |
            flows:
              - added|modified: "${{ env.WORKINGDIR }}/blocks.py"
              - added|modified: "${{ env.WORKINGDIR }}/deployments.py"

So, if my new commits to the repo haven’t touched any of the files listed in that filter this job has no reason to even run! For this workflow this shaves about 1 minute off the workflow run time. This type of conditional job setup can definitely be a good tool to keep in mind for other workflows.

There is one more job that I shared in the first post of this series. I am going to leave for you readers to grok.


  gcp_deploy:
    needs: changes
    if: needs.changes.outputs.code_dependencies_changed == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    # id-token: write is used by Google auth to request an OpenID Connect JWT Token https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
    outputs:
      image: ${{ steps.build-image.outputs.image }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Login to GAR
        uses: docker/login-action@v2
        with:
          registry: "${{ env.REGION }}-docker.pkg.dev"
          username: _json_key
          password: ${{ secrets.GCP_CREDENTIALS }}

      - name: Set docker image URI
        run: |
          echo "AGENT_IMG_URI=$REGION-docker.pkg.dev/$PROJECT/$GCP_AR_REPO/my-project-prefect-agent-$ENVIRONMENT:latest" >> $GITHUB_ENV
          echo "FLOWS_IMG_URI=$REGION-docker.pkg.dev/$PROJECT/$GCP_AR_REPO/$SUB_DIR-flows-$ENVIRONMENT:latest" >> $GITHUB_ENV

      - name: Build and Push Agent Docker Image
        id: build-agent-image
        working-directory: ints
        run: |
          docker build \
          --build-arg PREFECT_API_KEY=${{ secrets.PREFECT_API_KEY }} \
          --build-arg PREFECT_API_URL=https://FAKE_URL.COM \
          --build-arg PREFECT_WORK_QUEUE="$WORK_QUEUE" \
          -t "${{ env.AGENT_IMG_URI }}" \
          -f Dockerfile.agent .
          docker push "${{ env.AGENT_IMG_URI }}"

      - name: Build and Push Flows Docker Image
        id: build-flows-image
        working-directory: ${{ env.WORKINGDIR }}
        run: |
          docker build --build-arg ENVIRONMENT=$ENVIRONMENT -t "${{ env.FLOWS_IMG_URI }}" .
          docker push "${{ env.FLOWS_IMG_URI }}"

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          credentials_json: "${{ secrets.GCP_CREDENTIALS }}"

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1

      - name: Delete VM if exists
        continue-on-error: true
        run: gcloud compute instances delete "$GCE_INSTANCE" --zone "$GCE_INSTANCE_ZONE" --quiet

      # Scopes defines which services VM needs, SA sets permissions for those
      - name: Deploy Prefect Agent VM
        run: |
          gcloud compute instances create-with-container "$GCE_INSTANCE" \
            --zone "$GCE_INSTANCE_ZONE" \
            --machine-type "e2-micro" \
            --scopes "cloud-platform" \
            --service-account "[email protected]" \
            --container-image "${{ env.AGENT_IMG_URI }}"