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.
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.ymlon the default branch and validate required fields in the web UI. -
Use
gh issue create/list/view/edit/close/reopento 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.
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.
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
- Open Issues → New issue. Verify “Bug report” appears.
- Start the bug form and attempt to submit while one required field is empty. The form should prevent submission and identify missing required input.
-
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. -
Submit the issue and record its number as
BUG1. Verify thekind:buglabel was applied automatically.
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"
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?
Because the form becomes available only when the configuration reaches the default branch; the OID ties the hosted UI behavior to exact Git state.
What does gh issue close --duplicate-of preserve
that a bare close does not?
It records the canonical issue relationship instead of merely changing lifecycle state.
Why does the REST issues list filter
.pull_request == null?
GitHub’s Issues REST collection also represents pull requests for shared issue metadata; filtering avoids counting PRs as ordinary issues.
A required form field contains “N/A”. Has validation proved the bug is reproducible?
No. Required input proves only that a value was supplied. Human triage still evaluates quality and reproducibility.
Why is repository deletion delayed until after evidence capture?
Deleting the disposable repository destroys the hosted issue/timeline/settings evidence, so the checkpoint record must be preserved first.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.