Skip to content

Fix /help routing fallthrough, error handling, reaction, and mention sanitization#40476

Merged
pelikhan merged 16 commits into
mainfrom
copilot/add-slash-command-help-handler
Jun 20, 2026
Merged

Fix /help routing fallthrough, error handling, reaction, and mention sanitization#40476
pelikhan merged 16 commits into
mainfrom
copilot/add-slash-command-help-handler

Conversation

Copilot AI commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

The original /help builtin had several correctness gaps: it hard-returned even when disabled (blocking custom /help workflows), let API errors propagate uncaught, posted raw descriptions without sanitizing @mentions, skipped the immediate reaction that every other command path adds, and duplicated commands appearing in both centralized and decentralized sections.

Bug fixes

  • Fallthrough when disabled — when help_command: false, the /help block now falls through to normal resolveMatchingSlashRoutes dispatch instead of returning early; custom /help workflows fire correctly
  • Error handlingpostBuiltinHelpComment is now wrapped in try-catch (both REST and GraphQL paths); failures emit core.warning(...) and return false, matching the pattern used by dispatchWorkflow
  • Immediate reactionaddImmediateReaction("eyes") is called before posting the help comment, consistent with every other command path
  • @mention sanitization — descriptions are passed through neutralizeDescriptionMentions() (using named GITHUB_MENTION_RE regex) before interpolation to prevent notification pings
  • Command deduplication — commands with both centralized and decentralized flags now appear only in the centralized section

Cleanup

  • Removed dead getHelpDocsUrl() and DEFAULT_SLASH_COMMAND_DOCS_URL; buildHelpCommentBody reads the env var directly and omits the "Learn more" line when the URL is empty
  • Removed redundant if repoConfig != nil guard — IsHelpCommandEnabled() already handles nil receivers
  • Added // nil = use default (enabled) inline comment on HelpCommand *bool in the raw parse struct

Compile-time safety

  • buildHelpCommandEntries now logs a warning when any workflow registers "help" as a command name, surfacing the reservation at compile time

Warning message

  • generateCentralSlashCommandWorkflowWrapper now explicitly names help_command in the config-load-failure warning so operators know which flags revert to defaults

Tests added

JS: discussion-context /help via GraphQL, invalid GH_AW_HELP_COMMAND_ENABLED value, malformed/non-array GH_AW_HELP_COMMANDS, unsupported event warning path, immediate reaction assertion, @mention neutralization, centralized deduplication, API error graceful degradation, custom /help dispatch when builtin disabled

Go: TestBuildHelpCommandEntries_ConflictingDescriptions (first-wins), TestBuildHelpCommandEntries_ReservedHelpCommandName

Copilot AI and others added 7 commits June 20, 2026 15:08
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add builtin centralized /help command handler Add centralized builtin /help slash command with aw.json opt-out Jun 20, 2026
Copilot AI requested a review from pelikhan June 20, 2026 15:34
@pelikhan pelikhan marked this pull request as ready for review June 20, 2026 15:36
Copilot AI review requested due to automatic review settings June 20, 2026 15:36
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot also generate documentation for supported label commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (225 new lines across pkg/ and actions/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/40476-builtin-centralized-help-slash-command.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff (handler in route_slash_command.cjs, compile-time inventory in central_slash_command_workflow.go, and the help_command opt-out in repo_config.go).
  2. Complete the missing sections — there is one [TODO: verify] in the Context about what motivated the work; confirm or remove it, and refine the decision rationale and alternatives.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-40476: Builtin centralized /help slash command handler

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 40476-builtin-centralized-help-slash-command.md for PR #40476).

🔒 Blocking: link the completed ADR in the PR body to clear this gate.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 99.1 AIC · ⌖ 10.4 AIC · ⊞ 13.6K ·

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a centralized builtin /help slash command to the centralized router so users can discover supported slash commands in-context. It introduces a top-level help_command flag in aw.json to opt out, emits help metadata into the generated centralized router workflow, and updates the router script/tests and docs accordingly.

Changes:

  • Added help_command (default-enabled) to aw.json parsing + schema, with typed access via RepoConfig.IsHelpCommandEnabled().
  • Extended centralized router workflow generation to emit help command inventory/enablement/docs URL via env vars.
  • Implemented builtin /help handling in route_slash_command.cjs, with tests and documentation updates.
Show a summary per file
File Description
pkg/workflow/repo_config.go Adds help_command parsing and enablement helper.
pkg/workflow/repo_config_test.go Adds tests for default/true/false help_command behavior.
pkg/workflow/central_slash_command_workflow.go Emits help metadata/env vars during centralized workflow generation.
pkg/workflow/central_slash_command_workflow_test.go Verifies help env vars and metadata aggregation; tests config disablement.
pkg/parser/schemas/repo_config_schema.json Documents/validates help_command in the JSON schema.
pkg/cli/compile_post_processing.go Loads repo config and passes it to centralized workflow generator.
pkg/cli/compile_pipeline.go Wires gitRoot into the centralized workflow wrapper call.
docs/src/content/docs/reference/command-triggers.md Documents builtin centralized /help and help_command opt-out.
actions/setup/js/route_slash_command.test.cjs Adds tests for builtin /help behavior and disablement.
actions/setup/js/route_slash_command.cjs Implements builtin /help handling + comment posting.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 11/11 changed files
  • Comments generated: 3

Comment on lines 544 to +555
const commandName = selectedCommand;
if (commandName === "help") {
if (!isBuiltinHelpEnabled()) {
core.info("Builtin /help command is disabled by aw.json (help_command=false).");
return;
}
const posted = await postBuiltinHelpComment(buildHelpCommentBody(parseHelpCommandsMetadata()));
if (posted) {
core.info("Posted builtin /help command response.");
}
return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. When help_command: false (builtin disabled), the code now falls through to normal route dispatch instead of returning early — so any centralized /help workflow still fires. When the builtin is enabled, it intentionally preempts custom /help workflows (this is by design: the builtin handler is the primary purpose). A compile-time warning is now emitted via buildHelpCommandEntries whenever a workflow registers help as a command name, surfacing the conflict at gh aw compile time rather than silently at runtime.

Comment on lines +384 to +396
const issueNumber = context.payload?.issue?.number ?? context.payload?.pull_request?.number;
if (issueNumber) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: commentBody,
headers: {
"X-GitHub-Api-Version": GITHUB_API_VERSION,
},
});
return true;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. postBuiltinHelpComment is now wrapped in a single try-catch that returns false on any API error and emits a core.warning(...) with the error detail. @mentions in descriptions are neutralized via neutralizeDescriptionMentions() (using GITHUB_MENTION_RE) before interpolation into bullet lines, so they cannot trigger GitHub notifications. A test covers the API-error path.

