Merge branch 'main' into feature/more-cynical-review

This commit is contained in:
Brian 2025-12-15 07:50:26 +08:00 committed by GitHub
commit d5e5796ba3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 111 additions and 62 deletions

View File

@ -2,8 +2,21 @@
# Discord notification helper functions # Discord notification helper functions
# Escape markdown special chars and @mentions for safe Discord display # Escape markdown special chars and @mentions for safe Discord display
# Bracket expression: ] must be first, then other chars. In POSIX bracket expr, \ is literal. # Skips content inside <URL> wrappers to preserve URLs intact
esc() { sed -e 's/[][\*_()~`>]/\\&/g' -e 's/@/@ /g'; } esc() {
awk '{
result = ""; in_url = 0; n = length($0)
for (i = 1; i <= n; i++) {
c = substr($0, i, 1)
if (c == "<" && substr($0, i, 8) ~ /^<https?:/) in_url = 1
if (in_url) { result = result c; if (c == ">") in_url = 0 }
else if (c == "@") result = result "@ "
else if (index("[]\\*_()~`", c) > 0) result = result "\\" c
else result = result c
}
print result
}'
}
# Truncate to $1 chars (or 80 if wall-of-text with <3 spaces) # Truncate to $1 chars (or 80 if wall-of-text with <3 spaces)
trunc() { trunc() {
@ -13,3 +26,9 @@ trunc() {
[ "$spaces" -lt 3 ] && [ ${#txt} -gt 80 ] && txt=$(printf '%s' "$txt" | cut -c1-80) [ "$spaces" -lt 3 ] && [ ${#txt} -gt 80 ] && txt=$(printf '%s' "$txt" | cut -c1-80)
printf '%s' "$txt" printf '%s' "$txt"
} }
# Remove incomplete URL at end of truncated text (incomplete URLs are useless)
strip_trailing_url() { sed -E 's~<?https?://[^[:space:]]*$~~'; }
# Wrap URLs in <> to suppress Discord embeds (keeps links clickable)
wrap_urls() { sed -E 's~https?://[^[:space:]<>]+~<&>~g'; }

View File

@ -53,7 +53,11 @@ jobs:
TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc) TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc)
[ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
BODY=$(printf '%s' "$PR_BODY" | trunc $MAX_BODY | esc) BODY=$(printf '%s' "$PR_BODY" | trunc $MAX_BODY)
if [ -n "$PR_BODY" ] && [ ${#PR_BODY} -gt $MAX_BODY ]; then
BODY=$(printf '%s' "$BODY" | strip_trailing_url)
fi
BODY=$(printf '%s' "$BODY" | wrap_urls | esc)
[ -n "$PR_BODY" ] && [ ${#PR_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$PR_BODY" ] && [ ${#PR_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
[ -n "$BODY" ] && BODY=" · $BODY" [ -n "$BODY" ] && BODY=" · $BODY"
USER=$(printf '%s' "$PR_USER" | esc) USER=$(printf '%s' "$PR_USER" | esc)
@ -91,7 +95,11 @@ jobs:
TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc) TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc)
[ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." [ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
BODY=$(printf '%s' "$ISSUE_BODY" | trunc $MAX_BODY | esc) BODY=$(printf '%s' "$ISSUE_BODY" | trunc $MAX_BODY)
if [ -n "$ISSUE_BODY" ] && [ ${#ISSUE_BODY} -gt $MAX_BODY ]; then
BODY=$(printf '%s' "$BODY" | strip_trailing_url)
fi
BODY=$(printf '%s' "$BODY" | wrap_urls | esc)
[ -n "$ISSUE_BODY" ] && [ ${#ISSUE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$ISSUE_BODY" ] && [ ${#ISSUE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
[ -n "$BODY" ] && BODY=" · $BODY" [ -n "$BODY" ] && BODY=" · $BODY"
USER=$(printf '%s' "$USER" | esc) USER=$(printf '%s' "$USER" | esc)
@ -126,7 +134,11 @@ jobs:
TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc) TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc)
[ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." [ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY | esc) BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY)
if [ ${#COMMENT_BODY} -gt $MAX_BODY ]; then
BODY=$(printf '%s' "$BODY" | strip_trailing_url)
fi
BODY=$(printf '%s' "$BODY" | wrap_urls | esc)
[ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
USER=$(printf '%s' "$COMMENT_USER" | esc) USER=$(printf '%s' "$COMMENT_USER" | esc)
@ -162,7 +174,11 @@ jobs:
TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc) TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc)
[ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
BODY=$(printf '%s' "$REVIEW_BODY" | trunc $MAX_BODY | esc) BODY=$(printf '%s' "$REVIEW_BODY" | trunc $MAX_BODY)
if [ -n "$REVIEW_BODY" ] && [ ${#REVIEW_BODY} -gt $MAX_BODY ]; then
BODY=$(printf '%s' "$BODY" | strip_trailing_url)
fi
BODY=$(printf '%s' "$BODY" | wrap_urls | esc)
[ -n "$REVIEW_BODY" ] && [ ${#REVIEW_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$REVIEW_BODY" ] && [ ${#REVIEW_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
[ -n "$BODY" ] && BODY=": $BODY" [ -n "$BODY" ] && BODY=": $BODY"
USER=$(printf '%s' "$REVIEW_USER" | esc) USER=$(printf '%s' "$REVIEW_USER" | esc)
@ -194,7 +210,11 @@ jobs:
TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc) TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc)
[ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY | esc) BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY)
if [ ${#COMMENT_BODY} -gt $MAX_BODY ]; then
BODY=$(printf '%s' "$BODY" | strip_trailing_url)
fi
BODY=$(printf '%s' "$BODY" | wrap_urls | esc)
[ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
USER=$(printf '%s' "$COMMENT_USER" | esc) USER=$(printf '%s' "$COMMENT_USER" | esc)
@ -224,7 +244,11 @@ jobs:
REL_NAME=$(printf '%s' "$NAME" | trunc $MAX_TITLE | esc) REL_NAME=$(printf '%s' "$NAME" | trunc $MAX_TITLE | esc)
[ ${#NAME} -gt $MAX_TITLE ] && REL_NAME="${REL_NAME}..." [ ${#NAME} -gt $MAX_TITLE ] && REL_NAME="${REL_NAME}..."
BODY=$(printf '%s' "$RELEASE_BODY" | trunc $MAX_BODY | esc) BODY=$(printf '%s' "$RELEASE_BODY" | trunc $MAX_BODY)
if [ -n "$RELEASE_BODY" ] && [ ${#RELEASE_BODY} -gt $MAX_BODY ]; then
BODY=$(printf '%s' "$BODY" | strip_trailing_url)
fi
BODY=$(printf '%s' "$BODY" | wrap_urls | esc)
[ -n "$RELEASE_BODY" ] && [ ${#RELEASE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$RELEASE_BODY" ] && [ ${#RELEASE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
[ -n "$BODY" ] && BODY=" · $BODY" [ -n "$BODY" ] && BODY=" · $BODY"
TAG_ESC=$(printf '%s' "$TAG" | esc) TAG_ESC=$(printf '%s' "$TAG" | esc)
@ -275,7 +299,7 @@ jobs:
run: | run: |
set -o pipefail set -o pipefail
[ -z "$WEBHOOK" ] && exit 0 [ -z "$WEBHOOK" ] && exit 0
esc() { sed -e 's/[][\*_()~`>]/\\&/g' -e 's/@/@ /g'; } esc() { sed -e 's/[][\*_()~`]/\\&/g' -e 's/@/@ /g'; }
trunc() { tr '\n\r' ' ' | cut -c1-"$1"; } trunc() { tr '\n\r' ' ' | cut -c1-"$1"; }
REF_TRUNC=$(printf '%s' "$REF" | trunc 100) REF_TRUNC=$(printf '%s' "$REF" | trunc 100)

View File

@ -6,9 +6,11 @@ on:
version_bump: version_bump:
description: Version bump type description: Version bump type
required: true required: true
default: patch default: alpha
type: choice type: choice
options: options:
- alpha
- beta
- patch - patch
- minor - minor
- major - major
@ -49,7 +51,11 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump version - name: Bump version
run: npm run version:${{ github.event.inputs.version_bump }} run: |
case "${{ github.event.inputs.version_bump }}" in
alpha|beta) npm version prerelease --no-git-tag-version --preid=${{ github.event.inputs.version_bump }} ;;
*) npm version ${{ github.event.inputs.version_bump }} --no-git-tag-version ;;
esac
- name: Get new version and previous tag - name: Get new version and previous tag
id: version id: version
@ -61,34 +67,9 @@ jobs:
run: | run: |
sed -i 's/"version": ".*"/"version": "${{ steps.version.outputs.new_version }}"/' tools/installer/package.json sed -i 's/"version": ".*"/"version": "${{ steps.version.outputs.new_version }}"/' tools/installer/package.json
- name: Generate web bundles # TODO: Re-enable web bundles once tools/cli/bundlers/ is restored
run: npm run bundle # - name: Generate web bundles
# run: npm run bundle
- name: Package bundles for release
run: |
mkdir -p dist/release-bundles
# Copy web bundles
cp -r web-bundles dist/release-bundles/bmad-bundles-v${{ steps.version.outputs.new_version }}
# Verify bundles exist
if [ ! "$(ls -A dist/release-bundles/bmad-bundles-v${{ steps.version.outputs.new_version }})" ]; then
echo "❌ ERROR: No bundles found"
echo "This likely means 'npm run bundle' failed"
exit 1
fi
# Count and display bundles per module
for module in bmm bmb cis bmgd; do
if [ -d "dist/release-bundles/bmad-bundles-v${{ steps.version.outputs.new_version }}/$module/agents" ]; then
COUNT=$(find dist/release-bundles/bmad-bundles-v${{ steps.version.outputs.new_version }}/$module/agents -name '*.xml' 2>/dev/null | wc -l)
echo "✅ $module: $COUNT agents"
fi
done
# Create archive
tar -czf dist/release-bundles/bmad-bundles-v${{ steps.version.outputs.new_version }}.tar.gz \
-C dist/release-bundles/bmad-bundles-v${{ steps.version.outputs.new_version }} .
- name: Commit version bump - name: Commit version bump
run: | run: |
@ -185,25 +166,15 @@ jobs:
npm publish --tag latest npm publish --tag latest
fi fi
- name: Create GitHub Release with Bundles - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{ steps.version.outputs.new_version }} tag_name: v${{ steps.version.outputs.new_version }}
name: "BMad Method v${{ steps.version.outputs.new_version }}" name: "BMad Method v${{ steps.version.outputs.new_version }}"
body: | body: |
${{ steps.release_notes.outputs.RELEASE_NOTES }} ${{ steps.release_notes.outputs.RELEASE_NOTES }}
## 📦 Web Bundles
Download XML bundles for use in AI platforms (Claude Projects, ChatGPT, Gemini):
- `bmad-bundles-v${{ steps.version.outputs.new_version }}.tar.gz` - All modules (BMM, BMB, CIS, BMGD)
**Browse online** (bleeding edge): https://bmad-code-org.github.io/bmad-bundles/
draft: false draft: false
prerelease: ${{ contains(steps.version.outputs.new_version, 'alpha') || contains(steps.version.outputs.new_version, 'beta') }} prerelease: ${{ contains(steps.version.outputs.new_version, 'alpha') || contains(steps.version.outputs.new_version, 'beta') }}
files: |
dist/release-bundles/*.tar.gz
- name: Summary - name: Summary
run: | run: |
@ -212,7 +183,6 @@ jobs:
echo "### 📦 Distribution" >> $GITHUB_STEP_SUMMARY echo "### 📦 Distribution" >> $GITHUB_STEP_SUMMARY
echo "- **NPM**: Published with @latest tag" >> $GITHUB_STEP_SUMMARY echo "- **NPM**: Published with @latest tag" >> $GITHUB_STEP_SUMMARY
echo "- **GitHub Release**: https://github.com/bmad-code-org/BMAD-METHOD/releases/tag/v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY echo "- **GitHub Release**: https://github.com/bmad-code-org/BMAD-METHOD/releases/tag/v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Web Bundles**: Attached to GitHub Release (4 archives)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "### ✅ Installation" >> $GITHUB_STEP_SUMMARY echo "### ✅ Installation" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY

View File

@ -40,15 +40,50 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat
- Epics: keys starting with "epic-" (and not ending with "-retrospective") - Epics: keys starting with "epic-" (and not ending with "-retrospective")
- Retrospectives: keys ending with "-retrospective" - Retrospectives: keys ending with "-retrospective"
- Stories: everything else (e.g., 1-2-login-form) - Stories: everything else (e.g., 1-2-login-form)
<action>If any story has status `drafted`, treat as `ready-for-dev` (legacy status)</action> <action>Map legacy story status "drafted" → "ready-for-dev"</action>
<action>Count story statuses: backlog, ready-for-dev, in-progress, review, done</action> <action>Count story statuses: backlog, ready-for-dev, in-progress, review, done</action>
<action>Count epic statuses: backlog, contexted</action> <action>Map legacy epic status "contexted" → "in-progress"</action>
<action>Detect risks:</action> <action>Count epic statuses: backlog, in-progress, done</action>
- Stories in review but no reviewer assigned context → suggest `/bmad:bmm:workflows:code-review` <action>Count retrospective statuses: optional, completed</action>
- Stories in in-progress with no ready-for-dev items behind them → keep focus on the active story
- All epics backlog/contexted but no stories ready-for-dev → prompt to run `/bmad:bmm:workflows:create-story` <action>Validate all statuses against known values:</action>
- Stories in ready-for-dev may be unvalidated → suggest `/bmad:bmm:workflows:validate-create-story` before `dev-story` for quality check
</step> - Valid story statuses: backlog, ready-for-dev, in-progress, review, done, drafted (legacy)
- Valid epic statuses: backlog, in-progress, done, contexted (legacy)
- Valid retrospective statuses: optional, completed
<check if="any status is unrecognized">
<output>
⚠️ **Unknown status detected:**
{{#each invalid_entries}}
- `{{key}}`: "{{status}}" (not recognized)
{{/each}}
**Valid statuses:**
- Stories: backlog, ready-for-dev, in-progress, review, done
- Epics: backlog, in-progress, done
- Retrospectives: optional, completed
</output>
<ask>How should these be corrected?
{{#each invalid_entries}}
{{@index}}. {{key}}: "{{status}}" → [select valid status]
{{/each}}
Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue without fixing:</ask>
<check if="user provided corrections">
<action>Update sprint-status.yaml with corrected values</action>
<action>Re-parse the file with corrected statuses</action>
</check>
</check>
<action>Detect risks:</action>
- IF any story has status "review": suggest `/bmad:bmm:workflows:code-review`
- IF any story has status "in-progress" AND no stories have status "ready-for-dev": recommend staying focused on active story
- IF all epics have status "backlog" AND no stories have status "ready-for-dev": prompt `/bmad:bmm:workflows:create-story`
</step>
<step n="3" goal="Select next action recommendation"> <step n="3" goal="Select next action recommendation">
<action>Pick the next recommended workflow using priority:</action> <action>Pick the next recommended workflow using priority:</action>
@ -71,7 +106,7 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat
**Stories:** backlog {{count_backlog}}, ready-for-dev {{count_ready}}, in-progress {{count_in_progress}}, review {{count_review}}, done {{count_done}} **Stories:** backlog {{count_backlog}}, ready-for-dev {{count_ready}}, in-progress {{count_in_progress}}, review {{count_review}}, done {{count_done}}
**Epics:** backlog {{epic_backlog}}, contexted {{epic_contexted}} **Epics:** backlog {{epic_backlog}}, in-progress {{epic_in_progress}}, done {{epic_done}}
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}}) **Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
@ -141,8 +176,9 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
<template-output>count_review = {{count_review}}</template-output> <template-output>count_review = {{count_review}}</template-output>
<template-output>count_done = {{count_done}}</template-output> <template-output>count_done = {{count_done}}</template-output>
<template-output>epic_backlog = {{epic_backlog}}</template-output> <template-output>epic_backlog = {{epic_backlog}}</template-output>
<template-output>epic_contexted = {{epic_contexted}}</template-output> <template-output>epic_in_progress = {{epic_in_progress}}</template-output>
<template-output>warnings = {{risks}}</template-output> <template-output>epic_done = {{epic_done}}</template-output>
<template-output>risks = {{risks}}</template-output>
<action>Return to caller</action> <action>Return to caller</action>
</step> </step>