Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .github/workflows/github-actions-clang-tidy-bazel-post.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: clang-tidy-bazel-post

# Runs in the base repository's context with a writable GITHUB_TOKEN, so it
# can post reviewdog comments on PRs opened from forks (which the upstream
# `clang-tidy-bazel` workflow cannot, since fork pull_request runs get a
# read-only token by GitHub's design).
#
# Security: this workflow MUST NOT execute untrusted PR code. It only reads
# the text artifact (clang-tidy.txt) produced by the upstream workflow and
# the metadata file we wrote there ourselves. No checkout of the PR head,
# no bazel build, no scripts from the fork.

on:
workflow_run:
workflows: ["clang-tidy-bazel"]
types:
- completed

permissions:
contents: read
pull-requests: write
# Needed for actions/download-artifact@v4 to fetch from another workflow.
actions: read

jobs:
Post-Reviewdog:
# Skip if the upstream build failed before producing an artifact.
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ${{ vars.USE_SELF_HOSTED == 'true' && 'self-hosted' || 'ubuntu-latest' }}
steps:
# Reviewdog's github-pr-review reporter resolves the local git root
# before flushing comments and silently no-ops if .git is missing.
# Check out the base repo (default branch, shallow) so reviewdog has
# a .git directory to operate against. No fork code involved — this
# is the base repo at HEAD of its default branch.
- name: Check out base repo for reviewdog .git requirement
uses: actions/checkout@v6
with:
fetch-depth: 1
# Default ref in workflow_run context is the base repo's default
# branch, which is the safe choice here. Reviewdog uses GitHub
# API to fetch the actual PR diff, so the local SHA need not
# match the PR head.

