diff --git a/.git-hooks/README.md b/.git-hooks/README.md index bc11d353..f5b28462 100644 --- a/.git-hooks/README.md +++ b/.git-hooks/README.md @@ -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 ` + - 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`. diff --git a/.git-hooks/admins.txt b/.git-hooks/admins.txt new file mode 100644 index 00000000..1cc93a0a --- /dev/null +++ b/.git-hooks/admins.txt @@ -0,0 +1 @@ +rsmoke diff --git a/.git-hooks/pre-push b/.git-hooks/pre-push index 73d4ffd9..c5c0dda8 100755 --- a/.git-hooks/pre-push +++ b/.git-hooks/pre-push @@ -1,7 +1,12 @@ #!/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' @@ -9,65 +14,113 @@ 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 diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml index 6c1433ca..55018c74 100644 --- a/.github/workflows/brakeman.yml +++ b/.github/workflows/brakeman.yml @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 5d72e6a0..8146a258 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/README.md b/README.md index 1320ce5c..b08ac305 100644 --- a/README.md +++ b/README.md @@ -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)