Skip to content

Create Spec: Pseudo-function style for workflow names #2993

Create Spec: Pseudo-function style for workflow names

Create Spec: Pseudo-function style for workflow names #2993

Workflow file for this run

name: "Spec: Create"
run-name: "Create Spec: ${{ github.event.issue.title }}"
# Creates new specification with approval gate
# Flow:
# 1. User adds `spec-request` label → creates branch + PR
# 2. Maintainer adds `approved` label → merges PR to main
#
# Triggers:
# - spec-request label added → create specification + PR
# - approved label added (on spec-request issue) → merge PR
on:
issues:
types: [labeled]
concurrency:
group: spec-create-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
# ============================================================================
# Job 1: Create specification (triggered by spec-request label)
# ============================================================================
create:
if: github.event.label.name == 'spec-request'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
outputs:
specification_id: ${{ steps.process.outputs.specification_id }}
branch: ${{ steps.process.outputs.branch }}
pr_number: ${{ steps.pr.outputs.pr_number }}
steps:
- name: Check if already processed
id: check
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
# Skip if already has specification-id in title
if [[ "$ISSUE_TITLE" =~ ^\[[a-z0-9-]+\] ]]; then
echo "::notice::Skipping: Issue already has specification ID in title"
echo "should_run=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "should_run=true" >> $GITHUB_OUTPUT
- name: Checkout repository
if: steps.check.outputs.should_run == 'true'
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: React with eyes emoji
if: steps.check.outputs.should_run == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions \
-f content=eyes
- name: Process with Claude
if: steps.check.outputs.should_run == 'true'
id: process
continue-on-error: true
timeout-minutes: 30
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model opus"
prompt: |
## Task: Create New Specification
You are creating a new plot specification.
### Issue Details
- **Title:** ${{ github.event.issue.title }}
- **Number:** #${{ github.event.issue.number }}
- **Author:** ${{ github.event.issue.user.login }}
- **Body:**
```
${{ github.event.issue.body }}
```
---
## Instructions
1. **Read the rules:** `prompts/spec-id-generator.md`
2. **Check for duplicates:**
- List all existing specs: `ls plots/`
- Read existing specification files if titles seem similar
- If duplicate found: Post comment explaining which spec matches, then STOP
3. **Generate specification-id:**
- Format: `{type}-{variant}` or `{type}-{variant}-{modifier}`
- Examples: `scatter-basic`, `bar-grouped-horizontal`, `heatmap-correlation`
- All lowercase, hyphens only
4. **Create specification branch:**
```bash
git checkout -b "specification/{specification-id}"
```
5. **Post analysis comment:**
Post a SHORT comment (max 3-4 sentences) to the issue using `gh issue comment`:
- Is this a valid/useful plot type?
- Does it already exist? (check `ls plots/`)
- Any concerns?
6. **Analyze reference materials (if present in issue):**
- If the issue contains **images**: Analyze them to understand the desired visualization style, layout, and features. Incorporate visual requirements into the spec.
- If the issue contains **external links** (GitHub repos, documentation, examples):
- Use WebFetch to read the linked page
- Understand what the referenced tool/library does
- Extract relevant features and patterns for the specification
- Document insights from images/links in the spec's Description or Notes section
7. **Create specification files:**
- Read template: `prompts/templates/specification.md`
- Read metadata template: `prompts/templates/specification.yaml`
- Read tagging guide: `prompts/spec-tags-generator.md`
- Create directory: `plots/{specification-id}/`
- Create: `plots/{specification-id}/specification.md` (follow template structure)
- Create: `plots/{specification-id}/specification.yaml` with:
- `specification_id`: the generated id
- `title`: a proper title
- `created`: Use `$(date -u +"%Y-%m-%dT%H:%M:%SZ")` for current timestamp
- `issue`: ${{ github.event.issue.number }}
- `suggested`: ${{ github.event.issue.user.login }}
- `tags`: Follow tagging-system.md guide (4 dimensions: plot_type, data_type, domain, features)
- Create empty folder: `plots/{specification-id}/implementations/`
- Create empty folder: `plots/{specification-id}/metadata/`
8. **Commit and push:**
```bash
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add plots/{specification-id}/
git commit -m "spec: add {specification-id} specification
Created from issue #${{ github.event.issue.number }}"
git push -u origin "specification/{specification-id}"
```
9. **Update issue title:**
```bash
gh issue edit ${{ github.event.issue.number }} --title "[{specification-id}] {original title}"
```
10. **Output for workflow:**
After completing, print these lines exactly:
```
SPECIFICATION_ID={specification-id}
BRANCH=specification/{specification-id}
```
---
## Important Rules
- Do NOT create a PR (the workflow does that)
- Do NOT add labels
- Do NOT close the issue
- STOP after pushing the branch
- name: Retry Claude (on failure)
if: steps.check.outputs.should_run == 'true' && steps.process.outcome == 'failure'
id: process_retry
timeout-minutes: 30
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model opus"
prompt: |
## Task: Create New Specification
You are creating a new plot specification.
### Issue Details
- **Title:** ${{ github.event.issue.title }}
- **Number:** #${{ github.event.issue.number }}
- **Author:** ${{ github.event.issue.user.login }}
- **Body:**
```
${{ github.event.issue.body }}
```
---
## Instructions
1. **Read the rules:** `prompts/spec-id-generator.md`
2. **Check for duplicates:**
- List all existing specs: `ls plots/`
- Read existing specification files if titles seem similar
- If duplicate found: Post comment explaining which spec matches, then STOP
3. **Generate specification-id:**
- Format: `{type}-{variant}` or `{type}-{variant}-{modifier}`
- Examples: `scatter-basic`, `bar-grouped-horizontal`, `heatmap-correlation`
- All lowercase, hyphens only
4. **Create specification branch:**
```bash
git checkout -b "specification/{specification-id}"
```
5. **Post analysis comment:**
Post a SHORT comment (max 3-4 sentences) to the issue using `gh issue comment`:
- Is this a valid/useful plot type?
- Does it already exist? (check `ls plots/`)
- Any concerns?
6. **Analyze reference materials (if present in issue):**
- If the issue contains **images**: Analyze them to understand the desired visualization style, layout, and features. Incorporate visual requirements into the spec.
- If the issue contains **external links** (GitHub repos, documentation, examples):
- Use WebFetch to read the linked page
- Understand what the referenced tool/library does
- Extract relevant features and patterns for the specification
- Document insights from images/links in the spec's Description or Notes section
7. **Create specification files:**
- Read template: `prompts/templates/specification.md`
- Read metadata template: `prompts/templates/specification.yaml`
- Read tagging guide: `prompts/spec-tags-generator.md`
- Create directory: `plots/{specification-id}/`
- Create: `plots/{specification-id}/specification.md` (follow template structure)
- Create: `plots/{specification-id}/specification.yaml` with:
- `specification_id`: the generated id
- `title`: a proper title
- `created`: Use `$(date -u +"%Y-%m-%dT%H:%M:%SZ")` for current timestamp
- `issue`: ${{ github.event.issue.number }}
- `suggested`: ${{ github.event.issue.user.login }}
- `tags`: Follow tagging-system.md guide (4 dimensions: plot_type, data_type, domain, features)
- Create empty folder: `plots/{specification-id}/implementations/`
- Create empty folder: `plots/{specification-id}/metadata/`
8. **Commit and push:**
```bash
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add plots/{specification-id}/
git commit -m "spec: add {specification-id} specification
Created from issue #${{ github.event.issue.number }}"
git push -u origin "specification/{specification-id}"
```
9. **Update issue title:**
```bash
gh issue edit ${{ github.event.issue.number }} --title "[{specification-id}] {original title}"
```
10. **Output for workflow:**
After completing, print these lines exactly:
```
SPECIFICATION_ID={specification-id}
BRANCH=specification/{specification-id}
```
---
## Important Rules
- Do NOT create a PR (the workflow does that)
- Do NOT add labels
- Do NOT close the issue
- STOP after pushing the branch
- name: Extract outputs
if: steps.check.outputs.should_run == 'true'
id: extract
run: |
# Extract specification ID from issue title (now updated by Claude)
TITLE=$(gh issue view ${{ github.event.issue.number }} --json title -q .title)
SPEC_ID=$(echo "$TITLE" | sed -n 's/^\[\([a-z0-9-]*\)\].*/\1/p')
if [[ -z "$SPEC_ID" ]]; then
echo "::error::Could not extract specification ID from title: $TITLE"
exit 1
fi
# Reserved spec slugs collide with top-level frontend routes.
# Keep this list in sync with `RESERVED_TOP_LEVEL` in app/src/utils/paths.ts.
RESERVED_SLUGS=(plots specs libraries palette about legal mcp stats debug api og sitemap.xml robots.txt)
for reserved in "${RESERVED_SLUGS[@]}"; do
if [[ "$SPEC_ID" == "$reserved" ]]; then
echo "::error::Spec ID '$SPEC_ID' collides with reserved top-level route. Choose a different slug."
gh issue comment ${{ github.event.issue.number }} --body "**Error:** Generated spec ID \`${SPEC_ID}\` collides with a reserved top-level route. Please rename the issue with a more specific title and re-trigger."
exit 1
fi
done
echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT
echo "branch=specification/$SPEC_ID" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Validate specification was created (not duplicate)
if: steps.check.outputs.should_run == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.extract.outputs.specification_id }}
ISSUE_NUM: ${{ github.event.issue.number }}
run: |
# Check if specification directory was actually created
if [ ! -d "plots/${SPEC_ID}" ]; then
echo "::warning::Specification directory not created - likely a duplicate"
# Get Claude's comment to find duplicate info
COMMENT=$(gh issue view "$ISSUE_NUM" --json comments -q '.comments[-1].body' 2>/dev/null || echo "")
if echo "$COMMENT" | grep -qi "duplicate\|exists\|already"; then
echo "::notice::Claude detected duplicate - closing issue"
gh label create "duplicate" --color "cfd3d7" \
--description "Duplicate specification" 2>/dev/null || true
gh issue edit "$ISSUE_NUM" --remove-label "spec-request" 2>/dev/null || true
gh issue edit "$ISSUE_NUM" --add-label "duplicate" 2>/dev/null || true
gh issue close "$ISSUE_NUM" --comment "**Duplicate detected:** Claude's analysis indicates this specification already exists. See previous comment for details."
exit 0 # Not an error, just a duplicate
fi
# If not a duplicate, something else failed
echo "::error::Specification creation failed for unknown reason"
gh issue comment "$ISSUE_NUM" --body "**Error:** Specification creation failed. Please check the [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details."
exit 1
fi
echo "::notice::Specification directory created successfully"
- name: Create Pull Request
if: steps.check.outputs.should_run == 'true'
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.extract.outputs.specification_id }}
run: |
# Read the specification content for PR body
SPEC_CONTENT=$(cat "plots/${SPEC_ID}/specification.md")
PR_URL=$(gh pr create \
--base main \
--head "specification/${SPEC_ID}" \
--title "spec: add ${SPEC_ID} specification" \
--body "$(cat <<EOF
## New Specification: \`${SPEC_ID}\`
Related to #${{ github.event.issue.number }}
---
### specification.md
${SPEC_CONTENT}
---
**Next:** Add \`approved\` label to the issue to merge this PR.
---
:robot: *[spec-create workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*
EOF
)")
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
- name: Comment on issue
if: steps.check.outputs.should_run == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.extract.outputs.specification_id }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: |
SPEC_CONTENT=$(cat "plots/${SPEC_ID}/specification.md")
gh issue comment ${{ github.event.issue.number }} --body "$(cat <<EOF
## :white_check_mark: Specification Ready: \`${SPEC_ID}\`
**PR:** #${PR_NUMBER}
---
### specification.md
${SPEC_CONTENT}
---
**Next:** Add \`approved\` label to merge this specification to main.
---
:robot: *[spec-create workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*
EOF
)"
- name: Add rocket reaction on success
if: steps.check.outputs.should_run == 'true' && success()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions \
-f content=rocket
# ============================================================================
# Job 2: Merge specification (triggered by approved label)
# ============================================================================
merge:
if: >
github.event.label.name == 'approved' &&
contains(github.event.issue.labels.*.name, 'spec-request')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
# Prevent race conditions when multiple specs are approved simultaneously
concurrency:
group: spec-merge-main
cancel-in-progress: false # Queue merges instead of canceling
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Extract specification ID
id: extract
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
SPEC_ID=$(echo "$ISSUE_TITLE" | sed -n 's/^\[\([a-z0-9-]*\)\].*/\1/p')
if [[ -z "$SPEC_ID" ]]; then
echo "::error::Could not extract specification ID from title: $ISSUE_TITLE"
exit 1
fi
echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT
- name: Find and merge PR
id: merge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.extract.outputs.specification_id }}
run: |
# Find PR for this specification
PR_NUMBER=$(gh pr list --head "specification/${SPEC_ID}" --json number -q '.[0].number')
if [[ -z "$PR_NUMBER" ]]; then
echo "::error::No PR found for branch specification/${SPEC_ID}"
exit 1
fi
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
# Merge the PR
gh pr merge "$PR_NUMBER" --squash --delete-branch
echo "::notice::Merged PR #$PR_NUMBER for specification ${SPEC_ID}"
- name: Update issue labels
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Remove trigger, add status
gh issue edit ${{ github.event.issue.number }} --remove-label "spec-request" 2>/dev/null || true
gh issue edit ${{ github.event.issue.number }} --add-label "spec-ready"
- name: Comment on issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.extract.outputs.specification_id }}
PR_NUMBER: ${{ steps.merge.outputs.pr_number }}
run: |
gh issue comment ${{ github.event.issue.number }} --body "$(cat <<EOF
## :rocket: Specification Merged!
**Specification:** \`${SPEC_ID}\`
**PR:** #${PR_NUMBER} (merged)
The specification is now in \`main\` and synced to PostgreSQL.
---
**Next:** Add \`generate:{library}\` labels to start generating implementations.
Available libraries: \`matplotlib\`, \`seaborn\`, \`plotly\`, \`bokeh\`, \`altair\`, \`plotnine\`, \`pygal\`, \`highcharts\`, \`letsplot\`
Or use workflow dispatch for bulk operations.
---
:robot: *[spec-create workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*
EOF
)"