Add BlackRoad Completion Framework

- GitHub Actions workflows (auto-merge, branch-tracker, issue-to-board, stale-cleanup)
- Issue templates (agent-task, bug, task)
- PR template
- Automation scripts (slack-to-github, create-issue)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alexa Louise
2025-11-30 07:40:23 -06:00
parent 319d6d32ab
commit 6516003877
11 changed files with 817 additions and 65 deletions

85
.github/ISSUE_TEMPLATE/agent-task.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: "🤖 Agent Task"
description: "For Codex or other agents to pick up"
title: "[AGENT] "
labels: ["agent-task", "automated"]
body:
- type: markdown
attributes:
value: |
## Agent-Executable Task
This issue is designed to be picked up and executed by Codex or another AI agent.
Keep instructions clear, specific, and executable.
- type: dropdown
id: priority
attributes:
label: Priority
options:
- "P0 - Immediate"
- "P1 - Today"
- "P2 - This week"
- "P3 - When available"
validations:
required: true
- type: textarea
id: instruction
attributes:
label: Instruction
description: What should the agent do? Be specific and executable.
placeholder: |
Create a new API endpoint at /api/users/preferences that:
- Accepts GET and PUT requests
- GET returns current user preferences from database
- PUT updates preferences with validation
- Follow existing patterns in /api/users/profile
validations:
required: true
- type: textarea
id: files
attributes:
label: Files to touch
description: Which files should be created or modified?
placeholder: |
- Create: src/pages/api/users/preferences.ts
- Modify: src/types/user.ts (add PreferencesType)
- Modify: prisma/schema.prisma (if needed)
validations:
required: false
- type: textarea
id: done
attributes:
label: Definition of done
description: How does the agent know it's complete?
placeholder: |
- Endpoint responds correctly to GET/PUT
- TypeScript compiles without errors
- Tests pass (if tests exist for this area)
validations:
required: true
- type: textarea
id: constraints
attributes:
label: Constraints / Don'ts
description: What should the agent avoid?
placeholder: |
- Don't modify the auth middleware
- Don't add new dependencies
- Follow existing code style
validations:
required: false
- type: dropdown
id: agent
attributes:
label: Assigned agent
options:
- "Codex"
- "Any available agent"
- "Specific agent (note in context)"
validations:
required: false

71
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: "🐛 Bug"
description: "Something is broken"
title: "[BUG] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
## Bug Report
What's broken? Let's fix it.
- type: dropdown
id: severity
attributes:
label: Severity
description: How bad is it?
options:
- "🔥 Critical - Production down, data loss, security issue"
- "🟠 High - Major feature broken, no workaround"
- "🟡 Medium - Feature broken but has workaround"
- "🟢 Low - Minor issue, cosmetic, edge case"
validations:
required: true
- type: textarea
id: what
attributes:
label: What's broken?
description: One sentence description
placeholder: "Login button doesn't work on mobile Safari"
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What should happen?
placeholder: "Clicking login should open the auth modal"
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: What actually happens?
placeholder: "Nothing happens. Console shows: TypeError..."
validations:
required: true
- type: textarea
id: repro
attributes:
label: How to reproduce
description: Steps to trigger the bug
placeholder: |
1. Open site on iPhone Safari
2. Tap Login button
3. Nothing happens
validations:
required: true
- type: textarea
id: context
attributes:
label: Environment / Context
description: Browser, OS, device, account type, etc.
placeholder: "iOS 17, Safari, iPhone 15 Pro"
validations:
required: false

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: BlackRoad OS Docs - name: "💬 Quick question / discussion"
url: https://github.com/BlackRoad-OS/blackroad-os-docs url: https://github.com/YOUR_USERNAME/YOUR_REPO/discussions
about: Check the docs before opening an issue about: "For questions, ideas, or discussions that aren't actionable tasks yet"

65
.github/ISSUE_TEMPLATE/task.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: "🎯 Task"
description: "Standard work item - features, changes, improvements"
title: "[TASK] "
labels: ["task"]
body:
- type: markdown
attributes:
value: |
## Quick Task Creation
Keep it simple. One task = one thing to do.
- type: dropdown
id: priority
attributes:
label: Priority
description: How urgent is this?
options:
- "P0 - Do it now (blocks everything)"
- "P1 - Today"
- "P2 - This week"
- "P3 - Backlog"
validations:
required: true
- type: textarea
id: what
attributes:
label: What needs to happen?
description: One sentence. If it's more than one sentence, split into multiple issues.
placeholder: "Add a logout button to the nav bar"
validations:
required: true
- type: textarea
id: done
attributes:
label: Definition of done
description: How do we know this is complete?
placeholder: |
- Logout button visible in nav
- Clicking it clears session and redirects to /login
- Works on mobile
validations:
required: true
- type: textarea
id: context
attributes:
label: Context (optional)
description: Any additional info that helps
placeholder: "Related to issue #45. See Figma design at..."
validations:
required: false
- type: dropdown
id: assignee
attributes:
label: Who should handle this?
description: Human or agent?
options:
- "Codex / Agent"
- "Human (me)"
- "Unassigned - triage needed"
validations:
required: false