Comment on lines +398 to +404
if (context.eventName === "discussion" || context.eventName === "discussion_comment") {
const discussionID = context.payload?.discussion?.node_id;
if (!discussionID) {
core.warning("Unable to post builtin /help response: discussion node_id missing.");
return false;
}
await github.graphql(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The entire postBuiltinHelpComment body — both the REST createComment path and the GraphQL addDiscussionComment path — is now inside a single try-catch. Failures emit core.warning(...) and return false instead of propagating. A dedicated test covers the discussion-context GraphQL path.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd, /grill-with-docs, and /improve-codebase-architecture — requesting changes on a correctness gap and test coverage holes, with a couple of design suggestions.

📋 Key Themes & Highlights

Blocking issues

  • Silent dispatch suppression: if a repo defines command: [help] for a centralized workflow, the new /help intercept in main() will consume it with no warning — the user's dispatch silently never fires. A compile-time warning is needed.
  • Uncaught API errors in postBuiltinHelpComment: REST and GraphQL calls are not wrapped in try-catch; failures propagate as unhandled rejections with no diagnostic logging.

Test coverage gaps (non-blocking but important)

  • Discussion context (discussion/discussion_comment) — the GraphQL path has zero test coverage
  • Unsupported event type warning path (core.warning(...) at line 416)
  • isBuiltinHelpEnabled invalid-value warning (e.g. "banana")
  • parseHelpCommandsMetadata malformed-JSON and non-array paths
  • TestBuildHelpCommandEntries missing conflict-logging scenario

Design observations

  • Command appearing in both sections: a command with both centralized: true and decentralized: true renders in both the "Centralized" and "Non-centralized" lists — potentially confusing
  • getHelpDocsUrl() is dead code: GH_AW_SLASH_COMMAND_DOCS_URL is always baked into generated YAML, so the JS env fallback can never be reached
  • Redundant nil guard before IsHelpCommandEnabled call (the method already handles nil receivers)

Positive highlights

  • ✅ Excellent use of *bool + nil-receiver accessor pattern for the opt-out — clean and idiomatic Go
  • ✅ Deterministic description conflict resolution is well-thought-out and documented
  • ✅ The three-env-var boundary (compile → runtime) is a solid and auditable design
  • ✅ Good test breadth for the happy path and the disabled path in both JS and Go layers
  • DescriptionBy tracking in the aggregate struct is a nice touch for diagnostic logging

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 122.6 AIC · ⌖ 8.51 AIC · ⊞ 6.9K

setupActionRef := ResolveSetupActionReference(ctx, actionMode, GetVersion(), "", nil)

content, err := buildCentralSlashCommandWorkflowYAML(slashRoutesByCommand, labelRoutesByCommand, mergedEvents, resolveCentralSlashRunsOn(workflowDataList), setupActionRef)
helpCommands := buildHelpCommandEntries(workflowDataList)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] help is a reserved command name with no compile-time warning — if a user registers command: [help] for a centralized workflow, the intercept in main() will silently consume it and their dispatch will never fire.

💡 Suggested fix

In buildHelpCommandEntries or GenerateCentralSlashCommandWorkflow, check for and warn about the reserved name:

for _, command := range commands {
    if command == "help" {
        centralSlashCommandWorkflowLog.Printf(
            "Warning: 'help' is reserved for the builtin /help handler; " +
            "centralized workflows using this command name will not be dispatched")
    }
}

This surfaces the conflict at compile time instead of silently swallowing dispatches at runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. buildHelpCommandEntries now logs a warning via centralSlashCommandWorkflowLog.Printf when any workflow registers "help" as a command name, explaining that it is reserved for the builtin handler and won't dispatch unless help_command is disabled. A test (TestBuildHelpCommandEntries_ReservedHelpCommandName) verifies the entry is still included in the metadata.

}

function buildHelpCommentBody(helpCommands) {
const centralized = helpCommands.filter(entry => entry.centralized);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] A command with both centralized: true and decentralized: true (which buildHelpCommandEntries can produce) will appear in both sections of the help comment, confusing users.

💡 Example and fix

From TestBuildHelpCommandEntries, triage gets Centralized: true, Decentralized: true. buildHelpCommentBody filters independently, so it shows /triage twice.

Consider either:

  1. Listing such commands only once under Centralized commands (centralized takes precedence), or
  2. Adding a third section "Both" or a note like (also non-centralized).

This case should also be covered by a JS test asserting a command with both flags appears only once (or exactly twice with a clear label).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. buildHelpCommentBody now tracks the set of centralized command names and excludes them from the decentralized filter: decentralized = helpCommands.filter(entry => entry.decentralized && !centralizedNames.has(entry.command)). A test asserts a command with both flags appears only in the centralized section.


