diff --git a/.github/workflows/docs-lint-v2-comment.yml b/.github/workflows/docs-lint-v2-comment.yml new file mode 100644 index 00000000000..89c037c42fa --- /dev/null +++ b/.github/workflows/docs-lint-v2-comment.yml @@ -0,0 +1,67 @@ +name: Comment on external PRs with linter results + +# This is a continuation of ./docs-lint-v2.yml, to write comments on external +# PRs. +# +# SECURITY: +# This workflow runs with write permissions, in the context of code from an +# external PR. This is safe because no external code is executed. The +# stringified Markdown output from the linter (downloaded as an artifact) is +# directly written as the body of a PR comment. + +on: + workflow_run: + workflows: ['[Docs] Lint v2'] + types: + - completed + +permissions: + pull-requests: write + +jobs: + comment_on_pr: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + steps: + - id: download_artifact + name: 'Download artifact' + uses: actions/github-script@v7 + with: + script: | + const artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }} + }); + const matchingArtifact = artifacts?.data?.artifacts?.find( + (artifact) => artifact.name == 'lint_results' + ); + if (matchingArtifact) { + core.setOutput('contains_results', 'true') + const download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchingArtifact.id, + archive_format: 'zip', + }); + const fs = require('fs'); + fs.writeFileSync('${{ github.workspace }}/lint_results.zip', Buffer.from(download.data)); + } + - id: unzip_results + name: Unzip results file + if: steps.download_artifact.outputs.contains_results == 'true' + run: unzip lint_results.zip + - name: 'Comment on PR' + if: steps.download_artifact.outputs.contains_results == 'true' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const prNumber = Number(fs.readFileSync('./pr_number.txt')); + const lintResults = fs.readFileSync('./lint_results.txt', 'utf8'); + await github.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: lintResults + }); diff --git a/.github/workflows/docs-lint-v2.yml b/.github/workflows/docs-lint-v2.yml index 95735c2d117..cf543f15f1e 100644 --- a/.github/workflows/docs-lint-v2.yml +++ b/.github/workflows/docs-lint-v2.yml @@ -1,4 +1,19 @@ name: '[Docs] Lint v2' + +# Runs the docs linter on PRs that edit docs content. +# There are two branches of this workflow for internal and external PRs, due +# to the security design of GitHub Actions. +# +# Internal PRs: +# Have write permissions, so comments are written directly by reviewdog. +# +# External PRs: +# Have read-only permissions, so lint results are uploaded as an artifact, to +# be written to the PR in a subsequent workflow_run action that has write +# permissions. See ./docs/lint-v2-comment.yml. +# +# See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + on: pull_request: @@ -58,11 +73,11 @@ jobs: | { grep -E "^apps/docs/content/" || test $? = 1; } \ | xargs -r supa-mdx-lint --format rdf \ | reviewdog -f=rdjsonl -reporter=github-pr-review -tee - - name: run linter (external) + - id: external_lint + name: run linter (external) if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository env: BASE_REF: ${{ github.base_ref }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -o pipefail @@ -75,7 +90,15 @@ jobs: LINT_RESULTS=$(run_lints) LINT_EXIT_CODE=$? set -e + echo "LINT_EXIT_CODE=$LINT_EXIT_CODE" >> $GITHUB_OUTPUT if [[ $LINT_EXIT_CODE -ne 0 ]]; then - gh pr comment "$PR_NUMBER" --body "$LINT_RESULTS" - exit 1 + mkdir -p ./__github_actions__pr + echo "${{ github.event.number }}" > ./__github_actions__pr/pr_number.txt + echo "$LINT_RESULTS" > ./__github_actions__pr/lint_results.txt fi + - name: save results as artifact (external) + if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0 + uses: actions/upload-artifact@v4 + with: + name: lint_results + path: __github_actions__pr/