Chapter 05Lesson 02~150 minutes

Issues, Labels, Milestones, Templates, Forms, Discussions, and Triage: Guided Hands-On Workflow and Core Operations

Create a disposable work-tracking repository, configure labels and a validated bug issue form, exercise the current gh issue workflow, verify state through structured CLI/API output, and route question-like work to Discussions when appropriate.

gh issueLabels & milestonesYAML formsVerification

Learning objectives

  • Create a disposable GitHub Free-compatible repository and inspect its Issues/labels/settings before changing them.
  • Build a minimal label taxonomy and milestone with an explicit purpose for every piece of metadata.
  • Install a YAML bug issue form plus config.yml on the default branch and validate required fields in the web UI.
  • Use gh issue create/list/view/edit/close/reopen to perform and verify the issue lifecycle without printing credentials.
  • Create cross-references and handle a duplicate while preserving the canonical issue link.
  • Route a question-like issue to Discussions when enabled, or document the same routing decision with a free-compatible mock.
Availability: Required: GitHub.com personal account, GitHub CLI, Git, and permission to create/delete one disposable repository. GitHub Free is sufficient. The lab keeps the repository public so no paid feature is required. Issue forms are public preview. Organization issue types are intentionally not required.

1. Scenario: Atlas Agent intake repository

You maintain a tiny fictional deployment agent. The lab repository is not production code; it exists to make hosted work state observable. You will install three labels, one milestone, a bug form, and a chooser policy, then create issues with different intents.

Scope rule: use a fresh repository named from the commands below. Do not run cleanup commands against a repository that existed before the lab.

2. Preflight: identity, collision check, and current state

gh auth status --active --hostname github.com
OWNER=$(gh api user --jq .login)
REPO="atlas-c05-intake-lab"

gh repo view "$OWNER/$REPO" >/dev/null 2>&1 && {
  echo "STOP: $OWNER/$REPO already exists" >&2
  exit 2
}

printf 'owner=%s
repo=%s
' "$OWNER" "$REPO"

This is read-only identity/repository inspection. gh auth status reports account/scopes without printing the token itself. Stop on a collision; do not reuse or overwrite an existing repository.

3. Create and inspect the disposable repository

gh repo create "$OWNER/$REPO" --public --add-readme --clone
cd "$REPO"
DEFAULT_BRANCH=$(git branch --show-current)

gh repo view "$OWNER/$REPO"   --json nameWithOwner,visibility,defaultBranchRef,hasIssuesEnabled,hasDiscussionsEnabled,viewerPermission   --jq '{nameWithOwner,visibility,defaultBranch:.defaultBranchRef.name,hasIssuesEnabled,hasDiscussionsEnabled,viewerPermission}'

gh label list --repo "$OWNER/$REPO" --limit 100 --json name,isDefault --jq '.[]'

Repository creation changes a GitHub repository resource and creates a default branch because --add-readme makes an initial commit. The issue list is still empty. Labels are hosted metadata; GitHub also seeds default labels in new repositories, but the lab will create a small explicit taxonomy rather than depending on every default label.

4. Create a small taxonomy, not a label encyclopedia

gh label create "kind:bug" --repo "$OWNER/$REPO"   --description "Reproducible defect" --color D73A4A --force
gh label create "priority:p1" --repo "$OWNER/$REPO"   --description "Urgent service-impacting work" --color B60205 --force
gh label create "status:needs-info" --repo "$OWNER/$REPO"   --description "Blocked on reporter information" --color FBCA04 --force

gh label list --repo "$OWNER/$REPO" --limit 100   --json name,description --jq '.[] | select((.name|startswith("kind:")) or (.name|startswith("priority:")) or (.name|startswith("status:")))'

Creating/editing labels is a repository-hosted mutation. In an organization repository it requires Write or higher under the standard role model. The personal owner has that authority. Each prefix answers a different question: kind, priority, or workflow state.

5. Create one bounded milestone through the UI, then verify it through API

Open the repository’s Issues area, choose Milestones, create v0.1 intake hardening, give it a short description, and optionally set a due date. This is a hosted planning mutation; it does not create a Git ref or release tag.

gh api -H "X-GitHub-Api-Version: 2026-03-10"   "/repos/$OWNER/$REPO/milestones?state=all&per_page=100" --paginate   --jq '.[] | {number,title,state,due_on,open_issues,closed_issues}'

The API response proves the milestone exists. Under organization roles, applying an existing milestone can be done with Triage; creating/editing/deleting milestones requires Write or higher.

6. Version the bug intake contract as repository files

