Sitelet https://github.com/fluentassertions/fluentassertions/pull/3299
Skip to content

Fix Qodana PR scans failing on pull requests from forks - #3299

Merged
dennisdoomen merged 1 commit into
mainfrom
dennisdoomen-psychic-engine
Aug 10, 2026
Merged

dennisdoomen merged 1 commit into
mainfrom
dennisdoomen-psychic-engine

Conversation

@dennisdoomen

Copy link
Copy Markdown
Member

Problem

The Qodana (PR) workflow fails on every pull request that comes from a fork, for example run 31382941522 on #3297:

env:
  QODANA_TOKEN:            <- empty
...
Starting from version 2023.2 release versions of Qodana Linters require
connection to Qodana Cloud. ... provide the token as the QODANA_TOKEN
environment variable.
##[error]qodana scan failed with exit code 1

QODANA_TOKEN is empty because GitHub withholds every secret from a pull_request run that originates from a fork:

With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository.

— Events that trigger workflows → pull_request → Workflows in forked repositories

That restriction is enforced at the run level and covers environment secrets too, so scoping QODANA_TOKEN to the qodana-pr environment did not help. The comment in the workflow assumed that adding required reviewers to the environment would gate fork contributors and then hand them the token, but reviewer approval cannot unlock a secret the run was never entitled to receive.

The run history confirms the cause: the same branch passed while it lived in this repository and started failing once it was pushed to a fork.

Fix

Switch the trigger to pull_request_target, which runs in the trusted base-branch context and therefore does receive secrets, and check out refs/pull/<n>/head so the contributor's commit is still what gets analysed. That ref lives in this repository and resolves for forks, so no cross-repository clone is needed.

Because the job now runs untrusted code while a secret is in scope, the qodana-pr environment has been given required reviewers (@dennisdoomen and @jnyrup, configured in repository settings, not in this diff). A maintainer has to approve each run before a contributor's code executes with the token. QODANA_TOKEN remains scoped to that environment, so no other repository or organization secret is ever exposed.

The workflow comment has been rewritten to explain why both parts are needed, so neither gets removed later by accident.

Notes

  • This only takes effect once merged, because pull_request_target reads the workflow from the base branch. Re-running an existing fork PR before then will still fail.
  • Every push to a fork PR now queues a fresh approval. That is the cost of gating untrusted code against a secret.
  • Contributors can no longer change this workflow from inside their own pull request, which is intended hardening.
  • The couldn't find remote ref warning stays for fork PRs, since the head branch genuinely is not on origin. It is not fatal; the action falls back to the webhook payload to work out the merge base, which is what the previously passing runs did as well.

GitHub withholds every secret, including environment secrets, from a pull_request run that originates from a fork. QODANA_TOKEN was therefore empty on fork PRs and the scan aborted with qodana scan failed with exit code 1.

Switch the trigger to pull_request_target, which runs in the trusted base-branch context and does receive secrets, and check out refs/pull/<n>/head to still analyse the contributor's commit.

Because that runs untrusted code while a secret is in scope, the qodana-pr environment now has required reviewers, so a maintainer must approve each run first. QODANA_TOKEN stays scoped to that environment, so no other secret is exposed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@dennisdoomen
dennisdoomen requested a review from jnyrup August 10, 2026 12:37
@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Test Results

    37 files  ±0      37 suites  ±0   2m 40s ⏱️ -14s
 6 434 tests ±0   6 432 ✅ ±0  1 💤 ±0  1 ❌ ±0 
39 950 runs  ±0  39 943 ✅ ±0  6 💤 ±0  1 ❌ ±0 

For more details on these failures, see this check.

Results for commit cc462d0. ± Comparison against base commit 7c5f77a.

This pull request removes 10 and adds 8 tests. Note that renamed tests count towards both.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)

♻️ This comment has been updated with latest results.

@dennisdoomen
dennisdoomen merged commit 2b26218 into main Aug 10, 2026
9 of 12 checks passed
@dennisdoomen
dennisdoomen deleted the dennisdoomen-psychic-engine branch August 10, 2026 16:56
This was referenced Sep 14, 2026
This was referenced Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants