Submit
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 theSubmitted
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 thegithub/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.Token Permissions
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!1steps:
2 - name: Approve Command
3 id: approve
4 uses: github/command@vX.X.X
5 with:
6 allowed_contexts: issue
7 allowlist: octo-org/approvers
8 allowlist_pat: ${{ secrets.MY_TOKEN }}
9 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.1steps:
2 - name: Approve Command
3 id: approve
4 uses: github/command@vX.X.X
5 with:
6 allowed_contexts: issue
7 allowlist: octo-org/approvers
8 allowlist_pat: ${{ secrets.MY_TOKEN }}
9 command: .approve
10
11 ##############################################
12 # This is a great time to re-run validation! #
13 ##############################################
14
15 - if: ${{ steps.approve.outputs.continue == 'true' }}
16 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'
.1steps:
2 - name: Approve Command
3 id: approve
4 uses: github/command@vX.X.X
5 with:
6 allowed_contexts: issue
7 allowlist: octo-org/approvers
8 allowlist_pat: ${{ secrets.MY_TOKEN }}
9 command: .approve
10
11 - name: Deny Command
12 id: deny
13 uses: github/command@vX.X.X
14 with:
15 allowed_contexts: issue
16 allowlist: octo-org/approvers
17 allowlist_pat: ${{ secrets.MY_TOKEN }}
18 command: .deny
19
20 - if: ${{ steps.approve.outputs.continue == 'true' }}
21 run: echo "This request is approved :)"
22
23 - if: ${{ steps.deny.outputs.continue == 'true' }}
24 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
1name: Issue Comment
2
3# This workflow runs any time a comment is added to an issue. The comment body
4# is read and used to determine what action to take.
5on:
6 issue_comment:
7 types:
8 - created
9
10jobs:
11 # This job handles the case where a user comments with `.approve`.
12 approve:
13 name: Approve Request
14 runs-on: ubuntu-latest
15
16 steps:
17 - name: Approve Command
18 id: approve
19 uses: github/command@vX.X.X
20 with:
21 allowed_contexts: issue
22 allowlist: octo-org/approvers
23 allowlist_pat: ${{ secrets.MY_TOKEN }}
24 command: .approve
25
26 - if: ${{ steps.approve.outputs.continue == 'true' }}
27 run: echo "This request is approved!"
28
29 # This job handles the case where a user comments with `.deny`.
30 deny:
31 name: Deny Request
32 runs-on: ubuntu-latest
33
34 steps:
35 - name: Deny Command
36 id: deny
37 uses: github/command@vX.X.X
38 with:
39 allowed_contexts: issue
40 allowlist: octo-org/approvers
41 allowlist_pat: ${{ secrets.MY_TOKEN }}
42 command: .deny
43
44 - if: ${{ steps.deny.outputs.continue == 'true' }}
45 run: echo "This request is denied!"
Trigger the workflow
In the above workflow, both theapprove
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
1name: Issue Comment
2
3on:
4 issue_comment:
5 types:
6 - created
7
8jobs:
9 approve:
10 name: Approve Request
11 runs-on: ubuntu-latest
12
13 # Only run when the following conditions are true:
14 # - The issue has the `issueops:new-repository` label
15 # - The issue has the `issueops:validated` label
16 # - The issue does not have the `issueops:approved` label
17 # - The issue is open
18 if: |
19 contains(github.event.issue.labels.*.name, 'issueops:new-repository') &&
20 contains(github.event.issue.labels.*.name, 'issueops:validated') &&
21 contains(github.event.issue.labels.*.name, 'issueops:approved') == false &&
22 github.event.issue.state == 'open'
23
24 steps:
25 # ...
26
27 deny:
28 name: Deny Request
29 runs-on: ubuntu-latest
30
31 # Only run when the following conditions are true:
32 # - The issue has the `issueops:new-repository` label
33 # - The issue has the `issueops:validated` label
34 # - The issue does not have the `issueops:approved` label
35 # - The issue is open
36 if: |
37 contains(github.event.issue.labels.*.name, 'issueops:new-repository') &&
38 contains(github.event.issue.labels.*.name, 'issueops:validated') &&
39 contains(github.event.issue.labels.*.name, 'issueops:approved') == false &&
40 github.event.issue.state == 'open'
41
42 steps:
43 # ...
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.1name: Issue Comment
2
3on:
4 issue_comment:
5 types:
6 - created
7
8jobs:
9 validate:
10 name: Validate Request
11 runs-on: ubuntu-latest
12
13 # Only run when the following conditions are true:
14 # - The issue has the `issueops:new-repository` label
15 # - The issue has the `issueops:validated` label
16 # - The issue does not have the `issueops:approved` label
17 # - The issue is open
18 if: |
19 contains(github.event.issue.labels.*.name, 'issueops:new-repository') &&
20 contains(github.event.issue.labels.*.name, 'issueops:validated') &&
21 contains(github.event.issue.labels.*.name, 'issueops:approved') == false &&
22 github.event.issue.state == 'open'
23
24 permissions:
25 contents: read
26 id-token: write
27 issues: write
28
29 outputs:
30 request: ${{ steps.parse.outputs.request }}
31
32 steps:
33 - name: Remove Labels
34 id: remove-label
35 uses: issue-ops/labeler@vX.X.X
36 with:
37 action: remove
38 issue_number: ${{ github.event.issue.number }}
39 labels: |
40 issueops:validated
41 issueops:submitted
42
43 - name: Get App Token
44 id: token
45 uses: actions/create-github-app-token@vX.X.X
46 with:
47 app_id: ${{ secrets.MY_GITHUB_APP_ID }}
48 private_key: ${{ secrets.MY_GITHUB_APP_PEM }}
49 owner: ${{ github.repository_owner }}
50
51 - name: Checkout
52 id: checkout
53 uses: actions/checkout@vX.X.X
54
55 - name: Setup Node.js
56 id: setup-node
57 uses: actions/setup-node@vX.X.X
58 with:
59 node-version-file: .node-version
60 cache: npm
61
62 - name: Install Packages
63 id: npm
64 run: npm ci
65
66 - name: Parse Issue
67 id: parse
68 uses: issue-ops/parser@vX.X.X
69 with:
70 body: ${{ github.event.issue.body }}
71 issue-form-template: new-repository-request.yml
72
73 - name: Validate Issue
74 id: validate
75 uses: issue-ops/validator@vX.X.X
76 with:
77 issue-form-template: new-repository-request.yml
78 github-token: ${{ steps.token.outputs.token }}
79 parsed-issue-body: ${{ steps.parse.outputs.json }}
80
81 - if: ${{ steps.validate.outputs.result == 'success' }}
82 name: Add Validated Label
83 id: add-label
84 uses: issue-ops/labeler@vX.X.X
85 with:
86 action: add
87 issue_number: ${{ github.event.issue.number }}
88 labels: |
89 issueops:validated
90
91 approve:
92 name: Approve Request
93 runs-on: ubuntu-latest
94
95 # Only run after validation has completed.
96 needs: validate
97
98 steps:
99 # ...
100
101 deny:
102 name: Deny Request
103 runs-on: ubuntu-latest
104
105 # Only run after validation has completed.
106 needs: validate
107
108 steps:
109 # ...
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.