const issueNumber = context.payload?.issue?.number ?? context.payload?.pull_request?.number;
if (issueNumber) {
await github.rest.issues.createComment({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] createComment and graphql can throw on API errors, but neither call is wrapped in try-catch — failures will propagate as unhandled rejections through main(), losing all diagnostic context. Other paths in this file (e.g. dispatchWorkflow) wrap their API calls.

💡 Suggested fix
async function postBuiltinHelpComment(commentBody) {
  try {
    // ... existing REST / GraphQL code ...
  } catch (error) {
    core.warning(`Failed to post builtin /help comment: ${String(error)}`);
    return false;
  }
}

This also makes the posted check in main() meaningful for the error case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. postBuiltinHelpComment is now wrapped in try-catch (both REST and GraphQL branches). Errors emit core.warning("Failed to post builtin /help comment: ...") and return false. A test ("warns when postBuiltinHelpComment fails due to API error") confirms graceful degradation.

expect(issueCommentCalls[0].body).toContain("**Non-centralized commands**");
expect(issueCommentCalls[0].body).toContain("- `/local-summary` — Run summary workflow");
expect(issueCommentCalls[0].body).toContain("https://github.github.com/gh-aw/reference/command-triggers/");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No test covers /help in a discussion context. postBuiltinHelpComment has a distinct GraphQL branch (lines 398–413) for discussion / discussion_comment events, but it's untested — a mock mistake or API change here would go undetected.

💡 Suggested test outline
it("handles builtin /help on discussion_comment events via GraphQL", async () => {
  globals.context.eventName = "discussion_comment";
  globals.context.payload = { discussion: { node_id: "D_test123" }, comment: { body: "/help" } };

  await main();

  expect(dispatchCalls).toHaveLength(0);
  // Assert graphql was called with the addDiscussionComment mutation
  const graphqlCalls = globals.github.graphql.mock.calls;
  expect(graphqlCalls.length).toBeGreaterThan(0);
  // last graphql call should be the help comment, not a reaction
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. New test "handles builtin /help on discussion_comment events via GraphQL" sets eventName = "discussion_comment", provides a discussion.node_id, invokes main(), and asserts that github.graphql was called with the addDiscussionComment mutation and the expected discussionId and help body.

expect(dispatchCalls).toHaveLength(0);
expect(issueCommentCalls).toHaveLength(0);
expect(globals.core.info).toHaveBeenCalledWith(expect.stringContaining("Builtin /help command is disabled"));
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Three untested warning paths in the new helper functions:

  1. isBuiltinHelpEnabled with an invalid value (e.g. "banana") — line 310 emits a warning but falls through to enabled
  2. parseHelpCommandsMetadata with malformed JSON — the catch at line 339 is uncovered
  3. parseHelpCommandsMetadata with valid JSON that isn't an array — the !Array.isArray guard at line 318 is uncovered
💡 Suggested additions
it("isBuiltinHelpEnabled warns on invalid value and defaults to enabled", async () => {
  process.env.GH_AW_HELP_COMMAND_ENABLED = "banana";
  globals.context.payload.comment.body = "/help";
  await main();
  expect(globals.core.warning).toHaveBeenCalledWith(expect.stringContaining("Invalid value"));
  expect(issueCommentCalls).toHaveLength(1); // still posts help
});

it("parseHelpCommandsMetadata handles malformed JSON gracefully", () => {
  // Can test the exported function directly, or drive through main() with bad env.
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added all three paths: (1) "warns on invalid GH_AW_HELP_COMMAND_ENABLED value and still posts help" — sets env to "banana", asserts warning emitted and help still posted; (2) "handles malformed JSON in GH_AW_HELP_COMMANDS gracefully" — asserts warning + empty command list; (3) "handles non-array JSON in GH_AW_HELP_COMMANDS gracefully" — asserts - _None_ sections rendered.

}
}

function getHelpDocsUrl() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] getHelpDocsUrl() and DEFAULT_SLASH_COMMAND_DOCS_URL are dead code in practice: GH_AW_SLASH_COMMAND_DOCS_URL is always baked into the generated YAML with a fixed value (line 375 of central_slash_command_workflow.go), so the env-var fallback and the JS constant can never be reached at runtime.

💡 Options

Option A (simplest) — Remove getHelpDocsUrl(), DEFAULT_SLASH_COMMAND_DOCS_URL, and the env var override in JS; hardcode the URL directly in buildHelpCommentBody.

Option B (forward-looking) — Expose a docs_url field in aw.json alongside help_command, thread it through the Go generator, and then the JS env override becomes meaningful.

Option A is correct today; Option B adds future flexibility if the URL must be customizable per-repo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed getHelpDocsUrl() and DEFAULT_SLASH_COMMAND_DOCS_URL. buildHelpCommentBody now reads process.env.GH_AW_SLASH_COMMAND_DOCS_URL directly and only appends the "Learn more" line when the URL is non-empty — eliminating the dead fallback constant entirely.

content, err := buildCentralSlashCommandWorkflowYAML(slashRoutesByCommand, labelRoutesByCommand, mergedEvents, resolveCentralSlashRunsOn(workflowDataList), setupActionRef)
helpCommands := buildHelpCommandEntries(workflowDataList)
helpCommandEnabled := true
if repoConfig != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] The if repoConfig != nil guard is redundant — IsHelpCommandEnabled() already handles a nil receiver and returns true. The guard adds visual noise and could mislead future readers into thinking the method is not nil-safe.

💡 Suggested simplification
helpCommandEnabled := repoConfig.IsHelpCommandEnabled()

This removes the local helpCommandEnabled := true initialization and the branching block, relying on the nil-receiver contract that's already documented on IsHelpCommandEnabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed the if repoConfig != nil guard; helpCommandEnabled is now set directly via repoConfig.IsHelpCommandEnabled(), relying on its documented nil-receiver contract.


func typeSetKeys(typeSet map[string]bool) []string {
out := make([]string, 0, len(typeSet))
for key := range typeSet {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] TestBuildHelpCommandEntries doesn't cover the conflict-logging path: the test uses triage-inline with an empty description, so the conflict branch (existing.Description != "" && description != "") is never hit. A test with two non-empty conflicting descriptions would confirm first-wins determinism and that the warning is logged.

💡 Suggested addition
func TestBuildHelpCommandEntries_ConflictingDescriptions(t *testing.T) {
    var buf strings.Builder
    // Temporarily redirect the logger if it supports a writer, or capture via log.SetOutput
    data := []*WorkflowData{
        {WorkflowID: "wf-a", Command: []string{"deploy"}, CommandCentralized: true, Description: "Deploy A"},
        {WorkflowID: "wf-b", Command: []string{"deploy"}, CommandCentralized: true, Description: "Deploy B"},
    }
    entries := buildHelpCommandEntries(data)
    require.Equal(t, 1, len(entries))
    assert.Equal(t, "Deploy A", entries[0].Description, "first description should win")
    // Assert log output contains the conflict warning
    _ = buf
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TestBuildHelpCommandEntries_ConflictingDescriptions: two workflows register the same "deploy" command with different non-empty descriptions. The test asserts len(entries) == 1 and entries[0].Description == "Deploy service A" (first-wins). The conflict-log path executes during this test even though log output is not directly captured (the logger writes to stderr only when DEBUG is set).

return true;
}

core.warning(`Unable to post builtin /help response for event '${context.eventName}'.`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The core.warning(...) path for unrecognised events (line 416) has no test. It's reached when issueNumber is falsy AND the event is neither discussion nor discussion_comment — e.g. a workflow_dispatch or push trigger.

💡 Suggested test
it("warns and returns false for /help on unsupported event type", async () => {
  globals.context.eventName = "push";
  globals.context.payload = { comment: { body: "/help" } }; // no issue or PR number
  await main();
  expect(issueCommentCalls).toHaveLength(0);
  expect(globals.core.warning).toHaveBeenCalledWith(
    expect.stringContaining("Unable to post builtin /help response for event 'push'")
  );
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test "warns and returns false for /help on unsupported event type": sets eventName = "push" with no issue/PR number, invokes main(), and asserts core.warning is called with a message containing "Unable to post builtin /help response for event 'push'" and no comment is posted.

Comment thread pkg/workflow/repo_config.go Outdated
// Use an intermediate struct with json.RawMessage to defer maintenance parsing.
var raw struct {
GHES bool `json:"ghes,omitempty"`
HelpCommand *bool `json:"help_command,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The existing GHES bool in the intermediate raw struct uses omitempty, but HelpCommand *bool also uses it — while harmless here (pointer zero-value is nil), it's worth confirming the schema default semantics align: omitempty on a *bool means a JSON null is treated the same as a missing key, which is the intended "inherit default" behaviour. Consider a brief comment to make this contract explicit for future maintainers, similar to the comment above on GHES.

💡 Optional one-liner
HelpCommand *bool `json:"help_command,omitempty"` // nil = use default (enabled)

Low priority — the existing IsHelpCommandEnabled godoc covers this, but inline signal at the struct level helps readers who land here first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the inline comment // nil = use default (enabled) directly on the HelpCommand *bool field in the intermediate raw struct, consistent with the suggestion. The IsHelpCommandEnabled godoc already covers the nil-receiver contract; this adds a quick signal at the struct-field level for readers landing there first.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 89/100 — Excellent

Analyzed 8 new/substantively modified tests (6 Go, 2 JavaScript): 8 design tests, 0 implementation tests, 0 guideline violations. Minor: 2 new Go test functions are missing descriptive assertion messages.

📊 Metrics & Test Classification (8 tests analyzed)
Metric Value
New/modified tests analyzed 8
✅ Design tests (behavioral contracts) 8 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 5 (62.5%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0 (Go mock libraries / missing build tags / no assertion messages)

Note: 4 additional modified Go tests (DeletesWhenUnused, GeneratesForDecentralizedLabelsOnly, IncludesPullRequestsPermissionForIssueTriggers, UsesCentralizedRunsOnResolution) only update the function signature (nil fourth arg) with no new assertions — not counted as new tests.

Inflation ratios (test lines added / production lines added):

Test file Test +Δ Prod +Δ Ratio
route_slash_command.test.cjs 45 131 0.34 ✅
central_slash_command_workflow_test.go 62 107 0.58 ✅
repo_config_test.go 23 17 1.35 ✅
Test File Classification Issues Detected
TestGenerateCentralSlashCommandWorkflow_GeneratesWorkflow (modified) pkg/workflow/central_slash_command_workflow_test.go ✅ Design
TestGenerateCentralSlashCommandWorkflow_DisablesHelpCommandViaRepoConfig (new) pkg/workflow/central_slash_command_workflow_test.go ✅ Design ⚠️ Missing assertion messages
TestBuildHelpCommandEntries (new) pkg/workflow/central_slash_command_workflow_test.go ✅ Design ⚠️ Missing assertion message
TestLoadRepoConfig_EmptyObject (modified) pkg/workflow/repo_config_test.go ✅ Design
TestLoadRepoConfig_HelpCommandFalse (new) pkg/workflow/repo_config_test.go ✅ Design
TestLoadRepoConfig_HelpCommandTrue (new) pkg/workflow/repo_config_test.go ✅ Design
handles builtin /help by posting a context comment and skipping dispatch (new) actions/setup/js/route_slash_command.test.cjs ✅ Design
skips builtin /help when disabled (new) actions/setup/js/route_slash_command.test.cjs ✅ Design

Go: 6 (*_test.go); JavaScript: 2 (*.test.cjs). Both Go test files have //go:build !integration on line 1. No other languages detected.

⚠️ Flagged Tests — Non-blocking (2 issues)

TestGenerateCentralSlashCommandWorkflow_DisablesHelpCommandViaRepoConfig (pkg/workflow/central_slash_command_workflow_test.go:398) — ⚠️ Missing assertion messages on require.NoError and require.Contains calls. The sister tests in repo_config_test.go set the right example (require.NoError(t, err, "valid aw.json should load without error")). Suggested fix: add a context string to each assertion, e.g. require.Contains(t, string(content), "GH_AW_HELP_COMMAND_ENABLED: 'false'", "disabled RepoConfig should produce false flag in workflow").

TestBuildHelpCommandEntries (pkg/workflow/central_slash_command_workflow_test.go:418) — ⚠️ require.Equal has no message argument. Suggested fix: require.Equal(t, expected, actual, "buildHelpCommandEntries should return sorted, deduplicated entries with correct flags").

These are non-blocking style issues; the tests correctly verify observable output (sorted, deduplicated entry list; generated YAML content).

Verdict

Check passed. 0% implementation tests (threshold: 30%). All 8 new/modified tests verify observable behavior: correct generated YAML env vars, correct opt-out plumbing, correct config loading, and correct /help routing outputs. No mock-library violations and no missing build tags.

Score breakdown: Behavioral coverage 40/40 · Edge case coverage 19/30 · Low duplication 20/20 · Proportional growth 10/10 = 89/100

References:

🧪 Test quality analysis by Test Quality Sentinel · 178.5 AIC · ⌖ 8.33 AIC · ⊞ 8.3K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 89/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%).

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — three correctness/resiliency issues must be fixed before merge.

### Blocking issues

1. /help intercept is terminal in both states (route_slash_command.cjs:548)
The if (commandName === "help") block always returns — both when the builtin fires and when it's disabled. Any centralized workflow registered with command: help is permanently unreachable regardless of help_command setting. When the builtin is disabled the fix is to fall through to normal resolveMatchingSlashRoutes dispatch; when it's enabled, the preemption should be explicitly documented as a reservation of the help command name.

2. postBuiltinHelpComment propagates API errors uncaught (route_slash_command.cjs:396)
Unlike dispatchWorkflow, this function has no try/catch. A rate-limit, network failure, or permission error on issues.createComment / graphql throws straight through main() and hard-fails the action step. The call site already handles posted === false gracefully — wrap the body in try/catch and return false on error.

3. repoConfig = nil on load failure silently re-enables the builtin (compile_post_processing.go:130)
If aw.json is transiently unreadable in non-strict mode, IsHelpCommandEnabled() returns true for the nil receiver — a repo that explicitly set help_command: false gets the builtin re-enabled without any warning indicating that a behavior-controlling key was lost.

### Non-blocking observations
  • Missing reaction for /help (route_slash_command.cjs:550): Every other command path calls addImmediateReaction() before dispatching. /help skips this, leaving the user with no acknowledgment until the comment appears.

🔎 Code quality review by PR Code Quality Reviewer · 134.6 AIC · ⌖ 7.8 AIC · ⊞ 5.1K

if (commandName === "help") {
if (!isBuiltinHelpEnabled()) {
core.info("Builtin /help command is disabled by aw.json (help_command=false).");
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/help is an unconditional dead end — custom /help workflows are permanently unreachable. Even with help_command: false, this return exits main() before the resolveMatchingSlashRoutes call, so any centralized workflow registered with command: help silently never dispatches.

💡 Suggested fix

When the builtin is disabled, fall through to the normal routing path instead of returning early:

if (commandName === "help") {
  if (isBuiltinHelpEnabled()) {
    const posted = await postBuiltinHelpComment(buildHelpCommentBody(parseHelpCommandsMetadata()));
    if (posted) {
      core.info("Posted builtin /help command response.");
    }
    return;
  }
  // Builtin disabled — fall through to normal route dispatch below.
  core.info("Builtin /help command is disabled (help_command=false); routing normally.");
}

Also note: even when the builtin is enabled, the return at line 554 silently shadows any centralized /help workflow. If that preemption is intentional, document it explicitly in the help_command schema description and in IsHelpCommandEnabled's godoc — repo admins who had a /help workflow before this PR will see it silently stop firing with no warning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The /help block is now inverted: when the builtin is enabled, it intercepts, adds a reaction, posts the help comment, and returns. When the builtin is disabled, it logs an info message and falls through to resolveMatchingSlashRoutes — so custom /help workflows dispatch normally. Added a test ("dispatches custom /help workflow when builtin is disabled") that confirms dispatch when help_command=false.

},
});
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postBuiltinHelpComment lets API errors propagate uncaught, causing the action step to hard-fail instead of warning and continuing. dispatchWorkflow (line ~281) wraps its REST call in try/catch and returns false on failure. This function follows no such pattern — a permissions error, rate-limit 429, or transient network failure throws straight through main(), which has no surrounding try/catch either.

💡 Suggested fix

Wrap both REST and GraphQL calls the same way dispatchWorkflow does:

async function postBuiltinHelpComment(commentBody) {
  try {
    const issueNumber = context.payload?.issue?.number ?? context.payload?.pull_request?.number;
    if (issueNumber) {
      await github.rest.issues.createComment({ ... });
      return true;
    }
    if (context.eventName === "discussion" || context.eventName === "discussion_comment") {
      ...
      await github.graphql(...);
      return true;
    }
    core.warning(`Unable to post builtin /help response for event '${context.eventName}'.`);
    return false;
  } catch (error) {
    core.warning(`Failed to post builtin /help comment: ${String(error)}`);
    return false;
  }
}

This is especially important because the caller at line 550 already handles posted === false gracefully — the error-swallowing pattern is already expected by the call site.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. postBuiltinHelpComment now wraps the entire body in try-catch, mirroring the pattern used by dispatchWorkflow. API errors emit core.warning(...) and return false. The posted check in the caller handles false gracefully — no unhandled rejections.

core.info("Builtin /help command is disabled by aw.json (help_command=false).");
return;
}
const posted = await postBuiltinHelpComment(buildHelpCommentBody(parseHelpCommandsMetadata()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/help posts no immediate reaction, breaking the UX contract that every slash command acknowledges receipt with an emoji. Every other command path (label routes at line 508, slash routes at line 566) calls addImmediateReaction() before dispatching. /help silently skips this — the user who typed /help gets no :eyes: acknowledgment and has no way to know the command was received until the comment eventually appears.

💡 Suggested fix

Add a reaction before posting the help comment, consistent with the rest of the router:

if (commandName === "help") {
  if (!isBuiltinHelpEnabled()) { ... }
  // Acknowledge receipt the same way other commands do.
  await addImmediateReaction("eyes");
  const posted = await postBuiltinHelpComment(buildHelpCommentBody(parseHelpCommandsMetadata()));
  ...
}

Since /help has no ai_reaction config, hardcoding "eyes" (or a dedicated constant) is fine — the important thing is that the reaction fires before the async comment write.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. await addImmediateReaction("eyes") is now called before postBuiltinHelpComment(...) in the builtin-enabled path, consistent with the reaction-before-dispatch pattern used for label routes and slash routes. Added test "adds immediate reaction before posting builtin /help comment" to assert reactionCalls.length === 1.

return fmt.Errorf("failed to load repo config: %w", err)
}
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to load repo config: %v", err)))
repoConfig = nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When LoadRepoConfig fails in non-strict mode, setting repoConfig = nil silently re-enables the builtin /help even if help_command: false was set. IsHelpCommandEnabled() returns true for a nil receiver, so a repo that explicitly opted out gets the builtin re-enabled whenever aw.json is transiently unreadable or malformed. The warning message says nothing about help_command being overridden.

💡 Suggested fix

Either surface a more specific warning when config loss affects behavior-controlling keys, or default helpCommandEnabled to false when the config can't be loaded (fail-closed rather than fail-open):

if err != nil {
    if strict {
        return fmt.Errorf("failed to load repo config: %w", err)
    }
    fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
        fmt.Sprintf("Failed to load repo config; defaulting all repo-config flags (help_command etc.) to their defaults: %v", err),
    ))
    repoConfig = nil
}

Alternatively, make the callee accept an explicit helpCommandEnabled bool rather than a nullable *RepoConfig so nil can't accidentally re-enable things.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved the warning message in generateCentralSlashCommandWorkflowWrapper to explicitly mention help_command and other repo-config flags: "Failed to load repo config; repo-config flags (e.g. help_command) will use defaults: ...". This makes it clear to operators that behavior-controlling keys like help_command: false revert to their defaults when aw.json is unreadable.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot Run PR-Finisher skill.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot also generate documentation for supported label commands.

Done. Label commands are now included in the /help output:

  • helpCommandEntry gains a Label bool field (Go + JSON)
  • buildHelpCommandEntries now aggregates LabelCommand entries from every workflow, with the same conflict-aware description resolution used for slash commands
  • The JS router's parseHelpCommandsMetadata reads the new label field; buildHelpCommentBody grows a Label commands section (labels shown with backtick formatting, no / prefix since they're GitHub labels, not slash commands)
  • Tests updated in both Go and JS

Committed in Tighten buildHelpCommandEntries nil/empty guard (latest commit).

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI and others added 3 commits June 20, 2026 17:35
…action, sanitization, deduplication, dead code, compile warning

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…s empty

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…mand-help-handler

# Conflicts:
#	pkg/workflow/repo_config.go

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add centralized builtin /help slash command with aw.json opt-out Fix /help routing fallthrough, error handling, reaction, and mention sanitization Jun 20, 2026
@pelikhan

Copy link
Copy Markdown
Collaborator

…tion assertions

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix https://github.com/github/gh-aw/actions/runs/27878949266/job/82503843568

Fixed in the latest commit. Two test bugs were causing the failures:

  1. "unsupported event type" test — used push event, but resolveBodyText() doesn't map push to any payload field, so the slash command was never detected and postBuiltinHelpComment was never reached. Changed to pull_request_review_comment (which maps to comment.body) and updated the expected warning message to match.

  2. "neutralizes @mentions" testnot.toContain("@admin") was logically contradictory with toContain("@admin") since the backtick-wrapped form still contains @admin as a substring. Replaced the first assertion with not.toContain("Run @admin workflow") to verify the raw un-neutralized form is absent.

@pelikhan pelikhan merged commit 6de36af into main Jun 20, 2026
37 checks passed
@pelikhan pelikhan deleted the copilot/add-slash-command-help-handler branch June 20, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants