feat: add runner conversion scripts and strengthen cutover automation
This commit is contained in:
152
runners-conversion/periodVault/check-process.sh
Executable file
152
runners-conversion/periodVault/check-process.sh
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-process.sh
|
||||
# Process compliance checks for PR branches.
|
||||
# Validates: no main commits, no .DS_Store, scripts executable,
|
||||
# spec artifacts exist, iteration counter incremented, commit tags,
|
||||
# and file-scope allowlist enforcement.
|
||||
set -euo pipefail
|
||||
|
||||
BASE_REF="${1:-origin/main}"
|
||||
|
||||
if ! git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then
|
||||
BASE_REF="HEAD~1"
|
||||
fi
|
||||
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
# In GitHub Actions merge refs, HEAD is detached. Derive branch from GITHUB_HEAD_REF
|
||||
# or from the spec directory that matches changed files.
|
||||
if [[ "$BRANCH" == "HEAD" ]]; then
|
||||
if [[ -n "${GITHUB_HEAD_REF:-}" ]]; then
|
||||
BRANCH="$GITHUB_HEAD_REF"
|
||||
else
|
||||
# Fallback: find the spec directory from changed files
|
||||
for f in "${CHANGED_FILES[@]:-}"; do
|
||||
if [[ "$f" == specs/*/spec.md ]]; then
|
||||
BRANCH="${f#specs/}"
|
||||
BRANCH="${BRANCH%/spec.md}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
if [[ "$BRANCH" == "main" ]]; then
|
||||
echo "[check-process] Failing: direct changes on 'main' are not allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHANGED_FILES=()
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" ]] && CHANGED_FILES+=("$line")
|
||||
done < <(git diff --name-only "$BASE_REF"...HEAD)
|
||||
|
||||
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
|
||||
echo "[check-process] No changed files relative to $BASE_REF."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FAILURES=0
|
||||
|
||||
# --- Check 1: No .DS_Store ---
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
HAS_DS_STORE="$(printf '%s\n' "${CHANGED_FILES[@]}" | rg -q '(^|/)\.DS_Store$' && echo 1 || echo 0)"
|
||||
else
|
||||
HAS_DS_STORE="$(printf '%s\n' "${CHANGED_FILES[@]}" | grep -Eq '(^|/)\.DS_Store$' && echo 1 || echo 0)"
|
||||
fi
|
||||
if [[ "$HAS_DS_STORE" == "1" ]]; then
|
||||
echo "[check-process] FAIL: .DS_Store must not be committed."
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
# --- Check 2: Scripts executable ---
|
||||
for file in "${CHANGED_FILES[@]}"; do
|
||||
if [[ "$file" == scripts/*.sh ]] && [[ -f "$file" ]] && [[ ! -x "$file" ]]; then
|
||||
echo "[check-process] FAIL: script is not executable: $file"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Check 3: Spec artifacts exist ---
|
||||
SPEC_DIR="specs/${BRANCH}"
|
||||
if [[ -d "$SPEC_DIR" ]]; then
|
||||
for artifact in spec.md plan.md tasks.md allowed-files.txt; do
|
||||
if [[ ! -f "$SPEC_DIR/$artifact" ]]; then
|
||||
echo "[check-process] FAIL: missing spec artifact: $SPEC_DIR/$artifact"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "[check-process] FAIL: spec directory not found: $SPEC_DIR"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
# --- Check 4: ITERATION incremented ---
|
||||
if [[ -f ITERATION ]]; then
|
||||
BRANCH_ITER="$(tr -d '[:space:]' < ITERATION)"
|
||||
BASE_ITER="$(git show "$BASE_REF":ITERATION 2>/dev/null | tr -d '[:space:]' || echo "0")"
|
||||
if [[ "$BRANCH_ITER" -le "$BASE_ITER" ]] 2>/dev/null; then
|
||||
echo "[check-process] FAIL: ITERATION ($BRANCH_ITER) must be > base ($BASE_ITER)"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Check 5: Commit messages contain [iter N] ---
|
||||
# Skip merge commits (merge resolution, GitHub merge refs) — they don't carry iter tags.
|
||||
COMMITS_WITHOUT_TAG=0
|
||||
while IFS= read -r msg; do
|
||||
# Skip merge commits (start with "Merge " or "merge:")
|
||||
if echo "$msg" | grep -qEi '^(Merge |merge:)'; then
|
||||
continue
|
||||
fi
|
||||
if ! echo "$msg" | grep -qE '\[iter [0-9]+\]'; then
|
||||
echo "[check-process] FAIL: commit missing [iter N] tag: $msg"
|
||||
COMMITS_WITHOUT_TAG=$((COMMITS_WITHOUT_TAG + 1))
|
||||
fi
|
||||
done < <(git log --format='%s' "$BASE_REF"...HEAD)
|
||||
if [[ $COMMITS_WITHOUT_TAG -gt 0 ]]; then
|
||||
FAILURES=$((FAILURES + COMMITS_WITHOUT_TAG))
|
||||
fi
|
||||
|
||||
# --- Check 6: File-scope allowlist ---
|
||||
ALLOWLIST="$SPEC_DIR/allowed-files.txt"
|
||||
if [[ -f "$ALLOWLIST" ]]; then
|
||||
ALLOWED_PATTERNS=()
|
||||
while IFS= read -r line; do
|
||||
# Skip comments and blank lines
|
||||
line="$(echo "$line" | sed 's/#.*//' | xargs)"
|
||||
[[ -z "$line" ]] && continue
|
||||
ALLOWED_PATTERNS+=("$line")
|
||||
done < "$ALLOWLIST"
|
||||
|
||||
for file in "${CHANGED_FILES[@]}"; do
|
||||
MATCHED=false
|
||||
for pattern in "${ALLOWED_PATTERNS[@]}"; do
|
||||
# Use bash pattern matching (supports * and **)
|
||||
# Convert ** to match any path and * to match within directory
|
||||
local_pattern="${pattern}"
|
||||
# shellcheck disable=SC2254
|
||||
if [[ "$file" == $local_pattern ]]; then
|
||||
MATCHED=true
|
||||
break
|
||||
fi
|
||||
# Also try fnmatch-style: specs/foo/* should match specs/foo/bar.md
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
if python3 -c "import fnmatch; exit(0 if fnmatch.fnmatch('$file', '$local_pattern') else 1)" 2>/dev/null; then
|
||||
MATCHED=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ "$MATCHED" == "false" ]]; then
|
||||
echo "[check-process] FAIL: file not in allowlist: $file"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Result ---
|
||||
if [[ $FAILURES -gt 0 ]]; then
|
||||
echo "[check-process] FAILED ($FAILURES issues)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[check-process] PASS ($BASE_REF...HEAD)"
|
||||
Reference in New Issue
Block a user