Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 105 additions & 17 deletions .github/workflows/feature-ideation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,19 @@ on:
required: false
default: false
type: boolean
enhance_backlog:
description: 'Backlog enhancement sweep: enhance this repo''s open, human-authored, not-yet-enhanced Ideas (one comment each, idempotent). Combine with dry_run=true to preview.'
required: false
default: false
type: boolean
target_discussion:
description: 'Internal — set by `redispatch` when bridging a `discussion:created` event to workflow_dispatch (claude-code-action does not support discussion contexts). Not for manual use.'
required: false
default: ''
type: string
Comment on lines +72 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

2. Unvalidated discussion number 🐞 Bug ☼ Reliability

The new target_discussion workflow_dispatch input is documented as internal and intended to be a
Discussion number, but it is forwarded without validation. A manual dispatch can supply arbitrary
text, violating the workflow’s own contract and risking unpredictable downstream behavior when that
value is used to target a discussion.
Agent Prompt
## Issue description
`target_discussion` is intended to be a numeric Discussion number, but the workflow forwards `github.event.inputs.target_discussion` without checking it is either empty (normal scan) or a decimal integer.

## Issue Context
The value is set by the `redispatch` job on `discussion:created`, but the same input can be set manually via `workflow_dispatch`.

## Fix Focus Areas
- .github/workflows/feature-ideation.yml[72-76]
- .github/workflows/feature-ideation.yml[147-165]

## What to change
Add a validation step (or inline guard) in `prep` before writing outputs:
- Allow empty string.
- If non-empty, require `^[0-9]+$` (and optionally a reasonable length bound).
- Fail fast with a clear `::error::` message if invalid.

## Example
```bash
if [ -n "${TARGET_DISCUSSION:-}" ] && ! [[ "$TARGET_DISCUSSION" =~ ^[0-9]+$ ]]; then
  echo "::error::target_discussion must be empty or a numeric Discussion number"
  exit 1
fi
```

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# Auto-enhance a freshly-created idea: when a new Discussion is opened in the
# Ideas category, the analyst researches and refines it in single-idea mode.
# Ideas category, the `redispatch` job bridges it to workflow_dispatch (see below)
# so the analyst refines it in single-idea mode under a supported event.
discussion:
types: [created]

Expand All @@ -76,15 +87,86 @@ concurrency:
cancel-in-progress: false

jobs:
ideate:
# On the `discussion` trigger, only run for new ideas in the Ideas category
# and skip the bot's own creations (avoids re-enhancing scheduled output;
# enhancement posts a comment, which does not re-fire `discussion: created`).
# Schedule/dispatch runs are unaffected by this guard.
# ── #963 discussion→dispatch redispatch bridge ───────────────────────────────
# claude-code-action aborts on `discussion` event contexts ("Unsupported event
# type: discussion"). So on `discussion: created` we do NOT call the reusable
# inline; the `redispatch` job re-invokes this workflow via workflow_dispatch
# (under which the action runs cleanly), passing the number as target_discussion.
# Mirrors initiative-planner.yml's redispatch (#618). The `ideate` reusable call
# then runs only on workflow_dispatch/schedule.
redispatch:
if: >-
github.event_name != 'discussion' ||
(github.event.discussion.category.slug == 'ideas' &&
github.event.discussion.user.type != 'Bot')
github.event_name == 'discussion' &&
github.event.discussion.category.slug == 'ideas' &&
github.event.discussion.user.type != 'Bot'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Guard — PAT present
# A workflow_dispatch fired with GITHUB_TOKEN is accepted but never starts
# a run (loop prevention), so a PAT is required (same as initiative-planner).
env:
GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }}
run: |
if [ -z "${GH_TOKEN}" ]; then
echo "::error::GH_PAT_WORKFLOWS is required — a workflow_dispatch fired with GITHUB_TOKEN will not start a run."
exit 1
fi
- name: Re-dispatch under workflow_dispatch
env:
GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }}
REPO: ${{ github.repository }}
DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
run: |
gh workflow run feature-ideation.yml --repo "${REPO}" \
-f target_discussion="${DISCUSSION_NUMBER}"

# ── #571 input resolution — keep `inputs` out of the reusable `with:` ────────
# A reusable-workflow call graph (its `uses:` + `with:`) is validated at
# WORKFLOW SETUP, before — and regardless of — the calling job's `if:`. The
# `inputs` context is only populated for workflow_dispatch/workflow_call, so a
# `with:` that references `${{ inputs.* }}` trips a zero-job startup failure on
# the `discussion` event even though `ideate` is gated off it. We therefore
# resolve the dispatch inputs in this ordinary job (whose step expressions are
# evaluated at RUN time and are skipped on `discussion`) and hand them to
# `ideate` via `needs.prep.outputs.*` — an always-valid context that defers the
# `with:` evaluation to run time. Do not reference `inputs` in `ideate.with`.
prep:
if: github.event_name != 'discussion'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
outputs:
target_discussion: ${{ steps.resolve.outputs.target_discussion }}
focus_area: ${{ steps.resolve.outputs.focus_area }}
research_depth: ${{ steps.resolve.outputs.research_depth }}
dry_run: ${{ steps.resolve.outputs.dry_run }}
enhance_backlog: ${{ steps.resolve.outputs.enhance_backlog }}
steps:
- name: Resolve dispatch inputs
id: resolve
# `github.event.inputs.*` (not the `inputs` context) so this reads
# cleanly on schedule too, where it is simply null → the defaults below.
env:
TARGET_DISCUSSION: ${{ github.event.inputs.target_discussion }}
FOCUS_AREA: ${{ github.event.inputs.focus_area }}
RESEARCH_DEPTH: ${{ github.event.inputs.research_depth }}
DRY_RUN: ${{ github.event.inputs.dry_run }}
ENHANCE_BACKLOG: ${{ github.event.inputs.enhance_backlog }}
run: |
{
echo "target_discussion=${TARGET_DISCUSSION:-}"
echo "focus_area=${FOCUS_AREA:-}"
echo "research_depth=${RESEARCH_DEPTH:-standard}"
echo "dry_run=${DRY_RUN:-false}"
echo "enhance_backlog=${ENHANCE_BACKLOG:-false}"
} >> "$GITHUB_OUTPUT"
Comment on lines +151 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Github_output injection risk 🐞 Bug ⛨ Security

