From cc64a732308bb0f5cf5c573a0539f3db1ba4f4b0 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 17:39:56 +0800 Subject: [PATCH] one-click revert and ban --- .github/workflows/blacklist-command.yml | 113 ++++++++++++++++++++++++ .github/workflows/issue.yml | 4 + .github/workflows/revert-command.yml | 109 +++++++++++++++++++++++ .github/workflows/wiki-monitor.yml | 4 +- 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/blacklist-command.yml create mode 100644 .github/workflows/revert-command.yml diff --git a/.github/workflows/blacklist-command.yml b/.github/workflows/blacklist-command.yml new file mode 100644 index 00000000..4d0ff5f3 --- /dev/null +++ b/.github/workflows/blacklist-command.yml @@ -0,0 +1,113 @@ +name: Blacklist Command + +# An issue titled "blacklist: " opened by the maintainer +# or a write-access member adds that identity to the Blacklist wiki page +# (see wiki-guard.yml) and closes the issue. The wiki-monitor notification +# embeds a prefilled link that opens such an issue in one click. + +on: + issues: + types: [opened] + +permissions: + contents: write + issues: write + +# Share the wiki-guard concurrency group so we never push to the wiki +# at the same time as the guard. +concurrency: + group: wiki-guard + cancel-in-progress: false + +jobs: + blacklist: + if: startsWith(github.event.issue.title, 'blacklist:') + runs-on: ubuntu-latest + steps: + - name: Check authorization + id: auth + run: | + assoc="${{ github.event.issue.author_association }}" + case "$assoc" in + OWNER|MEMBER|COLLABORATOR) + echo "ok=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "issue author is not authorized ($assoc); ignoring" + echo "ok=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Checkout wiki repository + if: steps.auth.outputs.ok == 'true' + uses: actions/checkout@v7 + with: + repository: ${{ github.repository }}.wiki + path: wiki + + - name: Add the identity to the blacklist page + if: steps.auth.outputs.ok == 'true' + id: add + env: + TITLE: ${{ github.event.issue.title }} + run: | + target="$(printf '%s' "$TITLE" \ + | sed 's/^blacklist:[[:space:]]*//; s/^@//; s/[[:space:]].*$//' \ + | tr 'A-Z' 'a-z')" + case "$target" in + ''|*[!a-z0-9._+@-]*) + echo "invalid target: '$target'" + echo "result=invalid" >> "$GITHUB_OUTPUT" + exit 0 + ;; + esac + echo "target=$target" >> "$GITHUB_OUTPUT" + cd wiki + if [ ! -e Blacklist.md ]; then + echo "result=nopage" >> "$GITHUB_OUTPUT" + exit 0 + fi + if grep -Fxiq -- "- $target" Blacklist.md; then + echo "result=already" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ -n "$(tail -c1 Blacklist.md)" ]; then + echo >> Blacklist.md + fi + printf -- '- %s\n' "$target" >> Blacklist.md + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Blacklist.md + git commit -m "blacklist $target (requested in #${{ github.event.issue.number }})" + git push origin HEAD || { git pull --rebase && git push origin HEAD; } + echo "result=added" >> "$GITHUB_OUTPUT" + + - name: Reply and close + if: steps.auth.outputs.ok == 'true' + uses: actions/github-script@v9 + env: + RESULT: ${{ steps.add.outputs.result }} + TARGET: ${{ steps.add.outputs.target }} + with: + script: | + const result = process.env.RESULT; + const target = process.env.TARGET; + const messages = { + added: `\`${target}\` has been added to the [Blacklist](https://github.com/${context.repo.owner}/${context.repo.repo}/wiki/Blacklist). The wiki guard will revert their recent wiki changes on its next run.`, + already: `\`${target}\` is already on the blacklist.`, + invalid: "Could not parse a valid login or email from the issue title.", + nopage: "The Blacklist wiki page does not exist." + }; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: messages[result] || "No action taken." + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: "closed", + state_reason: result === "added" ? "completed" : "not_planned" + }); diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index bd6dc9e8..a72c18ab 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -57,6 +57,10 @@ jobs: } const issue = context.payload.issue; + if (issue.title.startsWith("blacklist:") || issue.title.startsWith("revert:")) { + // Handled by the Blacklist / Revert Command workflows. + return; + } if (issue.title.startsWith("Report bugs to")) { // Tracking issue for a third-party dns/deploy/notify api: // no upgrade boilerplate; assign it to the opener and label it. diff --git a/.github/workflows/revert-command.yml b/.github/workflows/revert-command.yml new file mode 100644 index 00000000..7a792bf4 --- /dev/null +++ b/.github/workflows/revert-command.yml @@ -0,0 +1,109 @@ +name: Revert Command + +# An issue titled "revert: " opened by the maintainer or +# a write-access member reverts that commit in the wiki repository and +# closes the issue. The wiki-monitor notification embeds a prefilled link +# that opens such an issue in one click. + +on: + issues: + types: [opened] + +permissions: + contents: write + issues: write + +# Share the wiki-guard concurrency group so we never push to the wiki +# at the same time as the guard. +concurrency: + group: wiki-guard + cancel-in-progress: false + +jobs: + revert: + if: startsWith(github.event.issue.title, 'revert:') + runs-on: ubuntu-latest + steps: + - name: Check authorization + id: auth + run: | + assoc="${{ github.event.issue.author_association }}" + case "$assoc" in + OWNER|MEMBER|COLLABORATOR) + echo "ok=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "issue author is not authorized ($assoc); ignoring" + echo "ok=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Checkout wiki repository + if: steps.auth.outputs.ok == 'true' + uses: actions/checkout@v7 + with: + repository: ${{ github.repository }}.wiki + path: wiki + fetch-depth: 0 + + - name: Revert the wiki commit + if: steps.auth.outputs.ok == 'true' + id: revert + env: + TITLE: ${{ github.event.issue.title }} + run: | + target="$(printf '%s' "$TITLE" \ + | sed 's/^revert:[[:space:]]*//; s/[[:space:]].*$//' \ + | tr 'A-Z' 'a-z')" + case "$target" in + *[!0-9a-f]*|"") + echo "invalid commit sha: '$target'" + echo "result=invalid" >> "$GITHUB_OUTPUT" + exit 0 + ;; + esac + echo "target=$target" >> "$GITHUB_OUTPUT" + cd wiki + if ! git cat-file -e "$target^{commit}" 2>/dev/null; then + echo "result=notfound" >> "$GITHUB_OUTPUT" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + if git revert --no-edit "$target"; then + git push origin HEAD || { git pull --rebase && git push origin HEAD; } + echo "result=reverted" >> "$GITHUB_OUTPUT" + else + git revert --abort || true + echo "result=conflict" >> "$GITHUB_OUTPUT" + fi + + - name: Reply and close + if: steps.auth.outputs.ok == 'true' + uses: actions/github-script@v9 + env: + RESULT: ${{ steps.revert.outputs.result }} + TARGET: ${{ steps.revert.outputs.target }} + with: + script: | + const result = process.env.RESULT; + const target = process.env.TARGET; + const messages = { + reverted: `Wiki commit \`${target}\` has been reverted.`, + conflict: `Reverting \`${target}\` conflicts with later edits; please revert manually from the page history.`, + notfound: `Commit \`${target}\` was not found in the wiki repository.`, + invalid: "Could not parse a commit sha from the issue title." + }; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: messages[result] || "No action taken." + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: "closed", + state_reason: result === "reverted" ? "completed" : "not_planned" + }); diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 349ef3f3..89bb1f3a 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -53,7 +53,9 @@ jobs: echo "Time: $now" echo "Page: [$page_name]($page_url) (Action: $page_action)" echo "Comment: $page_summary" - echo "[Click here to Revert](${page_url}/_history)" + echo "[Click here to Revert](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=revert%3A+${page_sha}&body=Revert+wiki+commit+${page_sha}+by+@${actor}.)" + echo "" + echo "[Click here to Blacklist @$actor](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=blacklist%3A+${actor}&body=Blacklist+@${actor},+requested+from+the+wiki+monitor.)" echo "" echo "----" echo "### diff:"