Submit

Tell GitHub Actions the issue is ready for processing.
  • Alpha

Overview

Once your issue has been parsed and validated, it's ready for processing! At this point, processing can mean a lot of things and is entirely dependent on your use case. For example, if you're using IssueOps to access administrative functions, you may require a human to review and approve the issue. Or, if you're using IssueOps to track PTO requests, you may not need any additional approvals and can simply mark the issue as processed.

This page walks through the process of submitting a request after it has been validated. In particular, it covers requesting approval from authorized users or teams.

Wouldn't opening the issue count as the act of submitting it?

Absolutely! However, the act of opening an issue may not be the best indicator that an issue is in the Submitted state in your workflow. What if you need to do additional processing on the validated request which requires confirmation from the user?

Using the new repository request as an example, your organization may want to enforce certain naming conventions for repositories, such as prefixing the name with the user's department. In this case, when a user opens a request and asks for a repository named pto-requests, you could have them confirm that the generated name of hr-pto-requests is acceptable before submitting the request for further processing.

Command actions

This is where the github/command action comes into play. This action allows you to specify the who, what, when, and where of activities that can be performed on an issue. For example, if you request approval for a new repository, the github/command action ensures that any user cannot approve the request. Instead, only users or teams you specify can.

As with other actions that call GitHub APIs, if you want to include GitHub teams in the allowlist feature, you must provide a valid token in the allowlist_pat input. This can be a token generated from a GitHub App!

steps:
  - name: Approve Command
    id: approve
    uses: github/command@vX.X.X
    with:
      allowed_contexts: issue
      allowlist: octo-org/approvers
      allowlist_pat: ${{ secrets.MY_TOKEN }}
      command: .approve

This step acts as the gate for any further processing of the issue. The continue output can be used to conditionally invoke further steps. For example, if the continue output is 'true', the user who commented on the issue with .approve was indeed authorized to approve the request.

steps:
  - name: Approve Command
    id: approve
    uses: github/command@vX.X.X
    with:
      allowed_contexts: issue
      allowlist: octo-org/approvers
      allowlist_pat: ${{ secrets.MY_TOKEN }}
      command: .approve

  ##############################################
  # This is a great time to re-run validation! #
  ##############################################

  - if: ${{ steps.approve.outputs.continue == 'true' }}
    run: echo "This request is approved!"

With any approval workflow, you should also consider what happens when a request is explicitly denied This is easy to implement as a separate github/command step that looks for the .deny command. As with the approval command, if the user who commented on the issue is authorized to deny requests, the continue output would be 'true'.

steps:
  - name: Approve Command
    id: approve
    uses: github/command@vX.X.X
    with:
      allowed_contexts: issue
      allowlist: octo-org/approvers
      allowlist_pat: ${{ secrets.MY_TOKEN }}
      command: .approve

  - name: Deny Command
    id: deny
    uses: github/command@vX.X.X
    with:
      allowed_contexts: issue
      allowlist: octo-org/approvers
      allowlist_pat: ${{ secrets.MY_TOKEN }}
      command: .deny

  - if: ${{ steps.approve.outputs.continue == 'true' }}
    run: echo "This request is approved :)"

  - if: ${{ steps.deny.outputs.continue == 'true' }}
    run: echo "This request is denied :("

New repository request

Up until this point, everything has been handled as part of the issue creation workflow. Now that the issue has been validated, any further processing is done via comments, labels, reactions, and so on.

Create the comment workflow file

The first step is to create a workflow file that will be triggered when a user comments on an issue. This workflow file will be responsible for parsing the comment and determining the following:

  • The comment is on an issue that is part of our IssueOps workflow
  • The comment is a command word
  • The user is authorized to call that command

In this example, we will set up two different jobs that will run when the request is approved or denied.

name: Issue Comment

# This workflow runs any time a comment is added to an issue. The comment body
# is read and used to determine what action to take.
on:
  issue_comment:
    types:
      - created

jobs:
  # This job handles the case where a user comments with `.approve`.
  approve:
    name: Approve Request
    runs-on: ubuntu-latest

    steps:
      - name: Approve Command
        id: approve
        uses: github/command@vX.X.X
        with:
          allowed_contexts: issue
          allowlist: octo-org/approvers
          allowlist_pat: ${{ secrets.MY_TOKEN }}
          command: .approve

      - if: ${{ steps.approve.outputs.continue == 'true' }}
        run: echo "This request is approved!"

  # This job handles the case where a user comments with `.deny`.
  deny:
    name: Deny Request
    runs-on: ubuntu-latest

    steps:
      - name: Deny Command
        id: deny
        uses: github/command@vX.X.X
        with:
          allowed_contexts: issue
          allowlist: octo-org/approvers
          allowlist_pat: ${{ secrets.MY_TOKEN }}
          command: .deny

      - if: ${{ steps.deny.outputs.continue == 'true' }}
        run: echo "This request is denied!"

Trigger the workflow

