[test] add a quick-deploy gesture for the mk50 nanosuit #12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Triage: AI" | |
| # AI-assisted issue triage using GitHub Models (actions/ai-inference). | |
| # On a new/reopened issue (or manual dispatch) it asks a model to classify the | |
| # issue against the AmbleKit label taxonomy, applies labels, sets the org | |
| # Priority/Effort fields (best effort), and posts a triage comment with a | |
| # likely-cause (bugs) / recommended-implementation (features) note. | |
| # | |
| # Idempotent: the comment carries a hidden <!-- auto-triage --> marker, so a | |
| # re-run edits the existing comment instead of stacking a new one. On a re-run | |
| # of an already-triaged issue it refreshes only the comment and leaves labels | |
| # untouched, so it never clobbers a maintainer's later label edits. | |
| # | |
| # If the model output can't be parsed it falls back to "S: Untriaged" (only on | |
| # the first triage). | |
| on: | |
| issues: | |
| types: [opened, reopened] | |
| workflow_dispatch: | |
| inputs: | |
| issue: | |
| description: "Issue number to (re)triage" | |
| required: true | |
| permissions: | |
| issues: write | |
| models: read | |
| contents: read | |
| concurrency: | |
| # key off trusted contexts only - issue number for issue events, run_id for | |
| # manual runs (the dispatch input is user-controlled, don't put it in the key) | |
| group: triage-${{ github.event.issue.number || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| triage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve issue number | |
| id: ctx | |
| env: | |
| NUM: ${{ github.event.issue.number || github.event.inputs.issue }} | |
| run: | | |
| # NUM can come from a user-controlled workflow_dispatch input - require | |
| # digits only so it can't inject newlines into $GITHUB_OUTPUT. | |
| if ! printf '%s' "$NUM" | grep -qE '^[0-9]+$'; then | |
| echo "invalid issue number: $NUM" >&2; exit 1 | |
| fi | |
| echo "num=$NUM" >> "$GITHUB_OUTPUT" | |
| - name: Fetch issue (for manual runs) + build prompt | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| NUM: ${{ steps.ctx.outputs.num }} | |
| EVENT_TITLE: ${{ github.event.issue.title }} | |
| EVENT_BODY: ${{ github.event.issue.body }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${EVENT_TITLE:-}" ]; then | |
| TITLE="$EVENT_TITLE"; BODY="$EVENT_BODY" | |
| else | |
| TITLE=$(gh issue view "$NUM" --repo "$REPO" --json title --jq '.title') | |
| BODY=$(gh issue view "$NUM" --repo "$REPO" --json body --jq '.body') | |
| fi | |
| { | |
| echo "Issue #$NUM" | |
| echo "Title: $TITLE" | |
| echo | |
| echo "Body:" | |
| echo "${BODY:-(no body)}" | |
| } > "$RUNNER_TEMP/triage-prompt.txt" | |
| - name: AI classify | |
| id: ai | |
| # don't fail the run if GitHub Models is unavailable (not enabled / rate-limited | |
| # / 403) - the Apply step falls back to S: Untriaged when the response is empty. | |
| continue-on-error: true | |
| uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1 | |
| with: | |
| model: openai/gpt-4o-mini | |
| max-tokens: 800 | |
| prompt-file: ${{ runner.temp }}/triage-prompt.txt | |
| system-prompt: | | |
| You triage GitHub issues for Timeless Heroes - a Fabric 1.20.1 Minecraft | |
| *content* mod (Java, mod id "timeless") that adds superhero suits with | |
| custom powers and abilities. | |
| Architecture you can reason about for the "insight" field: | |
| - Suits live in suit/ (Iron Man marks mk2/3/5/7/50/85, Spider-Man, Batman, | |
| Moon Knight, Superman). A Suit owns a PowerList; a "set" equips the four | |
| armour pieces. Per-player state is NBT via SuitItem.Data (auto-synced). | |
| - Powers live in power/impl/, registered in PowerRegistry, identified by | |
| Identifier. They have run/tick(server), tick(client), and client hooks | |
| (handlesRightClick/LeftClick, applyPose). 9 keybinds send UsePowerC2SPacket. | |
| - Rendering: SuitFeature draws the suit model over the player; SuitModel | |
| dispatches power poses; nano assembly uses a custom core shader. Animation | |
| system under mc/duzo/animation. Jarvis-style HUD in client/gui. | |
| - Networking is c2s/s2c FabricPacket records in network/. Datagen in datagen/. | |
| Classify the issue and reply with ONE LINE of minified JSON, no code | |
| fences, no prose. Schema (use EXACTLY these allowed values): | |
| { | |
| "type": "Bug" | "Feature" | "Task", | |
| "priority": "Urgent" | "High" | "Medium" | "Low", | |
| "effort": "High" | "Medium" | "Low", | |
| "difficulty": "DB" | "D3" | "D2" | "D1" | "D0", | |
| "areas": [ subset of: "A: Suits","A: Powers","A: Entities","A: Datagen","A: Admin Tooling","A: Core Tech","A: Build","A: Docs" ], | |
| "changes": [ subset of: "C: Render","C: UI","C: Networking","C: Textures","C: Audio","C: Translations" ], | |
| "summary": "one short lowercase sentence", | |
| "insight": "for a Bug: 2-4 sentences on the most likely cause, naming the suspect subsystem/class/package; for a Feature: 2-4 sentences on a recommended implementation approach, pointing at where in the codebase it would go; for a Task: empty string", | |
| "needs_info": true | false | |
| } | |
| Guidance: crashes / data loss -> priority High or Urgent. A vague report | |
| with no repro/logs -> needs_info=true. "Task" = docs/chores/proposals, | |
| "Feature" = new suit/power/content, "Bug" = something broken. difficulty | |
| is how much codebase knowledge a fix needs (DB beginner .. D0 extreme). | |
| Pick the 1-2 most relevant areas; do not invent labels outside the lists. | |
| Keep "insight" concrete but clearly a best guess - a maintainer confirms it. | |
| - name: Apply triage | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| NUM: ${{ steps.ctx.outputs.num }} | |
| RESP: ${{ steps.ai.outputs.response }} | |
| # optional overrides - set as repo/org Actions variables instead of editing | |
| # this file. find the ids via: gh api orgs/<org>/issue-fields | |
| PRIORITY_FIELD_ID: ${{ vars.PRIORITY_FIELD_ID }} | |
| EFFORT_FIELD_ID: ${{ vars.EFFORT_FIELD_ID }} | |
| run: | | |
| set -uo pipefail | |
| MARKER="<!-- auto-triage -->" | |
| # find our previous triage comment (if any). its presence means this issue | |
| # was already triaged, so a re-run refreshes the comment but leaves labels | |
| # and fields alone - never clobbering a maintainer's later edits. | |
| # | |
| # distinguish "lookup succeeded, no marker" (genuinely first triage) from | |
| # "lookup failed" (transient api/auth/network error). on failure we must NOT | |
| # default to first-triage, or a flaky api call could re-apply labels over a | |
| # maintainer's edits - so fail safe: assume re-triage (no label/field changes). | |
| # stderr is left visible on purpose: the if/else handles failure, so any | |
| # gh error (auth/scope/rate/network) should show up in the run log. | |
| if CID_RAW=$(gh api "repos/$REPO/issues/$NUM/comments" --paginate \ | |
| --jq "[.[] | select(.body | contains(\"$MARKER\")) | .id] | last // empty"); then | |
| # --paginate emits one (possibly empty) line per page; keep the matched id. | |
| CID=$(printf '%s\n' "$CID_RAW" | grep -E '^[0-9]+$' | tail -1 || true) | |
| FIRST_TRIAGE=true; [ -n "$CID" ] && FIRST_TRIAGE=false | |
| else | |
| echo "comment lookup failed - treating as re-triage so labels/fields aren't clobbered" | |
| CID=""; FIRST_TRIAGE=false | |
| fi | |
| # prefer the whole response if it already parses as json (expected case: | |
| # the model is asked for one line of minified json). otherwise strip any | |
| # code fences and fall back to the first {...} block. | |
| if printf '%s' "$RESP" | jq -e . >/dev/null 2>&1; then | |
| JSON="$RESP" | |
| else | |
| JSON=$(printf '%s' "$RESP" | tr -d '\r\n' | sed 's/```json//g; s/```//g' | grep -o '{.*}' | head -1 || true) | |
| fi | |
| echo "model json: $JSON" | |
| if [ -z "$JSON" ] || ! printf '%s' "$JSON" | jq -e . >/dev/null 2>&1; then | |
| if [ "$FIRST_TRIAGE" = "true" ]; then | |
| echo "no valid json -> S: Untriaged" | |
| gh issue edit "$NUM" --repo "$REPO" --add-label "S: Untriaged" || true | |
| else | |
| echo "no valid json on re-triage -> leaving already-triaged issue untouched" | |
| fi | |
| exit 0 | |
| fi | |
| TYPE=$(echo "$JSON" | jq -r '.type // empty') | |
| PRIORITY=$(echo "$JSON" | jq -r '.priority // empty') | |
| EFFORT=$(echo "$JSON" | jq -r '.effort // empty') | |
| DIFF=$(echo "$JSON" | jq -r '.difficulty // empty') | |
| SUMMARY=$(echo "$JSON" | jq -r '.summary // empty' | tr '\r\n' ' ' | cut -c1-200) | |
| INSIGHT=$(echo "$JSON" | jq -r '.insight // empty' | tr '\r\n' ' ' | cut -c1-1200) | |
| NEEDS=$(echo "$JSON" | jq -r '.needs_info // false') | |
| # labels + fields are only touched on the FIRST triage. on a re-run we keep | |
| # whatever the maintainers have set and just refresh the comment below. | |
| if [ "$FIRST_TRIAGE" = "true" ]; then | |
| # clear S: Untriaged now that we have a valid classification. | |
| gh issue edit "$NUM" --repo "$REPO" --remove-label "S: Untriaged" >/dev/null 2>&1 || true | |
| LABELS=() | |
| case "$TYPE" in | |
| Bug) LABELS+=("T: Bugfix");; | |
| Feature) LABELS+=("T: New Feature");; | |
| # Task has no T: label in the taxonomy on purpose. | |
| esac | |
| case "$PRIORITY" in | |
| Urgent) LABELS+=("P0: Critical");; | |
| High) LABELS+=("P1: High");; | |
| Medium) LABELS+=("P2: Raised");; | |
| Low) LABELS+=("P3: Standard");; | |
| esac | |
| case "$DIFF" in | |
| DB) LABELS+=("DB: Beginner Friendly");; | |
| D3) LABELS+=("D3: Low");; | |
| D2) LABELS+=("D2: Medium");; | |
| D1) LABELS+=("D1: High");; | |
| D0) LABELS+=("D0: Very High");; | |
| esac | |
| # allowlist the model's area/change labels. note: diff-derived changes | |
| # (C: No Java, C: Structures) are intentionally NOT here - those are decided | |
| # from a PR's changed files by .github/labeler.yml, not guessable from an issue. | |
| declare -A ALLOWED_AC=( | |
| ["A: Suits"]=1 ["A: Powers"]=1 ["A: Entities"]=1 ["A: Datagen"]=1 | |
| ["A: Admin Tooling"]=1 ["A: Core Tech"]=1 ["A: Build"]=1 ["A: Docs"]=1 | |
| ["C: Render"]=1 ["C: UI"]=1 ["C: Networking"]=1 ["C: Textures"]=1 | |
| ["C: Audio"]=1 ["C: Translations"]=1 | |
| ) | |
| while IFS= read -r l; do | |
| [ -n "$l" ] && [ -n "${ALLOWED_AC[$l]:-}" ] && LABELS+=("$l") | |
| done < <(echo "$JSON" | jq -r '((.areas // []) + (.changes // []))[]') | |
| [ "$NEEDS" = "true" ] && LABELS+=("Issue: Awaiting Response") | |
| # only apply labels that actually exist in the repo - page through all of | |
| # them into a set for O(1) membership instead of a nested scan. | |
| declare -A HAVE | |
| while IFS= read -r e; do [ -n "$e" ] && HAVE["$e"]=1; done \ | |
| < <(gh api "repos/$REPO/labels" --paginate --jq '.[].name') | |
| ADD=() | |
| for l in "${LABELS[@]:-}"; do | |
| [ -n "${HAVE[$l]:-}" ] && ADD+=("--add-label" "$l") | |
| done | |
| if [ "${#ADD[@]}" -gt 0 ]; then | |
| gh issue edit "$NUM" --repo "$REPO" "${ADD[@]}" || true | |
| fi | |
| # org Priority/Effort issue fields (best effort) | |
| RID=$(gh api "repos/$REPO" --jq '.id') | |
| set_field() { | |
| [ -z "$2" ] && return 0 | |
| # build the payload with jq so the value is properly json-escaped | |
| jq -n --argjson fid "$1" --arg val "$2" \ | |
| '{issue_field_values:[{field_id:$fid,value:$val}]}' \ | |
| | gh api --method POST "repositories/$RID/issues/$NUM/issue-field-values" --input - >/dev/null 2>&1 \ | |
| || echo "field $1 not set" | |
| } | |
| # ids default to this org's Priority/Effort fields; override via the | |
| # PRIORITY_FIELD_ID / EFFORT_FIELD_ID Actions variables if they differ. | |
| set_field "${PRIORITY_FIELD_ID:-30052706}" "$PRIORITY" | |
| set_field "${EFFORT_FIELD_ID:-30052709}" "$EFFORT" | |
| else | |
| echo "re-triage: refreshing comment only, leaving labels/fields as-is" | |
| fi | |
| # triage comment (hidden marker first so re-runs can find + edit it) | |
| { | |
| echo "$MARKER" | |
| echo "🤖 **auto-triage** (github models)" | |
| echo | |
| echo "| type | priority | effort | difficulty |" | |
| echo "|---|---|---|---|" | |
| echo "| ${TYPE:-?} | ${PRIORITY:-?} | ${EFFORT:-?} | ${DIFF:-?} |" | |
| echo | |
| [ -n "$SUMMARY" ] && echo "> $SUMMARY" | |
| echo | |
| if [ -n "$INSIGHT" ]; then | |
| case "$TYPE" in | |
| Bug) echo "### 🔎 Likely cause";; | |
| Feature) echo "### 🛠️ Recommended implementation";; | |
| *) echo "### 🔎 Insight";; | |
| esac | |
| echo | |
| echo "$INSIGHT" | |
| echo | |
| fi | |
| [ "$NEEDS" = "true" ] && [ "$FIRST_TRIAGE" = "true" ] && echo "looks like it's missing repro steps / logs - added \`Issue: Awaiting Response\`." | |
| [ "$FIRST_TRIAGE" = "false" ] && echo "_(re-triage: labels left as set by maintainers)_" | |
| echo | |
| echo "_labels and insight are a best guess, a maintainer will confirm._" | |
| } > "$RUNNER_TEMP/comment.md" | |
| if [ -n "$CID" ]; then | |
| jq -Rs '{body: .}' "$RUNNER_TEMP/comment.md" \ | |
| | gh api --method PATCH "repos/$REPO/issues/comments/$CID" --input - >/dev/null \ | |
| && echo "updated existing triage comment $CID" || echo "comment update failed" | |
| else | |
| gh issue comment "$NUM" --repo "$REPO" --body-file "$RUNNER_TEMP/comment.md" >/dev/null \ | |
| && echo "posted triage comment" || true | |
| fi |