diff --git a/.github/workflows/course-schedule-comment.yml b/.github/workflows/course-schedule-comment.yml new file mode 100644 index 000000000000..e5201d6ac38e --- /dev/null +++ b/.github/workflows/course-schedule-comment.yml @@ -0,0 +1,70 @@ +# Based on https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ +# +# This runs with the full repo permissions, but checks out the upstream branch, not the PR. +# Do not run arbitrary code from the PR here! +name: Comment PR with Course Schedule +on: + workflow_run: + workflows: ["Generate Course Schedule"] + types: [completed] + +jobs: + upload: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Setup Rust cache" + uses: ./.github/workflows/setup-rust-cache + + - name: "Generate Schedule on upstream branch" + run: | + cargo run -p mdbook-course --bin course-schedule > upstream-schedule + + - name: "Download artifact from PR workflow" + # actions/download-artifact@v4 cannot do this without being given a PAT, although that + # is not required for public forked repositories. + uses: actions/github-script@v7.0.1 + with: + script: | + var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "course-schedule" + })[0]; + var download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/course-schedule.zip', Buffer.from(download.data)); + + - name: "Unzip artifact" + run: unzip course-schedule.zip + + - name: "Comment on PR if schedules differ" + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var fs = require('fs'); + var pr_number = Number(fs.readFileSync('pr-number')); + var upstream = fs.readFileSync('upstream-schedule').toString(); + var schedule = fs.readFileSync('schedule').toString(); + if (upstream != schedule) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr_number, + body: schedule, + }); + } diff --git a/.github/workflows/course-schedule.yml b/.github/workflows/course-schedule.yml new file mode 100644 index 000000000000..c109f06d0bfa --- /dev/null +++ b/.github/workflows/course-schedule.yml @@ -0,0 +1,35 @@ +# Based on https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ +name: Generate Course Schedule + +on: + pull_request: + paths: + - "src/**.md" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust cache + uses: ./.github/workflows/setup-rust-cache + + - name: Generate Schedule + run: | + mkdir -p ./course-schedule + cargo run -p mdbook-course --bin course-schedule > course-schedule/schedule + + # GitHub does not provide a reliable way to determine the PR number from which + # a workflow_run was triggered (https://github.com/orgs/community/discussions/25220), + # so we'll do the slightly awkward thing and put that in the artifact. This means + # schedules could potentially be spammed to any PR in the repository, but that is + # not an awful outcome (and clear, reportable evidence of abuse). + echo ${{ github.event.number }} > ./course-schedule/pr-number + + - uses: actions/upload-artifact@v4 + with: + name: course-schedule + path: course-schedule/