Submit
- Alpha
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.
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 :("
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.
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:
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!"
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:
Submitted
stateWorkflow 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.
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.