Create two files. The form collects reproduction, expected behavior, and version. The chooser discourages blank contributor issues but preserves a maintainer-only escape hatch according to GitHub’s current behavior.

# .github/ISSUE_TEMPLATE/bug.yml
name: Bug report
description: Report a reproducible defect in the Atlas Agent lab.
title: "[bug] "
labels: ["kind:bug"]
body:
  - type: textarea
    id: observed
    attributes:
      label: Observed behavior
      description: What happened?
    validations:
      required: true
  - type: textarea
    id: reproduce
    attributes:
      label: Steps to reproduce
      description: Provide the smallest safe sequence that reproduces the problem.
      placeholder: |
        1. Start from...
        2. Run...
        3. Observe...
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: Expected behavior
    validations:
      required: true
  - type: input
    id: version
    attributes:
      label: Atlas Agent version
      placeholder: 0.1.0
    validations:
      required: true
  - type: checkboxes
    id: secrets
    attributes:
      label: Safety check
      options:
        - label: I removed tokens, passwords, private keys, and sensitive customer data.
          required: true
# .github/ISSUE_TEMPLATE/config.yml
blank_issues_enabled: false
contact_links:
  - name: Usage and design questions
    url: https://github.com/OWNER/REPO/discussions
    about: Use Discussions when enabled; otherwise explain the question in a normal test issue for this lab.

Replace OWNER/REPO in the contact link with the lab repository before committing. Then:

mkdir -p .github/ISSUE_TEMPLATE
# Save bug.yml and config.yml with the contents above, replacing OWNER/REPO.
git add .github/ISSUE_TEMPLATE
git diff --cached --check
git commit -m "chore: define issue intake form"
git push origin HEAD
FORM_OID=$(git rev-parse HEAD)
printf 'form_commit=%s
' "$FORM_OID"

The commit is Git state. Once it reaches the default branch, GitHub reads those files to render the issue chooser/form. That hosted UI is the downstream effect you will now test.

7. Validate the issue form in the web UI

  1. Open Issues → New issue. Verify “Bug report” appears.
  2. Start the bug form and attempt to submit while one required field is empty. The form should prevent submission and identify missing required input.
  3. Complete the fields with synthetic data only. Example observed behavior: “health endpoint returns 503 after reload”; steps: a three-step lab reproduction; version: 0.1.0.
  4. Submit the issue and record its number as BUG1. Verify the kind:bug label was applied automatically.
Security: never paste real logs containing tokens, cookies, private hostnames, customer data, or cloud credentials into a public issue. The checkbox is a prompt, not a secret scanner.

8. Use gh issue create for a second work item

