feat: Block internal PRs via pre-push hook and GitHub Action
Gas Town agents must push directly to main, not create PRs. This adds defense-in-depth: 1. .githooks/pre-push - Blocks pushes to non-main branches locally 2. .github/workflows/block-internal-prs.yml - Auto-closes PRs from the same repo (forks/contributors can still create PRs) 3. internal/git/git.go - Auto-configures core.hooksPath on clone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
32
.githooks/pre-push
Executable file
32
.githooks/pre-push
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Block pushes to non-main branches from internal clones
|
||||
# External contributors use forks, so this only affects Gas Town agents
|
||||
|
||||
# Allow: main, beads-sync
|
||||
# Block: feature branches, polecat/* branches, etc.
|
||||
|
||||
while read local_ref local_sha remote_ref remote_sha; do
|
||||
branch="${remote_ref#refs/heads/}"
|
||||
|
||||
case "$branch" in
|
||||
main|beads-sync)
|
||||
# Allowed branches
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Gas Town agents push directly to main."
|
||||
echo ""
|
||||
echo "Blocked push to: $branch"
|
||||
echo ""
|
||||
echo "If you're working on a fix:"
|
||||
echo " git checkout main"
|
||||
echo " git merge $branch"
|
||||
echo " git push origin main"
|
||||
echo " git branch -d $branch"
|
||||
echo ""
|
||||
echo "See CLAUDE.md: 'Crew workers push directly to main. No feature branches.'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
exit 0
|
||||
47
.github/workflows/block-internal-prs.yml
vendored
Normal file
47
.github/workflows/block-internal-prs.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Block Internal PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
block-internal-prs:
|
||||
name: Block Internal PRs
|
||||
# Only run if PR is from the same repo (not a fork)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close PR and comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.issue.number;
|
||||
const branch = context.payload.pull_request.head.ref;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: `🚫 **Internal PRs are not allowed.**
|
||||
|
||||
Gas Town agents push directly to main. PRs are for external contributors only.
|
||||
|
||||
To land your changes:
|
||||
\`\`\`bash
|
||||
git checkout main
|
||||
git merge ${branch}
|
||||
git push origin main
|
||||
git push origin --delete ${branch}
|
||||
\`\`\`
|
||||
|
||||
See CLAUDE.md: "Crew workers push directly to main. No feature branches. NEVER create PRs."`
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
core.setFailed('Internal PR blocked. Push directly to main instead.');
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -102,7 +104,8 @@ func (g *Git) Clone(url, dest string) error {
|
||||
if err := cmd.Run(); err != nil {
|
||||
return g.wrapError(err, stderr.String(), []string{"clone", url})
|
||||
}
|
||||
return nil
|
||||
// Configure hooks path for Gas Town clones
|
||||
return configureHooksPath(dest)
|
||||
}
|
||||
|
||||
// CloneWithReference clones a repository using a local repo as an object reference.
|
||||
@@ -114,7 +117,8 @@ func (g *Git) CloneWithReference(url, dest, reference string) error {
|
||||
if err := cmd.Run(); err != nil {
|
||||
return g.wrapError(err, stderr.String(), []string{"clone", "--reference-if-able", url})
|
||||
}
|
||||
return nil
|
||||
// Configure hooks path for Gas Town clones
|
||||
return configureHooksPath(dest)
|
||||
}
|
||||
|
||||
// CloneBare clones a repository as a bare repo (no working directory).
|
||||
@@ -129,6 +133,25 @@ func (g *Git) CloneBare(url, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureHooksPath sets core.hooksPath to use the repo's .githooks directory
|
||||
// if it exists. This ensures Gas Town agents use the pre-push hook that blocks
|
||||
// pushes to non-main branches (internal PRs are not allowed).
|
||||
func configureHooksPath(repoPath string) error {
|
||||
hooksDir := filepath.Join(repoPath, ".githooks")
|
||||
if _, err := os.Stat(hooksDir); os.IsNotExist(err) {
|
||||
// No .githooks directory, nothing to configure
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", "-C", repoPath, "config", "core.hooksPath", ".githooks")
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("configuring hooks path: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneBareWithReference clones a bare repository using a local repo as an object reference.
|
||||
func (g *Git) CloneBareWithReference(url, dest, reference string) error {
|
||||
cmd := exec.Command("git", "clone", "--bare", "--reference-if-able", reference, url, dest)
|
||||
|
||||
Reference in New Issue
Block a user