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
101 changes: 101 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# CI guards for rillmd/rill (PUBLIC repo).
#
# Three jobs, all claude-free:
# lint -- bash -n syntax check + pinned shellcheck (severity=warning)
# test -- the pure-shell test/cli/ suites on ubuntu + macos
# guard -- CJK allowlist scan + email/phone/secrets regex scan, each run
# twice: over the full tree and over the PR's commit messages
#
# The private-vocabulary PII scan is deliberately NOT here: that vocabulary
# cannot live in a public repo. It runs locally via
# bin/hooks/pre-push-pii-mapping-check.sh (terms read from an out-of-repo
# file). CI covers only vocabulary-independent checks.

name: CI

on:
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: bash -n (syntax) over bin/, plugins/, test/ shell files
run: |
set -euo pipefail
mapfile -t files < <(git ls-files \
'bin/rill' 'bin/rill-inbox-process' 'bin/hooks/*.sh' \
'plugins/*.sh' 'plugins/*/*.sh' 'plugins/*/lib/*.sh' \
'test/*.sh' 'test/*/*.sh')
echo "bash -n over ${#files[@]} files"
[ "${#files[@]}" -gt 0 ]
for f in "${files[@]}"; do bash -n "$f"; done

- name: shellcheck v0.11.0 (pinned) at severity=warning
run: |
set -euo pipefail
url="https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz"
sha256="8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198"
curl -fsSL "$url" -o /tmp/shellcheck.tar.xz
echo "$sha256 /tmp/shellcheck.tar.xz" | sha256sum -c -
tar -xf /tmp/shellcheck.tar.xz -C /tmp
sc=/tmp/shellcheck-v0.11.0/shellcheck
"$sc" --version
mapfile -t files < <(git ls-files \
'bin/rill' 'bin/rill-inbox-process' 'bin/hooks/*.sh' \
'plugins/_lib.sh' 'plugins/*/*.sh' 'plugins/*/lib/*.sh')
echo "shellcheck over ${#files[@]} files"
[ "${#files[@]}" -gt 0 ]
"$sc" -x --severity=warning "${files[@]}"

test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: pure-shell suites (test/cli/, no claude CLI)
run: |
set -euo pipefail
fail=0
for t in test/cli/test-*.sh; do
echo "== $t"
if ! bash "$t"; then
echo "== $t: FAILED"
fail=1
fi
done
exit "$fail"

guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # commit-message scans need base..head history

- name: CJK guard -- full tree (allowlisted)
run: python3 test/cli/cjk-guard.py

- name: CJK guard -- PR commit messages (no allowlist)
run: |
set -euo pipefail
git log --format=%B \
"${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" \
| python3 test/cli/cjk-guard.py --stdin

- name: PII regex guard -- full tree (allowlisted)
run: bash test/cli/pii-regex-guard.sh