- name: Download clang-tidy artifact
uses: actions/download-artifact@v4
with:
name: clang-tidy-bazel
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Load PR metadata
id: meta
run: |
# pr-meta.txt is produced by the upstream workflow from the
# pull_request event payload. It is text we wrote ourselves —
# not arbitrary fork content — and is parsed with a strict
# allowlist below before being exported.
if [ ! -f pr-meta.txt ]; then
echo "::error::pr-meta.txt missing from artifact"
exit 1
fi
while IFS='=' read -r key value; do
case "$key" in
pr_number|head_sha|base_sha|head_repo|base_repo) ;;
*) continue ;;
esac
# Validate values: numbers, hex SHAs, or owner/repo slugs only.
case "$key" in
pr_number)
[[ "$value" =~ ^[0-9]+$ ]] || { echo "::error::bad pr_number"; exit 1; } ;;
head_sha|base_sha)
[[ "$value" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::bad $key"; exit 1; } ;;
head_repo|base_repo)
[[ "$value" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "::error::bad $key"; exit 1; } ;;
esac
echo "$key=$value" >> "$GITHUB_OUTPUT"
done < pr-meta.txt

- name: Synthesize pull_request event payload
id: event
env:
PR_NUMBER: ${{ steps.meta.outputs.pr_number }}
HEAD_SHA: ${{ steps.meta.outputs.head_sha }}
BASE_SHA: ${{ steps.meta.outputs.base_sha }}
HEAD_REPO: ${{ steps.meta.outputs.head_repo }}
BASE_REPO: ${{ steps.meta.outputs.base_repo }}
run: |
# Reviewdog's `github-pr-review` reporter reads GITHUB_EVENT_PATH
# expecting a pull_request payload. The real event here is
# workflow_run, so we synthesize the minimum payload reviewdog
# needs and point GITHUB_EVENT_PATH at it for the next step.
EVENT_PATH="${RUNNER_TEMP}/pr-event.json"
python3 - <<'PY' > "$EVENT_PATH"
import json, os
payload = {
"action": "synchronize",
"number": int(os.environ["PR_NUMBER"]),
"pull_request": {
"number": int(os.environ["PR_NUMBER"]),
"head": {
"sha": os.environ["HEAD_SHA"],
"repo": {"full_name": os.environ["HEAD_REPO"]},
},
"base": {
"sha": os.environ["BASE_SHA"],
"repo": {"full_name": os.environ["BASE_REPO"]},
},
},
"repository": {
"full_name": os.environ["BASE_REPO"],
"owner": {"login": os.environ["BASE_REPO"].split("/")[0]},
"name": os.environ["BASE_REPO"].split("/")[1],
},
}
print(json.dumps(payload))
PY
echo "event_path=${EVENT_PATH}" >> "$GITHUB_OUTPUT"

- name: Set up reviewdog
uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest

- name: Run reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_EVENT_NAME: pull_request
GITHUB_EVENT_PATH: ${{ steps.event.outputs.event_path }}
GITHUB_SHA: ${{ steps.meta.outputs.head_sha }}
GITHUB_REPOSITORY: ${{ steps.meta.outputs.base_repo }}
run: |
reviewdog \
Comment thread
sombraSoft marked this conversation as resolved.
-efm="%E%f:%l:%c: error: %m" \
-efm="%W%f:%l:%c: warning: %m" \
-name="clang-tidy" \
-reporter=github-pr-review \
-filter-mode=added \
-fail-level=any \
< clang-tidy.txt
46 changes: 28 additions & 18 deletions .github/workflows/github-actions-clang-tidy-bazel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ on:
branches:
- master

# Read-only by design: fork PRs get a read-only GITHUB_TOKEN regardless of
# what this block requests, so this workflow only builds clang-tidy and
# uploads the findings as an artifact. The companion workflow
# `clang-tidy-bazel-post` runs on `workflow_run` in the base repo context
# with a writable token and posts the reviewdog comments.
permissions:
contents: read
pull-requests: write

jobs:
Clang-Tidy-Bazel:
Expand All @@ -17,7 +21,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: 'recursive'
# Need full history so reviewdog can diff against the PR base.
# Need full history so the post workflow's reviewdog can diff
# against the PR base via the API.
fetch-depth: 0

- name: Set up bazel
Expand All @@ -30,11 +35,6 @@ jobs:
bazelisk-version: 1.x
bazelisk-cache: true

- name: Set up reviewdog
uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest

- name: Run bazel clang-tidy
env:
BAZEL_CACHE_PASSWORD: ${{ secrets.BAZEL_CACHE_PASSWORD }}
Expand Down Expand Up @@ -88,15 +88,25 @@ jobs:
echo "::endgroup::"
echo "Findings: $(wc -l < clang-tidy.txt)"

- name: Run reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save PR metadata for post workflow
run: |
reviewdog \
-efm="%E%f:%l:%c: error: %m" \
-efm="%W%f:%l:%c: warning: %m" \
-name="clang-tidy" \
-reporter=github-pr-review \
-filter-mode=added \
-fail-level=any \
< clang-tidy.txt
# workflow_run.event.pull_requests[] is empty for fork PRs, so the
# post workflow needs the PR number and head SHA delivered via the
# artifact itself.
{
echo "pr_number=${{ github.event.pull_request.number }}"
echo "head_sha=${{ github.event.pull_request.head.sha }}"
echo "base_sha=${{ github.event.pull_request.base.sha }}"
echo "head_repo=${{ github.event.pull_request.head.repo.full_name }}"
echo "base_repo=${{ github.event.pull_request.base.repo.full_name }}"
} > pr-meta.txt

- name: Upload clang-tidy artifact
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore failing the PR check on lint findings

When a PR produces clang-tidy diagnostics, this pull_request workflow now finishes successfully after uploading clang-tidy.txt; the only remaining -fail-level=any is in the workflow_run companion, and GitHub documents workflow_run as running at the default-branch GITHUB_SHA, not the PR head. That means branch protection on the original clang-tidy-bazel PR check can pass even though the previous inline reviewdog step would have failed the PR check for the same diagnostics.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@sombraSoft sombraSoft May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in fcb0180.

Stage A now re-runs reviewdog with -reporter=local -filter-mode=added -fail-level=any after the artifact upload. Local mode only needs the filesystem and an explicit -diff command, so it works fine under the fork-PR read-only token. Step is positioned after upload-artifact so the post workflow still has the findings to comment on even when this check exits non-zero.

Stage B's guard relaxes from conclusion == 'success' to conclusion != 'cancelled' so it still runs and posts review comments when Stage A's check step failed. Net behaviour: same as the original single-stage workflow — clang-tidy findings in the PR diff fail the clang-tidy-bazel check (branch protection blocks merge) AND the post workflow posts inline review comments. Fork PRs still benefit from the split because the posting half runs in base-repo context with a writable token.

uses: actions/upload-artifact@v4
with:
name: clang-tidy-bazel
path: |
clang-tidy.txt
pr-meta.txt
retention-days: 7
if-no-files-found: error
Loading