Update upgrade-agent plugin to 1.1.290 #87
Workflow file for this run
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: External Plugin PR Quality Gates | |
| on: | |
| pull_request_target: | |
| branches: [main] | |
| paths: | |
| - "plugins/external.json" | |
| types: [opened, synchronize, reopened, edited, ready_for_review] | |
| concurrency: | |
| group: external-plugin-pr-quality-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| detect-changed-plugins: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| changed-plugins: ${{ steps.detect.outputs.changed-plugins }} | |
| changed-count: ${{ steps.detect.outputs.changed-count }} | |
| should-run: ${{ steps.detect.outputs.should-run }} | |
| steps: | |
| - name: Detect changed external plugins | |
| id: detect | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 | |
| with: | |
| script: | | |
| const filePath = 'plugins/external.json'; | |
| const baseRef = context.payload.pull_request.base.sha; | |
| const headRef = context.payload.pull_request.head.sha; | |
| function normalizePath(value) { | |
| if (!value || value === '/') { | |
| return ''; | |
| } | |
| return String(value).trim().replace(/^\/+|\/+$/g, '').toLowerCase(); | |
| } | |
| function toIdentity(plugin) { | |
| return [ | |
| String(plugin?.name ?? '').trim().toLowerCase(), | |
| String(plugin?.source?.repo ?? '').trim().toLowerCase(), | |
| normalizePath(plugin?.source?.path), | |
| ].join('|'); | |
| } | |
| async function readExternalJson(ref) { | |
| const response = await github.rest.repos.getContent({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| path: filePath, | |
| ref, | |
| }); | |
| const encoded = response.data?.content ?? ''; | |
| const decoded = Buffer.from(encoded, 'base64').toString('utf8'); | |
| return JSON.parse(decoded); | |
| } | |
| const basePlugins = await readExternalJson(baseRef); | |
| const headPlugins = await readExternalJson(headRef); | |
| const baseByIdentity = new Map(basePlugins.map((plugin) => [toIdentity(plugin), plugin])); | |
| const changedPlugins = headPlugins.filter((plugin) => { | |
| const identity = toIdentity(plugin); | |
| const basePlugin = baseByIdentity.get(identity); | |
| return !basePlugin || JSON.stringify(basePlugin) !== JSON.stringify(plugin); | |
| }); | |
| core.setOutput('changed-plugins', JSON.stringify(changedPlugins)); | |
| core.setOutput('changed-count', String(changedPlugins.length)); | |
| core.setOutput('should-run', changedPlugins.length > 0 ? 'true' : 'false'); | |
| run-quality-gates: | |
| runs-on: ubuntu-latest | |
| needs: detect-changed-plugins | |
| if: needs.detect-changed-plugins.outputs.should-run == 'true' | |
| outputs: | |
| quality-result: ${{ steps.quality.outputs.quality-result }} | |
| steps: | |
| - name: Checkout main branch | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: main | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Install GitHub Copilot CLI | |
| run: npm install -g @github/copilot | |
| - name: Install @microsoft/vally | |
| run: npm install @microsoft/vally | |
| - name: Run external plugin PR quality gates | |
| id: quality | |
| env: | |
| CHANGED_PLUGINS_JSON: ${{ needs.detect-changed-plugins.outputs.changed-plugins }} | |
| run: | | |
| result=$(node ./eng/external-plugin-pr-quality-gates.mjs --plugins-json "$CHANGED_PLUGINS_JSON") | |
| { | |
| echo 'quality-result<<EOF' | |
| echo "$result" | |
| echo 'EOF' | |
| } >> "$GITHUB_OUTPUT" | |
| sync-pr-state: | |
| runs-on: ubuntu-latest | |
| needs: [detect-changed-plugins, run-quality-gates] | |
| if: always() | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout main branch | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: main | |
| - name: Sync labels and PR status comment | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 | |
| env: | |
| DETECT_JOB_RESULT: ${{ needs.detect-changed-plugins.result }} | |
| SHOULD_RUN: ${{ needs.detect-changed-plugins.outputs.should-run }} | |
| CHANGED_COUNT: ${{ needs.detect-changed-plugins.outputs.changed-count }} | |
| QUALITY_RESULT_JSON: ${{ needs.run-quality-gates.outputs.quality-result }} | |
| QUALITY_JOB_RESULT: ${{ needs.run-quality-gates.result }} | |
| with: | |
| script: | | |
| const path = require('path'); | |
| const { pathToFileURL } = require('url'); | |
| const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); | |
| const marker = '<!-- external-plugin-pr-quality -->'; | |
| const detectJobResult = process.env.DETECT_JOB_RESULT; | |
| const shouldRun = process.env.SHOULD_RUN === 'true'; | |
| const changedCount = Number.parseInt(process.env.CHANGED_COUNT || '0', 10); | |
| const qualityJobResult = process.env.QUALITY_JOB_RESULT; | |
| let qualityResult = { | |
| overall_status: 'not_run', | |
| failure_class: 'none', | |
| checked_plugins: [], | |
| summary: 'No changed external plugin entries were detected in this PR.', | |
| }; | |
| if (detectJobResult === 'failure' || detectJobResult === 'cancelled') { | |
| qualityResult = { | |
| overall_status: 'infra_error', | |
| failure_class: 'infra', | |
| checked_plugins: [], | |
| version_match_status: 'infra_error', | |
| canvas_structure_status: 'infra_error', | |
| summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.', | |
| }; | |
| } else if (shouldRun) { | |
| if (qualityJobResult === 'failure' || qualityJobResult === 'cancelled') { | |
| qualityResult = { | |
| overall_status: 'infra_error', | |
| failure_class: 'infra', | |
| checked_plugins: [], | |
| version_match_status: 'infra_error', | |
| canvas_structure_status: 'infra_error', | |
| summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.', | |
| }; | |
| } else if (process.env.QUALITY_RESULT_JSON) { | |
| qualityResult = JSON.parse(process.env.QUALITY_RESULT_JSON); | |
| } else { | |
| qualityResult = { | |
| overall_status: 'infra_error', | |
| failure_class: 'infra', | |
| checked_plugins: [], | |
| version_match_status: 'infra_error', | |
| canvas_structure_status: 'infra_error', | |
| summary: 'External plugin PR quality checks did not return a result payload.', | |
| }; | |
| } | |
| } | |
| const stateLabel = qualityResult.failure_class === 'submitter_fixes' | |
| ? 'requires-submitter-fixes' | |
| : qualityResult.overall_status === 'pass' || !shouldRun | |
| ? 'ready-for-review' | |
| : 'awaiting-review'; | |
| const desiredLabels = new Set(['external-plugin', stateLabel]); | |
| await intakeState.syncExternalPluginIntakeLabels({ | |
| github, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issueNumber: context.issue.number, | |
| desiredLabels, | |
| }); | |
| const checkedPlugins = Array.isArray(qualityResult.checked_plugins) ? qualityResult.checked_plugins : []; | |
| const header = qualityResult.failure_class === 'submitter_fixes' | |
| ? '## ⚠️ External plugin PR checks require submitter fixes' | |
| : qualityResult.overall_status === 'pass' || !shouldRun | |
| ? '## ✅ External plugin PR checks passed' | |
| : '## ⚠️ External plugin PR checks need maintainer follow-up'; | |
| const MAX_GATE_OUTPUT_CHARS = 2000; | |
| const escapeHtml = (value) => | |
| String(value || '') | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| const escapeMarkdownTableCell = (value) => | |
| String(value ?? '') | |
| .replace(/\r\n?|\n/g, '\n') | |
| .split('\n') | |
| .map((line) => | |
| Array.from(line, (character) => | |
| /^[A-Za-z0-9 .-]$/.test(character) | |
| ? character | |
| : `&#${character.codePointAt(0)};` | |
| ).join('') | |
| ) | |
| .join('<br>'); | |
| const normalizeGitHubUrl = (value) => { | |
| const raw = String(value || '').trim(); | |
| if (!raw) { | |
| return ''; | |
| } | |
| try { | |
| const parsed = new URL(raw); | |
| if (parsed.protocol !== 'https:' || parsed.hostname !== 'github.com') { | |
| return ''; | |
| } | |
| return parsed.toString(); | |
| } catch { | |
| return ''; | |
| } | |
| }; | |
| const TRUNCATED_OUTPUT_MARKER = '\n...output truncated...'; | |
| const truncateGateOutput = (rawOutput) => { | |
| const normalized = escapeHtml(String(rawOutput || '').trim()); | |
| if (!normalized) { | |
| return '_No output captured._'; | |
| } | |
| if (normalized.length <= MAX_GATE_OUTPUT_CHARS) { | |
| return normalized; | |
| } | |
| return `${normalized.slice(0, Math.max(0, MAX_GATE_OUTPUT_CHARS - TRUNCATED_OUTPUT_MARKER.length))}${TRUNCATED_OUTPUT_MARKER}`; | |
| }; | |
| const formatGateOutput = (pluginName, gateName, gateStatus, rawOutput) => { | |
| const summaryPluginName = escapeHtml(String(pluginName || 'unknown')); | |
| const summaryGateName = escapeHtml(String(gateName || 'gate')); | |
| const summaryGateStatus = escapeHtml(String(gateStatus || 'not_run')); | |
| const output = truncateGateOutput(rawOutput); | |
| return [ | |
| '<details>', | |
| `<summary>${summaryPluginName} - ${summaryGateName} (${summaryGateStatus})</summary>`, | |
| '', | |
| '<pre><code>', | |
| output, | |
| '</code></pre>', | |
| '</details>', | |
| ].join('\n'); | |
| }; | |
| const rows = checkedPlugins.length > 0 | |
| ? checkedPlugins.map((entry) => { | |
| const name = escapeMarkdownTableCell(entry?.name || 'unknown'); | |
| const quality = entry?.quality || {}; | |
| const sourceUrl = normalizeGitHubUrl(entry?.source_tree_url || ''); | |
| const locator = escapeMarkdownTableCell(entry?.source?.sha || entry?.source?.ref || 'repository'); | |
| const sourceCell = sourceUrl ? `[${locator}](<${sourceUrl}>)` : locator; | |
| const vallyLintStatus = escapeMarkdownTableCell(quality.vally_lint_status || 'not_run'); | |
| const smokeStatus = escapeMarkdownTableCell(quality.smoke_status || 'not_run'); | |
| const versionMatchStatus = escapeMarkdownTableCell(quality.version_match_status || 'not_run'); | |
| const refShaConsistencyStatus = escapeMarkdownTableCell(quality.ref_sha_consistency_status || 'not_run'); | |
| const canvasStructureStatus = escapeMarkdownTableCell(quality.canvas_structure_status || 'not_run'); | |
| const overallStatus = escapeMarkdownTableCell(quality.overall_status || 'not_run'); | |
| return `| ${name} | ${vallyLintStatus} | ${smokeStatus} | ${versionMatchStatus} | ${refShaConsistencyStatus} | ${canvasStructureStatus} | ${overallStatus} | ${sourceCell} |`; | |
| }) | |
| : ['| _none_ | not_run | not_run | not_run | not_run | not_run | not_run | _n/a_ |']; | |
| const failureDetails = checkedPlugins.flatMap((entry) => { | |
| const name = String(entry?.name || 'unknown'); | |
| const quality = entry?.quality || {}; | |
| const shouldShowVally = quality.vally_lint_status === 'fail' || quality.vally_lint_status === 'infra_error' || String(quality.vally_lint_output || '').trim().length > 0; | |
| const shouldShowSmoke = quality.smoke_status === 'fail' || quality.smoke_status === 'infra_error' || String(quality.smoke_output || '').trim().length > 0; | |
| const shouldShowVersionMatch = quality.version_match_status === 'fail' || quality.version_match_status === 'infra_error' || String(quality.version_match_output || '').trim().length > 0; | |
| const shouldShowRefShaConsistency = quality.ref_sha_consistency_status === 'fail' || quality.ref_sha_consistency_status === 'infra_error' || String(quality.ref_sha_consistency_output || '').trim().length > 0; | |
| const shouldShowCanvasStructure = quality.canvas_structure_status === 'fail' || quality.canvas_structure_status === 'infra_error' || String(quality.canvas_structure_output || '').trim().length > 0; | |
| const details = []; | |
| if (shouldShowVally) { | |
| details.push(formatGateOutput(name, 'vally lint', quality.vally_lint_status, quality.vally_lint_output)); | |
| } | |
| if (shouldShowSmoke) { | |
| details.push(formatGateOutput(name, 'install smoke test', quality.smoke_status, quality.smoke_output)); | |
| } | |
| if (shouldShowVersionMatch) { | |
| details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output)); | |
| } | |
| if (shouldShowRefShaConsistency) { | |
| details.push(formatGateOutput(name, 'ref/sha consistency', quality.ref_sha_consistency_status, quality.ref_sha_consistency_output)); | |
| } | |
| if (shouldShowCanvasStructure) { | |
| details.push(formatGateOutput(name, 'canvas structure', quality.canvas_structure_status, quality.canvas_structure_output)); | |
| } | |
| return details; | |
| }); | |
| const body = [ | |
| marker, | |
| header, | |
| '', | |
| `- **Changed entries detected:** ${changedCount}`, | |
| `- **Workflow state label:** \`${stateLabel}\``, | |
| '', | |
| '### Per-plugin quality summary', | |
| '', | |
| '| Plugin | vally lint | install smoke test | version match | ref/sha consistency | canvas structure | overall | source tree |', | |
| '|---|---|---|---|---|---|---|---|', | |
| ...rows, | |
| '', | |
| ...(failureDetails.length > 0 | |
| ? [ | |
| '### Gate output details', | |
| '', | |
| ...failureDetails, | |
| '', | |
| ] | |
| : []), | |
| String(qualityResult.summary || '').trim() | |
| ? `<pre><code>${escapeHtml(String(qualityResult.summary).trim())}</code></pre>` | |
| : '_No summary provided._', | |
| ].join('\n'); | |
| await intakeState.upsertExternalPluginIntakeComment({ | |
| github, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issueNumber: context.issue.number, | |
| marker, | |
| body, | |
| }); |