Skip to content

fix(tests): repair PR #8899 VRT setup and cleanup #3309

fix(tests): repair PR #8899 VRT setup and cleanup

fix(tests): repair PR #8899 VRT setup and cleanup #3309

Workflow file for this run

# .github/workflows/pr-enrichment.yml
# Enriches pull request titles and descriptions with contextual information
# based on the files changed, scope of changes, and related issues/PRs.
#
# Configuration:
# Set the following in your repository settings to disable/customize:
# - DISABLE_PR_ENRICHMENT=true - Completely disable PR enrichment
# - DISABLE_TITLE_ENHANCEMENT=true - Only disable title enhancement
name: PR Enrichment
on:
pull_request:
types: [opened, reopened, synchronize]
workflow_call:
inputs:
pr_number:
description: 'PR number to enrich'
required: true
type: string
permissions:
contents: read
pull-requests: write
issues: write
jobs:
enrich-pr:
name: Enrich PR Title and Description
runs-on: ubuntu-latest
# Explicitly define GH_TOKEN at the job level to ensure it's available for all steps.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
ref: refs/pull/${{ inputs.pr_number || github.event.pull_request.number }}/head
fetch-depth: 0
- name: Setup Environment
uses: ./.github/actions/setup-env
- name: Check if PR Enrichment is Disabled
id: check-disabled
env:
PR_TITLE_EVENT: ${{ github.event.pull_request.title }}
HEAD_REF_EVENT: ${{ github.event.pull_request.head.ref || github.head_ref }}
run: |
# 1. Check for manual disable toggle
if [ "${DISABLE_PR_ENRICHMENT:-false}" == "true" ]; then
echo "disabled=true" >> $GITHUB_OUTPUT
echo "::notice::PR enrichment is disabled (DISABLE_PR_ENRICHMENT=true)"
exit 0
fi
# 2. Automatically disable for E2E tests to prevent interfering with test expectations
# Consistent with pr-orchestrator.yml bypass logic.
if [[ "$PR_TITLE_EVENT" == *"E2E Test PR"* ]] || [[ "$HEAD_REF_EVENT" == "e2e-test-"* ]]; then
echo "disabled=true" >> $GITHUB_OUTPUT
echo "::notice::PR enrichment is disabled for E2E Test PR (Title: '$PR_TITLE_EVENT', Branch: '$HEAD_REF_EVENT')"
exit 0
fi
echo "disabled=false" >> $GITHUB_OUTPUT
- name: Setup PR Context
id: pr_context
env:
PR_NUMBER_INPUT: ${{ inputs.pr_number }}
PR_NUMBER_EVENT: ${{ github.event.pull_request.number }}
run: |
source scripts/ci/github-utils.sh
if [ -n "$PR_NUMBER_INPUT" ]; then
PR_NUMBER="$PR_NUMBER_INPUT"
else
PR_NUMBER="$PR_NUMBER_EVENT"
fi
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
# Fetch PR details immediately (polling removed).
# Validation ensures both title and head SHA are available.
# REDUCED REDUNDANCY: Metrics (additions, deletions, files) are handled in the Analyze step.
FIELDS="title,body,baseRefName,headRefName,baseRefOid,headRefOid,author"
JQ_FILTER='(. // {}) | {title: (.title // ""), body: (.body // ""), base_ref: (.baseRefName // ""), head_ref: (.headRefName // ""), base_sha: (.baseRefOid // ""), head_sha: (.headRefOid // ""), author: (.author.login // "")}'
VALIDATION='.title != "" and .head_sha != ""'
PR_DATA=$(poll_pr_view "$PR_NUMBER" 12 10 "$FIELDS" "$JQ_FILTER" "$VALIDATION")
if [ -z "$PR_DATA" ]; then
echo "::error::Could not retrieve PR details from GitHub API after retries."
exit 1
fi
# Securely write PR metadata to GITHUB_ENV using EOF delimiters to prevent command injection.
echo "PR_TITLE<<EOF" >> $GITHUB_ENV
echo "$PR_DATA" | jq -r '.title // "Untitled PR"' >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "PR_AUTHOR<<EOF" >> $GITHUB_ENV
echo "$PR_DATA" | jq -r '.author // "unknown"' >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "BASE_SHA=$(echo "$PR_DATA" | jq -r '.base_sha')" >> $GITHUB_ENV
echo "BASE_REF_NAME=$(echo "$PR_DATA" | jq -r '.base_ref')" >> $GITHUB_ENV
echo "HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head_sha')" >> $GITHUB_ENV
# Securely write multiline body to GITHUB_ENV
echo "PR_BODY<<EOF" >> $GITHUB_ENV
echo "$PR_DATA" | jq -r '.body' >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Analyze PR Changes
if: steps.check-disabled.outputs.disabled == 'false'
id: analyze
run: |
source scripts/ci/github-utils.sh
PR_NUMBER=${{ env.PR_NUMBER }}
# Retrieval for PR metrics using GitHub API only.
# poll_pr_metrics encapsulates the logic for cross-referencing metadata and diffs (polling removed).
METRICS_JSON=$(poll_pr_metrics "$PR_NUMBER" 18 10)
if [ -z "$METRICS_JSON" ]; then
echo "::error::Could not retrieve PR metrics after retries."
exit 1
fi
FILES=$(echo "$METRICS_JSON" | jq -r '.files')
FILE_COUNT=$(echo "$METRICS_JSON" | jq -r '.file_count')
ADDITIONS=$(echo "$METRICS_JSON" | jq -r '.additions')
DELETIONS=$(echo "$METRICS_JSON" | jq -r '.deletions')
echo "FILES<<EOF" >> $GITHUB_ENV
echo "$FILES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# Categorize changes
CATEGORIES=()
if echo "$FILES" | grep -qE '^app/|^components/|^lib/'; then
CATEGORIES+=("features")
fi
if echo "$FILES" | grep -qE '^tests/|\.test\.|\.spec\.'; then
CATEGORIES+=("testing")
fi
if echo "$FILES" | grep -qE '^\.github/workflows/|scripts/'; then
CATEGORIES+=("ci")
fi
if echo "$FILES" | grep -qE '\.md$|docs/'; then
CATEGORIES+=("docs")
fi
if echo "$FILES" | grep -qE 'package\.json|tsconfig|eslint|prettier'; then
CATEGORIES+=("build-config")
fi
if echo "$FILES" | grep -qE '\.css$|theme/|styles'; then
CATEGORIES+=("styling")
fi
# Store categories
echo "CATEGORIES=${CATEGORIES[*]}" >> $GITHUB_ENV
# Export metrics
echo "FILE_COUNT=$FILE_COUNT" >> $GITHUB_ENV
echo "ADDITIONS=$ADDITIONS" >> $GITHUB_ENV
echo "DELETIONS=$DELETIONS" >> $GITHUB_ENV
# Determine change scope
if [ "$FILE_COUNT" -le 3 ] && [ "${ADDITIONS:-0}" -le 100 ]; then
SCOPE="small"
elif [ "$FILE_COUNT" -le 10 ] && [ "${ADDITIONS:-0}" -le 500 ]; then
SCOPE="medium"
else
SCOPE="large"
fi
echo "SCOPE=$SCOPE" >> $GITHUB_ENV
- name: Generate PR Description
id: generate-description
if: steps.check-disabled.outputs.disabled == 'false'
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
PR_TEMPLATE_PATH: .github/PULL_REQUEST_TEMPLATE.md
run: |
PROMPT_FILE=$(mktemp)
SCRIPT_FILE=$(mktemp)
# Use a here-document to write the Node.js script to a temporary file.
cat <<'EOF' > "$SCRIPT_FILE"
const fs = require("fs");
// Read environment variables inside the Node.js script
const templatePath = process.env.PR_TEMPLATE_PATH;
const prTitle = process.env.PR_TITLE;
const prAuthor = process.env.PR_AUTHOR;
const prBody = process.env.PR_BODY;
const fileCount = process.env.FILE_COUNT;
const additions = process.env.ADDITIONS;
const deletions = process.env.DELETIONS;
const categories = process.env.CATEGORIES;
const template = fs.readFileSync(templatePath, "utf8");
const prompt = `You are an AI assistant. Your task is to generate a pull request description based on the provided template and PR data.
### PR Template
${template}
### PR Data
PR Title: ${prTitle}
PR Author: ${prAuthor}
PR Body: ${prBody}
Files Changed: ${fileCount}
Lines Added: ${additions}
Lines Deleted: ${deletions}
Impact Areas: ${categories}
### Instructions
Your primary task is to populate the provided pull request template using the PR data.
The final output MUST be a valid JSON object containing a single key, "description".
The value of "description" should be a complete markdown string based on the template.
**Crucial Formatting Rule for 'Change Type':**
Inside the markdown string you generate for the "description" field, you MUST format the "Change Type" section as a single line.
- **Correct format:** \`## Change Type: 🐛 Bug fix (non-breaking change fixing an issue)\`
- **Incorrect format:** Do NOT include the list of other options or checkboxes (\`- [ ] ...\`).
### Output Format
You MUST return a valid JSON object. Do not include markdown formatting like \`\`\`json.
{
"description": "The full, populated pull request description as a markdown string."
}
`;
fs.writeFileSync(process.argv[2], prompt);
EOF
# Execute the script, passing the prompt file path as an argument.
node "$SCRIPT_FILE" "$PROMPT_FILE"
npx tsx scripts/gemini-client.ts --task-file "$PROMPT_FILE" --output "pr_description.json"
- name: Update PR Description
if: steps.check-disabled.outputs.disabled == 'false'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ env.PR_NUMBER }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
try {
let rawData = fs.readFileSync('pr_description.json', 'utf8');
// Extract JSON from markdown code blocks if present
const jsonMatch = rawData.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
rawData = jsonMatch[1].trim();
}
// Try to parse as JSON, handling potential control characters
let result;
try {
result = JSON.parse(rawData);
} catch (parseError) {
// If parsing fails, try to extract the description field manually
const descMatch = rawData.match(/"description"\s*:\s*"([\s\S]*?)(?<!\\)"/);
if (descMatch) {
result = { description: descMatch[1].replace(/\\n/g, '\n').replace(/\\\//g, '/') };
} else {
throw new Error(`Failed to parse JSON and extract description: ${parseError.message}`);
}
}
if (result.description) {
const newBody = result.description;
const prNumber = parseInt(process.env.PR_NUMBER);
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
const currentBody = pr.data.body || '';
// 1. Find the Jules task line anywhere in the current body to ensure it's always found.
const julesTaskRegex = /^PR created automatically by Jules for task .*$/m;
const julesTaskMatch = currentBody.match(julesTaskRegex);
const julesTaskLine = julesTaskMatch ? julesTaskMatch[0] : '';
// 2. Determine the "true" original body content for the details block.
// Using a more flexible regex to handle variations in whitespace or line endings.
let trueOriginalBody = '';
const detailsRegex = /<details>\s*<summary>\s*Original PR Body\s*<\/summary>([\s\S]*?)<\/details>/i;
const detailsMatch = currentBody.match(detailsRegex);
if (detailsMatch) {
// On a re-run, the true original body is inside the existing details block.
trueOriginalBody = detailsMatch[1].trim();
} else {
// On the first run, the entire body is the original body.
trueOriginalBody = currentBody.trim();
}
// 3. Clean the original body by removing the Jules task line from it.
const cleanedOriginalBody = trueOriginalBody.replace(julesTaskRegex, '').trim();
// 4. Build the final body.
let finalBody = '';
if (julesTaskLine) {
finalBody += julesTaskLine + '\n\n';
}
finalBody += newBody;
finalBody += '\n\n<details>\n<summary>Original PR Body</summary>\n\n' + cleanedOriginalBody + '\n</details>';
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
body: finalBody.trim(),
});
} else {
core.setFailed('Generated description is empty or missing from the JSON output.');
}
} catch (error) {
console.error('Error processing description result:', error);
console.error('Raw data:', fs.readFileSync('pr_description.json', 'utf8'));
core.setFailed('Failed to update PR description: ' + error.message);
}
- name: Cleanup
if: always()
run: rm -f pr_description.json
- name: Enhance PR Title
if: steps.check-disabled.outputs.disabled == 'false' && env.DISABLE_TITLE_ENHANCEMENT != 'true'
run: |
CURRENT_TITLE="$PR_TITLE"
# Define Regex Patterns for Conventional Commits
readonly CONVENTIONAL_REGEX='^(feat|fix|docs|style|refactor|test|chore|ci)\('
readonly GENERIC_SCOPE_REGEX='^[a-zA-Z]+\('
# Only enhance if title doesn't have a conventional commit prefix
if [[ ! "$CURRENT_TITLE" =~ $CONVENTIONAL_REGEX ]]; then
# Determine prefix based on categories
CATEGORIES="${{ env.CATEGORIES }}"
if echo "$CATEGORIES" | grep -q "features"; then
PREFIX="feat"
elif echo "$CATEGORIES" | grep -q "ci"; then
PREFIX="ci"
elif echo "$CATEGORIES" | grep -q "docs"; then
PREFIX="docs"
elif echo "$CATEGORIES" | grep -q "testing"; then
PREFIX="test"
elif echo "$CATEGORIES" | grep -q "styling"; then
PREFIX="style"
else
PREFIX="chore"
fi
# Determine scope
SCOPE="${{ env.SCOPE }}"
# Create enhanced title only if it doesn't already have a generic scope format
if [[ ! "$CURRENT_TITLE" =~ $GENERIC_SCOPE_REGEX ]]; then
NEW_TITLE="$PREFIX($SCOPE): $CURRENT_TITLE"
echo "Updating title from: $CURRENT_TITLE"
echo "Updating title to: $NEW_TITLE"
REPO_URL="https://api.github.com/repos/${{ github.repository }}"
PR_API_URL="$REPO_URL/pulls/$PR_NUMBER"
curl -X PATCH -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"title\": \"$NEW_TITLE\"}" \
"$PR_API_URL" || (echo "::error::Could not update PR title" && exit 1)
fi
fi
- name: Log PR Enrichment
run: |
echo "## PR Enrichment Summary"
echo ""
echo "**Scope**: ${{ env.SCOPE }}"
echo "**Files Changed**: ${{ env.FILE_COUNT }}"
echo "**Lines Added**: ${{ env.ADDITIONS }}"
echo "**Lines Deleted**: ${{ env.DELETIONS }}"
echo "**Categories**: ${{ env.CATEGORIES }}"