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`;