View File

@@ -1,39 +1,22 @@
## Summary ## What
<!-- What does this PR do? One paragraph max. -->
<!-- One line: what does this PR do? -->
## Linked Issue
Fixes #
<!-- The "Fixes #123" syntax auto-closes the issue when merged -->
## Changes ## Changes
<!-- Bulleted list of specific changes -->
-
## Type <!-- Optional: bullet list of key changes if not obvious -->
<!-- Check one -->
- [ ] Feature
- [ ] Bug fix
- [ ] Infrastructure / CI
- [ ] Documentation
- [ ] Refactor
- [ ] Config change
## Tests ---
<!-- How was this tested? -->
- [ ] Unit tests pass
- [ ] Integration tests pass
- [ ] Manual testing (describe below)
- [ ] N/A (docs only)
**Manual testing steps:** <!--
This PR will auto-merge when CI passes.
No manual approval required.
If CI fails, fix it or add the 'blocked' label if you need help.
## Risk / Impact -->
<!-- What could go wrong? What's the blast radius? -->
- Risk level: Low / Medium / High
- Affected services:
## Checklist
- [ ] Code follows project conventions
- [ ] No secrets or credentials committed
- [ ] README updated (if applicable)
- [ ] This is a single logical change (atomic PR)
## Related Issues
<!-- Closes #123, Relates to #456 -->

View File

@@ -1,12 +1,19 @@
name: Auto Merge name: Auto-Approve and Merge
# This workflow automatically approves and merges PRs when:
# 1. CI passes
# 2. PR is from a trusted source (you, Codex, or designated bots)
#
# No human approval required. CI is the reviewer.
on: on:
pull_request_target: pull_request:
types: [opened, synchronize, reopened, labeled] types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted]
check_suite: check_suite:
types: [completed] types: [completed]
workflow_run:
workflows: ["CI"] # Replace with your actual CI workflow name
types: [completed]
permissions: permissions:
contents: write contents: write
@@ -15,33 +22,60 @@ permissions:
jobs: jobs:
auto-merge: auto-merge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only run for trusted actors
# Add your GitHub username, Codex bot, any other trusted sources
if: | if: |
(github.event.pull_request.user.login == 'dependabot[bot]' || github.actor == 'YOUR_GITHUB_USERNAME' ||
contains(github.event.pull_request.labels.*.name, 'automerge')) && github.actor == 'codex-bot' ||
github.event.pull_request.draft == false github.actor == 'dependabot[bot]' ||
github.actor == 'github-actions[bot]'
steps: steps:
- name: Check if all checks passed - name: Checkout
id: checks uses: actions/checkout@v4
run: |
gh pr checks "$PR_URL" --json state --jq 'all(.[] | .state == "SUCCESS")'
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable auto-merge - name: Wait for CI to complete
if: steps.checks.outputs.result == 'true' uses: fountainhead/action-wait-for-check@v1.1.0
run: | id: wait-for-ci
echo "All checks passed - enabling auto-merge" with:
gh pr merge --auto --squash "$PR_URL" token: ${{ secrets.GITHUB_TOKEN }}
env: checkName: build # Replace with your CI check name
PR_URL: ${{ github.event.pull_request.html_url }} ref: ${{ github.event.pull_request.head.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} timeoutSeconds: 300
intervalSeconds: 10
- name: Comment on PR - name: Auto-approve PR
if: steps.checks.outputs.result == 'true' if: steps.wait-for-ci.outputs.conclusion == 'success'
run: | uses: hmarr/auto-approve-action@v4
gh pr comment "$PR_URL" --body "🤖 Auto-merge enabled. PR will merge when all required checks pass and approvals are met." with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Auto-merge PR
if: steps.wait-for-ci.outputs.conclusion == 'success'
uses: pascalgn/automerge-action@v0.16.2
env: env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MERGE_METHOD: squash
MERGE_COMMIT_MESSAGE: pull-request-title
MERGE_DELETE_BRANCH: true
UPDATE_METHOD: rebase
- name: Add blocked label on CI failure
if: steps.wait-for-ci.outputs.conclusion == 'failure'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['blocked', 'ci-failed']
});
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: '🔴 **CI Failed** - Auto-merge blocked. Check the logs and fix the issue.'
});

