fix: Correct pre-commit template path in PrecommitHooksFixer #372
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: AgentReady Assessment (On-Demand) | |
| on: | |
| issue_comment: | |
| types: [created] | |
| pull_request_review_comment: | |
| types: [created] | |
| workflow_dispatch: | |
| jobs: | |
| check-agentready-acl: | |
| # Check if user is authorized to run /agentready assess command | |
| # Always runs to provide output for dependent jobs | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| is_authorized: ${{ steps.check-agentready-acl.outputs.is_authorized }} | |
| command_invoked: ${{ steps.check-agentready-acl.outputs.command_invoked }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Check AgentReady ACL | |
| id: check-agentready-acl | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| COMMENT_USER: ${{ github.event.comment.user.login || '' }} | |
| COMMENT_BODY: ${{ github.event.comment.body || '' }} | |
| run: | | |
| # workflow_dispatch (manual trigger) is always authorized | |
| if [ "$EVENT_NAME" == "workflow_dispatch" ]; then | |
| echo "Manual workflow dispatch is always authorized" | |
| echo "is_authorized=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # For comment events, check if command is present | |
| if [ "$EVENT_NAME" != "issue_comment" ] && [ "$EVENT_NAME" != "pull_request_review_comment" ]; then | |
| echo "is_authorized=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Check if comment contains the command | |
| if ! echo "$COMMENT_BODY" | grep -qi "/agentready assess"; then | |
| echo "command_invoked=false" >> "$GITHUB_OUTPUT" | |
| echo "is_authorized=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "command_invoked=true" >> "$GITHUB_OUTPUT" | |
| # Read ACL file and check if user is authorized | |
| if [ ! -f ".github/agentready-acl.yml" ]; then | |
| echo "::error::ACL file not found: .github/agentready-acl.yml" | |
| echo "is_authorized=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| # Extract authorized users from YAML (simple grep approach for authorized_users list) | |
| # This handles the YAML list format: - username | |
| AUTHORIZED_USERS=$(grep -E "^\s*-\s+" .github/agentready-acl.yml | sed 's/^\s*-\s*//' | tr '\n' ' ') | |
| # Check if COMMENT_USER is in the authorized list | |
| if echo "$AUTHORIZED_USERS" | grep -qw "$COMMENT_USER"; then | |
| echo "User $COMMENT_USER is authorized" | |
| echo "is_authorized=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "User $COMMENT_USER is not authorized" | |
| echo "is_authorized=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| unauthorized: | |
| # Respond to unauthorized users with helpful message | |
| needs: check-agentready-acl | |
| if: | | |
| needs.check-agentready-acl.outputs.command_invoked == 'true' && | |
| needs.check-agentready-acl.outputs.is_authorized == 'false' && | |
| (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Post unauthorized message | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const user = context.payload.comment.user.login; | |
| const body = `Hi @${user}! Thanks for your interest in AgentReady.\n\n` + | |
| `The \`/agentready assess\` command is currently restricted to repository maintainers.\n\n` + | |
| `**To assess your own repository:**\n` + | |
| `\`\`\`bash\n` + | |
| `pip install agentready\n` + | |
| `agentready assess .\n` + | |
| `\`\`\`\n\n` + | |
| `See [AgentReady documentation](https://github.com/ambient-code/agentready) for more information.`; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: body | |
| }); | |
| assess: | |
| # Only run on /agentready assess command (from authorized users) or manual trigger | |
| needs: check-agentready-acl | |
| if: needs.check-agentready-acl.outputs.is_authorized == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - name: Parse command and extract repository URL | |
| id: parse | |
| env: | |
| COMMENT_BODY: ${{ github.event.comment.body || '' }} | |
| run: | | |
| # SAFE: Using environment variable to avoid command injection | |
| # Extract repository URL (case-insensitive) | |
| REPO_URL=$(echo "$COMMENT_BODY" | grep -ioP '/agentready\s+assess\s+\Khttps://github\.com/[^\s]+' || echo "") | |
| # Default to current repo if no URL | |
| if [ -z "$REPO_URL" ]; then | |
| REPO_URL="https://github.com/${{ github.repository }}" | |
| ASSESS_CURRENT_REPO="true" | |
| echo "::notice::No repository URL provided, assessing current repository" | |
| else | |
| ASSESS_CURRENT_REPO="false" | |
| echo "::notice::Assessing external repository: $REPO_URL" | |
| fi | |
| # Clean URL and extract org/repo (using bash parameter expansion) | |
| REPO_URL="${REPO_URL%.git}" | |
| ORG_REPO="${REPO_URL#https://github.com/}" | |
| # Output all variables in one block | |
| { | |
| echo "repo_url=$REPO_URL" | |
| echo "org_repo=$ORG_REPO" | |
| echo "assess_current=$ASSESS_CURRENT_REPO" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Validate repository URL and access | |
| id: validate | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO_URL: ${{ steps.parse.outputs.repo_url }} | |
| ORG_REPO: ${{ steps.parse.outputs.org_repo }} | |
| ASSESS_CURRENT: ${{ steps.parse.outputs.assess_current }} | |
| run: | | |
| # Skip validation for current repo | |
| if [ "$ASSESS_CURRENT" == "true" ]; then | |
| echo "✅ Assessing current repository (validation skipped)" | |
| echo "is_valid=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Validate URL format | |
| if ! echo "$REPO_URL" | grep -qE '^https://github\.com/[a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+$'; then | |
| echo "::error::Invalid repository URL format: $REPO_URL" | |
| echo "is_valid=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| # Verify repository is public | |
| if ! gh repo view "$ORG_REPO" --json isPrivate -q '.isPrivate' > /tmp/is_private.txt 2>&1; then | |
| echo "::error::Repository not found: $ORG_REPO" | |
| echo "is_valid=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| IS_PRIVATE=$(cat /tmp/is_private.txt) | |
| if [ "$IS_PRIVATE" == "true" ]; then | |
| echo "::error::Repository $ORG_REPO is private. Only public repos allowed." | |
| echo "is_valid=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| echo "✅ Repository $ORG_REPO is public and accessible" | |
| echo "is_valid=true" >> "$GITHUB_OUTPUT" | |
| - name: Checkout current repository | |
| if: steps.parse.outputs.assess_current == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.ref || github.ref }} | |
| - name: Clone external repository | |
| if: steps.parse.outputs.assess_current == 'false' && steps.validate.outputs.is_valid == 'true' | |
| env: | |
| REPO_URL: ${{ steps.parse.outputs.repo_url }} | |
| run: | | |
| echo "Cloning $REPO_URL..." | |
| git clone "$REPO_URL" /tmp/repo-to-assess | |
| if [ ! -d "/tmp/repo-to-assess/.git" ]; then | |
| echo "::error::Failed to clone repository" | |
| exit 1 | |
| fi | |
| echo "✅ Repository cloned successfully" | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.12' | |
| - name: Install AgentReady | |
| run: pip install -e . | |
| - name: Run AgentReady Assessment | |
| id: assessment | |
| env: | |
| ASSESS_CURRENT: ${{ steps.parse.outputs.assess_current }} | |
| run: | | |
| # Set paths based on mode | |
| if [ "$ASSESS_CURRENT" == "true" ]; then | |
| REPO_PATH="." | |
| OUTPUT_DIR=".agentready" | |
| else | |
| REPO_PATH="/tmp/repo-to-assess" | |
| OUTPUT_DIR="/tmp/assessment-output" | |
| mkdir -p "$OUTPUT_DIR" | |
| fi | |
| echo "Assessing: $REPO_PATH" | |
| agentready assess "$REPO_PATH" --verbose --output-dir "$OUTPUT_DIR" | |
| # Verify report generated | |
| if [ ! -f "$OUTPUT_DIR/report-latest.md" ]; then | |
| echo "::error::Assessment failed - no report" | |
| exit 1 | |
| fi | |
| echo "output_dir=$OUTPUT_DIR" >> "$GITHUB_OUTPUT" | |
| - name: Extract assessment summary | |
| if: steps.assessment.outcome == 'success' | |
| id: summary | |
| env: | |
| OUTPUT_DIR: ${{ steps.assessment.outputs.output_dir }} | |
| run: | | |
| # Extract key metrics from JSON | |
| SCORE=$(jq -r '.overall_score' "$OUTPUT_DIR/assessment-latest.json") | |
| CERT_LEVEL=$(jq -r '.certification_level' "$OUTPUT_DIR/assessment-latest.json") | |
| # Extract top 5 failing attributes sorted by tier (tier 1 = highest impact) | |
| # Format: tier,name for sorting, then extract just names | |
| FAILING=$(jq -r '.findings[] | select(.status == "fail") | "\(.attribute.tier),\(.attribute.name)"' "$OUTPUT_DIR/assessment-latest.json" | \ | |
| sort -n | \ | |
| cut -d',' -f2 | \ | |
| head -5 | \ | |
| paste -sd "," -) | |
| # Output all variables in one block | |
| { | |
| echo "score=$SCORE" | |
| echo "cert_level=$CERT_LEVEL" | |
| echo "failing=$FAILING" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Upload Assessment Reports | |
| uses: actions/upload-artifact@v5 | |
| if: always() | |
| with: | |
| name: agentready-reports-${{ github.run_id }} | |
| path: ${{ steps.assessment.outputs.output_dir }} | |
| retention-days: 30 | |
| - name: Post assessment results | |
| if: always() | |
| uses: actions/github-script@v8 | |
| env: | |
| OUTPUT_DIR: ${{ steps.assessment.outputs.output_dir }} | |
| REPO_URL: ${{ steps.parse.outputs.repo_url }} | |
| ORG_REPO: ${{ steps.parse.outputs.org_repo }} | |
| ASSESS_CURRENT: ${{ steps.parse.outputs.assess_current }} | |
| VALIDATION_FAILED: ${{ steps.validate.outputs.is_valid == 'false' }} | |
| ASSESSMENT_FAILED: ${{ steps.assessment.outcome == 'failure' }} | |
| SCORE: ${{ steps.summary.outputs.score }} | |
| CERT_LEVEL: ${{ steps.summary.outputs.cert_level }} | |
| FAILING: ${{ steps.summary.outputs.failing }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let body; | |
| const validationFailed = process.env.VALIDATION_FAILED === 'true'; | |
| const assessmentFailed = process.env.ASSESSMENT_FAILED === 'true'; | |
| const orgRepo = process.env.ORG_REPO; | |
| const outputDir = process.env.OUTPUT_DIR; | |
| const assessCurrent = process.env.ASSESS_CURRENT === 'true'; | |
| const repoUrl = process.env.REPO_URL; | |
| const score = process.env.SCORE; | |
| const certLevel = process.env.CERT_LEVEL; | |
| const failing = process.env.FAILING; | |
| // Validation failure | |
| if (validationFailed) { | |
| body = `## ❌ AgentReady Assessment Failed\n\n` + | |
| `Could not assess **${orgRepo}**\n\n` + | |
| `**Possible reasons:**\n` + | |
| `- Repository does not exist\n` + | |
| `- Repository is private (only public repos)\n` + | |
| `- Invalid URL format\n\n` + | |
| `**Expected:** \`/agentready assess https://github.com/owner/repo\``; | |
| } | |
| // Assessment failure | |
| else if (assessmentFailed) { | |
| body = `## ❌ AgentReady Assessment Failed\n\n` + | |
| `Assessment of **${orgRepo}** failed.\n\n` + | |
| `[View logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; | |
| } | |
| // Success - Structured format with summary and collapsed details | |
| else { | |
| const reportPath = `${outputDir}/report-latest.md`; | |
| const report = fs.readFileSync(reportPath, 'utf8'); | |
| const repoContext = assessCurrent ? '(Current Repository)' : `([${orgRepo}](${repoUrl}))`; | |
| // Certificate emoji | |
| const certEmoji = { | |
| 'Platinum': '🏆', | |
| 'Gold': '🥇', | |
| 'Silver': '🥈', | |
| 'Bronze': '🥉', | |
| 'Needs Improvement': '📋' | |
| }[certLevel] || '📊'; | |
| // Build summary section | |
| let summary = `## ${certEmoji} AgentReady Assessment ${repoContext}\n\n`; | |
| summary += `**Score:** ${score}/100 | **Level:** ${certLevel}\n\n`; | |
| // Next steps - top 5 by impact | |
| summary += `### 🎯 Next Steps (by impact)\n\n`; | |
| if (failing && failing.length > 0) { | |
| const failingList = failing.split(',').map((f, i) => `${i + 1}. Fix: ${f}`).join('\n'); | |
| summary += `${failingList}\n\n`; | |
| } else { | |
| summary += `✅ All critical attributes passing! Consider improving optional attributes.\n\n`; | |
| } | |
| // Links section | |
| const artifactUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| summary += `📦 [Download Full Reports](${artifactUrl}) • `; | |
| summary += `🔗 [View Workflow](${artifactUrl})\n\n`; | |
| // Collapsed verbose output | |
| summary += `<details>\n<summary>📄 Full Assessment Report (click to expand)</summary>\n\n`; | |
| summary += `${report}\n\n`; | |
| summary += `</details>`; | |
| body = summary; | |
| } | |
| // Post comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: body | |
| }); |