Validate

Validate Early. Validate Often.
  • Alpha

Overview

Once an issue has been parsed, it can be validated against any rules that you require. When used in public repositories, issue form templates do enforce some validation rules such as required fields, selection options, and more. However, you may have additional needs that apply to your specific use case. For example, if you are creating an IssueOps workflow for users to request membership to GitHub teams, the issue form template is not able to validate if a value provided by a user is in fact a team in your organization.

The issue-ops/validator action takes the parsed output of the issue body and validates it against the issue form template and any custom rules you define.

After a request is initially validated, there is nothing stopping a user from editing the issue and submitting it with invalid inputs. You should run your validation logic any time the following events occur:

  • The issue is opened
  • The issue body is edited
  • The issue is closed and reopened
  • The request is submitted for provisioning/creation

Basic validation

The most basic validation compares each input field to the rules specified in your issue form template and, if any are violated, comments with an error message.

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

For example, if you have an input field for users to select the visibility of their new repository, you can specify that the field is required and only one option can be chosen.

- type: dropdown
id: visibility
attributes:
label: Repository Visibility
description: The visibility of the repository.
multiple: false
options:
- private
- public
validations:
required: true

When run against an issue submitted with this template, the validator will comment on the issue with an error message if any of the following occur:

  • The field is empty
  • The field is missing from the issue body
  • An option other than private or public is present

Custom validation

For each form field, you can also specify custom validation logic. This is done using several files in your repository:

  • The validator configuration file (.github/validator/config.yml)
  • One or more validator scripts (.github/validator/<script-name>.js)

Configuration file

This file defines the mapping of validator scripts to form fields. For example, if your issue form has input fields named Read Team and Write Team, you can specify a validator script (check_team_exists.js) to run against those fields.

validators:
- field: read_team
script: check_team_exists
- field: write_team
script: check_team_exists

Validator scripts

If you want to run custom validators that access GitHub APIs, you will need to provide a value for the github-token input. This is another good scenario for GitHub App authentication!

Validator scripts are run on the associated fields in the configuration file. The script must specify a default export of a function with the following behavior:

  • Accept inputs of the following types:
    • string (Input and Textarea)
    • string[] (Dropdown)
    • { label: string; required: boolean } (Checkboxes)
  • Return 'success' for successful validation
  • Return an error message (string) for unsuccessful validation

The following is an example of a validator script that checks if a team exists. This can also be found in the issue-ops/validator repository.

module.exports = async (field) => {
if (typeof field !== 'string') return 'Field type is invalid'
const { getOctokit } = require('@actions/github')
const core = require('@actions/core')
const octokit = getOctokit(core.getInput('github-token', { required: true }))
try {
await octokit.rest.teams.getByName({
org: process.env.ORGANIZATION ?? '',
team_slug: field
})
core.info(`Team '${field}' exists`)
return 'success'
} catch (error) {
if (error.status === 404) {
core.error(`Team '${field}' does not exist`)
return `Team '${field}' does not exist`
}
}
}

New repository request

Recall from the issue form template that the new repository request expects the following inputs:

InputRequiredOptions
Repository Name
Visibilityprivate, public
Read Team
Write Team
Auto Inittrue, false
Topics

Since the Visibility and Auto Init inputs must be one of several predefined values, they can be handled by basic validation. The other fields, however, must meet additional requirements:

FieldRequirement
Repository NameMust not be an existing repository
Read TeamMust be a team in the organization
Write TeamMust be a team in the organization
TopicsMust be a list of 20 or fewer
Each topic must be lowercase
Each topic must be 50 or fewer characters
Each topic must contain only letters, numbers, and hyphens

Create a configuration file

In order to configure custom validation, first create a configuration file in the repository.

File path: .github/validator/config.yml

validators:
- field: repository_name
script: repo_doesnt_exist
- field: read_team
script: team_exists
- field: write_team
script: team_exists
- field: topics
script: topics_valid

Create validator scripts

The following scripts can be used to validate the new repository request.

.github/validator/repo_doesnt_exist.js

module.exports = async (field) => {
if (typeof field !== 'string') return 'Field type is invalid'
const { getOctokit } = require('@actions/github')
const core = require('@actions/core')
const octokit = getOctokit(core.getInput('github-token', { required: true }))
try {
// This should throw a 404 error
await octokit.rest.repos.get({
org: '<org-name>',
repo: field
})
core.error(`Repository '${field}' already exists`)
return `Repository '${field}' already exists`
} catch (error) {
if (error.status === 404) {
core.info(`Repository '${field}' does not exist`)
return 'success'
}
}
}

.github/validator/team_exists.js

module.exports = async (field) => {
if (typeof field !== 'string') return 'Field type is invalid'
const { getOctokit } = require('@actions/github')
const core = require('@actions/core')
const octokit = getOctokit(core.getInput('github-token', { required: true }))
try {
await octokit.rest.teams.getByName({
org: process.env.ORGANIZATION ?? '',
team_slug: field
})
core.info(`Team '${field}' exists`)
return 'success'
} catch (error) {
if (error.status === 404) {
core.error(`Team '${field}' does not exist`)
return `Team '${field}' does not exist`
}
}
}

.github/validator/topics_valid.js

For details about the requirements for repository topics, see About topics.

module.exports = async (field) => {
if (typeof field !== 'string') return 'Field type is invalid'
const topics = field.split(/[\r\n]+/)
if (topics.length > 20)
return `There are ${request.topics.length} topics (max: 20)`
const invalidTopics = []
for (const topic of topics) {
if (
topic !== topic.toLowerCase() ||
topic.length > 50 ||
!topic.match(/^[a-z0-9-]+$/)
)
invalidTopics.push(topic)
}
if (invalidTopics.length > 0)
return `The following topics are invalid: ${JSON.stringify(invalidTopics)}`
}

Update the workflow

Now that issue validation has been configured, you can add it as a step to your workflow. Additional updates are noted with comments.

name: Issue Opened/Edited/Reopened
on:
issues:
types:
- opened
- edited
- reopened
jobs:
new-repository-request:
name: New Repository Request
runs-on: ubuntu-latest
if: contains(github.event.issue.labels.*.name, 'issueops:new-repository')
# Since the validation step involves adding comments to issues, you will
# need to give it write permissions. If you are using a GitHub App to call
# other GitHub APIs, you will also need to add the appropriate permissions.
permissions:
contents: read
id-token: write
issues: write
steps:
# Always remove the validated/submitted labels first! This ensures that
# the validation logic runs any time the issue body is edited. It also
# ensures the issue must be re-submitted after editing.
- 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
# If your validation script checks things beyond the scope of the
# repository it is running in, you will need to create a GitHub App and
# generate an installation access token in your workflow.
- 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
# Install Node.js so the workflow can add npm packages that are used by
# the custom validator scripts (e.g. '@octokit/rest').
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@vX.X.X
with:
node-version-file: .node-version
cache: npm
# Install NPM packages needed by the validator scripts.
- 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
# Add a step to validate the issue.
- 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 }}
# Add a label to mark the request as validated.
- 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

Next steps

Congratulations! Your request has been successfully transitioned to the Validated state. Next, we're going to submit the request for approval.

Continue to the next section to learn how to submit the request for approval by an authorized user.