83
.github/workflows/branch-tracker.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
name: Track Branch Activity
# When a branch is created that matches an issue number,
# automatically update the project board to show work has started
on:
create:
branches:
- 'issue-*'
- 'fix-*'
- 'feat-*'
push:
branches:
- 'issue-*'
- 'fix-*'
- 'feat-*'
permissions:
issues: write
repository-projects: write
jobs:
track-branch:
runs-on: ubuntu-latest
steps:
- name: Extract issue number from branch name
id: extract
run: |
BRANCH_NAME="${{ github.ref_name }}"
echo "Branch: $BRANCH_NAME"
# Extract issue number from branch name
# Supports: issue-123-description, fix-123-bug, feat-123-feature
ISSUE_NUM=$(echo "$BRANCH_NAME" | grep -oP '(?<=issue-|fix-|feat-)\d+' | head -1)
if [ -n "$ISSUE_NUM" ]; then
echo "issue_number=$ISSUE_NUM" >> $GITHUB_OUTPUT
echo "Found issue number: $ISSUE_NUM"
else
echo "No issue number found in branch name"
echo "issue_number=" >> $GITHUB_OUTPUT
fi
- name: Add 'in-progress' label to linked issue
if: steps.extract.outputs.issue_number != ''
uses: actions/github-script@v7
with:
script: |
const issueNumber = ${{ steps.extract.outputs.issue_number }};
try {
// Add in-progress label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['in-progress']
});
// Remove triage/inbox labels if present
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: 'triage'
});
} catch (e) {
// Label might not exist, that's fine
}
// Add comment showing work has started
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `🚀 **Work started** on branch \`${{ github.ref_name }}\`\n\nActor: ${{ github.actor }}`
});
console.log(`Updated issue #${issueNumber} - work in progress`);
} catch (error) {
console.log(`Could not update issue #${issueNumber}: ${error.message}`);
}

56
.github/workflows/issue-to-board.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Add Issue to Project Board
# Automatically adds new issues to your GitHub Project board
# No manual card creation needed
on:
issues:
types: [opened]
permissions:
issues: write
repository-projects: write
env:
# Replace with your Project number (find in Project URL)
# Example: https://github.com/users/YOUR_USERNAME/projects/1 → PROJECT_NUMBER=1
PROJECT_NUMBER: 1
# Replace with your GitHub username or org
PROJECT_OWNER: YOUR_GITHUB_USERNAME
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- name: Add issue to project
uses: actions/add-to-project@v0.5.0
with:
project-url: https://github.com/users/${{ env.PROJECT_OWNER }}/projects/${{ env.PROJECT_NUMBER }}
github-token: ${{ secrets.PROJECT_TOKEN }}
# Note: You need a PAT with project permissions, not GITHUB_TOKEN
# Create one at: Settings → Developer settings → Personal access tokens
# Scopes needed: project, repo
- name: Set initial status to Inbox
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_TOKEN }}
script: |
// This sets the Status field to "Inbox" on the project board
// You'll need to customize field IDs for your specific project
console.log('Issue #${{ github.event.issue.number }} added to project board');
console.log('Title: ${{ github.event.issue.title }}');
- name: Add triage label if no labels
if: github.event.issue.labels[0] == null
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['triage']
});

69
.github/workflows/stale-cleanup.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Stale Issue Cleanup
# Automatically flags and closes stale issues
# Keeps your board clean without manual gardening
on:
schedule:
# Run daily at midnight UTC
- cron: '0 0 * * *'
workflow_dispatch:
# Allow manual trigger
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Mark and close stale issues
uses: actions/stale@v9
with:
# Issues
stale-issue-message: |
⏰ **This issue has been inactive for 14 days.**
If this is still needed, please comment or update the issue.
Otherwise, it will be closed in 7 days.
To keep this open: add a comment or remove the `stale` label.
close-issue-message: |
🗂️ **Closed due to inactivity.**
If this is still needed, reopen the issue or create a new one.
stale-issue-label: 'stale'
days-before-issue-stale: 14
days-before-issue-close: 7
# PRs - more aggressive since they should flow fast
stale-pr-message: |
⏰ **This PR has been inactive for 7 days.**
If CI is failing, fix it. If it's blocked, add the `blocked` label.
Otherwise, this PR will be closed in 3 days.
close-pr-message: |
🗂️ **Closed due to inactivity.**
If this work is still needed, create a new PR.
stale-pr-label: 'stale'
days-before-pr-stale: 7
days-before-pr-close: 3
# Exemptions
exempt-issue-labels: 'pinned,security,blocked,p0-now'
exempt-pr-labels: 'pinned,security,blocked'
# Don't mark issues that have recent commits on linked branches
exempt-all-pr-milestones: true
# Operations per run (GitHub API limits)
operations-per-run: 100
- name: Report cleanup stats
uses: actions/github-script@v7
with:
script: |
console.log('Stale cleanup completed');
console.log('Check the Actions log for details on marked/closed items');

123
scripts/create-issue.sh Normal file
View File

@@ -0,0 +1,123 @@
#!/bin/bash
# create-issue.sh
# Quick issue creation from the command line
#
# Usage:
# ./create-issue.sh "Fix the login redirect bug"
# ./create-issue.sh "Add dark mode toggle" --priority p1
# ./create-issue.sh "Refactor auth module" --agent
#
# Requirements:
# - GitHub CLI (gh) installed and authenticated
# - Run from within a git repo
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
PRIORITY="P2"
LABELS="task"
AGENT_MODE=false
TITLE=""
BODY=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--priority|-p)
PRIORITY="${2^^}" # Uppercase
shift 2
;;
--agent|-a)
AGENT_MODE=true
LABELS="agent-task,automated"
shift
;;
--bug|-b)
LABELS="bug"
shift
;;
--body)
BODY="$2"
shift 2
;;
--help|-h)
echo "Usage: $0 \"Issue title\" [options]"
echo ""
echo "Options:"
echo " --priority, -p <P0|P1|P2|P3> Set priority (default: P2)"
echo " --agent, -a Mark as agent task"
echo " --bug, -b Mark as bug"
echo " --body \"text\" Add body text"
echo ""
echo "Examples:"
echo " $0 \"Fix login redirect\""
echo " $0 \"Add dark mode\" --priority p1"
echo " $0 \"Refactor auth\" --agent --priority p0"
exit 0
;;
*)
if [[ -z "$TITLE" ]]; then
TITLE="$1"
fi
shift
;;
esac
done
# Validate
if [[ -z "$TITLE" ]]; then
echo -e "${RED}Error: Issue title required${NC}"
echo "Usage: $0 \"Issue title\" [options]"
exit 1
fi
# Check gh is installed
if ! command -v gh &> /dev/null; then
echo -e "${RED}Error: GitHub CLI (gh) not installed${NC}"
echo "Install: https://cli.github.com/"
exit 1
fi
# Build the issue
echo -e "${YELLOW}Creating issue...${NC}"
# Add priority label
case $PRIORITY in
P0) LABELS="$LABELS,p0-now" ;;
P1) LABELS="$LABELS,p1-today" ;;
P2) LABELS="$LABELS,p2-week" ;;
P3) LABELS="$LABELS,p3-backlog" ;;
esac
# Construct title prefix
if [[ "$LABELS" == *"agent-task"* ]]; then
FULL_TITLE="[AGENT] $TITLE"
elif [[ "$LABELS" == *"bug"* ]]; then
FULL_TITLE="[BUG] $TITLE"
else
FULL_TITLE="[TASK] $TITLE"
fi
# Create the issue
if [[ -n "$BODY" ]]; then
ISSUE_URL=$(gh issue create --title "$FULL_TITLE" --body "$BODY" --label "$LABELS" 2>&1)
else
ISSUE_URL=$(gh issue create --title "$FULL_TITLE" --body "Created via CLI" --label "$LABELS" 2>&1)
fi
echo -e "${GREEN}✅ Issue created${NC}"
echo "$ISSUE_URL"
# Extract issue number for convenience
ISSUE_NUM=$(echo "$ISSUE_URL" | grep -oP '\d+$')
if [[ -n "$ISSUE_NUM" ]]; then
echo ""
echo -e "Branch name: ${YELLOW}issue-$ISSUE_NUM-$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-' | head -c 30)${NC}"
fi

183
scripts/slack-to-github.js Normal file
View File

