diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index b1604fd8..bb7c28b8 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -93,7 +93,7 @@ jobs: id: source-branch if: inputs.enforce_source_branches continue-on-error: true - uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-source-branch@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-source-branch@v1.20.0-beta.1 with: github-token: ${{ secrets.MANAGE_TOKEN || github.token }} allowed-branches: ${{ inputs.allowed_source_branches }} @@ -103,7 +103,7 @@ jobs: - name: Validate PR title id: title continue-on-error: true - uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-title@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-title@v1.20.0-beta.1 with: github-token: ${{ github.token }} types: ${{ inputs.pr_title_types }} @@ -113,7 +113,7 @@ jobs: - name: Validate PR description id: description continue-on-error: true - uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-description@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-description@v1.20.0-beta.1 with: min-length: ${{ inputs.min_description_length }} @@ -161,7 +161,7 @@ jobs: - name: Check PR metadata id: metadata continue-on-error: true - uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-metadata@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-metadata@v1.20.0-beta.1 with: github-token: ${{ secrets.MANAGE_TOKEN || github.token }} dry-run: ${{ inputs.dry_run && 'true' || 'false' }} @@ -169,7 +169,7 @@ jobs: - name: Check PR size id: size continue-on-error: true - uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-size@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-size@v1.20.0-beta.1 with: github-token: ${{ secrets.MANAGE_TOKEN || github.token }} base-ref: ${{ github.base_ref }} @@ -179,7 +179,7 @@ jobs: id: labels if: inputs.enable_auto_labeler && !inputs.dry_run continue-on-error: true - uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-labels@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-labels@v1.20.0-beta.1 with: github-token: ${{ secrets.MANAGE_TOKEN || github.token }} config-path: ${{ inputs.labeler_config_path }} @@ -202,7 +202,7 @@ jobs: steps: - name: PR Checks Summary - uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-checks-summary@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-checks-summary@v1.20.0-beta.1 with: source-branch-result: ${{ needs.blocking-checks.outputs.source-branch-result || 'skipped' }} title-result: ${{ needs.blocking-checks.outputs.title-result || 'skipped' }} @@ -217,7 +217,7 @@ jobs: name: Notify needs: [blocking-checks, advisory-checks, pr-checks-summary] if: always() && github.event.pull_request.draft != true && !inputs.dry_run - uses: LerianStudio/github-actions-shared-workflows/.github/workflows/slack-notify.yml@v1.19.1-beta.2 + uses: LerianStudio/github-actions-shared-workflows/.github/workflows/slack-notify.yml@v1.20.0-beta.1 with: status: ${{ (needs.blocking-checks.outputs.source-branch-result == 'failure' || needs.blocking-checks.outputs.title-result == 'failure' || needs.blocking-checks.outputs.description-result == 'failure') && 'failure' || 'success' }} workflow_name: "PR Validation" diff --git a/src/lint/pinned-actions/action.yml b/src/lint/pinned-actions/action.yml index 1d11075b..b95030b8 100644 --- a/src/lint/pinned-actions/action.yml +++ b/src/lint/pinned-actions/action.yml @@ -1,5 +1,5 @@ name: Pinned Actions Check -description: Ensure external action references use final release versions (vX or vX.Y.Z); internal actions may use pre-releases with a warning. +description: Ensure external actions are pinned by commit SHA; internal actions (LerianStudio) accept any semver tag. inputs: files: @@ -7,7 +7,7 @@ inputs: required: false default: "" warn-patterns: - description: Pipe-separated org/owner prefixes to warn instead of fail (e.g. internal orgs not yet on a release tag) + description: Pipe-separated org/owner prefixes to treat as internal (warn instead of fail) required: false default: "LerianStudio/" @@ -57,28 +57,28 @@ runs: done if [ "$is_internal" = true ]; then - # Internal: final releases (vX, vX.Y.Z) pass silently; pre-releases (beta, rc) warn - if printf '%s\n' "$ref" | grep -Eq '^v[0-9]+$|^v[0-9]+\.[0-9]+\.[0-9]+$'; then + # Internal: any semver ref passes — vX, vX.Y.Z, vX.Y.Z-beta.N, vX.Y.Z-rc.N, branches like develop/main + if printf '%s\n' "$ref" | grep -Eq '^v[0-9]+(\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)?$|^(develop|main)$'; then continue fi - echo "::warning file=${file},line=${line_num}::Internal action not pinned to a final release version: $normalized" + echo "::warning file=${file},line=${line_num}::Internal action not pinned to a version: $normalized" warnings=$((warnings + 1)) else - # External: only final releases allowed — vX or vX.Y.Z (no beta, no rc) - if printf '%s\n' "$ref" | grep -Eq '^v[0-9]+$|^v[0-9]+\.[0-9]+\.[0-9]+$'; then + # External: must be a commit SHA (40 or 64 hex chars) + if printf '%s\n' "$ref" | grep -Eiq '^[0-9a-f]{40,64}$'; then continue fi - echo "::error file=${file},line=${line_num}::Unpinned action found: $normalized" + echo "::error file=${file},line=${line_num}::External action not pinned by SHA: $normalized (use full commit SHA with a # vX.Y.Z comment)" violations=$((violations + 1)) fi done < <(grep -nE '^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]].*@' "$file" 2>/dev/null || true) done if [ "$warnings" -gt 0 ]; then - echo "::warning::Found $warnings internal action(s) not pinned to a release version. Consider pinning to vX.Y.Z." + echo "::warning::Found $warnings internal action(s) not pinned to a version. Consider pinning to vX.Y.Z." fi if [ "$violations" -gt 0 ]; then - echo "::error::Found $violations unpinned external action(s). Pin to a final release version (vX or vX.Y.Z)." + echo "::error::Found $violations external action(s) not pinned by commit SHA. Pin using the full SHA with a version comment (e.g., @abc123 # v6)." exit 1 fi - echo "All external actions are properly pinned." + echo "All actions are properly pinned." diff --git a/src/notify/pr-lint-reporter/action.yml b/src/notify/pr-lint-reporter/action.yml index afd81104..a2a215e0 100644 --- a/src/notify/pr-lint-reporter/action.yml +++ b/src/notify/pr-lint-reporter/action.yml @@ -74,7 +74,7 @@ runs: using: composite steps: - name: Post lint report to PR - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: github-token: ${{ inputs.github-token }} script: | @@ -155,36 +155,42 @@ runs: body += `| ${c.label} | ${filesSummary(c)} | ${icon(c.result)} ${c.result} |\n`; } - // ── Failures collapse with annotations ── + // ── Fetch annotations for failures and warnings ── const failed = checks.filter(c => c.result === 'failure'); - if (failed.length > 0) { - const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId, - }); - - const jobAnnotations = {}; - for (const job of jobs) { - if (!failed.find(c => c.jobName === job.name)) continue; - try { - const annotations = await github.paginate(github.rest.checks.listAnnotations, { - owner: context.repo.owner, - repo: context.repo.repo, - check_run_id: job.id, - per_page: 100, - }); - jobAnnotations[job.name] = annotations.filter(a => a.annotation_level === 'failure'); - } catch (e) { - core.warning(`Could not fetch annotations for ${job.name}: ${e.message}`); - } + const needsAnnotations = checks.filter(c => c.result === 'failure' || c.result === 'success'); + + const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId, + }); + + const jobFailureAnnotations = {}; + const jobWarningAnnotations = {}; + for (const job of jobs) { + const check = needsAnnotations.find(c => c.jobName === job.name); + if (!check) continue; + try { + const annotations = await github.paginate(github.rest.checks.listAnnotations, { + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: job.id, + per_page: 100, + }); + jobFailureAnnotations[job.name] = annotations.filter(a => a.annotation_level === 'failure'); + jobWarningAnnotations[job.name] = annotations.filter(a => a.annotation_level === 'warning'); + } catch (e) { + core.warning(`Could not fetch annotations for ${job.name}: ${e.message}`); } + } + // ── Failures collapse ── + if (failed.length > 0) { const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; body += `\n
\n❌ Failures (${failed.length})\n\n`; for (const c of failed) { - const annotations = jobAnnotations[c.jobName] || []; + const annotations = jobFailureAnnotations[c.jobName] || []; body += `### ${c.label}\n\n`; if (annotations.length === 0) { @@ -192,7 +198,6 @@ runs: continue; } - // Group by file path const byFile = {}; for (const a of annotations) { const key = a.path || '__general__'; @@ -216,6 +221,39 @@ runs: body += '
\n\n'; } + // ── Warnings collapse ── + const checksWithWarnings = checks.filter(c => (jobWarningAnnotations[c.jobName] || []).length > 0); + if (checksWithWarnings.length > 0) { + const totalWarnings = checksWithWarnings.reduce((sum, c) => sum + (jobWarningAnnotations[c.jobName] || []).length, 0); + body += `
\n⚠️ Warnings (${totalWarnings})\n\n`; + + for (const c of checksWithWarnings) { + const annotations = jobWarningAnnotations[c.jobName] || []; + body += `### ${c.label}\n\n`; + + const byFile = {}; + for (const a of annotations) { + const key = a.path || '__general__'; + (byFile[key] = byFile[key] || []).push(a); + } + + for (const [file, warns] of Object.entries(byFile)) { + if (file === '__general__') { + for (const w of warns) body += `- ${w.message}\n`; + } else { + body += `**\`${file}\`**\n`; + for (const w of warns) { + const loc = w.start_line ? ` (line ${w.start_line})` : ''; + body += `- \`${file}${loc}\` — ${w.message}\n`; + } + } + body += '\n'; + } + } + + body += '
\n\n'; + } + // ── Footer ── const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; body += `---\n🔍 [View full scan logs](${runUrl})\n`; diff --git a/src/security/codeql-config/action.yml b/src/security/codeql-config/action.yml index 9d0f0e1e..35f65e47 100644 --- a/src/security/codeql-config/action.yml +++ b/src/security/codeql-config/action.yml @@ -51,6 +51,10 @@ runs: echo "$PATHS" | while IFS= read -r p; do echo " - '$p'" done + echo "" + echo "query-filters:" + echo " - exclude:" + echo " id: actions/unpinned-tag" } > "$CONFIG_FILE" echo "skip=false" >> "$GITHUB_OUTPUT" diff --git a/src/security/codeql-reporter/action.yml b/src/security/codeql-reporter/action.yml index 398041db..9c766141 100644 --- a/src/security/codeql-reporter/action.yml +++ b/src/security/codeql-reporter/action.yml @@ -30,7 +30,7 @@ runs: steps: - name: Post CodeQL report to PR id: report - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: SARIF_PATH: ${{ inputs.sarif-path }} LANGUAGES: ${{ inputs.languages }} @@ -125,8 +125,12 @@ runs: return findings; } + // ── Filter suppressed rules ── + // unpinned-tag is handled by our own pinned-actions lint check with org-aware logic + const SUPPRESSED_RULES = ['actions/unpinned-tag']; + // ── Build Report ── - const findings = readSarifFiles(); + const findings = readSarifFiles().filter(f => !SUPPRESSED_RULES.includes(f.rule)); findingsCount = findings.length; body += `## \u{1F6E1}\uFE0F CodeQL Analysis Results\n\n`;