Skip to content

feat: support discussion_comment event #101

@wadackel

Description

@wadackel

Summary

Extend command-action to support the discussion_comment event in addition to issue_comment, so IssueOps commands can be triggered from comments on GitHub Discussions.

To stay aligned with the I/F finalized in #100, this change extends the same output vocabulary (number, context) and the allowed_contexts input vocabulary to cover discussions.

Note

This issue depends on #100. Implement it after the number / context output redesign in #100 has landed, and base the new behavior on that redesigned surface.

Motivation

  • command-action currently only handles issue_comment and emits a warning for any other eventName (src/main.ts:11-14). Workflows triggered by on: discussion_comment are dropped without doing any parsing.
  • GitHub Discussions are a first-class collaboration surface in many repositories, and IssueOps-style commands (.deploy, .preview, .label, etc.) are equally useful in discussion threads (Q&A triage, RFC voting, release-note drafting, etc.).
  • The I/F redesign in feat(output): add number and context outputs, soft-deprecate issue_number #100 already moves the output vocabulary toward an event-agnostic shape (number / context). Extending it once more to cover discussions is cheap if it ships right after feat(output): add number and context outputs, soft-deprecate issue_number #100, and avoids a third migration later.

Proposed changes

1. Accept discussion_comment in isValidContext

  • src/main.ts:11-14 currently rejects any event other than issue_comment. Replace the equality check with an allow-list of {"issue_comment", "discussion_comment"}.
  • Keep the warning message for unsupported events but list both accepted events.

2. Extend allowed_contexts vocabulary

  • Add discussion to the allowed values of allowed_contexts (currently issue / pull_request).
  • Update the validContexts set in src/main.ts:8 to {"issue", "pull_request", "discussion"}.
  • Update the default of allowed_contexts in action.yaml:14 from issue,pull_request to issue,pull_request,discussion. This is a strict addition: workflows that do not listen to discussion_comment are unaffected, and workflows that do gain support without manual opt-in.

3. Determine the triggering context

Introduce a single helper that, given the current @actions/github context, returns the discriminated triple { kind: "issue" | "pull_request" | "discussion", number, commentId, actor, body }. Use it from both isValidContext (for the allowed_contexts filter) and the output emission site, so the kind is computed in exactly one place.

Determination rules:

Event Condition kind
issue_comment payload.issue.pull_request != null "pull_request"
issue_comment otherwise "issue"
discussion_comment always "discussion"

