diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25d5f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +scripts/settings-plan.sh diff --git a/scripts/drop-noop-approve-step.sh b/scripts/drop-noop-approve-step.sh new file mode 100755 index 0000000..6576588 --- /dev/null +++ b/scripts/drop-noop-approve-step.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# Fleet-wide remediation for smartwatermelon/dev-env#21: no repo in either +# org currently requires an approving review (required_approving_review_count +# is 0 everywhere), so the `gh pr review --approve` line in +# dependabot-auto-merge.yml is a dead no-op. This drops that line, leaving +# only `gh pr merge --auto --squash --delete-branch`, and opens one PR per +# repo. It does NOT touch the `can_approve_pull_request_reviews` repo +# setting — that's a separate plan file for the operator to review and +# apply by hand (see generate-settings-plan.sh in this same directory). +# +# Usage: scripts/drop-noop-approve-step.sh +# +# Only the 26 repos below (from the dev-env#21 audit comment) that actually +# have a dependabot-auto-merge.yml with the inert approve line are touched. +# This script creates real PRs (going through normal pre-commit/pre-push +# review hooks) but never merges anything — merging is a separate, +# explicitly authorized step per repo, same as every other PR in this +# workflow. +# +# NOTE: generate-settings-plan.sh's repo list has 28 entries, 2 more than +# here (nightowlstudiollc/reliquarist, nightowlstudiollc/.github). Those two +# have can_approve_pull_request_reviews=true but no dependabot-auto-merge.yml +# at all, so there's no workflow-file step to remove for them — they only +# need the repo-setting flip, not a PR from this script. Intentional, not +# a mismatch. +# +# macOS-only: uses BSD sed (`sed -i ''`). This is an operator script for +# this user's own machines (this one, TILSIT, MIMOLETTE), all macOS — not +# intended to run on Linux/CI. + +set -euo pipefail + +if ! auth_status_output=$(gh auth status 2>&1); then + echo "gh is not authenticated — aborting before touching any repo:" >&2 + echo "${auth_status_output}" >&2 + exit 1 +fi + +REPOS=( + "smartwatermelon/claude-config" + "smartwatermelon/ralph-burndown" + "smartwatermelon/dev-env" + "smartwatermelon/dotfiles" + "smartwatermelon/smartwatermelon-marketplace" + "smartwatermelon/archive-resolver" + "smartwatermelon/mac-dev-server-setup" + "smartwatermelon/scripts" + "smartwatermelon/claude-wrapper" + "smartwatermelon/qwen-sidebar" + "smartwatermelon/slack-mcp" + "smartwatermelon/swift-progress-indicator" + "smartwatermelon/projectinsomnia" + "smartwatermelon/lock-sync" + "smartwatermelon/spokane-snow" + "smartwatermelon/homebrew-tap" + "smartwatermelon/crazy-larry" + "nightowlstudiollc/tensegrity" + "nightowlstudiollc/kebab-tax-netlify" + "nightowlstudiollc/kebab-tax" + "nightowlstudiollc/tnjcleaning" + "nightowlstudiollc/amelia-boone" + "nightowlstudiollc/financial-agent" + "nightowlstudiollc/networth-agent" + "nightowlstudiollc/vpn-lan-bridge" + "nightowlstudiollc/night-owl-studio" +) + +WORKFLOW_FILE=".github/workflows/dependabot-auto-merge.yml" +BRANCH_NAME="chore/drop-noop-pr-approve-$(date +%Y%m%d-%H%M%S)-$$" +SCRATCH_ROOT=$(mktemp -d) +RESULTS_LOG="${SCRATCH_ROOT}/results.tsv" +FINAL_LOG="${SCRATCH_ROOT%/}-drop-noop-approve-results.tsv" +printf 'repo\tstatus\tdetail\n' >"${RESULTS_LOG}" + +trap 'cp "${RESULTS_LOG}" "${FINAL_LOG}" 2>/dev/null || true; rm -rf "${SCRATCH_ROOT}"' EXIT + +for repo in "${REPOS[@]}"; do + echo "=== ${repo} ===" + clone_dir="${SCRATCH_ROOT}/${repo//\//-}" + + if ! git clone --quiet "git@github.com:${repo}.git" "${clone_dir}" 2>"${SCRATCH_ROOT}/clone-err.log"; then + echo " clone failed, skipping" + printf '%s\tskipped\tclone failed\n' "${repo}" >>"${RESULTS_LOG}" + continue + fi + + workflow_path="${clone_dir}/${WORKFLOW_FILE}" + if [[ ! -f "${workflow_path}" ]]; then + echo " no ${WORKFLOW_FILE}, skipping" + printf '%s\tskipped\tno workflow file\n' "${repo}" >>"${RESULTS_LOG}" + continue + fi + + if ! grep -qE '^[[:space:]]*gh pr review --approve' "${workflow_path}"; then + echo " no inert approve line found, skipping" + printf '%s\tskipped\tno approve line present\n' "${repo}" >>"${RESULTS_LOG}" + continue + fi + + sed -i '' '/^[[:space:]]*gh pr review --approve/d' "${workflow_path}" + + if grep -qE '^[[:space:]]*gh pr review --approve' "${workflow_path}"; then + echo " sed failed to remove the approve line, skipping" + printf '%s\tfailed\tsed did not remove approve line\n' "${repo}" >>"${RESULTS_LOG}" + continue + fi + + commit_msg_file="${SCRATCH_ROOT}/commit-msg.txt" + cat >"${commit_msg_file}" <<'EOF' +chore(ci): drop no-op gh pr review --approve step + +No repo in either org currently requires an approving review +(required_approving_review_count is 0 fleet-wide), so this step never +did anything -- gh pr merge --auto already proceeds once required +status checks pass. Removing the dead step; auto-merge behavior is +unchanged. + +Ref: smartwatermelon/dev-env#21 +EOF + + git_err_log="${SCRATCH_ROOT}/git-err.log" + git_err_detail="" + + if ! git -C "${clone_dir}" checkout -b "${BRANCH_NAME}" >"${git_err_log}" 2>&1; then + git_err_detail=$(tr '\n\t' ' ' <"${git_err_log}") + echo " git checkout failed: ${git_err_detail}" + printf '%s\tfailed\tgit checkout failed: %s\n' "${repo}" "${git_err_detail}" >>"${RESULTS_LOG}" + continue + fi + if ! git -C "${clone_dir}" add "${WORKFLOW_FILE}" >"${git_err_log}" 2>&1; then + git_err_detail=$(tr '\n\t' ' ' <"${git_err_log}") + echo " git add failed: ${git_err_detail}" + printf '%s\tfailed\tgit add failed: %s\n' "${repo}" "${git_err_detail}" >>"${RESULTS_LOG}" + continue + fi + # Note: git hooks are NOT cloned with a repo, so no local pre-commit + # review hook runs here regardless of what's installed in a normal + # working copy of these repos -- a commit failure below is a plain + # git-mechanics problem, not a hook rejection. + if ! git -C "${clone_dir}" commit -F "${commit_msg_file}" >"${git_err_log}" 2>&1; then + git_err_detail=$(tr '\n\t' ' ' <"${git_err_log}") + echo " git commit failed: ${git_err_detail}" + printf '%s\tfailed\tgit commit failed: %s\n' "${repo}" "${git_err_detail}" >>"${RESULTS_LOG}" + continue + fi + if ! git -C "${clone_dir}" push -u origin "${BRANCH_NAME}" >"${git_err_log}" 2>&1; then + git_err_detail=$(tr '\n\t' ' ' <"${git_err_log}") + if grep -qi 'already exists' "${git_err_log}"; then + echo " branch ${BRANCH_NAME} already exists on remote for this repo — likely a re-run; delete it remotely and re-run, or use a fresh timestamp" + printf '%s\tskipped\tbranch already exists on remote\n' "${repo}" >>"${RESULTS_LOG}" + else + echo " git push failed: ${git_err_detail}" + printf '%s\tfailed\tgit push failed: %s\n' "${repo}" "${git_err_detail}" >>"${RESULTS_LOG}" + fi + continue + fi + + pr_body_file="${SCRATCH_ROOT}/pr-body.txt" + cat >"${pr_body_file}" <<'EOF' +Removes the `gh pr review --approve` line from `dependabot-auto-merge.yml`. + +No repo in either org currently requires an approving review +(`required_approving_review_count` is 0 fleet-wide, confirmed by audit +in smartwatermelon/dev-env#21), so this step has never done anything — +`gh pr merge --auto` already merges once required status checks pass. +Auto-merge behavior is unchanged; this only removes dead weight and +the associated `pull-requests: write` usage for the approve call. + +Part of the fleet-wide remediation tracked in smartwatermelon/dev-env#21. +This PR is not merged automatically — merge requires separate explicit +authorization, same as any other PR. +EOF + + if pr_url=$(gh pr create --repo "${repo}" \ + --head "${BRANCH_NAME}" \ + --title "chore(ci): drop no-op gh pr review --approve step" \ + --body-file "${pr_body_file}" \ + 2>"${SCRATCH_ROOT}/pr-err.log") && [[ "${pr_url}" =~ ^https://github\.com/ ]]; then + echo " PR opened: ${pr_url}" + printf '%s\topened\t%s\n' "${repo}" "${pr_url}" >>"${RESULTS_LOG}" + else + pr_err_detail=$(tr '\n\t' ' ' <"${SCRATCH_ROOT}/pr-err.log") + echo " gh pr create failed, see log" + printf '%s\tfailed\t%s\n' "${repo}" "${pr_err_detail}" >>"${RESULTS_LOG}" + fi +done + +echo +echo "=== Summary ===" +column -t -s $'\t' "${RESULTS_LOG}" || cat "${RESULTS_LOG}" +echo +echo "Results will be saved to ${FINAL_LOG} on exit" diff --git a/scripts/generate-settings-plan.sh b/scripts/generate-settings-plan.sh new file mode 100755 index 0000000..bfbf4ff --- /dev/null +++ b/scripts/generate-settings-plan.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Generates the exact gh api commands to flip can_approve_pull_request_reviews +# to false for the 28 repos identified as remediation candidates in +# smartwatermelon/dev-env#21 (fleet-wide "Allow GitHub Actions to approve +# PRs" audit). This script only WRITES a plan file -- it never calls the +# PATCH endpoint itself. Review the output, then run the commands yourself. +# +# Usage: scripts/generate-settings-plan.sh [output-file] +# Default output-file: scripts/settings-plan.sh + +set -euo pipefail + +REPOS=( + "smartwatermelon/claude-config" + "smartwatermelon/ralph-burndown" + "smartwatermelon/dev-env" + "smartwatermelon/dotfiles" + "smartwatermelon/smartwatermelon-marketplace" + "smartwatermelon/archive-resolver" + "smartwatermelon/mac-dev-server-setup" + "smartwatermelon/scripts" + "smartwatermelon/claude-wrapper" + "smartwatermelon/qwen-sidebar" + "smartwatermelon/slack-mcp" + "smartwatermelon/swift-progress-indicator" + "smartwatermelon/projectinsomnia" + "smartwatermelon/lock-sync" + "smartwatermelon/spokane-snow" + "smartwatermelon/homebrew-tap" + "smartwatermelon/crazy-larry" + "nightowlstudiollc/tensegrity" + "nightowlstudiollc/reliquarist" + "nightowlstudiollc/kebab-tax-netlify" + "nightowlstudiollc/kebab-tax" + "nightowlstudiollc/tnjcleaning" + "nightowlstudiollc/amelia-boone" + "nightowlstudiollc/financial-agent" + "nightowlstudiollc/networth-agent" + "nightowlstudiollc/.github" + "nightowlstudiollc/vpn-lan-bridge" + "nightowlstudiollc/night-owl-studio" +) + +OUT_FILE="${1:-$(dirname "${BASH_SOURCE[0]}")/settings-plan.sh}" +DRAFT_FILE=$(mktemp) +trap 'rm -f "${DRAFT_FILE}"' EXIT + +generated_date=$(date +%Y-%m-%d) +failed_repos=() + +{ + echo "#!/usr/bin/env bash" + echo "# Generated ${generated_date} from smartwatermelon/dev-env#21 remediation list." + echo "# Review before running. Flips can_approve_pull_request_reviews to false" + echo "# for each repo, reusing default_workflow_permissions values captured" + echo "# AT GENERATION TIME (${generated_date}) -- if you changed that setting on" + echo "# any of these repos since generating this plan, re-run" + echo "# generate-settings-plan.sh before applying, or it will revert your change." + echo "set -euo pipefail" + echo + for repo in "${REPOS[@]}"; do + if ! current_default=$(gh api "repos/${repo}/actions/permissions/workflow" --jq '.default_workflow_permissions' 2>/dev/null); then + failed_repos+=("${repo}") + continue + fi + echo "echo '=== ${repo} ==='" + echo "gh api --method PUT repos/${repo}/actions/permissions/workflow \\" + echo " -f default_workflow_permissions='${current_default}' \\" + echo " -F can_approve_pull_request_reviews=false" + done +} >"${DRAFT_FILE}" + +if [[ ${#failed_repos[@]} -gt 0 ]]; then + echo "ERROR: could not fetch default_workflow_permissions for ${#failed_repos[@]} repo(s) — aborting without writing a plan file:" >&2 + printf ' %s\n' "${failed_repos[@]}" >&2 + echo "Fix access/auth for the repos above, then re-run." >&2 + exit 1 +fi + +cp "${DRAFT_FILE}" "${OUT_FILE}" +chmod +x "${OUT_FILE}" +line_count=$(wc -l <"${OUT_FILE}" | tr -d ' ') +echo "Plan written to ${OUT_FILE} (${line_count} lines)" +echo "Review it, then run: bash ${OUT_FILE}"