In the above workflow, both the approve and deny jobs are triggered when a user comments on an issue or PR. Though the github/command actions will act as one gate, you may want to add additional conditions to ensure that the workflow is not run when the issue is in a state that does not require approval. For example, this workflow doesn't need to run if:

  • The issue is not part of this IssueOps workflow
  • The request is not in the Submitted state
  • The request is already approved

Workflow conditions can be used to control when the workflow jobs are invoked.

It's always a good idea to consider negative cases when writing workflows. What would happen if someone comments .approve but the request has already been approved? Adding a comment stating the request has already been approved helps prevent confusion. Communication is key!

name: Issue Comment

on:
  issue_comment:
    types:
      - created

jobs:
  approve:
    name: Approve Request
    runs-on: ubuntu-latest

    # Only run when the following conditions are true:
    #   - The issue has the `issueops:new-repository` label
    #   - The issue has the `issueops:validated` label
    #   - The issue does not have the `issueops:approved` label
    #   - The issue is open
    if: |
      contains(github.event.issue.labels.*.name, 'issueops:new-repository') &&
      contains(github.event.issue.labels.*.name, 'issueops:validated') &&
      contains(github.event.issue.labels.*.name, 'issueops:approved') == false &&
      github.event.issue.state == 'open'

    steps:
      # ...

  deny:
    name: Deny Request
    runs-on: ubuntu-latest

    # Only run when the following conditions are true:
    #   - The issue has the `issueops:new-repository` label
    #   - The issue has the `issueops:validated` label
    #   - The issue does not have the `issueops:approved` label
    #   - The issue is open
    if: |
      contains(github.event.issue.labels.*.name, 'issueops:new-repository') &&
      contains(github.event.issue.labels.*.name, 'issueops:validated') &&
      contains(github.event.issue.labels.*.name, 'issueops:approved') == false &&
      github.event.issue.state == 'open'

    steps:
      # ...

This seems like duplication of the same checks. Plus, we haven't followed our own rule: Validate Early. Validate Often. Instead, lets move this to a separate job that re-runs validation checks.

name: Issue Comment

on:
  issue_comment:
    types:
      - created

jobs:
  validate:
    name: Validate Request
    runs-on: ubuntu-latest

    # Only run when the following conditions are true:
    #   - The issue has the `issueops:new-repository` label
    #   - The issue has the `issueops:validated` label
    #   - The issue does not have the `issueops:approved` label
    #   - The issue is open
    if: |
      contains(github.event.issue.labels.*.name, 'issueops:new-repository') &&
      contains(github.event.issue.labels.*.name, 'issueops:validated') &&
      contains(github.event.issue.labels.*.name, 'issueops:approved') == false &&
      github.event.issue.state == 'open'

    permissions:
      contents: read
      id-token: write
      issues: write

    outputs:
      request: ${{ steps.parse.outputs.request }}

    steps:
      - name: Remove Labels
        id: remove-label
        uses: issue-ops/labeler@vX.X.X
        with:
          action: remove
          issue_number: ${{ github.event.issue.number }}
          labels: |
            issueops:validated
            issueops:submitted

      - name: Get App Token
        id: token
        uses: actions/create-github-app-token@vX.X.X
        with:
          app_id: ${{ secrets.MY_GITHUB_APP_ID }}
          private_key: ${{ secrets.MY_GITHUB_APP_PEM }}
          owner: ${{ github.repository_owner }}

      - name: Checkout
        id: checkout
        uses: actions/checkout@vX.X.X

      - name: Setup Node.js
        id: setup-node
        uses: actions/setup-node@vX.X.X
        with:
          node-version-file: .node-version
          cache: npm

      - name: Install Packages
        id: npm
        run: npm ci

      - name: Parse Issue
        id: parse
        uses: issue-ops/parser@vX.X.X
        with:
          body: ${{ github.event.issue.body }}
          issue-form-template: new-repository-request.yml

      - name: Validate Issue
        id: validate
        uses: issue-ops/validator@vX.X.X
        with:
          issue-form-template: new-repository-request.yml
          github-token: ${{ steps.token.outputs.token }}
          parsed-issue-body: ${{ steps.parse.outputs.json }}

      - if: ${{ steps.validate.outputs.result == 'success' }}
        name: Add Validated Label
        id: add-label
        uses: issue-ops/labeler@vX.X.X
        with:
          action: add
          issue_number: ${{ github.event.issue.number }}
          labels: |
            issueops:validated

  approve:
    name: Approve Request
    runs-on: ubuntu-latest

    # Only run after validation has completed.
    needs: validate

    steps:
      # ...

  deny:
    name: Deny Request
    runs-on: ubuntu-latest

    # Only run after validation has completed.
    needs: validate

    steps:
      # ...

With this workflow, we know that the request has been validated before we handle any approval or denial. This is a good example of Validate Early. Validate Often.

Next steps

Depending on if the request is approved or denied, you may want to take further actions. For example, if the request is approved, you could create the repository, add a comment to the issue, and close it as completed. On the other hand, if the request is denied, you could close the issue as not planned.

Continue to the approve or deny sections to learn more.