diff --git a/.github/workflows/gptchangelog.yml b/.github/workflows/gptchangelog.yml
index 1c2ebe1..01a2b9e 100644
--- a/.github/workflows/gptchangelog.yml
+++ b/.github/workflows/gptchangelog.yml
@@ -255,7 +255,7 @@ jobs:
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3
id: app-token
with:
- app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
+ client-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
private-key: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_PRIVATE_KEY }}
- name: Checkout repository
@@ -732,8 +732,10 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Send Slack notification for sync PR
- uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
+ uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
+ webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
+ webhook-type: incoming-webhook
payload: |
{
"blocks": [
@@ -770,6 +772,3 @@ jobs:
}
]
}
- env:
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
diff --git a/.github/workflows/helm-update-chart.yml b/.github/workflows/helm-update-chart.yml
index 561b7df..0b9e76a 100644
--- a/.github/workflows/helm-update-chart.yml
+++ b/.github/workflows/helm-update-chart.yml
@@ -22,7 +22,7 @@ on:
type: string
required: true
base_branch:
- description: 'Target branch for the PR (default: develop)'
+ description: 'Target branch for the PR. Allowed: main, develop, master, staging (default: main)'
type: string
default: 'main'
scripts_path:
@@ -93,16 +93,33 @@ on:
description: 'Slack bot user ID for Severino (@severino)'
required: false
+permissions:
+ contents: read
+
jobs:
update-chart:
name: Update Chart
runs-on: ${{ inputs.runner_type }}
steps:
+ - name: Validate base_branch
+ env:
+ BASE_BRANCH: ${{ inputs.base_branch }}
+ run: |
+ case "${BASE_BRANCH}" in
+ main|develop|master|staging)
+ echo "✅ base_branch '${BASE_BRANCH}' is allowed"
+ ;;
+ *)
+ echo "::error::Invalid base_branch '${BASE_BRANCH}'. Allowed: main, develop, master, staging"
+ exit 1
+ ;;
+ esac
+
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
- app-id: ${{ secrets.APP_ID }}
+ client-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Extract payload
@@ -149,16 +166,19 @@ jobs:
# Save components array to file for processing
jq -c '.components' /tmp/payload.json > /tmp/components.json
- # CodeQL: untrusted-checkout — false positive. This is a workflow_call
- # triggered by internal dispatch, not a PR event. The ref is a controlled
- # branch name (develop/main), not an untrusted PR head.
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ steps.app-token.outputs.token }}
- ref: ${{ inputs.base_branch }}
fetch-depth: 0
+ - name: Switch to base branch
+ env:
+ BASE_BRANCH: ${{ inputs.base_branch }}
+ run: |
+ git fetch --no-tags origin "${BASE_BRANCH}"
+ git checkout -B "${BASE_BRANCH}" "origin/${BASE_BRANCH}"
+
- name: Import GPG key
if: ${{ inputs.gpg_sign_commits }}
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
diff --git a/.github/workflows/release-notification.yml b/.github/workflows/release-notification.yml
index a36cd34..a8e74bc 100644
--- a/.github/workflows/release-notification.yml
+++ b/.github/workflows/release-notification.yml
@@ -105,6 +105,9 @@ on:
type: boolean
default: false
+permissions:
+ contents: read
+
jobs:
notify:
name: Release Notification
@@ -117,7 +120,7 @@ jobs:
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3
id: app-token
with:
- app-id: ${{ secrets.APP_ID }}
+ client-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b2de026..6aca0ee 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -33,6 +33,9 @@ on:
type: string
default: '2'
+permissions:
+ contents: read
+
jobs:
prepare:
runs-on: ${{ inputs.runner_type }}
@@ -106,7 +109,7 @@ jobs:
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
with:
- app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
+ client-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
private-key: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_PRIVATE_KEY }}
- name: Checkout repository
diff --git a/.github/workflows/self-release.yml b/.github/workflows/self-release.yml
index 2e6be73..0e32d93 100644
--- a/.github/workflows/self-release.yml
+++ b/.github/workflows/self-release.yml
@@ -43,3 +43,39 @@ jobs:
runner_type: ubuntu-latest
stable_releases_only: true
secrets: inherit
+
+ # Force-update the floating major version tag (e.g. v1) to point at the
+ # latest stable release. Enables downstream composites to pin to @v1 and
+ # always resolve to the latest stable v1.x.x release. Stable-only: gated to
+ # main so beta/rc releases from develop/release-candidate don't move v1.
+ update-major-tag:
+ needs: publish-release
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
+ id: app-token
+ with:
+ client-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
+ private-key: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_PRIVATE_KEY }}
+
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+ token: ${{ steps.app-token.outputs.token }}
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
+ with:
+ gpg_private_key: ${{ secrets.LERIAN_CI_CD_USER_GPG_KEY }}
+ passphrase: ${{ secrets.LERIAN_CI_CD_USER_GPG_KEY_PASSWORD }}
+ git_committer_name: ${{ secrets.LERIAN_CI_CD_USER_NAME }}
+ git_committer_email: ${{ secrets.LERIAN_CI_CD_USER_EMAIL }}
+ git_config_global: true
+ git_user_signingkey: true
+ git_tag_gpgsign: true
+
+ - name: Update floating major version tag
+ uses: ./src/config/update-major-tag
diff --git a/.github/workflows/typescript-release.yml b/.github/workflows/typescript-release.yml
index 511efab..0617952 100644
--- a/.github/workflows/typescript-release.yml
+++ b/.github/workflows/typescript-release.yml
@@ -117,7 +117,7 @@ jobs:
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
with:
- app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
+ client-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
private-key: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_PRIVATE_KEY }}
- name: Checkout repository
diff --git a/src/config/update-major-tag/README.md b/src/config/update-major-tag/README.md
new file mode 100644
index 0000000..b66b548
--- /dev/null
+++ b/src/config/update-major-tag/README.md
@@ -0,0 +1,75 @@
+
+
+  |
+ update-major-tag |
+
+
+
+Force-update the floating major version tag (e.g. `v1`) to point at the latest stable `vX.Y.Z` tag in the repository. Intended to run after a successful stable release so callers can pin composite actions to `@v1` and always resolve to the latest stable release.
+
+### Behavior
+
+1. Fetches all tags from the remote.
+2. Finds the greatest stable tag matching `^v[0-9]+\.[0-9]+\.[0-9]+$` (pre-release tags like `-beta.N` / `-rc.N` are ignored).
+3. Derives the major prefix (`v1.26.0 → v1`).
+4. If the major tag already points at the resolved commit, exits with a notice — idempotent.
+5. Otherwise, creates/moves the major tag as an annotated tag and force-pushes it.
+
+### Assumptions
+
+- The caller has already checked out the repository with `fetch-depth: 0` (so all tags are reachable).
+- The checkout was authenticated with a token that has permission to push tags (typically via `actions/checkout@... with.token:`).
+- For signed tags, the caller has imported a GPG key and enabled `git_tag_gpgsign` (`git tag -a` will auto-sign when `tag.gpgSign=true` is set globally).
+
+## Inputs
+
+_None._ All behavior is derived from the repository's tag list.
+
+## Outputs
+
+| Output | Description |
+|---|---|
+| `skip` | `true` when no tag update was performed — either no stable tag was found, or the major tag already pointed at the latest stable commit |
+| `tag-updated` | `true` when the floating major tag was force-pushed to a new commit |
+| `major-tag` | The major tag name that was considered (e.g. `v1`). Empty when no stable tag was found |
+| `latest-tag` | The latest stable tag the major tag was aligned with (e.g. `v1.26.0`). Empty when no stable tag was found |
+
+## Usage
+
+```yaml
+jobs:
+ update-major-tag:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/create-github-app-token@ # v3.1.1
+ id: app-token
+ with:
+ client-id: ${{ secrets.APP_ID }}
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
+
+ - uses: actions/checkout@ # v6
+ with:
+ fetch-depth: 0
+ token: ${{ steps.app-token.outputs.token }}
+
+ - uses: crazy-max/ghaction-import-gpg@ # v7
+ with:
+ gpg_private_key: ${{ secrets.GPG_KEY }}
+ passphrase: ${{ secrets.GPG_PASSPHRASE }}
+ git_committer_name: ${{ secrets.CI_USER_NAME }}
+ git_committer_email: ${{ secrets.CI_USER_EMAIL }}
+ git_config_global: true
+ git_user_signingkey: true
+ git_tag_gpgsign: true
+
+ - uses: LerianStudio/github-actions-shared-workflows/src/config/update-major-tag@v1
+```
+
+## Required permissions
+
+```yaml
+permissions:
+ contents: write
+```
diff --git a/src/config/update-major-tag/action.yml b/src/config/update-major-tag/action.yml
new file mode 100644
index 0000000..48a0527
--- /dev/null
+++ b/src/config/update-major-tag/action.yml
@@ -0,0 +1,83 @@
+name: Update Major Version Tag
+description: Force-update the floating major version tag (e.g. v1) to point at the latest stable semver tag (vX.Y.Z) found in the repository.
+
+outputs:
+ skip:
+ description: 'true when no tag update was performed (no stable tag found, or major already points at the latest stable commit)'
+ value: ${{ steps.update.outputs.skip }}
+ tag-updated:
+ description: 'true when the floating major tag was force-pushed to a new commit'
+ value: ${{ steps.update.outputs.tag-updated }}
+ major-tag:
+ description: 'The major tag name that was considered (e.g. v1). Empty when no stable tag was found.'
+ value: ${{ steps.update.outputs.major-tag }}
+ latest-tag:
+ description: 'The latest stable tag the major tag was aligned with (e.g. v1.26.0). Empty when no stable tag was found.'
+ value: ${{ steps.update.outputs.latest-tag }}
+
+runs:
+ using: composite
+ steps:
+ - id: update
+ name: Resolve and push major version tag
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ git fetch --tags --force --prune
+
+ LATEST=$(git tag --list \
+ | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
+ | sort -V \
+ | tail -n1 || true)
+
+ if [ -z "$LATEST" ]; then
+ echo "::notice::No stable release tag found — skipping major tag update."
+ {
+ echo "skip=true"
+ echo "tag-updated=false"
+ echo "major-tag="
+ echo "latest-tag="
+ } >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ VERSION="${LATEST#v}"
+ MAJOR="v${VERSION%%.*}"
+ SHA=$(git rev-list -n1 "refs/tags/$LATEST")
+
+ CURRENT_SHA=""
+ # Ref snapshot taken right after the initial fetch: using this as the
+ # lease ensures the push aborts if another run advances $MAJOR between
+ # our fetch and our push (re-reading the remote later would defeat the
+ # lease, since it would reflect the other run's update).
+ CURRENT_MAJOR_REF=""
+ if git rev-parse --verify --quiet "refs/tags/$MAJOR" >/dev/null; then
+ CURRENT_SHA=$(git rev-list -n1 "refs/tags/$MAJOR")
+ CURRENT_MAJOR_REF=$(git rev-parse "refs/tags/$MAJOR")
+ fi
+
+ if [ "$CURRENT_SHA" = "$SHA" ]; then
+ echo "::notice::$MAJOR already points at $LATEST ($SHA) — nothing to do."
+ {
+ echo "skip=true"
+ echo "tag-updated=false"
+ echo "major-tag=$MAJOR"
+ echo "latest-tag=$LATEST"
+ } >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ echo "Moving $MAJOR → $LATEST ($SHA)"
+ git tag -f -a "$MAJOR" "$SHA" -m "Release $MAJOR ($LATEST)"
+
+ LEASE_SHA="${CURRENT_MAJOR_REF:-0000000000000000000000000000000000000000}"
+ git push origin "refs/tags/$MAJOR:refs/tags/$MAJOR" \
+ --force-with-lease="refs/tags/$MAJOR:$LEASE_SHA"
+
+ {
+ echo "skip=false"
+ echo "tag-updated=true"
+ echo "major-tag=$MAJOR"
+ echo "latest-tag=$LATEST"
+ } >> "$GITHUB_OUTPUT"
diff --git a/src/lint/pinned-actions/README.md b/src/lint/pinned-actions/README.md
index 02578de..41bba25 100644
--- a/src/lint/pinned-actions/README.md
+++ b/src/lint/pinned-actions/README.md
@@ -5,35 +5,62 @@
-Ensure all third-party GitHub Action references use pinned versions (`@vX.Y.Z` or `@sha`), not mutable refs like `@main` or `@master`.
+Enforces the repository pinning policy on every `uses:` reference in workflow and composite files. External actions must be pinned by commit SHA; internal (`LerianStudio/`) references are validated against the composite-vs-reusable policy below.
+
+## Policy
+
+| Ref kind | Detection | Required pin | Severity on violation |
+|---|---|---|---|
+| **External action** (anything outside the org) | `uses:` path does not match any `warn-patterns` prefix | Full commit SHA (40–64 hex chars) | ❌ **error** — fails the step |
+| **Internal composite** | `uses:` path contains `/src/` | Floating major tag (`@v1`, `@v2`, …) or testing branch (`@develop` / `@main`) | ⚠️ **warning** |
+| **Internal reusable workflow** | `uses:` path contains `/.github/workflows/` | Exact version tag (`@v1.2.3`, `@v1.2.3-beta.1`) or testing branch (`@develop` / `@main`) | ⚠️ **warning** |
+| **Internal, unknown shape** | matches `warn-patterns` but neither path pattern above | Any semver-like tag or testing branch (legacy tolerance) | ⚠️ **warning** |
+
+**Rationale**
+
+- Composites are small, low-risk building blocks. Floating `@v1` lets callers track the latest stable major without bumping per patch — the repo release pipeline moves `@v1` atomically on each stable release in `main`.
+- Reusable workflows orchestrate entire pipelines with broader blast radius. Exact pinning (`@v1.2.3`) forces explicit, auditable caller updates.
+- External actions use SHA pins because upstream tags are mutable — Dependabot keeps the SHAs fresh automatically.
## Inputs
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
-| `files` | Space-separated list of workflow/composite files to check | No | `` |
-| `ignore-patterns` | Pipe-separated org/owner prefixes to skip (e.g. internal actions) | No | `LerianStudio/` |
+| `files` | Comma-separated list of workflow/composite files to check (empty = skip) | No | `""` |
+| `warn-patterns` | Pipe-separated org/owner prefixes treated as internal (warn-only) | No | `LerianStudio/` |
-## Usage as composite step
+## Usage
```yaml
-- name: Checkout
- uses: actions/checkout@v4
-
- name: Pinned Actions Check
- uses: LerianStudio/github-actions-shared-workflows/src/lint/pinned-actions@v1.2.3
+ uses: LerianStudio/github-actions-shared-workflows/src/lint/pinned-actions@v1
with:
- files: ".github/workflows/ci.yml .github/workflows/deploy.yml"
- ignore-patterns: "LerianStudio/|my-org/"
+ files: ".github/workflows/ci.yml,.github/workflows/deploy.yml"
```
-## Usage via reusable workflow
+## Examples
```yaml
-jobs:
- lint:
- uses: LerianStudio/github-actions-shared-workflows/.github/workflows/pr-lint.yml@v1.2.3
- secrets: inherit
+# ✅ External action — SHA pin (required)
+uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+
+# ❌ External action — tag pin (error)
+uses: actions/checkout@v6
+
+# ✅ Internal composite — floating major
+uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@v1
+
+# ⚠️ Internal composite — exact version (warning, should be @v1)
+uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@v1.24.1
+
+# ✅ Internal reusable workflow — exact version
+uses: LerianStudio/github-actions-shared-workflows/.github/workflows/release.yml@v1.26.0
+
+# ⚠️ Internal reusable workflow — floating major (warning, should be @vX.Y.Z)
+uses: LerianStudio/github-actions-shared-workflows/.github/workflows/release.yml@v1
+
+# ✅ Either kind — develop/main for testing a change
+uses: LerianStudio/github-actions-shared-workflows/src/config/labels-sync@develop
```
## Required permissions
diff --git a/src/lint/pinned-actions/action.yml b/src/lint/pinned-actions/action.yml
index b95030b..b3d8bdc 100644
--- a/src/lint/pinned-actions/action.yml
+++ b/src/lint/pinned-actions/action.yml
@@ -57,12 +57,32 @@ runs:
done
if [ "$is_internal" = true ]; 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
+ # Internal pinning policy:
+ # - Composite actions (path contains /src/) → floating major tag @vN
+ # - Reusable workflows (path contains /.github/workflows/) → exact version tag @vN.M.P[-pre]
+ # - Testing branches (develop|main) are always accepted for both
+ # - Unknown internal ref shapes fall back to any semver ref (legacy tolerance)
+ target=${normalized%@*}
+
+ if printf '%s\n' "$target" | grep -Fq '/src/'; then
+ if printf '%s\n' "$ref" | grep -Eq '^v[0-9]+$|^(develop|main)$'; then
+ continue
+ fi
+ echo "::warning file=${file},line=${line_num}::Internal composite must use floating major tag (e.g. @v1) or develop/main for testing: $normalized"
+ warnings=$((warnings + 1))
+ elif printf '%s\n' "$target" | grep -Fq '/.github/workflows/'; then
+ 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 reusable workflow must use exact version tag (e.g. @v1.2.3) or develop/main for testing: $normalized"
+ warnings=$((warnings + 1))
+ else
+ 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 version: $normalized"
+ warnings=$((warnings + 1))
fi
- echo "::warning file=${file},line=${line_num}::Internal action not pinned to a version: $normalized"
- warnings=$((warnings + 1))
else
# External: must be a commit SHA (40 or 64 hex chars)
if printf '%s\n' "$ref" | grep -Eiq '^[0-9a-f]{40,64}$'; then
diff --git a/src/security/codeql-reporter/README.md b/src/security/codeql-reporter/README.md
index c042875..9eb3a98 100644
--- a/src/security/codeql-reporter/README.md
+++ b/src/security/codeql-reporter/README.md
@@ -7,6 +7,8 @@
Composite action that reads CodeQL SARIF output and posts a formatted security report as a PR comment. Uses an idempotent comment strategy (updates existing comment on re-runs). Designed to run after [`codeql-analyze`](../codeql-analyze/).
+Findings parsed from SARIF are cross-referenced with the Code Scanning REST API (`listAlertsForRepo` on `refs/pull//merge`) so alerts that were **dismissed** via the Security tab or are already **fixed** on the merge ref do not show up in the PR comment. A footer notes the number of hidden findings so reviewers know the comment omitted some. If the API call fails (e.g. missing permissions), the action degrades gracefully to the SARIF-only view and emits a warning.
+
## Inputs
| Input | Description | Required | Default |
@@ -54,7 +56,9 @@ steps:
```yaml
permissions:
contents: read
- security-events: write
+ security-events: write # required for alert-state enrichment; `read` is enough on its own
pull-requests: write
actions: read
```
+
+> `security-events: write` (already required by [`codeql-analyze`](../codeql-analyze/)) covers the `listAlertsForRepo` call used to filter dismissed/fixed alerts. If only `read` is granted the enrichment still works; if the permission is missing entirely the action falls back to the raw SARIF view with a warning.
diff --git a/src/security/codeql-reporter/action.yml b/src/security/codeql-reporter/action.yml
index 9c76614..da87a60 100644
--- a/src/security/codeql-reporter/action.yml
+++ b/src/security/codeql-reporter/action.yml
@@ -73,6 +73,18 @@ runs:
const truncate = (str, max) =>
str.length > max ? str.substring(0, max - 3) + '...' : str;
+ // Normalize file paths to repo-relative form. SARIF can emit absolute
+ // file:// URIs (e.g. file:///github/workspace/src/foo.js) or leading
+ // ./ prefixes, while the Code Scanning API always returns clean
+ // repo-relative paths — normalizing both sides keeps the (rule, path,
+ // line) lookup key aligned.
+ const normalizePath = (p) =>
+ String(p ?? '')
+ .replace(/^file:\/\/\/github\/workspace\//, '')
+ .replace(/^file:\/\//, '')
+ .replace(/^\.\//, '')
+ .replace(/^\//, '');
+
// ── Read SARIF files ──
function readSarifFiles() {
const findings = [];
@@ -109,7 +121,7 @@ runs:
rule: result.ruleId || 'unknown',
severity: result.level || 'warning',
message: result.message?.text || rule.shortDescription?.text || 'No description',
- file: location?.artifactLocation?.uri || 'unknown',
+ file: normalizePath(location?.artifactLocation?.uri) || 'unknown',
line: location?.region?.startLine || 0,
description: rule.fullDescription?.text || rule.shortDescription?.text || '',
tags: rule.properties?.tags || [],
@@ -129,8 +141,72 @@ runs:
// unpinned-tag is handled by our own pinned-actions lint check with org-aware logic
const SUPPRESSED_RULES = ['actions/unpinned-tag'];
+ // ── Enrich findings with Code Scanning alert states ──
+ // SARIF is structural: it re-reports the same locations on every run, even after
+ // a reviewer dismisses the alert via the Security tab. Cross-reference the REST
+ // API so dismissed/fixed alerts stop showing up in the PR comment.
+ async function filterByAlertStates(rawFindings) {
+ const prNumber = context.payload.pull_request?.number || context.issue?.number;
+ if (!prNumber) {
+ return { findings: rawFindings, hidden: 0, enriched: false };
+ }
+
+ let alerts;
+ try {
+ alerts = await github.paginate(
+ github.rest.codeScanning.listAlertsForRepo,
+ {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: `refs/pull/${prNumber}/merge`,
+ per_page: 100,
+ }
+ );
+ } catch (e) {
+ core.warning(
+ `Could not fetch Code Scanning alert states (falling back to SARIF-only view): ${e.message}`
+ );
+ return { findings: rawFindings, hidden: 0, enriched: false };
+ }
+
+ // Bucket alert states by (ruleId, path, startLine). A single location can carry
+ // multiple alert rows across history; if any are currently open, we must not
+ // suppress it — only suppress when every alert at that location is dismissed/fixed.
+ const statesByKey = new Map();
+ for (const alert of alerts) {
+ const ruleId = alert.rule?.id;
+ const loc = alert.most_recent_instance?.location;
+ const alertPath = normalizePath(loc?.path);
+ if (!ruleId || !alertPath) continue;
+ const key = `${ruleId}\u0000${alertPath}\u0000${loc?.start_line ?? 0}`;
+ if (!statesByKey.has(key)) statesByKey.set(key, []);
+ statesByKey.get(key).push(alert.state);
+ }
+
+ const suppressedKeys = new Set();
+ for (const [key, states] of statesByKey) {
+ if (states.includes('open')) continue;
+ if (states.some(s => s === 'dismissed' || s === 'fixed')) {
+ suppressedKeys.add(key);
+ }
+ }
+
+ const kept = [];
+ let hidden = 0;
+ for (const f of rawFindings) {
+ const key = `${f.rule}\u0000${f.file}\u0000${f.line || 0}`;
+ if (suppressedKeys.has(key)) {
+ hidden++;
+ } else {
+ kept.push(f);
+ }
+ }
+ return { findings: kept, hidden, enriched: true };
+ }
+
// ── Build Report ──
- const findings = readSarifFiles().filter(f => !SUPPRESSED_RULES.includes(f.rule));
+ const rawFindings = readSarifFiles().filter(f => !SUPPRESSED_RULES.includes(f.rule));
+ const { findings, hidden: hiddenCount, enriched } = await filterByAlertStates(rawFindings);
findingsCount = findings.length;
body += `## \u{1F6E1}\uFE0F CodeQL Analysis Results\n\n`;
@@ -142,6 +218,9 @@ runs:
body += `\u26A0\uFE0F Some SARIF files could not be parsed and no findings were recovered. Check the analysis step logs.\n\n`;
} else if (findings.length === 0) {
body += `\u2705 No security issues found.\n\n`;
+ if (enriched && hiddenCount > 0) {
+ body += `_${hiddenCount} finding(s) hidden (dismissed or fixed). See the Security tab for the full list._\n\n`;
+ }
} else {
hasFindings = true;
@@ -179,6 +258,10 @@ runs:
}
body += '\n';
+
+ if (enriched && hiddenCount > 0) {
+ body += `_${hiddenCount} finding(s) hidden (dismissed or fixed). See the Security tab for the full list._\n\n`;
+ }
}
// ── Useful Links ──