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.