- name: PII regex guard -- PR commit messages (no allowlist)
run: |
set -euo pipefail
git log --format=%B \
"${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" \
| bash test/cli/pii-regex-guard.sh --stdin
21 changes: 21 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# shellcheck policy for this repo (initial adoption, task rill-ci-guards).
#
# Gate tiering:
# - CI (and the documented local command) runs `shellcheck --severity=warning`:
# warning-and-above findings BLOCK, note/style findings do not.
# `severity` is a command-line-only option (not honored in this rc file,
# verified against shellcheck v0.11.0), so the threshold lives in
# .github/workflows/ci.yml, not here.
# - A bare local `shellcheck <file>` intentionally still shows note/style
# findings (SC2001/SC2005/SC2012/SC2295/SC1091 class): visible for cleanup,
# but not enforced. Fix them opportunistically; do not scatter silent
# per-line disables to hide them.
#
# Per-line disables are allowed only with a reason comment at the call site
# (current inventory: SC2088 in plugins/twitter/requires.sh and
# plugins/voice-memo/requires.sh -- require_dir expands ~ itself).
#
# Local parity command (pin v0.11.0, same as CI):
# git ls-files 'bin/rill' 'bin/rill-inbox-process' 'bin/hooks/*.sh' \
# 'plugins/_lib.sh' 'plugins/*/*.sh' 'plugins/*/lib/*.sh' \
# | xargs shellcheck -x --severity=warning
84 changes: 84 additions & 0 deletions bin/hooks/pre-push-pii-mapping-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/bin/bash
# pre-push-pii-mapping-check.sh -- block pushes whose commits contain private
# vocabulary (real client/product names), read from an OUT-OF-REPO terms file.
#
# Role split (two hooks, two repos, two audiences):
# - bin/hooks/pre-commit-pii-check.sh = vault-content guard. Installed into
# end-user VAULTS via `rill crypt init` / `rill crypt hook`; scans vault
# files for emails/phones with the vault-local .rill/pii-allowlist.txt.
# - THIS file = rillmd/rill SOURCE-REPO guard. Manually self-installed by a
# contributor into this repo's .git/hooks; never distributed to vaults.
# It scans pushed commits (added diff lines + commit messages) for the
# contributor's private term list, which must never appear in this PUBLIC
# repo -- so the list itself lives OUTSIDE the repo:
# $RILL_DEV_PII_TERMS_FILE, or ~/.config/rill-dev/pii-terms.txt
# (one term per line, # comments and blank lines ignored, matched
# case-insensitively as fixed strings).
# No terms file -> the hook is a no-op: the repo ships the MECHANISM only.
#
# Install (from the repo root):
# ln -s ../../bin/hooks/pre-push-pii-mapping-check.sh .git/hooks/pre-push
# (or cp; for worktrees, use `git rev-parse --git-path hooks`)
#
# stdin (per git pre-push contract): <local-ref> <local-sha> <remote-ref> <remote-sha>
# Exit: 0 allow push, 1 block push, 2 internal error.

set -euo pipefail

ZERO40="0000000000000000000000000000000000000000"

terms_file="${RILL_DEV_PII_TERMS_FILE:-$HOME/.config/rill-dev/pii-terms.txt}"
if [ ! -f "$terms_file" ]; then
echo "pre-push-pii-mapping-check: no terms file at $terms_file -- skipping (mechanism-only mode)" >&2
exit 0
fi

# Strip comments and blank lines; an empty pattern line would match everything.
terms="$(grep -vE '^[[:space:]]*(#|$)' "$terms_file" || true)"
if [ -z "$terms" ]; then
echo "pre-push-pii-mapping-check: terms file is empty -- skipping" >&2
exit 0
fi

blocked=0

scan_commit() {
local sha="$1"
local msg added
msg="$(git log -1 --format=%B "$sha")"
# Added diff lines only (strip the +++ file header); merges diff against
# the first parent, which is what lands on the remote branch.
added="$(git show --first-parent --format= "$sha" | grep '^+' | grep -v '^+++' || true)"
if printf '%s\n%s\n' "$msg" "$added" | grep -iFq -- "$terms" 2>/dev/null; then
echo "pre-push-pii-mapping-check: BLOCKED -- private term found in commit $sha ($(git log -1 --format=%s "$sha"))" >&2
blocked=1
fi
}

while read -r _local_ref local_sha _remote_ref remote_sha; do
# Branch deletion: nothing new is pushed.
[ "$local_sha" = "$ZERO40" ] && continue

if [ "$remote_sha" = "$ZERO40" ]; then
# New remote branch: no remote tip to diff against. Prefer the merge-base
# with origin/main; fall back to "commits not already on any origin ref".
base="$(git merge-base "$local_sha" origin/main 2>/dev/null || true)"
if [ -n "$base" ]; then
revs="$(git rev-list "$base..$local_sha")"
else
revs="$(git rev-list "$local_sha" --not --remotes=origin)"
fi
else
revs="$(git rev-list "$remote_sha..$local_sha")"
fi

for sha in $revs; do
scan_commit "$sha"
done
done

if [ "$blocked" -ne 0 ]; then
echo "pre-push-pii-mapping-check: push rejected -- remove the private term(s) (rewrite the commit/message) and retry" >&2
exit 1
fi
exit 0
Loading
Loading