@@ -0,0 +1,183 @@
/**
* slack-to-github.js
*
* Webhook handler that creates GitHub issues from Slack messages.
* Deploy as: Cloudflare Worker, Vercel Function, or any serverless platform.
*
* Trigger: Slack slash command or bot mention
* Example: /issue Fix the login redirect bug
* Example: @blackroad-bot create issue: Add dark mode toggle
*
* Setup:
* 1. Create a Slack App with slash commands or bot
* 2. Set the request URL to your deployed function
* 3. Set environment variables: GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO, SLACK_SIGNING_SECRET
*/
// For Cloudflare Workers
export default {
async fetch(request, env) {
return handleRequest(request, env);
}
};
// For Vercel/other platforms, use:
// export default async function handler(req, res) { ... }
async function handleRequest(request, env) {
// Verify this is a POST request
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
try {
// Parse the Slack payload
const formData = await request.formData();
const payload = Object.fromEntries(formData);
// For slash commands, the text is in payload.text
// For bot mentions, you'd parse payload differently
const text = payload.text || '';
const userId = payload.user_id || 'unknown';
const userName = payload.user_name || 'unknown';
const channelId = payload.channel_id || '';
if (!text.trim()) {
return slackResponse('Please provide an issue title. Usage: `/issue Fix the login bug`');
}
// Parse priority from text if included
let priority = 'P2';
let title = text;
const priorityMatch = text.match(/\b(p[0-3])\b/i);
if (priorityMatch) {
priority = priorityMatch[1].toUpperCase();
title = text.replace(priorityMatch[0], '').trim();
}
// Detect if this should be an agent task
const isAgentTask = text.toLowerCase().includes('[agent]') ||
text.toLowerCase().includes('--agent');
title = title.replace(/\[agent\]/gi, '').replace(/--agent/gi, '').trim();
// Detect if this is a bug
const isBug = text.toLowerCase().includes('[bug]') ||
text.toLowerCase().includes('--bug');
title = title.replace(/\[bug\]/gi, '').replace(/--bug/gi, '').trim();
// Build labels
const labels = [];
if (isBug) {
labels.push('bug');
} else if (isAgentTask) {
labels.push('agent-task', 'automated');
} else {
labels.push('task');
}
// Add priority label
switch (priority) {
case 'P0': labels.push('p0-now'); break;
case 'P1': labels.push('p1-today'); break;
case 'P2': labels.push('p2-week'); break;
case 'P3': labels.push('p3-backlog'); break;
}
// Build title prefix
let fullTitle = title;
if (isBug) {
fullTitle = `[BUG] ${title}`;
} else if (isAgentTask) {
fullTitle = `[AGENT] ${title}`;
} else {
fullTitle = `[TASK] ${title}`;
}
// Create the GitHub issue
const issue = await createGitHubIssue({
title: fullTitle,
body: `Created from Slack by @${userName}\n\nChannel: <#${channelId}>`,
labels: labels,
env: env
});
// Send success response back to Slack
return slackResponse(
`✅ Issue created: <${issue.html_url}|#${issue.number} ${title}>\n` +
`Priority: ${priority} | Labels: ${labels.join(', ')}`
);
} catch (error) {
console.error('Error:', error);
return slackResponse(`❌ Failed to create issue: ${error.message}`);
}
}
async function createGitHubIssue({ title, body, labels, env }) {
const response = await fetch(
`https://api.github.com/repos/${env.GITHUB_OWNER}/${env.GITHUB_REPO}/issues`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
'User-Agent': 'BlackRoad-Slack-Bot'
},
body: JSON.stringify({
title,
body,
labels
})
}
);
if (!response.ok) {
const error = await response.text();
throw new Error(`GitHub API error: ${response.status} - ${error}`);
}
return response.json();
}
function slackResponse(text) {
return new Response(
JSON.stringify({
response_type: 'in_channel', // visible to everyone, or 'ephemeral' for private
text: text
}),
{
headers: {
'Content-Type': 'application/json'
}
}
);
}
/**
* Environment variables needed:
*
* GITHUB_TOKEN - Personal access token with repo scope
* GITHUB_OWNER - Your GitHub username or org
* GITHUB_REPO - Repository name
* SLACK_SIGNING_SECRET - (optional) For verifying Slack requests
*
* To verify Slack requests (recommended for production):
*
* async function verifySlackRequest(request, signingSecret) {
* const timestamp = request.headers.get('x-slack-request-timestamp');
* const signature = request.headers.get('x-slack-signature');
* const body = await request.text();
*
* const baseString = `v0:${timestamp}:${body}`;
* const hmac = crypto.createHmac('sha256', signingSecret);
* hmac.update(baseString);
* const computed = `v0=${hmac.digest('hex')}`;
*
* return crypto.timingSafeEqual(
* Buffer.from(signature),
* Buffer.from(computed)
* );
* }
*/