From 27fb338f79aad54e40f239251da47658b697bbd3 Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Wed, 5 Apr 2023 10:47:47 -0400 Subject: [PATCH] Remind to not delete assets (#36161) --- .../deleted-assets-pr-comment.js | 68 +++++++++++++++++++ .github/workflows/dont-delete-assets.yml | 65 ++++++++++++++++++ script/deleted-assets-pr-comment.js | 31 +++++++++ 3 files changed, 164 insertions(+) create mode 100755 .github/actions-scripts/deleted-assets-pr-comment.js create mode 100644 .github/workflows/dont-delete-assets.yml create mode 100755 script/deleted-assets-pr-comment.js diff --git a/.github/actions-scripts/deleted-assets-pr-comment.js b/.github/actions-scripts/deleted-assets-pr-comment.js new file mode 100755 index 0000000000..3aa9352478 --- /dev/null +++ b/.github/actions-scripts/deleted-assets-pr-comment.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +import * as github from '@actions/github' +import core from '@actions/core' + +const { GITHUB_TOKEN } = process.env +const context = github.context + +if (!GITHUB_TOKEN) { + throw new Error(`GITHUB_TOKEN environment variable not set`) +} + +// When this file is invoked directly from action as opposed to being imported +if (import.meta.url.endsWith(process.argv[1])) { + const owner = context.repo.owner + const repo = context.payload.repository.name + const baseSHA = context.payload.pull_request.base.sha + const headSHA = context.payload.pull_request.head.sha + + const markdown = await main(owner, repo, baseSHA, headSHA) + core.setOutput('markdown', markdown) +} + +async function main(owner, repo, baseSHA, headSHA) { + const octokit = github.getOctokit(GITHUB_TOKEN) + // get the list of file changes from the PR + const response = await octokit.rest.repos.compareCommitsWithBasehead({ + owner, + repo, + basehead: `${baseSHA}...${headSHA}`, + }) + + const { files } = response.data + + const oldFilenames = [] + for (const file of files) { + const { filename, status } = file + if (!filename.startsWith('assets')) continue + if (status === 'removed') { + // Bad + oldFilenames.push(filename) + } else if (status === 'renamed') { + // Also bad + const previousFilename = file.previous_filename + oldFilenames.push(previousFilename) + } + } + + if (!oldFilenames.length) { + return '' + } + + let markdown = '⚠️ 🙀 **You deleted some assets** 🙀 ⚠️\n\n' + markdown += + "Even if you don't reference these assets anymore, as of this branch, you should not delete them.\n" + markdown += 'They might still be referenced in translated content.\n' + markdown += 'The weekly "Delete orphaned assets" workflow will worry about cleaning those up.\n\n' + markdown += '**To *undo* these removals run this command:**\n\n' + markdown += ` +\`\`\`sh +git checkout origin/main -- ${oldFilenames.join(' ')} +\`\`\` +` + + return markdown +} + +export default main diff --git a/.github/workflows/dont-delete-assets.yml b/.github/workflows/dont-delete-assets.yml new file mode 100644 index 0000000000..e3035c3878 --- /dev/null +++ b/.github/workflows/dont-delete-assets.yml @@ -0,0 +1,65 @@ +name: Don't delete assets + +# **What it does**: +# If the PR (against main) involves deletion of assets, if any of +# them are deletions or renames, post a comment, and ultimately +# fail the check. +# **Why we have it**: +# If you delete the reference to an image, the English content is fine +# because it no longer tries to serve an image that doesn't exist. +# But this is not the case for translations. +# **Who does it impact**: Docs content. + +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - 'assets/**' + +permissions: + contents: read + pull-requests: write + +jobs: + dont-delete-assets: + # It's 'docubot' that creates those PR from "Delete orphaned assets" + if: github.event.pull_request.user.login != 'docubot' && (github.repository == 'github/docs-internal' || github.repository == 'github/docs') + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + + - uses: ./.github/actions/node-npm-setup + + - name: Get comment markdown + id: comment + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: .github/actions-scripts/deleted-assets-pr-comment.js + + - name: Find possible previous comment + if: ${{ steps.comment.outputs.markdown != '' }} + uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 + id: findComment + with: + issue-number: ${{ github.event.number }} + comment-author: 'github-actions[bot]' + body-includes: '' + + - name: Update comment + if: ${{ steps.comment.outputs.markdown != '' }} + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d + with: + comment-id: ${{ steps.findComment.outputs.comment-id }} + issue-number: ${{ github.event.number }} + body: ${{ steps.comment.outputs.markdown }} + edit-mode: replace + + - name: Ultimately fail the workflow for attention + if: ${{ steps.comment.outputs.markdown != '' }} + run: | + echo "More than 1 asset image was deleted as part of this PR." + echo "See posted PR commented about how to get them back." + exit 1 diff --git a/script/deleted-assets-pr-comment.js b/script/deleted-assets-pr-comment.js new file mode 100755 index 0000000000..d5dbebd81e --- /dev/null +++ b/script/deleted-assets-pr-comment.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +// [start-readme] +// +// For testing the GitHub Action that executes +// .github/actions-scripts/deleted-assets-pr-comment.js but doing it +// locally. +// This is more convenient and faster than relying on seeing that the +// Action produces in a PR. +// +// To try it you need to generate a local `GITHUB_TOKEN` that has read-access +// "content" and "pull requests" on the repo. +// Example use: +// +// export GITHUB_TOKEN=github_pat_11AAAG..... +// ./script/deleted-assets-pr-comment.js github docs-internal main 4a0b0f2 +// +// [end-readme] + +import { program } from 'commander' +import main from '../.github/actions-scripts/deleted-assets-pr-comment.js' + +program + .description('If applicable, print a snippet of Markdown about deleted assets') + .arguments('owner repo base_sha head_sha', 'Simulate what the Actions workflow does') + .parse(process.argv) + +const opts = program.opts() +const args = program.args + +console.log(await main(...args, { ...opts }))