diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04639c09..5678091a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,3 +24,73 @@ jobs: fail-fast: false with: os: ${{ matrix.os }} + notify-failure: + needs: [linux-unit-tests, windows-unit-tests, nuts] + if: >- + failure() && + github.actor != 'svc-cli-bot' && + github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Get failed jobs + id: failed + run: | + FAILED="" + if [ "$LINUX" = "failure" ]; then FAILED="linux-unit-tests"; fi + if [ "$WINDOWS" = "failure" ]; then FAILED="${FAILED:+$FAILED, }windows-unit-tests"; fi + if [ "$NUTS" = "failure" ]; then FAILED="${FAILED:+$FAILED, }nuts"; fi + echo "jobs=$FAILED" >> "$GITHUB_OUTPUT" + env: + LINUX: ${{ needs.linux-unit-tests.result }} + WINDOWS: ${{ needs.windows-unit-tests.result }} + NUTS: ${{ needs.nuts.result }} + - name: Get PR info + id: pr + run: | + PR_JSON=$(gh api "repos/${REPO}/pulls?head=${OWNER}:${BRANCH}&state=open" --jq '.[0] | {number, title}') + echo "number=$(echo "$PR_JSON" | jq -r '.number')" >> "$GITHUB_OUTPUT" + echo "title=$(echo "$PR_JSON" | jq -r '.title')" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + OWNER: ${{ github.repository_owner }} + BRANCH: ${{ github.ref_name }} + - name: Get Slack user ID + id: slack-user + run: | + SLACK_ID=$(echo "$USER_MAP" | jq -r --arg user "$GITHUB_ACTOR" '.[$user] // empty') + if [ -n "$SLACK_ID" ]; then + echo "mention=<@${SLACK_ID}>" >> "$GITHUB_OUTPUT" + else + echo "mention=${GITHUB_ACTOR}" >> "$GITHUB_OUTPUT" + fi + env: + USER_MAP: ${{ secrets.SLACK_USER_MAP }} + GITHUB_ACTOR: ${{ github.actor }} + - name: Notify Slack + if: steps.pr.outputs.number != 'null' + uses: slackapi/slack-github-action@v1.26.0 + env: + SLACK_WEBHOOK_URL: ${{ secrets.CLI_ALERTS_SLACK_WEBHOOK }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + with: + payload: | + { + "text": "Workflow \"tests/${{ steps.failed.outputs.jobs }}\" failed in ${{ github.event.repository.name }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":x: Workflow \"tests/${{ steps.failed.outputs.jobs }}\" failed in ${{ github.event.repository.name }} :x:" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*PR:* <${{ github.event.repository.html_url }}/pull/${{ steps.pr.outputs.number }}|#${{ steps.pr.outputs.number }} ${{ steps.pr.outputs.title }}>\n*Author:* ${{ steps.slack-user.outputs.mention }}\n*Job url:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + } + ] + } diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index cd7cc1b7..adaed912 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -17,7 +17,7 @@ import { EOL } from 'node:os'; import { resolve as pathResolve, join } from 'node:path'; import { spawn } from 'node:child_process'; -import { Flags, loglevel, SfCommand } from '@salesforce/sf-plugins-core'; +import { Flags, loglevel, SfCommand, StandardColors } from '@salesforce/sf-plugins-core'; import { Lifecycle, Messages, SfError } from '@salesforce/core'; import open from 'open'; import got from 'got'; @@ -73,7 +73,14 @@ export default class Doctor extends SfCommand { this.outputDir = pathResolve(flags['output-dir'] ?? process.cwd()); lifecycle.on('Doctor:diagnostic', async (data) => { - this.log(`${data.status} - ${data.testName}`); + const colorMap = { + pass: StandardColors.success, + fail: StandardColors.error, + warn: StandardColors.warning, + unknown: StandardColors.warning, + } as const; + const colorFn = colorMap[data.status]; + this.log(`${colorFn(data.status)} - ${data.testName}`); return Promise.resolve(this.doctor.addDiagnosticStatus(data)); }); @@ -122,7 +129,14 @@ export default class Doctor extends SfCommand { this.log(); this.styledHeader('Suggestions'); - diagnosis.suggestions.forEach((s) => this.log(` * ${s}`)); + const pinnedCount = 2; + const pinned = diagnosis.suggestions.slice(0, pinnedCount); + const actionable = diagnosis.suggestions.slice(pinnedCount); + if (actionable.length) { + actionable.forEach((s) => this.log(` ${StandardColors.warning('⚠')} ${s}`)); + this.log(); + } + pinned.forEach((s) => this.log(` * ${s}`)); if (flags['create-issue']) { const raw = 'https://raw.githubusercontent.com/forcedotcom/cli/main/.github/ISSUE_TEMPLATE/bug_report.md'; @@ -162,51 +176,42 @@ export default class Doctor extends SfCommand { } private generateIssueMarkdown(body: string, diagnosis: SfDoctorDiagnosis): string { - const info = ` -\`\`\` -CLI: -${diagnosis.cliConfig.userAgent} + const diagnosticIcon = (status: string): string => { + if (status === 'pass') return ':white_check_mark:'; + if (status === 'warn') return ':warning:'; + return ':x:'; + }; -Plugin Version: -${diagnosis.versionDetail.pluginVersions.join(EOL)} -\`\`\` -${ - diagnosis.sfdxEnvVars.length - ? ` -\`\`\` -SFDX ENV. VARS. -${diagnosis.sfdxEnvVars.join(EOL)} -\`\`\` -` - : '' -} -${ - diagnosis.sfEnvVars.length - ? ` -\`\`\` -SF ENV. VARS. -${diagnosis.sfEnvVars.join(EOL)} -\`\`\` -` - : '' -} -\`\`\` -Windows: ${diagnosis.cliConfig.windows} -Shell: ${diagnosis.cliConfig.shell} -Channel: ${diagnosis.cliConfig.channel} -\`\`\` ---- -### Diagnostics -${this.doctor - .getDiagnosis() - .diagnosticResults.map( - (res) => `${res.status === 'pass' ? ':white_check_mark:' : ':x:'} ${res.status} - ${res.testName}` - ) - .join(EOL)} -`; + const systemInfo = [ + '```', + 'CLI:', + diagnosis.cliConfig.userAgent, + '', + 'Plugin Version:', + ...diagnosis.versionDetail.pluginVersions, + '```', + ...(diagnosis.sfdxEnvVars.length ? ['', '```', 'SFDX ENV. VARS.', ...diagnosis.sfdxEnvVars, '```'] : []), + ...(diagnosis.sfEnvVars.length ? ['', '```', 'SF ENV. VARS.', ...diagnosis.sfEnvVars, '```'] : []), + '', + '```', + `Windows: ${diagnosis.cliConfig.windows}`, + `Shell: ${diagnosis.cliConfig.shell}`, + `Channel: ${diagnosis.cliConfig.channel}`, + '```', + '---', + '### Diagnostics', + ...this.doctor + .getDiagnosis() + .diagnosticResults.map((res) => `${diagnosticIcon(res.status)} ${res.status} - ${res.testName}`), + ].join(EOL); + + // Remove the frontmatter and Note block, but preserve Summary, Steps To Reproduce, + // and all other sections. Replace the System Information placeholder with actual data. return body - .replace(/---(?:.*\n)*>\s.*\n/gm, '') - .replace(/