In prep, workflow_dispatch-controlled values are appended to $GITHUB_OUTPUT using plain
key=value lines, so a crafted input containing newlines can inject/override additional outputs.
Those outputs are then passed into the privileged reusable-workflow with: (including booleans cast
via fromJSON), enabling unintended parameter tampering (e.g., flipping
dry_run/enhance_backlog).
Agent Prompt
## Issue description
The workflow writes untrusted workflow_dispatch inputs into `$GITHUB_OUTPUT` using `echo "key=${VALUE}"`, which is vulnerable to newline/output injection.

## Issue Context
The `prep` job resolves `github.event.inputs.*` and forwards them to `ideate` via `needs.prep.outputs.*`, which are then used as reusable-workflow inputs.

## Fix Focus Areas
- .github/workflows/feature-ideation.yml[135-165]

## What to change
1. Encode outputs using the multiline-safe `$GITHUB_OUTPUT` delimiter format (preferred), OR strictly validate and reject values containing `\r`/`\n` before writing outputs.
2. Apply the fix to **all** emitted outputs (`target_discussion`, `focus_area`, `research_depth`, `dry_run`, `enhance_backlog`).

## Example approach (delimiter)
In the `Resolve dispatch inputs` step, replace `echo "k=v"` with delimiter blocks, e.g.:
```bash
safe_out() {
  local key="$1"; shift
  local val="$1"
  local delim="EOF_${RANDOM}_${RANDOM}"
  {
    echo "${key}<<${delim}"
    printf '%s\n' "$val"
    echo "${delim}"
  } >> "$GITHUB_OUTPUT"
}

safe_out focus_area "${FOCUS_AREA:-}"
# ... repeat for others
```
(Any equivalent multiline-safe method is fine.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


ideate:
# Runs on workflow_dispatch (including the redispatch bridge above) and schedule.
needs: [prep]
if: github.event_name != 'discussion'
# Permissions cascade from the calling job to the reusable workflow.
# The reusable workflow's two jobs (gather-signals + analyze) need:
# - contents: read (checkout, file reads)
Expand All @@ -100,12 +182,14 @@ jobs:
discussions: write
id-token: write
actions: read
uses: petry-projects/.github/.github/workflows/feature-ideation-reusable.yml@feature-ideation/v1-stable # v1
uses: petry-projects/.github/.github/workflows/feature-ideation-reusable.yml@feature-ideation/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref
with:
# On the `discussion: created` trigger this carries the new Discussion's
# number → the reusable runs single-idea enhancement mode. Empty on
# schedule/dispatch → normal scan. Do not edit this line.
target_discussion: ${{ github.event.discussion.number }}
# All values below come from `needs.prep.outputs.*` (resolved in the `prep`
# job) — NOT the `inputs` context — so this reusable `with:` compiles on the
# `discussion` event without a zero-job startup failure (#571).
# `target_discussion` is forwarded by the `redispatch` bridge above on
# `discussion: created`; empty on schedule/dispatch → normal scan.
target_discussion: ${{ needs.prep.outputs.target_discussion }}
# === CUSTOMISE THIS PER REPO — the only required edit ===
# Replace this paragraph with a 3-5 sentence description of your project,
# its target users, and the competitive landscape. The more specific you
Expand All @@ -128,8 +212,12 @@ jobs:
# workflow defaults to that path, so you only need to uncomment and change
# sources_file below if you store the list somewhere else.
# sources_file: 'docs/feature-ideation-sources.md'
focus_area: ${{ inputs.focus_area || '' }}
research_depth: ${{ inputs.research_depth || 'standard' }}
dry_run: ${{ inputs.dry_run || false }}
focus_area: ${{ needs.prep.outputs.focus_area }}
research_depth: ${{ needs.prep.outputs.research_depth }}
# prep emits the string 'true'/'false'; fromJSON casts it to the boolean
# the reusable's typed inputs require.
dry_run: ${{ fromJSON(needs.prep.outputs.dry_run) }}
# Backlog enhancement sweep of existing un-enhanced Ideas (workflow_dispatch).
enhance_backlog: ${{ fromJSON(needs.prep.outputs.enhance_backlog) }}
secrets:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Loading