Skip to content

Commit fd1ab0f

Browse files
committed
feat(suggestions): permission gate + full loop for external contributors
pm-suggestions.yml: - Write access → PM processes immediately - No write access → tag @deepgram-devrel with needs:approval label; agent does nothing until devrel adds 'approved' label (re-triggers workflow) - Bots silently skipped - schedule/workflow_dispatch always pass (no user context) engineer.md: - PRs now include 'Closes #N' when queue issue originated from a public suggestion — GitHub closes the original issue on PR merge, notifying the contributor their request has been released setup-labels.yml: - Add needs:approval, approved, needs:clarification labels Full contributor loop: Public opens issue → devrel adds 'approved' → PM queues it → Researcher + Engineer build it → PR merges → original issue closed with notification to contributor
1 parent f8ecc9e commit fd1ab0f

4 files changed

Lines changed: 119 additions & 3 deletions

File tree

.github/workflows/pm-suggestions.yml

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
types:
66
- opened # Every new issue, regardless of labels or format
77
- reopened
8+
- labeled # Re-process when devrel adds 'approved' to external issues
89
schedule:
910
- cron: '47 * * * *' # Hourly sweep to catch anything missed
1011
workflow_dispatch:
@@ -15,7 +16,6 @@ concurrency:
1516

1617
jobs:
1718
run:
18-
# Always run — PM reads the issue and decides what to do with it
1919
runs-on: ubuntu-latest
2020
permissions:
2121
contents: write
@@ -32,7 +32,90 @@ jobs:
3232
git config user.name "examples-bot"
3333
git config user.email "noreply@deepgram.com"
3434
35+
# ── Permission gate ──────────────────────────────────────────────────
36+
# Write access = act immediately.
37+
# No write access = hold for devrel approval; don't run PM agent.
38+
# Schedule/dispatch runs bypass the check (no user context).
39+
- name: Check write permission
40+
id: permission
41+
env:
42+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
run: |
44+
EVENT="${{ github.event_name }}"
45+
USERNAME="${{ github.event.issue.user.login }}"
46+
47+
# Schedule and dispatch runs always proceed
48+
if [[ "$EVENT" == "schedule" || "$EVENT" == "workflow_dispatch" ]]; then
49+
echo "gate=pass" >> $GITHUB_OUTPUT
50+
echo "reason=scheduled" >> $GITHUB_OUTPUT
51+
exit 0
52+
fi
53+
54+
# If the 'approved' label was just added, treat as approved regardless of opener
55+
LABEL="${{ github.event.label.name }}"
56+
if [[ "$EVENT" == "issues" && "$LABEL" == "approved" ]]; then
57+
echo "gate=pass" >> $GITHUB_OUTPUT
58+
echo "reason=devrel-approved" >> $GITHUB_OUTPUT
59+
exit 0
60+
fi
61+
62+
# Check if the issue opener already has the 'approved' label (re-opened etc.)
63+
HAS_APPROVED=$(gh issue view ${{ github.event.issue.number }} \
64+
--repo ${{ github.repository }} \
65+
--json labels --jq '[.labels[].name] | contains(["approved"])' 2>/dev/null || echo "false")
66+
if [[ "$HAS_APPROVED" == "true" ]]; then
67+
echo "gate=pass" >> $GITHUB_OUTPUT
68+
echo "reason=already-approved" >> $GITHUB_OUTPUT
69+
exit 0
70+
fi
71+
72+
# Skip bots
73+
if [[ "$USERNAME" == *"[bot]"* ]]; then
74+
echo "gate=skip" >> $GITHUB_OUTPUT
75+
echo "reason=bot" >> $GITHUB_OUTPUT
76+
exit 0
77+
fi
78+
79+
# Check write permission on this repo
80+
PERM=$(gh api "repos/${{ github.repository }}/collaborators/${USERNAME}/permission" \
81+
--jq '.permission' 2>/dev/null || echo "none")
82+
echo "Permission for $USERNAME: $PERM"
83+
84+
if [[ "$PERM" == "write" || "$PERM" == "maintain" || "$PERM" == "admin" ]]; then
85+
echo "gate=pass" >> $GITHUB_OUTPUT
86+
echo "reason=has-write-access" >> $GITHUB_OUTPUT
87+
else
88+
echo "gate=hold" >> $GITHUB_OUTPUT
89+
echo "reason=no-write-access" >> $GITHUB_OUTPUT
90+
echo "username=$USERNAME" >> $GITHUB_OUTPUT
91+
fi
92+
93+
# ── Hold: tag devrel for external suggestions ──────────────────────
94+
- name: Hold for devrel approval
95+
if: steps.permission.outputs.gate == 'hold'
96+
env:
97+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
98+
run: |
99+
USERNAME="${{ steps.permission.outputs.username }}"
100+
ISSUE="${{ github.event.issue.number }}"
101+
102+
# Only post once — skip if already held
103+
ALREADY=$(gh issue view "$ISSUE" --repo ${{ github.repository }} \
104+
--json labels --jq '[.labels[].name] | contains(["needs:approval"])' 2>/dev/null || echo "false")
105+
if [[ "$ALREADY" == "true" ]]; then
106+
echo "Already on hold — skipping duplicate comment"
107+
exit 0
108+
fi
109+
110+
gh issue edit "$ISSUE" --repo ${{ github.repository }} \
111+
--add-label "needs:approval" 2>/dev/null || true
112+
113+
BODY="@deepgram-devrel — new suggestion from @${USERNAME} (no write access). Add the \`approved\` label to process it, or close to decline."
114+
gh issue comment "$ISSUE" --repo ${{ github.repository }} --body "$BODY"
115+
116+
# ── Pass: run PM agent ────────────────────────────────────────────
35117
- name: Route issue
118+
if: steps.permission.outputs.gate == 'pass'
36119
uses: anthropics/claude-code-action@beta
37120
with:
38121
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -48,6 +131,7 @@ jobs:
48131
- Event: ${{ github.event_name }}
49132
- Issue number (if triggered by issue): ${{ github.event.issue.number }}
50133
- Repository: ${{ github.repository }}
134+
- Trigger reason: ${{ steps.permission.outputs.reason }}
51135
env:
52136
KAPA_API_KEY: ${{ secrets.KAPA_API_KEY }}
53137
KAPA_PROJECT_ID: ${{ vars.KAPA_PROJECT_ID }}

