Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .git-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,40 @@ git push --no-verify
```

However, this should be used sparingly and only in exceptional circumstances.

## Working with Protected Branches

### Branch Protection

- The `staging` and `main` branches are protected. Direct pushes are only allowed for admins.
- Non-admins must create pull requests and get approval before merging to protected branches.

### Pre-Push Hook

- This repo uses a pre-push hook (`.git-hooks/pre-push`) for additional safety:
- Blocks direct pushes to protected branches for non-admins.
- Runs the test suite when pushing code changes to protected branches.
- Allows skipping tests with: `SKIP_TESTS=1 git push origin <branch>`
- Admins listed in `.git-hooks/admins.txt` can push directly.

### Admins

- To allow a user to push directly, add their Git username or email to `.git-hooks/admins.txt`.

### Common Errors

- **Direct pushes to staging are not allowed. Please create a pull request.**
- Solution: Open a pull request instead of pushing directly.
- **Tests failed. Push aborted.**
- Solution: Fix test failures before pushing.

### Contributing Workflow

1. Make changes on a feature branch.
2. Open a pull request to `staging` or `main`.
3. Wait for required approvals and test suite to pass.
4. Admins may push directly if necessary.

---

For more details, see `.git-hooks/pre-push` and `.git-hooks/admins.txt`.
1 change: 1 addition & 0 deletions .git-hooks/admins.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rsmoke
129 changes: 91 additions & 38 deletions .git-hooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,73 +1,126 @@
#!/usr/bin/env bash

# Get the current branch name
current_branch=$(git symbolic-ref --short HEAD)
# Configurable variables
PROTECTED_BRANCHES=("staging" "main")
ADMIN_FILE=".git-hooks/admins.txt"
LOG_FILE=".git-hooks/pre-push.log"
TEST_CMD="${TEST_CMD:-bundle exec rspec}"

CODE_EXTENSIONS="rb|js|py|go|java|ts|cpp|c|cs|php|swift|kt|rs|scala|pl|sh"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

log_failure() {
echo "$(date +'%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}

current_branch=$(git symbolic-ref --short HEAD)
echo -e "${YELLOW}Running pre-push hook on branch ${current_branch}${NC}"

# Check if we're pushing to staging or main
protected_branches="^(staging|main)$"
if [[ "$current_branch" =~ $protected_branches ]]; then
# Check if this is a git hooks setup commit
# Load admin list from file if it exists (POSIX compatible)
ADMINS=()
if [[ -f "$ADMIN_FILE" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
ADMINS+=("$line")
done < "$ADMIN_FILE"
else
ADMINS=("rsmoke")
fi

GIT_USER=$(git config user.name)
GIT_EMAIL=$(git config user.email)
is_admin=false
for admin in "${ADMINS[@]}"; do
if [[ "$GIT_USER" == "$admin" || "$GIT_EMAIL" == "$admin" ]]; then
is_admin=true
break
fi
# Partial match for email domain
if [[ "$admin" == *"@"* ]] && [[ "$GIT_EMAIL" == *"${admin#*@}" ]]; then
is_admin=true
break
fi
done

# Build protected branch regex
protected_regex="^($(IFS='|'; echo "${PROTECTED_BRANCHES[*]}"))$"

# DRY RUN or HELP
if [[ "$1" == "--dry-run" || "$1" == "--help" ]]; then
echo -e "${GREEN}Pre-push hook dry-run/help mode${NC}"
echo "Admins: ${ADMINS[*]}"
echo "Protected branches: ${PROTECTED_BRANCHES[*]}"
echo "Test command: $TEST_CMD"
exit 0
fi

# Print hook configuration summary (separate echos)
echo -e "${YELLOW}Admins: ${ADMINS[*]}${NC}"
echo -e "${YELLOW}Protected branches: ${PROTECTED_BRANCHES[*]}${NC}"
echo -e "${YELLOW}Test command: $TEST_CMD${NC}"

# Main branch check
if [[ "$current_branch" =~ $protected_regex ]]; then
if [[ "$is_admin" == "true" ]]; then
echo -e "${GREEN}Admin detected ($GIT_USER). Direct push allowed.${NC}"
exit 0
fi
if git diff --name-only HEAD~1 HEAD | grep -q "^\.git-hooks/"; then
echo -e "${YELLOW}Detected git hooks setup changes - allowing direct push${NC}"
exit 0
fi

echo -e "${RED}Direct pushes to $current_branch are not allowed. Please create a pull request.${NC}"
log_failure "Direct push blocked for $GIT_USER to $current_branch"
exit 1
fi

# Get the target branch
while read local_ref local_sha remote_ref remote_sha
do
target_branch=${remote_ref##refs/heads/}
if [[ "$target_branch" =~ $protected_branches ]]; then
# Check if this is a git hooks setup commit
if [[ "$target_branch" =~ $protected_regex ]]; then
if [[ "$is_admin" == "true" ]]; then
echo -e "${GREEN}Admin detected ($GIT_USER). Direct push allowed.${NC}"
exit 0
fi
if git diff --name-only HEAD~1 HEAD | grep -q "^\.git-hooks/"; then
echo -e "${YELLOW}Detected git hooks setup changes - allowing push without tests${NC}"
exit 0
fi

echo -e "${YELLOW}Pushing to $target_branch - running full test suite...${NC}"

# Stash any uncommitted changes
if ! git diff --quiet HEAD; then
echo "Stashing uncommitted changes..."
git stash push -u
STASHED=1
fi

# Run the test suite
if bundle exec rspec; then
echo -e "${GREEN}All tests passed!${NC}"

# Pop stashed changes if we stashed them
if [ "$STASHED" = "1" ]; then
echo "Popping stashed changes..."
git stash pop
# Only run tests if code files changed
if git diff --name-only origin/$target_branch | grep -E "\.($CODE_EXTENSIONS)$" > /dev/null; then
# Stash any uncommitted changes
if ! git diff --quiet HEAD; then
echo "Stashing uncommitted changes..."
git stash push -u
STASHED=1
fi

exit 0
else
echo -e "${RED}Tests failed. Push aborted.${NC}"

# Pop stashed changes if we stashed them
if [ "$STASHED" = "1" ]; then
echo "Popping stashed changes..."
git stash pop
if [[ -z "$SKIP_TESTS" ]]; then
if $TEST_CMD; then
echo -e "${GREEN}All tests passed!${NC}"
[ "$STASHED" = "1" ] && git stash pop
exit 0
else
echo -e "${RED}Tests failed. Push aborted.${NC}"
[ "$STASHED" = "1" ] && git stash pop
log_failure "Tests failed for $GIT_USER on $target_branch"
exit 1
fi
else
echo -e "${YELLOW}SKIP_TESTS set, skipping test run.${NC}"
[ "$STASHED" = "1" ] && git stash pop
exit 0
fi

exit 1
else
echo -e "${YELLOW}No code changes detected, skipping tests.${NC}"
exit 0
fi
fi
done

# If we're not pushing to a protected branch, allow the push without running tests
exit 0
2 changes: 1 addition & 1 deletion .github/workflows/brakeman.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ GEM
rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1)
ruby-progressbar (1.13.0)
ruby-saml (1.18.0)
ruby-saml (1.18.1)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.2)
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,16 @@ This application uses SendGrid for email delivery in the production environment.

Emails are automatically configured to be sent asynchronously through Sidekiq background jobs.

## Protected Branches and Pre-Push Hook

This repository uses [branch protection rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/branch-protection-rules) for `staging` and `main` branches.
Direct pushes are restricted and enforced by a pre-push hook.

**Summary:**

- Non-admins: Must open a Pull Request to contribute to protected branches.
- Admins: Can push directly if listed in `.git-hooks/admins.txt`.
- All pushes to protected branches run tests automatically, unless skipped.
- For details on hook installation, admin setup, and troubleshooting, see [.git-hooks/README.md](.git-hooks/README.md).

## This project is licensed under the [MIT License](https://github.com/your-repo/lsa-evaluate/blob/main/LICENSE)
Loading