diff --git a/.github/workflows/diff.yml b/.github/workflows/diff.yml new file mode 100644 index 000000000..39f1d19eb --- /dev/null +++ b/.github/workflows/diff.yml @@ -0,0 +1,181 @@ +name: Munge PR + +on: + pull_request_target: + +permissions: + contents: read + pull-requests: write + +defaults: + run: + shell: 'bash -Eeuo pipefail -x {0}' + +env: + # https://github.com/docker-library/bashbrew/issues/10 + GIT_LFS_SKIP_SMUDGE: 1 + +jobs: + + gather: + name: Gather Metadata + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + # ideally this would be "github.event.pull_request.merge_commit_sha" but according to https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls#get-a-pull-request if "mergeable" is null (meaning there's a background job in-progress to check mergeability), that value is undefined... + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - id: gather + name: Affected Images + run: | + (set +x; echo "::stop-commands::$(echo -n ${{ github.token }} | sha256sum | head -c 64)") + git fetch --quiet https://github.com/docker-library/official-images.git master + externalPins="$(git diff --no-renames --name-only FETCH_HEAD...HEAD -- '.external-pins/*/**')" + externalPinTags="$( + if [ -n "$externalPins" ]; then + # doing backflips to run "tag.sh" from master instead of from the PR + git show FETCH_HEAD:.external-pins/tag.sh > ~/master-external-pins-tag.sh + chmod +x ~/master-external-pins-tag.sh + ~/master-external-pins-tag.sh $externalPins + fi + )" + images="$(git diff --no-renames --name-only FETCH_HEAD...HEAD -- library/)" + if [ -n "$images" ]; then + new="$(git diff --no-renames --name-only --diff-filter=A FETCH_HEAD...HEAD -- $images)" + deleted="$(git diff --no-renames --name-only --diff-filter=D FETCH_HEAD...HEAD -- $images)" + else + new= + deleted= + fi + export images new deleted externalPins externalPinTags + images="$(jq -cn ' + (env.images | rtrimstr("\n") | split("\n")) as $images + | (env.new | rtrimstr("\n") | split("\n")) as $new + | (env.deleted | rtrimstr("\n") | split("\n")) as $deleted + | (env.externalPins | rtrimstr("\n") | split("\n")) as $externalPins + | (env.externalPinTags | rtrimstr("\n") | split("\n")) as $externalPinTags + | { + images: $images, + count: ($images | length), + new: $new, + deleted: $deleted, + externalPins: $externalPins, + externalPinTags: $externalPinTags, + externalPinsCount: ($externalPins | length), + } + | .imagesAndExternalPinsCount = (.count + .externalPinsCount) # man, I *really* do not love GitHub Actions expressions... + ')" + jq . <<<"$images" + set +x + echo "::$(echo -n ${{ github.token }} | sha256sum | head -c 64)::" + echo "images=$images" >> "$GITHUB_OUTPUT" + outputs: + images: '${{ steps.gather.outputs.images }}' + + diff: + name: Diff Comment + runs-on: ubuntu-latest + needs: gather + if: fromJSON(needs.gather.outputs.images).imagesAndExternalPinsCount > 0 + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + # again, this would ideally be "github.event.pull_request.merge_commit_sha" but we might not have that yet when this runs, so we compromise by checkout out the latest code from the target branch (so we get the latest "diff-pr.sh" script to run) + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 0 + - uses: docker-library/official-images/.github/workflows/.bashbrew@master + with: + build: 'docker' + - name: Prepare Environment + run: | + # this avoids running repo-local scripts (to avoid CVE-2020-15228 via the scripts being updated to write nasty things to $GITHUB_ENV) + docker build --tag oisupport/bashbrew:diff-pr . + - name: Gather Maintainers + env: + IMAGES: ${{ needs.gather.outputs.images }} + run: | + files="$(jq <<<"$IMAGES" -r '.images | map(@sh) | join(" ")')" + eval "set -- $files" + for f; do + if [ -s "$f" ]; then + docker run --rm --read-only --tmpfs /tmp oisupport/bashbrew:diff-pr \ + bashbrew cat --format ' - `{{ $.RepoName }}`:{{ range .Manifest.Global.Maintainers }} @{{ .Handle }}{{ end }}' "$f" + fi + done | tee "$GITHUB_WORKSPACE/oi-pr.maint" + - name: Generate Diff + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + docker run --rm --read-only --tmpfs /tmp oisupport/bashbrew:diff-pr ./diff-pr.sh "$GITHUB_PR_NUMBER" | tee "$GITHUB_WORKSPACE/oi-pr.diff" + - name: Comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentText = 'Diff for ' + context.payload.pull_request.head.sha + ':'; + + const fs = require('fs'); + const diff = fs.readFileSync(process.env.GITHUB_WORKSPACE + '/oi-pr.diff').toString().trimEnd(); + var body = "
\n" + commentText + "\n\n```diff\n" + diff + "\n```\n\n
"; + fs.writeFileSync(process.env.GITHUB_STEP_SUMMARY, body); + + // https://docs.github.com/en/graphql/reference/mutations#minimizecomment + const minimizeql = ` + mutation($comment: ID!) { + minimizeComment(input: { classifier: OUTDATED, clientMutationId: "doi-munge-pr", subjectId: $comment }) { + clientMutationId + minimizedComment { + isMinimized + minimizedReason + } + } + } + `; + // https://docs.github.com/en/graphql/reference/mutations#unminimizecomment + const unminimizeql = ` + mutation($comment: ID!) { + unminimizeComment(input: { clientMutationId: "doi-munge-pr", subjectId: $comment }) { + clientMutationId + unminimizedComment { + isMinimized + minimizedReason + } + } + } + `; + + needNewComment = true; + console.log('Reviewing existing comments...'); + for await (const { data: comments } of github.paginate.iterator( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + } + )) { + for (const comment of comments) { + if (comment.user.login === 'github-actions[bot]') { + if (needNewComment && comment.body.includes(commentText)) { + needNewComment = false; + console.log('Unhiding comment: ' + comment.id + ' (' + comment.node_id + ')'); + const result = await github.graphql(unminimizeql, { comment: comment.node_id }); + console.log('- result: ' + JSON.stringify(result)); + } else { + console.log('Hiding comment: ' + comment.id + ' (' + comment.node_id + ')'); + const result = await github.graphql(minimizeql, { comment: comment.node_id }); + console.log('- result: ' + JSON.stringify(result)); + } + } + } + } + if (needNewComment) { + console.log('Creating new comment...'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: body, + }); + }