This replaces the existing isPr boolean in src/main.ts:26 (which #100 already plans to lift out — coordinate so both changes converge on the same helper rather than introducing two competing abstractions).

4. Outputs on the discussion context

After isValidContext passes, emit the following for a discussion kind:

Output Value Notes
number payload.discussion.number Same semantics as #100's number for issue/PR.
context "discussion" Extends #100's `"issue"
comment_id payload.comment.id Same payload shape as issue_comment.
actor payload.comment.user.login Same payload shape as issue_comment.
command / params / continue Per existing logic Comment body parsing is event-agnostic.
issue_number not emitted The legacy name does not apply to discussions; downstream consumers should migrate to number (per #100's deprecation). For issue_comment events, issue_number continues to be emitted unchanged.

5. Parser reuse

  • parse(body) in src/parse.ts already operates on a plain comment body string. No parser changes required.
  • Confirm via a unit test on main.ts that a discussion comment body flows through the same parsing path.

6. README updates

  • Update §Inputs / §Outputs tables (regenerated by pnpm generate):
    • allowed_contexts default changes to issue,pull_request,discussion.
    • Description adds discussion as an allowed value.
    • context output description lists the three possible values.
  • Add a §Getting Started example for discussions:
    • on: discussion_comment: types: [created]
    • permissions: discussions: write (required for adding reactions to discussion comments via github.graphqladdReaction mutation; same level of detail as the existing Tips section for issue/PR reactions).
    • Brief callout linking to allowed_contexts if the workflow wants to restrict to discussions only.

7. Tests

src/main.ts currently has no unit tests. Add src/main.test.ts (vitest) covering the matrix below. Use vi.mock("@actions/github", ...) to inject eventName and payload per case, and spy on core.setOutput / core.warning to assert emission.

Case Event Payload shape Expected
Issue comment issue_comment payload.issue.pull_request == null context="issue", number=payload.issue.number, issue_number emitted
PR comment issue_comment payload.issue.pull_request != null context="pull_request", number=payload.issue.number, issue_number emitted
Discussion comment discussion_comment payload.discussion.number set context="discussion", number=payload.discussion.number, issue_number not emitted
Unsupported event e.g., push n/a warning emitted, continue="false", no other outputs
allowed_contexts filter mismatch discussion_comment with allowed_contexts="issue" discussion payload info emitted, continue="false"

Spec details

action.yaml

inputs:
  allowed_contexts:
    description: 'The comment contexts that trigger the IssueOps command, specified as a comma-separated list. Allowed values: "issue", "pull_request", "discussion".'
    default: 'issue,pull_request,discussion'

outputs:
  context:
    description: 'The context that triggered this action. One of "issue", "pull_request", or "discussion".'
  # (other outputs unchanged from #100)

src/main.ts (sketch)

const validContexts = new Set(['issue', 'pull_request', 'discussion'] as const);
type CommentKind = 'issue' | 'pull_request' | 'discussion';

const resolveCommentContext = (): {
  kind: CommentKind;
  number: number;
  commentId: number;
  actor: string;
  body: string;
} | null => {
  if (context.eventName === 'issue_comment') {
    const isPr = context.payload.issue?.pull_request != null;
    return {
      kind: isPr ? 'pull_request' : 'issue',
      number: context.payload.issue!.number!,
      commentId: context.payload.comment!.id,
      actor: context.payload.comment!.user.login,
      body: (context.payload.comment?.body ?? '') as string,
    };
  }
  if (context.eventName === 'discussion_comment') {
    return {
      kind: 'discussion',
      number: context.payload.discussion!.number,
      commentId: context.payload.comment!.id,
      actor: context.payload.comment!.user.login,
      body: (context.payload.comment?.body ?? '') as string,
    };
  }
  return null;
};

Output emission inside run then becomes:

core.setOutput('number', ctx.number);
core.setOutput('context', ctx.kind);
core.setOutput('comment_id', ctx.commentId);
core.setOutput('actor', ctx.actor);
if (ctx.kind !== 'discussion') {
  core.setOutput('issue_number', ctx.number);
}

Acceptance criteria

  • action.yaml declares allowed_contexts default issue,pull_request,discussion and lists discussion in the description.
  • action.yaml's context output description lists "issue", "pull_request", "discussion".
  • src/main.ts accepts eventName of either issue_comment or discussion_comment and rejects others with a warning.
  • A single helper computes { kind, number, commentId, actor, body } and is used by both the allowed_contexts filter and the output emission.
  • For discussion_comment: number === payload.discussion.number, context === "discussion", issue_number is not emitted.
  • For issue_comment (issue or PR): behavior matches feat(output): add number and context outputs, soft-deprecate issue_number #100 (number === payload.issue.number, issue_number still emitted).
  • When allowed_contexts does not include discussion, a discussion_comment event sets continue="false" and emits an info message, without setting number / context / etc.
  • src/main.test.ts covers the five cases in the test matrix above.
  • README §Inputs / §Outputs tables are regenerated via pnpm generate and reflect the new default and discussion value.
  • README has a discussion_comment example workflow including permissions: discussions: write.
  • pnpm build produces an updated dist/, committed in the same PR.
  • pnpm typecheck, pnpm lint, pnpm test, pnpm generate (no drift) all pass in CI.

Out of scope

  • Filtering by Discussion category (Q&A, General, Announcements, etc.). If demand surfaces, design a separate input (e.g. allowed_discussion_categories) in a follow-up issue.
  • Answering / marking discussion comments as the chosen answer. Belongs to user-side workflow logic via the GraphQL API, not this action.
  • Reaction support. Already handled by the README Tips section pattern and out of scope for this action.
  • Removal of issue_number. Tracked separately under feat(output): add number and context outputs, soft-deprecate issue_number #100's "next major release" plan.
  • Locked / archived discussion handling. GitHub already prevents new comments in those states, so no special handling needed here.

Migration

For existing v1 consumers, no migration is required:

on:
  issue_comment:
    types: [created]
  discussion_comment:
    types: [created]

permissions:
  contents: read
  issues: write
  pull-requests: write
  discussions: write

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - id: command
        uses: knowledge-work/command-action@v1
        with:
          command: 'greet'

      - if: ${{ steps.command.outputs.continue == 'true' }}
        run: |
          echo "context=${{ steps.command.outputs.context }}"
          echo "number=${{ steps.command.outputs.number }}"
          echo "name=${{ fromJSON(steps.command.outputs.params).name }}"

Assumptions

  • The discussion_comment payload exposes discussion.number, comment.id, comment.body, and comment.user.login in the same shape as documented in GitHub Webhooks docs. Verify against @actions/github's WebhookPayload typings during implementation; if the field shape differs, adjust the helper accordingly.
  • Expanding the allowed_contexts default to include discussion is treated as a non-breaking addition because the action still requires the workflow's on: trigger to fire discussion_comment for the new path to activate.
  • Naming the new context value "discussion" (singular, matching allowed_contexts) is consistent with the existing "issue" / "pull_request" vocabulary in feat(output): add number and context outputs, soft-deprecate issue_number #100.

References

  • feat(output): add number and context outputs, soft-deprecate issue_number #100 — output redesign that introduces number and context. This issue extends that vocabulary.
  • src/main.ts:8validContexts set.
  • src/main.ts:11-14 — current eventName rejection.
  • src/main.ts:26 — current isPr computation.
  • src/main.ts:58-60 — current output emission site.
  • action.yaml:14 — current allowed_contexts default.
  • action.yaml:25-28 — current outputs declaration.
  • GitHub Webhooks: discussion_comment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions