• Home
  • About
    • Harshad Ranganathan photo

      Harshad Ranganathan

      Multi-Cloud ☁ | Kubernetes Certified

    • Learn More
    • Email
    • LinkedIn
    • Github
    • StackOverflow
  • Posts
    • All Posts
    • All Tags
  • Projects

Github Actions - Pull Request Policy Checks

14 Aug 2023

Reading time ~13 minutes

Table Of Contents

  • github-actions-examples
    • Workflow Name
    • Workflow Trigger
    • Default Values
    • Environment Variables
    • Workflow Permissions
    • Workflow Jobs
    • Workflow Steps
      • Checkout Repo
      • Changed Files
      • Run Commands
      • Validate Policy
      • Add PR Comment
      • Action Result
    • PR Status Checks

We will look into examples of how to write Github actions to do policy checks on PR’s and some gotchas with respect to writing those actions.

HarshadRanganathan

github-actions-examples

  • 0
    Stars
  • 0
    Fork

Workflows have to be created inside .github/workflows folder.

Workflows can use any names but the file extension should be either .yml or .yaml.

For our example, let’s create a action file named policy-check-release-label.yml inside .github/workflows folder.

Purpose of this workflow is to check on every PR, the Step Function template (AWS cloud service) uses expected EMR (AWS Cloud Spark job) version and not stale EMR versions which may have security vulnerabilities or be non-compliant for our project needs.

Workflow Name

We define a name for the workflow:

name: 'Policy Check: EMR Release Label'

These are the names that will be displayed in the Actions page and in PR build status checks.

Workflow Trigger

Next, we define, how our workflow needs to be triggered.

name: 'Policy Check: EMR Release Label'

on: [pull_request]

Above, defines that the workflow needs to be triggered on PR’s. You can refine this to make it more specific such as run only if PR’s raised for specific branches/tags (or) define the action types such as trigger when PR is closed etc,.

All of these are documented at https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows

You can also filter to have workflows triggered only when certain files are changed in the PR to avoid getting invoked for every change e.g. ignore for README file updates and save on billing.

on:
  pull_request:
    paths:
      - '**.tpl'

There is a shortcoming with above approach of using path based filters. Consider, a scenario where you mandate this workflow needs to pass as part of PR status checks to prevent non-compliant changes to be merged.

If a PR gets raised that doesn’t have **.tpl file changes, we still need the status check to pass however, since we have added this condition as part of workflow trigger, the workflow doesn’t get triggered and hence the build check goes to Pending state/Waiting for status to be reported blocking your PR to be merged.

This is a limitation and hence we recommend not to use path based filters on PR trigger condition and instead use another approach of filtering and status checks to pass with minimal billing.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks

Like the content ? 

Default Values

You can set default settings that will apply to all jobs/steps in a workflow.

We use it to set default shell for our workflow.

name: 'Policy Check: EMR Release Label'

on: [pull_request]

defaults:
  run:
    shell: bash

Environment Variables

You can set environment variables to be used in any steps of all jobs in your workflow.

For example, below we set TARGET_RELEASE_LABEL as an environment variable so that we can refer it anywhere/change in one place in the workflow.

Note, there are many context variables that Github actions provides but their scope varies i.e. which part of actions they can be accessed.

Check this documentation on context availability: https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability

name: 'Policy Check: EMR Release Label'

on: [pull_request]

defaults:
  run:
    shell: bash

env:
  TARGET_RELEASE_LABEL: 6.10.0

Workflow Permissions

GitHub provides a token that you can use to authenticate on behalf of GitHub Actions.

This token is available via GITHUB_TOKEN secret.

You will have to explicitly defines certain permissions, for example, to use the token to add a comment to a PR.

We define such permissions in the workflow.

name: 'Policy Check: EMR Release Label'

on: [pull_request]

defaults:
  run:
    shell: bash

env:
  TARGET_RELEASE_LABEL: 6.10.0

permissions:
  id-token: write
  contents: read 
  pull-requests: write
Like the content ? 

Workflow Jobs

Your workflow contains one or more jobs which can run in sequential order or in parallel. Each job will run inside its own virtual machine runner, or inside a container, and has one or more steps that either run a script that you define or run an action, which is a reusable extension that can simplify your workflow.

name: 'Policy Check: Managed Scaling'

on: [pull_request]

defaults:
  run:
    shell: bash
    
permissions:
  id-token: write
  contents: read 
  pull-requests: write
  
jobs:
  pr_checks:
    name: 'PR Checks'

We define a job named PR Checks which is the name which will show up in the build status checks (Workflow name followed by Job name), also in action summary view.

Workflow Steps

A job contains a sequence of tasks called steps. Steps can run commands, run setup tasks, or run an action in your repository, a public repository, or an action published in a Docker registry.

Checkout Repo

Our first step is to checkout the workspace, so we use another action which performs it for us actions/checkout@v3.

You can invoke a public action using the version tag, action in same repo or reference it from other container registries.

jobs:
  pr_checks:
    name: 'PR Checks'
        
    steps:
      - name: Checkout
        uses: actions/checkout@v3

Changed Files

Previously, we recommended not to use path based filters on workflow triggers as they block PR status checks.

As a workaround, one of the approaches is to use another public action tj-actions/changed-files which helps to get list of all changed pull request files (added, copied, modified, deleted, renamed, type changed, unmerged, unknown).

jobs:
  pr_checks:
    name: 'PR Checks'
        
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get Changed Files
        id: changed-files
        uses: tj-actions/changed-files@v29.0.7
        with:
          files: '**/*.tpl'

We can use files input to define patterns to be matched for returning changed files instead of all changed files.

In order to skip execution of other steps if the PR doesn’t contain the file we are looking for, we can use the if conditional checks on the steps. This helps to break the execution flow and return success if the PR has changes e.g. just README update which we aren’t interested on for our policy checks but would require the build checks to pass on.

jobs:
  pr_checks:
    name: 'PR Checks'
        
    steps:
        - name: Validate Policy
          id: validate-policy 
          if: steps.changed-files.outputs.any_changed == 'true'

For example, in above, the step Validate Policy will be skipped based on the if condition i.e. we use the outputs of changed-files action based on filters to determine if the PR has any changed files we are interested in using the expression steps.changed-files.outputs.any_changed == 'true'.

You can refer the documentation of public actions to see what outputs they produce which can be referenced using the steps context as follows: steps.<step_id>.outputs.<output_name>

Complete file is as follows:

name: 'Policy Check: EMR Release Label'

on: [pull_request]

defaults:
  run:
    shell: bash
    
env:
  TARGET_RELEASE_LABEL: 6.10.0
  
permissions:
  id-token: write
  contents: read 
  pull-requests: write
  
jobs:
  pr_checks:
    name: 'PR Checks'
        
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get Changed Files
        id: changed-files
        uses: tj-actions/changed-files@v29.0.7
        with:
          files: '**/*.tpl'

      - name: Validate Policy
        id: validate-policy 
        if: steps.changed-files.outputs.any_changed == 'true'
Like the content ? 

Run Commands

If the PR contains the files we are interested in, our Validate Policy step will be executed.

We can use run to execute multiple commands in a shell environment.

Note, that run is not the same as say bash execution. It’s an action construct which does variable substitution first i.e. replaces values for supported contexts specified using placeholders ${{ }} and then supplies the commands to shell environment.

name: 'Policy Check: EMR Release Label'

jobs:
  pr_checks:
    name: 'PR Checks'
        
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get Changed Files
        id: changed-files
        uses: tj-actions/changed-files@v29.0.7
        with:
          files: '**/*.tpl'

      - name: Validate Policy
        id: validate-policy 
        if: steps.changed-files.outputs.any_changed == 'true'
        run: |
            echo 'Hello'
            echo 'World'

Validate Policy

Let’s start writing out the logic for validating the changed files.

Before we do that, there are 3 temporary files that get created by the runner for each workflow execution we need to understand:

  Description Example Limitations
GITHUB_ENV You can make an environment variable available to any subsequent steps in a workflow job by defining or updating the environment variable and writing this to the GITHUB_ENV environment file. echo “{environment_variable_name}={value}” » “$GITHUB_ENV”  
GITHUB_OUTPUT Sets a step’s output parameter. Note that the step will need an id to be defined to later retrieve the output value. echo “{name}={value}” » “$GITHUB_OUTPUT” Outputs are Unicode strings, and can be a maximum of 1 MB. The total of all outputs in a workflow run can be a maximum of 50 MB.
GITHUB_STEP_SUMMARY You can set some custom Markdown for each job so that it will be displayed on the summary page of a workflow run and doesn’t need to go to logs. echo “{markdown content}” » $GITHUB_STEP_SUMMARY GITHUB_STEP_SUMMARY is unique for each step in a job.

We will be using them in our policy validation logic.

Firstly, we want the results of our validation to be shown in Job Summary page (when you open the action execution run) so that the team members don’t have to look into the logs as to what happened.

We generate such summary statements by continuously appending the markdown content to the $GITHUB_STEP_SUMMARY file as shown below. Note that the file will be unique for each step so your content will be lost in the subsequent steps which affects your design.

jobs:
  pr_checks:
    name: 'PR Checks'

    - name: Validate Policy
      id: validate-policy 
      if: steps.changed-files.outputs.any_changed == 'true'
      run: |
        {
            echo "|Result |File |Reason |" 
            echo "|--- |--- |--- |" 
        } >> "$GITHUB_STEP_SUMMARY"

Also, we need to indicate if the workflow execution was success or not. For that purpose, we store the result in $GITHUB_OUTPUT file which can be accessed in later steps.

jobs:
  pr_checks:
    name: 'PR Checks'

    - name: Validate Policy
      id: validate-policy 
      if: steps.changed-files.outputs.any_changed == 'true'
      run: |
        {
            echo "|Result |File |Reason |" 
            echo "|--- |--- |--- |" 
        } >> "$GITHUB_STEP_SUMMARY"

        echo "result=pass" >> "$GITHUB_OUTPUT"

We then write the execution logic and append the markdown results. In our example usecase, we would like to pass the PR if it uses the expected release label.

So, we grep the file to see if that’s the case and write the appropriate result.

Since, we are executing the commands in run we have access to contexts such as steps, env etc, which we can use to get workflow values.

jobs:
  pr_checks:
    name: 'PR Checks'

    - name: Validate Policy
      id: validate-policy 
      if: steps.changed-files.outputs.any_changed == 'true'
      run: |
        {
            echo "|Result |File |Reason |" 
            echo "|--- |--- |--- |" 
        } >> "$GITHUB_STEP_SUMMARY"

        echo "result=pass" >> "$GITHUB_OUTPUT"

        for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            if [ "${file: -4}" == ".tpl" ]; then
              if ! grep -q emr-$TARGET_RELEASE_LABEL "$file"; then
                echo "|👎 |$file | Not using EMR release label $TARGET_RELEASE_LABEL |" >> $GITHUB_STEP_SUMMARY
                echo "result=fail" >> "$GITHUB_OUTPUT"
              else
                echo "|👍  |$file | Using EMR release label $TARGET_RELEASE_LABEL |" >> $GITHUB_STEP_SUMMARY
              fi
            fi
        done

        echo "" >> $GITHUB_STEP_SUMMARY
        echo "Update your template to use \"ReleaseLabel\": \"emr-$TARGET_RELEASE_LABEL\"" >> $GITHUB_STEP_SUMMARY

As you can see above, we iterate the changed files, and then grep based on our condition.

If the changes are compliant we append the success result as markdown table with emoji’s and if not otherwise.

Also, note that if the result is a failure, we can override the content of the $GITHUB_OUTPUT file which we will be using in later steps to indicate success/failure of the workflow execution.

echo "result=fail" >> "$GITHUB_OUTPUT"

Finally, we also append the $GITHUB_STEP_SUMMARY to $GITHUB_ENV. Reason for doing so is the Actions UI is badly designed in such a way that the Job Summary is useless.

If any of the actions you use have any warnings, currently you can’t suppress them and they pollute the summary page in such a way that your Job Summary goes to the bottom.

Also, clicking on the execution links in PR status checks doesn’t take you to the Job Summary but to the logs first.

echo "summary<<EOF"  >> $GITHUB_ENV
cat $GITHUB_STEP_SUMMARY >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV

You will have to use above syntax to pass multi-line string to Github environment file.

Like the content ? 

Complete file is as follows:

name: 'Policy Check: EMR Release Label'

on: [pull_request]

defaults:
  run:
    shell: bash
    
env:
  TARGET_RELEASE_LABEL: 6.10.0
  
permissions:
  id-token: write
  contents: read 
  pull-requests: write
  
