diff --git a/.github/workflows/golden-test-build.yml b/.github/workflows/golden-test-build.yml new file mode 100644 index 00000000..6fcb0121 --- /dev/null +++ b/.github/workflows/golden-test-build.yml @@ -0,0 +1,47 @@ +name: Golden Test + +# NilAway output may change due to introduction of new feature or bug fixes. Since NilAway is still +# at early stage of development, constantly updating / maintaining the golden test output will be +# a burden. Therefore, we run this as a separate CI job and post the differences as a PR comment +# for manual reviews. +# +# Note that this workflow is triggered on `pull_request` event, where if the PR is created from +# forked repository, the GITHUB_TOKEN will not have necessary write permission to post the comments. +# To work around this (and to provide proper isolation), we follow the recommended approach [1] of +# separating job into two parts: (1) build and upload results as artifacts in untrusted environment +# (here), and then (2) trigger a follow-up job that downloads the artifacts and posts the comment in +# trusted environment (see .github/workflows/golden-test-comment.yml). +# +# [1]: https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ +on: + pull_request: + +jobs: + golden-test: + name: Golden Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + name: Check out repository + + - name: Fetch base branch (${{ github.event.pull_request.base.ref }}) locally + run: git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.22.x + cache: false + + - name: Golden Test + id: golden_test + # Run golden test by comparing HEAD and the base branch (the target branch of the PR). + # GitHub Actions terminates the job if it hits the resource limits. Here we limit the + # memory usage to 8GiB to avoid that. + run: | + make golden-test GOMEMLIMIT=8192MiB ARGS="-base-branch ${{ github.event.pull_request.base.ref }} -result-file ${{ runner.temp }}/golden-test-result.md" + + - uses: actions/upload-artifact@v4 + with: + name: golden-test-comment.md + path: ${{ runner.temp }}/golden-test-result.md diff --git a/.github/workflows/golden-test-comment.yml b/.github/workflows/golden-test-comment.yml new file mode 100644 index 00000000..cdedaff7 --- /dev/null +++ b/.github/workflows/golden-test-comment.yml @@ -0,0 +1,107 @@ +name: Golden Test [Comment] + +# See ".github/workflows/golden-test-build.yml" for more details. +on: + workflow_run: + workflows: ["Golden Test"] + types: + - completed + +jobs: + upload: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + # We do not have a good way to find the PR number from the workflow run event [1]. Therefore, + # we try to use the search API to find the PR that has the same SHA as the workflow run. + # This is a workaround until GitHub provides a better way to find the associated PR. + # + # [1]: https://github.com/orgs/community/discussions/25220 + - name: Find associated pull request + id: pr + uses: actions/github-script@v7 + with: + script: | + const response = await github.rest.search.issuesAndPullRequests({ + q: 'repo:${{ github.repository }} is:pr is:merged sha:${{ github.event.workflow_run.head_sha }}', + per_page: 1, + }); + const items = response.data.items; + if (items.length < 1) { + core.setFailed('No pull request found for the workflow run') + return; + } + const prNumber = items[0].number; + console.info("Pull request number is", prNumber); + return prNumber; + + - name: Download Golden Test result 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 matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "golden-test-result.md"; + })[0]; + const download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + }); + const fsp = require('fs').promises; + await fsp.writeFile('${{github.workspace}}/golden-test-result.md', Buffer.from(download.data)); + + - name: Upload the Golden Test result + uses: actions/github-script@v7 + with: + script: | + const fsp = require('fs').promises; + + const issueNumber = ${{ steps.pr.outputs.result }}; + const owner = context.repo.owner; + const repo = context.repo.repo; + const rawData = await fsp.readFile('./golden-test-result.md', 'utf8'); + + // GitHub API has a limit of 65536 bytes for a comment body, so here we shrink the + // diff part (anything between
and
) to 10,000 characters if it + // is too long. + const pattern = /(
)([\s\S]*?)(<\/details>)/; + + const body = rawData.replace(pattern, function(match, p1, p2, p3) { + if (p2.length > 10000) { + return p1 + p2.substring(0, 5000) + '\n\n ...(truncated)...\n\n' + p2.substring(p2.length - 5000) + p3; + } + // No need to change anything if it is not too long. + return match; + }); + + // First find the comments made by the bot. + const comments = await github.rest.issues.listComments({ + owner: owner, + repo: repo, + issue_number: issueNumber + }); + const botComment = comments.data.find(comment => comment.user.login === 'github-actions[bot]' && comment.body.startsWith('## Golden Test')); + + // Update or create the PR comment. + if (botComment) { + await github.rest.issues.updateComment({ + owner: owner, + repo: repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: issueNumber, + body: body + }); + } diff --git a/.github/workflows/golden-test.yml b/.github/workflows/golden-test.yml deleted file mode 100644 index 7fd98863..00000000 --- a/.github/workflows/golden-test.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Golden Test - -# NilAway output may change due to introduction of new feature or bug fixes. Since NilAway is still -# at early stage of development, constantly updating / maintaining the golden test output will be -# a burden. Therefore, we run this as a separate CI job and post the differences as a PR comment -# for manual reviews. -on: - pull_request: - -jobs: - golden-test: - name: Golden Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - name: Check out repository - - - name: Fetch base branch (${{ github.event.pull_request.base.ref }}) locally - run: git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }} - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.22.x - cache: false - - - name: Golden Test - id: golden_test - # Run golden test by comparing HEAD and the base branch (the target branch of the PR). - # GitHub Actions terminates the job if it hits the resource limits. Here we limit the - # memory usage to 8GiB to avoid that. - run: | - make golden-test GOMEMLIMIT=8192MiB ARGS="-base-branch ${{ github.event.pull_request.base.ref }} -result-file ${{ runner.temp }}/golden-test-result.md" - - - uses: actions/github-script@v7 - with: - script: | - const fsp = require('fs').promises; - - const issueNumber = context.issue.number; - const owner = context.repo.owner; - const repo = context.repo.repo; - const rawData = await fsp.readFile(`${{ runner.temp }}/golden-test-result.md`, 'utf8'); - - // GitHub API has a limit of 65536 bytes for a comment body, so here we shrink the - // diff part (anything between
and
) to 10,000 characters if it - // is too long. - const pattern = /(
)([\s\S]*?)(<\/details>)/; - - const body = rawData.replace(pattern, function(match, p1, p2, p3) { - if (p2.length > 10000) { - return p1 + p2.substring(0, 5000) + '\n\n ...(truncated)...\n\n' + p2.substring(p2.length - 5000) + p3; - } - // No need to change anything if it is not too long. - return match; - }); - - // First find the comments made by the bot. - const comments = await github.rest.issues.listComments({ - owner: owner, - repo: repo, - issue_number: issueNumber - }); - const botComment = comments.data.find(comment => comment.user.login === 'github-actions[bot]' && comment.body.startsWith('## Golden Test')); - - // Update or create the PR comment. - if (botComment) { - await github.rest.issues.updateComment({ - owner: owner, - repo: repo, - comment_id: botComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: issueNumber, - body: body - }); - }