ISSUE2_URL=$(gh issue create --repo "$OWNER/$REPO"   --title "Docs: explain reload behavior"   --body "Document when operators should expect a temporary 503 during a lab reload."   --label "status:needs-info"   --assignee "@me")
ISSUE2=${ISSUE2_URL##*/}
printf 'issue2=%s url=%s
' "$ISSUE2" "$ISSUE2_URL"

This creates a hosted issue and assignment; it does not commit a file. @me chooses the authenticated user. The label must already exist.

9. Inspect with gh issue list and gh issue view

gh issue list --repo "$OWNER/$REPO" --state all --limit 50   --json number,title,state,labels,assignees,milestone,url   --jq '.[] | {number,title,state,labels:[.labels[].name],assignees:[.assignees[].login],milestone:(.milestone.title // null)}'

gh issue view "$ISSUE2" --repo "$OWNER/$REPO"   --json number,title,state,labels,assignees,body,url --jq .

Structured output is evidence. The JSON fields are safer for automation than parsing decorative terminal rendering.

10. Triage with gh issue edit: add only evidence-backed metadata

Assign the first bug to yourself, add P1 only if your synthetic scenario is intentionally service-impacting, and attach the milestone by name. Replace BUG1 with the issue number from the web form.

BUG1=REPLACE_WITH_FORM_ISSUE_NUMBER

gh issue edit "$BUG1" --repo "$OWNER/$REPO"   --add-assignee "@me"   --add-label "priority:p1"   --milestone "v0.1 intake hardening"

gh issue view "$BUG1" --repo "$OWNER/$REPO"   --json number,state,labels,assignees,milestone   --jq '{number,state,labels:[.labels[].name],assignees:[.assignees[].login],milestone:(.milestone.title // null)}'

The command changes hosted metadata only. A production triager should not add P1 merely because a reporter says “urgent”; priority should reflect an agreed impact model.

11. Create a cross-reference and a duplicate with canonical context

DUP_URL=$(gh issue create --repo "$OWNER/$REPO"   --title "Reload gives 503"   --body "Same synthetic behavior as #$BUG1; this record exists to practice duplicate triage.")
DUP=${DUP_URL##*/}

gh issue close "$DUP" --repo "$OWNER/$REPO"   --duplicate-of "$BUG1"

gh issue view "$DUP" --repo "$OWNER/$REPO"   --json number,state,stateReason,comments,url --jq .

The current CLI provides --duplicate-of, preserving the canonical issue relationship. Do not replace this with a bare close that forces future readers to search manually for the surviving report.

12. Exercise close and reopen as lifecycle—not deletion

gh issue close "$ISSUE2" --repo "$OWNER/$REPO"   --reason "not planned"   --comment "Lab decision: this documentation change is outside the v0.1 scope."

gh issue view "$ISSUE2" --repo "$OWNER/$REPO"   --json state,stateReason,closedAt,url --jq .

gh issue reopen "$ISSUE2" --repo "$OWNER/$REPO"   --comment "Lab exercise: scope changed; reopening to demonstrate reversible lifecycle state."

gh issue view "$ISSUE2" --repo "$OWNER/$REPO"   --json state,stateReason,updatedAt,url --jq .

Close/reopen preserves the record and timeline. Deleting an issue is a different, destructive admin-only operation and is not used in this chapter.

13. Route a question-like record to Discussions—or simulate the decision

Create one test issue: “Question: should reloads be zero-downtime?” This is intentionally exploratory. If Discussions is enabled, the repository owner/admin can use the web UI to convert it to a Discussion and select a category. Record the conversion in your evidence. If Discussions is not enabled or your policy forbids enabling it, keep the issue open only long enough to write: “Routing decision: this should be a Discussion because there is no committed work yet,” then close it as not planned.

Enabling Discussions changes repository settings and requires sufficient permission. It is optional. The chapter’s learning goal is the routing decision, not toggling a feature on a valuable repository.

14. Verify the hosted queue with read-only REST queries

gh api -H "X-GitHub-Api-Version: 2026-03-10"   "/repos/$OWNER/$REPO/issues?state=all&per_page=100" --paginate   --jq '.[] | select(.pull_request == null) | {number,title,state,state_reason,labels:[.labels[].name],milestone:(.milestone.title // null),assignees:[.assignees[].login]}'

gh api -H "X-GitHub-Api-Version: 2026-03-10"   "/repos/$OWNER/$REPO/milestones?state=all&per_page=100" --paginate   --jq '.[] | {number,title,state,open_issues,closed_issues}'

Your observed counts should match the UI/CLI state. If not, do not “fix” the data immediately—first identify whether you are viewing a different repository, filtering by state, or counting pull requests through the Issues REST collection.

15. Challenge: choose the surface before the command

For each item, write Issue, Discussion, Pull Request, or external incident/support channel, and justify it before taking action: (1) reproducible CLI crash; (2) proposal for a future architecture; (3) production outage requiring immediate coordination; (4) concrete code change already implemented and ready for review. Then identify which metadata, if any, is appropriate.

16. Cleanup after preserving evidence

Export or copy your small evidence table first: repository, form commit OID, issue numbers, canonical duplicate link, milestone name, and final state. Then delete only the disposable repository created by this lesson.

cd ..
gh repo delete "$OWNER/$REPO" --yes
rm -rf "$REPO"
Destructive: gh repo delete permanently removes the disposable repository and its hosted Issues/Discussions/settings. Verify $OWNER/$REPO exactly before running it. Never adapt this cleanup block to a valuable repository.

17. Lesson summary

You created a free-compatible intake system where form configuration is versioned in Git, issue state is hosted on GitHub, triage changes are independently inspectable, duplicate context is preserved, and Discussions is treated as a routing choice rather than a dumping ground.

Knowledge check

Why did the lab verify the form commit OID before testing the chooser?

What does gh issue close --duplicate-of preserve that a bare close does not?

Why does the REST issues list filter .pull_request == null?

A required form field contains “N/A”. Has validation proved the bug is reproducible?

Why is repository deletion delayed until after evidence capture?

Next lesson

Design the policy instead of accumulating features

Lesson 03 compares free-form/template/form intake, Issue/Discussion/PR boundaries, label and milestone policy, and safe automation-assisted triage.

Authoritative references

 Configuring issue templates
 Syntax for issue forms
 Managing labels
 About milestones
 GitHub Discussions quickstart
 Moderating discussions
 Repository roles
 gh issue create
 gh issue list
 gh issue view
 gh issue edit
 gh issue close
 gh issue reopen
 gh label
 REST API endpoints for issues
 REST API endpoints for milestones
 REST API versions

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.