.github/workflows/setup-labels.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ jobs:
3838
create_label "queue:new-example" "bfd4f2" "Queue: build a new example"
3939
create_label "queue:fix-example" "bfd4f2" "Queue: fix a broken example"
4040
41+
# External suggestion approval labels
42+
create_label "needs:approval" "fbca04" "Waiting for devrel to approve external suggestion"
43+
create_label "approved" "0e8a16" "Approved by devrel — PM agent will process"
44+
create_label "needs:clarification" "e4e669" "PM needs more info before acting"
45+
4146
# Product labels
4247
create_label "product:stt" "5319e7" "Speech-to-text"
4348
create_label "product:tts" "5319e7" "Text-to-speech"

instructions/engineer.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ git add "examples/${NEXT}-${SLUG}/"
239239
git commit -m "feat(examples): add ${NEXT} — {description}"
240240
git push origin "$BRANCH"
241241

242+
# Check if the queue issue has an origin issue to close when this PR merges.
243+
# The PM sets "Requested in #{N}" in the queue issue body for external suggestions.
244+
ORIGIN_ISSUE=""
245+
QUEUE_BODY=$(gh issue view {issue_number} --json body --jq '.body' 2>/dev/null || echo "")
246+
ORIGIN_NUM=$(echo "$QUEUE_BODY" | grep -oE 'Requested in #([0-9]+)' | grep -oE '[0-9]+' | head -1)
247+
if [ -n "$ORIGIN_NUM" ]; then
248+
ORIGIN_ISSUE="Closes #${ORIGIN_NUM}"
249+
fi
250+
242251
PR_URL=$(gh pr create \
243252
--title "[Example] ${NEXT} — {Title}" \
244253
--label "type:example,language:{lang},integration:{slug}" \
@@ -263,6 +272,8 @@ integrations: {integration-slug}
263272
### Required secrets
264273
{vars beyond DEEPGRAM_API_KEY, or "None — only DEEPGRAM_API_KEY required"}
265274
275+
${ORIGIN_ISSUE}
276+
266277
---
267278
*Built by Engineer on {date}*
268279
EOF

instructions/pm-suggestions.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,27 @@ Requested in #{number}: {brief quote from original issue}
136136
{Any links from the original issue, or blank}
137137
138138
---
139-
*Queued by PM from #${number} on {date}*
139+
*Queued by PM from #{number} on {date}*
140140
EOF
141141
)"
142142

143-
gh issue close {number} --comment "Queued as a new example. Tracking in #{new_issue_number}."
143+
# Get the new queue issue number
144+
NEW_ISSUE=$(gh issue list --repo ${{ github.repository }} \
145+
--label "queue:new-example" --state open --limit 1 \
146+
--json number --jq '.[0].number' 2>/dev/null)
147+
148+
# Close the original suggestion with a friendly message
149+
# Note: use "closes #{PR_NUMBER}" in the PR body later so GitHub auto-closes
150+
# the original issue when the example PR merges — completing the full loop
151+
gh issue comment {number} --body "Thanks for the suggestion! 🎉
152+
153+
I've queued this for the team. The Engineer will build the example and open a PR.
154+
I'll update this issue and close it when the example is released.
155+
156+
Tracking: #{NEW_ISSUE}"
157+
158+
# Don't close yet — leave it open so the contributor can track progress.
159+
# The PR body will include 'Closes #{number}' so GitHub closes it on merge.
144160
```
145161

146162
---

0 commit comments

Comments
 (0)