diff --git a/.github/workflows/issue-labeler.lock.yml b/.github/workflows/issue-labeler.lock.yml new file mode 100644 index 00000000000..37bfd367c31 --- /dev/null +++ b/.github/workflows/issue-labeler.lock.yml @@ -0,0 +1,601 @@ +# This file was automatically generated by gh-aw. DO NOT EDIT. +# To update this file, edit the corresponding .md file and run: +# gh aw compile + +name: "Issue Labeler" +on: + issues: + types: + - opened + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" + +run-name: "Issue Labeler" + +jobs: + issue-labeler: + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + discussions: read + deployments: read + models: read + outputs: + output: ${{ steps.collect_output.outputs.output }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup agent output + id: setup_agent_output + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const crypto = require('crypto'); + // Generate a random filename for the output file + const randomId = crypto.randomBytes(8).toString('hex'); + const outputFile = `/tmp/aw_output_${randomId}.txt`; + // Ensure the /tmp directory exists and create empty output file + fs.mkdirSync('/tmp', { recursive: true }); + fs.writeFileSync(outputFile, '', { mode: 0o644 }); + // Verify the file was created and is writable + if (!fs.existsSync(outputFile)) { + throw new Error(`Failed to create output file: ${outputFile}`); + } + // Set the environment variable for subsequent steps + core.exportVariable('GITHUB_AW_OUTPUT', outputFile); + console.log('Created agentic output file:', outputFile); + // Also set as step output for reference + core.setOutput('output_file', outputFile); + - name: Setup MCPs + run: | + mkdir -p /tmp/mcp-config + cat > /tmp/mcp-config/mcp-servers.json << 'EOF' + { + "mcpServers": { + "github": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server:sha-45e90ae" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + } + } + } + } + EOF + - name: Create prompt + env: + GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} + run: | + mkdir -p /tmp/aw-prompts + cat > /tmp/aw-prompts/prompt.txt << 'EOF' + ## Issue Labeler + - analyze issue #${{ github.event.issue.number }} content + - categorize the content as 'bug' or 'feature' + - label the issue accordingly + + + --- + + **IMPORTANT**: If you need to provide output that should be captured as a workflow output variable, write it to the file "${{ env.GITHUB_AW_OUTPUT }}". This file is available for you to write any output that should be exposed from this workflow. The content of this file will be made available as the 'output' workflow output. + EOF + - name: Print prompt to step summary + run: | + echo "## Generated Prompt" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````markdown' >> $GITHUB_STEP_SUMMARY + cat /tmp/aw-prompts/prompt.txt >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + - name: Generate agentic run info + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: "", + version: "", + workflow_name: "Issue Labeler", + experimental: false, + supports_tools_whitelist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + created_at: new Date().toISOString() + }; + + // Write to /tmp directory to avoid inclusion in PR + const tmpPath = '/tmp/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + - name: Upload agentic run info + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_info.json + path: /tmp/aw_info.json + if-no-files-found: warn + - name: Execute Claude Code Action + id: agentic_execution + uses: anthropics/claude-code-base-action@v0.0.56 + with: + # Allowed tools (sorted): + # - Glob + # - Grep + # - LS + # - NotebookRead + # - Read + # - Task + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_issue + # - mcp__github__get_issue_comments + # - mcp__github__get_job_logs + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issues + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + allowed_tools: "Glob,Grep,LS,NotebookRead,Read,Task,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issues,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_secret_scanning_alerts,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + claude_env: | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} + mcp_config: /tmp/mcp-config/mcp-servers.json + prompt_file: /tmp/aw-prompts/prompt.txt + timeout_minutes: 5 + env: + GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} + - name: Capture Agentic Action logs + if: always() + run: | + # Copy the detailed execution file from Agentic Action if available + if [ -n "${{ steps.agentic_execution.outputs.execution_file }}" ] && [ -f "${{ steps.agentic_execution.outputs.execution_file }}" ]; then + cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/issue-labeler.log + else + echo "No execution file output found from Agentic Action" >> /tmp/issue-labeler.log + fi + + # Ensure log file exists + touch /tmp/issue-labeler.log + - name: Check if workflow-complete.txt exists, if so upload it + id: check_file + run: | + if [ -f workflow-complete.txt ]; then + echo "File exists" + echo "upload=true" >> $GITHUB_OUTPUT + else + echo "File does not exist" + echo "upload=false" >> $GITHUB_OUTPUT + fi + - name: Upload workflow-complete.txt + if: steps.check_file.outputs.upload == 'true' + uses: actions/upload-artifact@v4 + with: + name: workflow-complete + path: workflow-complete.txt + - name: Collect agent output + id: collect_output + uses: actions/github-script@v7 + with: + script: | + /** + * Sanitizes content for safe output in GitHub Actions + * @param {string} content - The content to sanitize + * @returns {string} The sanitized content + */ + function sanitizeContent(content) { + if (!content || typeof content !== 'string') { + return ''; + } + // Read allowed domains from environment variable + const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS; + const defaultAllowedDomains = [ + 'github.com', + 'github.io', + 'githubusercontent.com', + 'githubassets.com', + 'github.dev', + 'codespaces.new' + ]; + const allowedDomains = allowedDomainsEnv + ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d) + : defaultAllowedDomains; + let sanitized = content; + // Neutralize @mentions to prevent unintended notifications + sanitized = neutralizeMentions(sanitized); + // Remove control characters (except newlines and tabs) + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); + // XML character escaping + sanitized = sanitized + .replace(/&/g, '&') // Must be first to avoid double-escaping + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + // URI filtering - replace non-https protocols with "(redacted)" + // Step 1: Temporarily mark HTTPS URLs to protect them + sanitized = sanitizeUrlProtocols(sanitized); + // Domain filtering for HTTPS URIs + // Match https:// URIs and check if domain is in allowlist + sanitized = sanitizeUrlDomains(sanitized); + // Limit total length to prevent DoS (0.5MB max) + const maxLength = 524288; + if (sanitized.length > maxLength) { + sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]'; + } + // Limit number of lines to prevent log flooding (65k max) + const lines = sanitized.split('\n'); + const maxLines = 65000; + if (lines.length > maxLines) { + sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]'; + } + // Remove ANSI escape sequences + sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ''); + // Neutralize common bot trigger phrases + sanitized = neutralizeBotTriggers(sanitized); + // Trim excessive whitespace + return sanitized.trim(); + /** + * Remove unknown domains + * @param {string} s - The string to process + * @returns {string} The string with unknown domains redacted + */ + function sanitizeUrlDomains(s) { + s = s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => { + // Extract the hostname part (before first slash, colon, or other delimiter) + const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); + // Check if this domain or any parent domain is in the allowlist + const isAllowed = allowedDomains.some(allowedDomain => { + const normalizedAllowed = allowedDomain.toLowerCase(); + return hostname === normalizedAllowed || hostname.endsWith('.' + normalizedAllowed); + }); + return isAllowed ? match : '(redacted)'; + }); + return s; + } + /** + * Remove unknown protocols except https + * @param {string} s - The string to process + * @returns {string} The string with non-https protocols redacted + */ + function sanitizeUrlProtocols(s) { + // Match both protocol:// and protocol: patterns + // This covers URLs like https://example.com, javascript:alert(), mailto:user@domain.com, etc. + return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => { + // Allow https (case insensitive), redact everything else + return protocol.toLowerCase() === 'https' ? match : '(redacted)'; + }); + } + /** + * Neutralizes @mentions by wrapping them in backticks + * @param {string} s - The string to process + * @returns {string} The string with neutralized mentions + */ + function neutralizeMentions(s) { + // Replace @name or @org/team outside code with `@name` + return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, + (_m, p1, p2) => `${p1}\`@${p2}\``); + } + /** + * Neutralizes bot trigger phrases by wrapping them in backticks + * @param {string} s - The string to process + * @returns {string} The string with neutralized bot triggers + */ + function neutralizeBotTriggers(s) { + // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc. + return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, + (match, action, ref) => `\`${action} #${ref}\``); + } + } + async function main() { + const fs = require("fs"); + const outputFile = process.env.GITHUB_AW_OUTPUT; + if (!outputFile) { + console.log('GITHUB_AW_OUTPUT not set, no output to collect'); + core.setOutput('output', ''); + return; + } + if (!fs.existsSync(outputFile)) { + console.log('Output file does not exist:', outputFile); + core.setOutput('output', ''); + return; + } + const outputContent = fs.readFileSync(outputFile, 'utf8'); + if (outputContent.trim() === '') { + console.log('Output file is empty'); + core.setOutput('output', ''); + } else { + const sanitizedContent = sanitizeContent(outputContent); + console.log('Collected agentic output (sanitized):', sanitizedContent.substring(0, 200) + (sanitizedContent.length > 200 ? '...' : '')); + core.setOutput('output', sanitizedContent); + } + } + await main(); + - name: Print agent output to step summary + env: + GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} + run: | + echo "## Agent Output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````markdown' >> $GITHUB_STEP_SUMMARY + cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() && steps.collect_output.outputs.output != '' + uses: actions/upload-artifact@v4 + with: + name: aw_output.txt + path: ${{ env.GITHUB_AW_OUTPUT }} + if-no-files-found: warn + - name: Upload agent logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: issue-labeler.log + path: /tmp/issue-labeler.log + if-no-files-found: warn + - name: Generate git patch + if: always() + run: | + # Check current git status + echo "Current git status:" + git status + # Get the initial commit SHA from the base branch of the pull request + if [ "$GITHUB_EVENT_NAME" = "pull_request" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ]; then + INITIAL_SHA="$GITHUB_BASE_REF" + else + INITIAL_SHA="$GITHUB_SHA" + fi + echo "Base commit SHA: $INITIAL_SHA" + # Configure git user for GitHub Actions + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + # Stage any unstaged files + git add -A || true + # Check if there are staged files to commit + if ! git diff --cached --quiet; then + echo "Staged files found, committing them..." + git commit -m "[agent] staged files" || true + echo "Staged files committed" + else + echo "No staged files to commit" + fi + # Check updated git status + echo "Updated git status after committing staged files:" + git status + # Show compact diff information between initial commit and HEAD (committed changes only) + echo '## Git diff' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + git diff --name-only "$INITIAL_SHA"..HEAD >> $GITHUB_STEP_SUMMARY || true + echo '```' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + # Check if there are any committed changes since the initial commit + if git diff --quiet "$INITIAL_SHA" HEAD; then + echo "No committed changes detected since initial commit" + echo "Skipping patch generation - no committed changes to create patch from" + else + echo "Committed changes detected, generating patch..." + # Generate patch from initial commit to HEAD (committed changes only) + git format-patch "$INITIAL_SHA"..HEAD --stdout > /tmp/aw.patch || echo "Failed to generate patch" > /tmp/aw.patch + echo "Patch file created at /tmp/aw.patch" + ls -la /tmp/aw.patch + # Show the first 50 lines of the patch for review + echo '## Git Patch' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```diff' >> $GITHUB_STEP_SUMMARY + head -50 /tmp/aw.patch >> $GITHUB_STEP_SUMMARY || echo "Could not display patch contents" >> $GITHUB_STEP_SUMMARY + echo '...' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + fi + - name: Upload git patch + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw.patch + path: /tmp/aw.patch + if-no-files-found: ignore + + add_labels: + needs: issue-labeler + if: github.event.issue.number || github.event.pull_request.number + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + timeout-minutes: 10 + outputs: + labels_added: ${{ steps.add_labels.outputs.labels_added }} + steps: + - name: Add Labels + id: add_labels + uses: actions/github-script@v7 + env: + GITHUB_AW_AGENT_OUTPUT: ${{ needs.issue-labeler.outputs.output }} + GITHUB_AW_LABELS_ALLOWED: "bug,feature" + GITHUB_AW_LABELS_MAX_COUNT: 3 + with: + script: | + async function main() { + // Read the agent output content from environment variable + const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT; + if (!outputContent) { + console.log('No GITHUB_AW_AGENT_OUTPUT environment variable found'); + return; + } + if (outputContent.trim() === '') { + console.log('Agent output content is empty'); + return; + } + console.log('Agent output content length:', outputContent.length); + // Read the allowed labels from environment variable (mandatory) + const allowedLabelsEnv = process.env.GITHUB_AW_LABELS_ALLOWED; + if (!allowedLabelsEnv) { + core.setFailed('GITHUB_AW_LABELS_ALLOWED environment variable is required but missing'); + return; + } + const allowedLabels = allowedLabelsEnv.split(',').map(label => label.trim()).filter(label => label); + if (allowedLabels.length === 0) { + core.setFailed('Allowed labels list is empty. At least one allowed label must be specified'); + return; + } + console.log('Allowed labels:', allowedLabels); + // Read the max-count limit from environment variable (default: 3) + const maxCountEnv = process.env.GITHUB_AW_LABELS_MAX_COUNT; + const maxCount = maxCountEnv ? parseInt(maxCountEnv, 10) : 3; + if (isNaN(maxCount) || maxCount < 1) { + core.setFailed(`Invalid max-count value: ${maxCountEnv}. Must be a positive integer`); + return; + } + console.log('Max count:', maxCount); + // Check if we're in an issue or pull request context + const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment'; + const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment'; + if (!isIssueContext && !isPRContext) { + core.setFailed('Not running in issue or pull request context, skipping label addition'); + return; + } + // Determine the issue/PR number + let issueNumber; + let contextType; + if (isIssueContext) { + if (context.payload.issue) { + issueNumber = context.payload.issue.number; + contextType = 'issue'; + } else { + core.setFailed('Issue context detected but no issue found in payload'); + return; + } + } else if (isPRContext) { + if (context.payload.pull_request) { + issueNumber = context.payload.pull_request.number; + contextType = 'pull request'; + } else { + core.setFailed('Pull request context detected but no pull request found in payload'); + return; + } + } + if (!issueNumber) { + core.setFailed('Could not determine issue or pull request number'); + return; + } + // Parse labels from agent output (one per line, ignore empty lines) + const lines = outputContent.split('\n'); + const requestedLabels = []; + for (const line of lines) { + const trimmedLine = line.trim(); + // Skip empty lines + if (trimmedLine === '') { + continue; + } + // Reject lines that start with '-' (removal indication) + if (trimmedLine.startsWith('-')) { + core.setFailed(`Label removal is not permitted. Found line starting with '-': ${trimmedLine}`); + return; + } + requestedLabels.push(trimmedLine); + } + console.log('Requested labels:', requestedLabels); + // Validate that all requested labels are in the allowed list + const validLabels = requestedLabels.filter(label => allowedLabels.includes(label)); + // Remove duplicates from requested labels + let uniqueLabels = [...new Set(validLabels)]; + // Enforce max-count limit + if (uniqueLabels.length > maxCount) { + console.log(`too many labels, keep ${maxCount}`) + uniqueLabels = uniqueLabels.slice(0, maxCount); + } + if (uniqueLabels.length === 0) { + console.log('No labels to add'); + core.setOutput('labels_added', ''); + await core.summary.addRaw(` + ## Label Addition + No labels were added (no valid labels found in agent output). + `).write(); + return; + } + console.log(`Adding ${uniqueLabels.length} labels to ${contextType} #${issueNumber}:`, uniqueLabels); + try { + // Add labels using GitHub API + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: uniqueLabels + }); + console.log(`Successfully added ${uniqueLabels.length} labels to ${contextType} #${issueNumber}`); + // Set output for other jobs to use + core.setOutput('labels_added', uniqueLabels.join('\n')); + // Write summary + const labelsListMarkdown = uniqueLabels.map(label => `- \`${label}\``).join('\n'); + await core.summary.addRaw(` + ## Label Addition + Successfully added ${uniqueLabels.length} label(s) to ${contextType} #${issueNumber}: + ${labelsListMarkdown} + `).write(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('Failed to add labels:', errorMessage); + core.setFailed(`Failed to add labels: ${errorMessage}`); + } + } + await main(); + diff --git a/.github/workflows/issue-labeler.md b/.github/workflows/issue-labeler.md new file mode 100644 index 00000000000..3f734b4b7e1 --- /dev/null +++ b/.github/workflows/issue-labeler.md @@ -0,0 +1,12 @@ +--- +on: + issues: + types: [opened] +output: + labels: + allowed: [bug, feature] +--- +## Issue Labeler +- analyze issue #${{ github.event.issue.number }} content +- categorize the content as 'bug' or 'feature' +- label the issue accordingly diff --git a/README.md b/README.md index ab4b32ac63d..6a673c6fd78 100644 --- a/README.md +++ b/README.md @@ -47,40 +47,55 @@ You can explore other samples at [githubnext/agentics](https://github.com/github ## 📝 Agentic Workflow Example -Here's what a simple agentic workflow looks like. This example automatically triages new issues: +Here's what a simple agentic issue labeler workflow. This example automatically labels an incoming issue. +The section between `---` at the start is called the frontmatter and contains a superset of GitHub Actions Workflow syntax. ```markdown --- on: issues: types: [opened] - -permissions: - contents: read # Minimal permissions for main job - -tools: - github: - allowed: [add_issue_comment] - output: - issue: - title-prefix: "[triage] " - labels: [automation, triage] - -timeout_minutes: 5 + labels: + allowed: [bug, feature] --- +## Issue Labeller +- analyze issue #${{ github.event.issue.number }} content +- categorize the content as 'bug' or 'feature' +- label the issue accordingly +``` -# Issue Triage - -Analyze issue #${{ github.event.issue.number }} and help with triage: - -1. Read the issue content -2. Post a helpful comment summarizing the issue -3. Write your analysis to ${{ env.GITHUB_AW_OUTPUT }} for automatic issue creation +The compiler will compile this agentic workflow into a GitHub Action Workflow file (`.yml`) +which splits the agentic execution in two jobs (containers): +- the agentic job with `read` permissions only +- the label creation job with `write` permissions only +- the output of the agent is sanitized before being passed to the label creation APIs -Keep responses concise and helpful. +```yaml +on: + issues: + types: [opened] +jobs: + issue-labeler: + permissions: + contents: read # agent container only has read access + outputs: + output: # agent output text + steps: + # setup mcps + # run agent with markdown prompt + # save outputs + add_labels: + needs: issue-labeler # wait for agent results + permissions: + issues: write # write access to add labels + steps: + # sanitize output + # use github apis to add labels ``` +The resulting workflow provides a safe execution environment for agentic tasks by isolating permissions and sanitizing outputs. + > **💡 Learn more**: For complete workflow configuration details, see the [Documentation](docs/index.md) > **📚 Workflow commands**: See [Commands Documentation](docs/commands.md) for complete workflow management commands including `list`, `status`, `enable`, `disable`, and more.