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
29 changes: 27 additions & 2 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jobs:
pr-check:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -62,9 +61,35 @@ jobs:
run: cargo build --release --manifest-path ci/Cargo.toml -p pr-check

- name: Run pr-check
id: run-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
# For pull_request events (including forks), write the comment to a
# file instead of posting it directly. The fork's GITHUB_TOKEN does
# not have write access to the base repository, so direct posting
# returns 403. The pr-comment workflow picks up this artifact and
# posts the comment with the right permissions.
COMMENT_OUTPUT_FILE: ${{ github.event_name == 'pull_request' && 'pr-check-output/comment.md' || '' }}
run: |
ci/target/release/pr-check ${{ steps.changed.outputs.files }}
mkdir -p pr-check-output
echo "$PR_NUMBER" > pr-check-output/pr_number.txt
if ci/target/release/pr-check ${{ steps.changed.outputs.files }}; then
echo "passed" > pr-check-output/result.txt
else
echo "failed" > pr-check-output/result.txt
fi

- name: Upload check results
if: always() && github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: pr-check-output
path: pr-check-output/

- name: Fail if checks did not pass
if: always()
run: |
result=$(cat pr-check-output/result.txt 2>/dev/null || echo "failed")
[ "$result" = "passed" ]
40 changes: 40 additions & 0 deletions .github/workflows/pr-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: PR Check Comment

on:
workflow_run:
workflows: ["PR Check"]
types: [completed]

jobs:
comment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Download check results
uses: actions/download-artifact@v4
with:
name: pr-check-output
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
continue-on-error: true

- name: Post or update PR comment
if: hashFiles('pr_number.txt') != '' && hashFiles('comment.md') != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
PR_NUMBER=$(cat pr_number.txt)
COMMENT_BODY=$(cat comment.md)

EXISTING_ID=$(gh api "repos/$GH_REPO/issues/$PR_NUMBER/comments" \
--jq '[.[] | select(.body | contains("<!-- pr-check-bot -->"))] | first | .id // empty')

if [ -n "$EXISTING_ID" ]; then
gh api --method PATCH "repos/$GH_REPO/issues/comments/$EXISTING_ID" \
--field body="$COMMENT_BODY"
else
gh api --method POST "repos/$GH_REPO/issues/$PR_NUMBER/comments" \
--field body="$COMMENT_BODY"
fi
37 changes: 30 additions & 7 deletions ci/pr-check/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@
//! - More than one contributor
//! - Repository is at least 3 months old
//!
//! The results are posted as a single comment on the PR (updating an existing
//! bot comment if one already exists). The process exits with a non-zero status
//! code when any hard criterion is not met, causing CI to fail.
//! The results are either posted as a single comment on the PR (updating an
//! existing bot comment if one already exists) or written to a file when the
//! `COMMENT_OUTPUT_FILE` environment variable is set. The latter mode is used
//! in CI to work around the GitHub Actions restriction that prevents fork PRs
//! from writing to the base repository. A separate `pr-comment` workflow then
//! picks up the file and posts the comment with the necessary permissions.
//!
//! The process exits with a non-zero status code when any hard criterion is
//! not met, causing CI to fail.
//!
//! Expected environment variables:
//! GITHUB_TOKEN - a token with `pull-requests: write` permission
//! GITHUB_REPOSITORY - owner/repo, e.g. "analysis-tools-dev/static-analysis"
//! PR_NUMBER - the pull request number
//! GITHUB_TOKEN - a token with `pull-requests: write` permission
//! GITHUB_REPOSITORY - owner/repo, e.g. "analysis-tools-dev/static-analysis"
//! PR_NUMBER - the pull request number
//! COMMENT_OUTPUT_FILE - (optional) path to write the rendered comment body
//! to instead of posting it directly via the API.

use anyhow::{Context, Result, bail};
use askama::Template;
Expand Down Expand Up @@ -470,7 +478,22 @@ async fn main() -> Result<()> {

let comment_body = render_comment(&reports)?;

upsert_comment(&client, &gh_repo, pr_number, &comment_body).await?;
// If COMMENT_OUTPUT_FILE is set, write the comment to that file instead of
// posting it via the API. This is used by the `pull_request` CI workflow to
// avoid the 403 that GitHub returns when a fork PR tries to write comments.
// A separate `pr-comment` workflow picks up the file and posts the comment
// with the write permissions it has as a `workflow_run` job.
if let Ok(output_file) = env::var("COMMENT_OUTPUT_FILE") {
if let Some(parent) = std::path::Path::new(&output_file).parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory for {output_file}"))?;
}
std::fs::write(&output_file, &comment_body)
.with_context(|| format!("Failed to write comment to {output_file}"))?;
eprintln!("Comment written to {output_file}");
} else {
upsert_comment(&client, &gh_repo, pr_number, &comment_body).await?;
}

let any_failures = reports.iter().any(|r| r.any_fail());
if any_failures {
Expand Down
Loading