Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 8 additions & 2 deletions docs/roadmap/STATUS.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"last_updated": "2026-03-25T03:00:00Z",
"revision_note": "Integrated GA control plane integrity checks and PR9 trust intelligence layer hardening with deterministic manifest verification.",
"last_updated": "2026-03-28T01:30:00Z",
"revision_note": "Reassessed GA readiness and reduced branch-protection drift false positives by distinguishing unknown verification states from confirmed policy drift.",
"initiatives": [
{
"id": "branch-protection-drift-signal-hardening",
"status": "completed",
"owner": "codex",
"notes": "Updated branch-protection drift checker to emit drift_status (in_sync|drift_detected|unknown), avoid false drift positives when GitHub metadata is inaccessible, and add explicit fail-on-unknown behavior with regression tests."
},
{
"id": "pr9-trust-intelligence-layer",
"status": "completed",
Expand Down
95 changes: 67 additions & 28 deletions scripts/release/check_branch_protection_drift.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ EXCEPTIONS_FILE="${REPO_ROOT}/docs/ci/REQUIRED_CHECKS_EXCEPTIONS.yml"
OUT_DIR="artifacts/release-train"
VERBOSE=false
FAIL_ON_DRIFT=false
FAIL_ON_UNKNOWN=false

usage() {
cat << 'EOF'
Expand All @@ -42,6 +43,7 @@ Options:
--exceptions FILE Exceptions file path (default: docs/ci/REQUIRED_CHECKS_EXCEPTIONS.yml)
--out-dir DIR Output directory (default: artifacts/release-train)
--fail-on-drift Exit with code 1 if drift is detected
--fail-on-unknown Exit with code 1 if branch protection cannot be evaluated
--verbose Enable verbose logging
--help Show this help

Expand Down Expand Up @@ -100,6 +102,10 @@ while [[ $# -gt 0 ]]; do
FAIL_ON_DRIFT=true
shift
;;
--fail-on-unknown)
FAIL_ON_UNKNOWN=true
shift
;;
--verbose)
VERBOSE=true
shift
Expand Down Expand Up @@ -212,40 +218,47 @@ GITHUB_CHECKS=""
GITHUB_COUNT=0
API_ACCESSIBLE=true

# Try to fetch branch protection
set +e
API_RESPONSE=$(gh api "$API_ENDPOINT" 2>&1)
API_EXIT_CODE=$?
set -e

if [[ $API_EXIT_CODE -ne 0 ]]; then
if ! command -v gh &>/dev/null; then
API_ACCESSIBLE=false

if echo "$API_RESPONSE" | grep -q "404"; then
API_ERROR="Branch protection not configured for $BRANCH"
log_warn "$API_ERROR"
elif echo "$API_RESPONSE" | grep -q "403"; then
API_ERROR="Insufficient permissions to read branch protection (requires admin or read:org scope)"
log_warn "$API_ERROR"
else
API_ERROR="API error: $API_RESPONSE"
log_warn "$API_ERROR"
fi
API_ERROR="GitHub CLI (gh) is not installed; cannot query branch protection"
log_warn "$API_ERROR"
else
# Extract required contexts (check names)
GITHUB_CHECKS=$(echo "$API_RESPONSE" | jq -r '.contexts[]? // empty' 2>/dev/null | sort || echo "")
# Try to fetch branch protection
set +e
API_RESPONSE=$(gh api "$API_ENDPOINT" 2>&1)
API_EXIT_CODE=$?
set -e

if [[ $API_EXIT_CODE -ne 0 ]]; then
API_ACCESSIBLE=false

if echo "$API_RESPONSE" | grep -q "404"; then
API_ERROR="Branch protection not configured for $BRANCH"
log_warn "$API_ERROR"
elif echo "$API_RESPONSE" | grep -q "403"; then
API_ERROR="Insufficient permissions to read branch protection (requires admin or read:org scope)"
log_warn "$API_ERROR"
else
API_ERROR="API error: $API_RESPONSE"
log_warn "$API_ERROR"
fi
else
# Extract required contexts (check names)
GITHUB_CHECKS=$(echo "$API_RESPONSE" | jq -r '.contexts[]? // empty' 2>/dev/null | sort || echo "")

# Also try the newer 'checks' array format
if [[ -z "$GITHUB_CHECKS" ]]; then
GITHUB_CHECKS=$(echo "$API_RESPONSE" | jq -r '.checks[]?.context // empty' 2>/dev/null | sort || echo "")
fi
# Also try the newer 'checks' array format
if [[ -z "$GITHUB_CHECKS" ]]; then
GITHUB_CHECKS=$(echo "$API_RESPONSE" | jq -r '.checks[]?.context // empty' 2>/dev/null | sort || echo "")
fi

GITHUB_COUNT=$(echo "$GITHUB_CHECKS" | grep -c . || echo 0)
log "GitHub requires $GITHUB_COUNT status checks"
GITHUB_COUNT=$(echo "$GITHUB_CHECKS" | grep -c . || echo 0)
log "GitHub requires $GITHUB_COUNT status checks"
fi
fi

# --- Step 4: Compare sets ---
DRIFT_DETECTED=false
DRIFT_STATUS="in_sync"
MISSING_IN_GITHUB=()
EXTRA_IN_GITHUB=()
EXCEPTED_MISSING=()
Expand Down Expand Up @@ -285,7 +298,11 @@ if [[ "$API_ACCESSIBLE" == "true" ]]; then
log "Missing in GitHub: ${#MISSING_IN_GITHUB[@]} (${#EXCEPTED_MISSING[@]} excepted)"
log "Extra in GitHub: ${#EXTRA_IN_GITHUB[@]} (${#EXCEPTED_EXTRA[@]} excepted)"
else
DRIFT_DETECTED=true # Unknown state is treated as potential drift
DRIFT_STATUS="unknown"
fi

if [[ "$DRIFT_STATUS" != "unknown" && "$DRIFT_DETECTED" == "true" ]]; then
DRIFT_STATUS="drift_detected"
fi

# --- Step 5: Generate reports ---
Expand All @@ -311,6 +328,7 @@ cat > "$OUT_DIR/branch_protection_drift_report.json" << EOF
"exceptions_loaded": $EXCEPTIONS_LOADED,
"api_accessible": $API_ACCESSIBLE,
"api_error": $(jq -n --arg err "$API_ERROR" 'if $err == "" then null else $err end'),
"drift_status": "$DRIFT_STATUS",
"drift_detected": $DRIFT_DETECTED,
"summary": {
"policy_check_count": $POLICY_COUNT,
Expand Down Expand Up @@ -354,6 +372,7 @@ cat > "$OUT_DIR/branch_protection_drift_report.md" << EOF
| Extra in GitHub | ${#EXTRA_IN_GITHUB[@]} |
| Excepted (Missing) | ${#EXCEPTED_MISSING[@]} |
| Excepted (Extra) | ${#EXCEPTED_EXTRA[@]} |
| Drift Status | $DRIFT_STATUS |
| **Drift Detected** | $DRIFT_DETECTED |

---
Expand Down Expand Up @@ -382,6 +401,20 @@ Unable to read branch protection settings. This could mean:
EOF
fi

if [[ "$DRIFT_STATUS" == "unknown" ]]; then
cat >> "$OUT_DIR/branch_protection_drift_report.md" << EOF
## Status: Unknown (Verification Blocked)

Branch protection drift could not be evaluated because GitHub branch protection metadata
was not accessible in this environment.

> This is a governance visibility blocker, not confirmed policy drift.

---

EOF
fi

if [[ ${#MISSING_IN_GITHUB[@]} -gt 0 ]]; then
cat >> "$OUT_DIR/branch_protection_drift_report.md" << EOF
## Missing in GitHub Branch Protection
Expand Down Expand Up @@ -537,7 +570,7 @@ log_info "Drift report generated:"
log_info " JSON: $OUT_DIR/branch_protection_drift_report.json"
log_info " Markdown: $OUT_DIR/branch_protection_drift_report.md"

if [[ "$DRIFT_DETECTED" == "true" ]]; then
if [[ "$DRIFT_STATUS" == "drift_detected" ]]; then
log_warn "DRIFT DETECTED - Policy and GitHub branch protection are out of sync"
if [[ ${#MISSING_IN_GITHUB[@]} -gt 0 ]]; then
log_warn " Missing in GitHub: ${MISSING_IN_GITHUB[*]}"
Expand All @@ -550,6 +583,12 @@ if [[ "$DRIFT_DETECTED" == "true" ]]; then
log_error "Failing due to detected drift (--fail-on-drift active)"
exit 1
fi
elif [[ "$DRIFT_STATUS" == "unknown" ]]; then
log_warn "DRIFT STATUS UNKNOWN - unable to evaluate branch protection in this environment"
if [[ "$FAIL_ON_UNKNOWN" == "true" ]]; then
log_error "Failing due to unknown drift status (--fail-on-unknown active)"
exit 1
fi
else
log_info "No drift detected - Policy and GitHub branch protection are in sync"
fi
Expand Down
96 changes: 96 additions & 0 deletions scripts/release/tests/check_branch_protection_drift.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# check_branch_protection_drift.test.sh
# Focused regression tests for unknown-vs-drift handling in drift checks.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RELEASE_SCRIPTS="${SCRIPT_DIR}/.."
TEMP_DIR=""

TESTS_RUN=0
TESTS_FAILED=0

setup() {
TEMP_DIR=$(mktemp -d)
}

teardown() {
if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then
rm -rf "${TEMP_DIR}"
fi
}

assert_eq() {
local expected="$1"
local actual="$2"
local message="$3"
TESTS_RUN=$((TESTS_RUN + 1))
if [[ "${expected}" != "${actual}" ]]; then
echo "[FAIL] ${message} (expected='${expected}' actual='${actual}')"
TESTS_FAILED=$((TESTS_FAILED + 1))
else
echo "[PASS] ${message}"
fi
}

test_unknown_when_gh_unavailable() {
setup

local out_dir="${TEMP_DIR}/out"
mkdir -p "${out_dir}"

# Ensure gh is not discoverable for this test process.
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
"${RELEASE_SCRIPTS}/check_branch_protection_drift.sh" \
Comment on lines +47 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make gh-unavailable tests actually hide GitHub CLI

The regression test claims to make gh undiscoverable, but this PATH still includes standard bins (/usr/local/bin, /usr/bin, etc.) where gh is commonly installed. In environments with GitHub CLI available, the script will take the live API path instead of the intended unknown path, so the assertions become flaky and no longer validate the new unknown-state behavior deterministically.

Useful? React with 👍 / 👎.

--repo BHG/summit \
--branch main \
--out-dir "${out_dir}" >/dev/null 2>&1 || true

local report="${out_dir}/branch_protection_drift_report.json"
local drift_status
local drift_detected
drift_status=$(jq -r '.drift_status' "${report}")
drift_detected=$(jq -r '.drift_detected' "${report}")

assert_eq "unknown" "${drift_status}" "drift_status is unknown when gh metadata is inaccessible"
assert_eq "false" "${drift_detected}" "drift_detected is false when status is unknown"

teardown
}
Comment on lines +40 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current test structure can leak temporary directories if a test fails before teardown is called. This can happen if a command fails and the script exits due to set -e. To make the test more robust, you can use trap to ensure the cleanup logic in teardown is always executed when the function returns. This also allows you to remove the explicit call to teardown at the end of the function.

test_unknown_when_gh_unavailable() {
  setup
  trap teardown RETURN

  local out_dir="${TEMP_DIR}/out"
  mkdir -p "${out_dir}"

  # Ensure gh is not discoverable for this test process.
  PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
    "${RELEASE_SCRIPTS}/check_branch_protection_drift.sh" \
      --repo BHG/summit \
      --branch main \
      --out-dir "${out_dir}" >/dev/null 2>&1 || true

  local report="${out_dir}/branch_protection_drift_report.json"
  local drift_status
  local drift_detected
  drift_status=$(jq -r '.drift_status' "${report}")
  drift_detected=$(jq -r '.drift_detected' "${report}")

  assert_eq "unknown" "${drift_status}" "drift_status is unknown when gh metadata is inaccessible"
  assert_eq "false" "${drift_detected}" "drift_detected is false when status is unknown"
}


test_fail_on_unknown_exits_nonzero() {
setup

local out_dir="${TEMP_DIR}/out"
mkdir -p "${out_dir}"

set +e
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
"${RELEASE_SCRIPTS}/check_branch_protection_drift.sh" \
--repo BHG/summit \
--branch main \
--fail-on-unknown \
--out-dir "${out_dir}" >/dev/null 2>&1
local exit_code=$?
set -e

assert_eq "1" "${exit_code}" "--fail-on-unknown exits with status 1 when status is unknown"

teardown
}
Comment on lines +65 to +84
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve robustness and prevent resource leaks, this test should also use trap to guarantee that teardown is called. This ensures the temporary directory is cleaned up even if an error causes the test to exit prematurely.

Suggested change
test_fail_on_unknown_exits_nonzero() {
setup
local out_dir="${TEMP_DIR}/out"
mkdir -p "${out_dir}"
set +e
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
"${RELEASE_SCRIPTS}/check_branch_protection_drift.sh" \
--repo BHG/summit \
--branch main \
--fail-on-unknown \
--out-dir "${out_dir}" >/dev/null 2>&1
local exit_code=$?
set -e
assert_eq "1" "${exit_code}" "--fail-on-unknown exits with status 1 when status is unknown"
teardown
}
test_fail_on_unknown_exits_nonzero() {
setup
trap teardown RETURN
local out_dir="${TEMP_DIR}/out"
mkdir -p "${out_dir}"
set +e
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
"${RELEASE_SCRIPTS}/check_branch_protection_drift.sh" \
--repo BHG/summit \
--branch main \
--fail-on-unknown \
--out-dir "${out_dir}" >/dev/null 2>&1
local exit_code=$?
set -e
assert_eq "1" "${exit_code}" "--fail-on-unknown exits with status 1 when status is unknown"
}


main() {
test_unknown_when_gh_unavailable
test_fail_on_unknown_exits_nonzero

echo
echo "Tests run: ${TESTS_RUN}"
if [[ "${TESTS_FAILED}" -gt 0 ]]; then
echo "Failures: ${TESTS_FAILED}"
exit 1
fi
echo "All tests passed."
}

main "$@"
Loading