jobs:
  pr_checks:
    name: 'PR Checks'
        
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get Changed Files
        id: changed-files
        uses: tj-actions/changed-files@v29.0.7
        with:
          files: '**/*.tpl'

      - name: Validate Policy
        id: validate-policy 
        if: steps.changed-files.outputs.any_changed == 'true'
        run: |
          {
            echo "|Result |File |Reason |" 
            echo "|--- |--- |--- |" 
          } >> "$GITHUB_STEP_SUMMARY"
          echo "result=pass" >> "$GITHUB_OUTPUT"
          
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            if [ "${file: -4}" == ".tpl" ]; then
              if ! grep -q emr-$TARGET_RELEASE_LABEL "$file"; then
                echo "|👎 |$file | Not using EMR release label $TARGET_RELEASE_LABEL |" >> $GITHUB_STEP_SUMMARY
                echo "result=fail" >> "$GITHUB_OUTPUT"
              else
                echo "|👍  |$file | Using EMR release label $TARGET_RELEASE_LABEL |" >> $GITHUB_STEP_SUMMARY
              fi
            fi
          done
          
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "Update your template to use \"ReleaseLabel\": \"emr-$TARGET_RELEASE_LABEL\"" >> $GITHUB_STEP_SUMMARY
          
          echo "summary<<EOF"  >> $GITHUB_ENV
          cat $GITHUB_STEP_SUMMARY >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

Add PR Comment

We also publish the result to the PR as a comment so that team’s don’t have to navigate to the actions summary page.

This can be achieved by using actions/github-script and $GITHUB_TOKEN secret which has token with permissions to add a comment to the PR.

Our summary is available via environment variable to which we set in last step which we can access using process.env.summary.

jobs:
  pr_checks:
    name: 'PR Checks'

    steps:
      - name: Update Pull Request
        if: steps.changed-files.outputs.any_changed == 'true'
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.summary
            })

Action Result

Finally, you need to indicate the action result for the PR status check to pass/fail.

This can be done by accessing the step output and setting exit code.

jobs:
  pr_checks:
    name: 'PR Checks'

    steps:
      - name: Result
        if: steps.changed-files.outputs.any_changed == 'true' && steps.validate-policy.outputs.result != 'pass'
        run: exit 1

Complete file:

name: 'Policy Check: EMR Release Label'

on: [pull_request]

defaults:
  run:
    shell: bash
    
env:
  TARGET_RELEASE_LABEL: 6.10.0
  
permissions:
  id-token: write
  contents: read 
  pull-requests: write
  
jobs:
  pr_checks:
    name: 'PR Checks'
        
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get Changed Files
        id: changed-files
        uses: tj-actions/changed-files@v29.0.7
        with:
          files: '**/*.tpl'

      - name: Validate Policy
        id: validate-policy 
        if: steps.changed-files.outputs.any_changed == 'true'
        run: |
          {
            echo "|Result |File |Reason |" 
            echo "|--- |--- |--- |" 
          } >> "$GITHUB_STEP_SUMMARY"
          echo "result=pass" >> "$GITHUB_OUTPUT"
          
          for file in $; do
            if [ "${file: -4}" == ".tpl" ]; then
              if ! grep -q emr-$TARGET_RELEASE_LABEL "$file"; then
                echo "|👎 |$file | Not using EMR release label $TARGET_RELEASE_LABEL |" >> $GITHUB_STEP_SUMMARY
                echo "result=fail" >> "$GITHUB_OUTPUT"
              else
                echo "|👍  |$file | Using EMR release label $TARGET_RELEASE_LABEL |" >> $GITHUB_STEP_SUMMARY
              fi
            fi
          done
          
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "Update your template to use \"ReleaseLabel\": \"emr-$TARGET_RELEASE_LABEL\"" >> $GITHUB_STEP_SUMMARY
          
          echo "summary<<EOF"  >> $GITHUB_ENV
          cat $GITHUB_STEP_SUMMARY >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV
          
      - name: Update Pull Request
        if: steps.changed-files.outputs.any_changed == 'true'
        uses: actions/github-script@v6
        with:
          github-token: $
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.summary
            })
      
      - name: Result
        if: steps.changed-files.outputs.any_changed == 'true' && steps.validate-policy.outputs.result != 'pass'
        run: exit 1
Like the content ? 

PR Status Checks

In order to mandate these PR status checks, ensure all your workflows have same job name so that all of them are executed for every PR.

In your branch protection rules, enable below setting and add the job names which needs to be mandatory to pass.

Like the content ? 



understanding github actionslearn github actionsquickstart for github actionscreating actionsgithub actions tutorialgithub actions examplegithub actions stepsgithub actions syntaxhow do i create an action in Githubhow will you create Github actions with an example Share Tweet +1