sim-shape 0.1.1

Shape algebra, comparison, and match-hook helpers.
Documentation
# Generated by sim-private: sh bin/simctl authorship-ci install
name: authorship

on:
  pull_request:
    types: [opened, synchronize, reopened, edited, ready_for_review]
  push:
    branches: ["**"]

permissions:
  contents: read
  pull-requests: read

jobs:
  authorship:
    name: authorship
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Check commit and pull request metadata
        env:
          GITHUB_EVENT_NAME: ${{ github.event_name }}
          GITHUB_EVENT_PATH: ${{ github.event_path }}
        run: |
          python3 <<'PY'
          import json
          import os
          import re
          import subprocess
          import sys

          BAD_NAME = re.compile(r"claude|anthropic|^\s*noreply\s*$", re.I)
          BAD_EMAIL = re.compile(r"claude|anthropic", re.I)
          CO_TRAILER = re.compile(r"^\s*co-authored-by:\s*(?P<name>[^<]*)<(?P<email>[^>]*)>", re.I)
          CLAUDE_SESSION = re.compile(r"^\s*claude-session\s*:", re.I)
          GENERATED_CLAUDE = re.compile(r"generated\s+(with|by)\s+.*claude", re.I)
          GENERATED_AI = re.compile(r"generated\s+(with|by)\s+.*\bAI\b", re.I)
          AI_TRAILER = re.compile(r"^\s*(ai-authored-by|ai-assisted-by|generated-by)\s*:", re.I)
          FS = "\x1f"
          ZEROS = "0" * 40

          def bad_identity(name, email):
              return bool(BAD_NAME.search(name or "") or BAD_EMAIL.search(email or ""))

          def bad_coauthor_line(line):
              match = CO_TRAILER.match(line or "")
              return bool(match and bad_identity(match.group("name"), match.group("email")))

          def bad_attribution_line(line):
              line = line or ""
              return bool(
                  bad_coauthor_line(line)
                  or CLAUDE_SESSION.search(line)
                  or GENERATED_CLAUDE.search(line)
                  or GENERATED_AI.search(line)
                  or AI_TRAILER.search(line)
              )

          def classify_text(text):
              return [line for line in (text or "").splitlines() if bad_attribution_line(line)]

          def git_log(args):
              fmt = FS.join(["%H", "%an", "%ae", "%cn", "%ce", "%B"])
              proc = subprocess.run(
                  ["git", "log", "-z", f"--format={fmt}", *args],
                  text=True,
                  stdout=subprocess.PIPE,
                  stderr=subprocess.PIPE,
              )
              if proc.returncode != 0:
                  print(proc.stderr, file=sys.stderr)
                  raise SystemExit(proc.returncode)
              return proc.stdout

          def git_ok(args):
              return subprocess.run(
                  ["git", *args],
                  stdout=subprocess.DEVNULL,
                  stderr=subprocess.DEVNULL,
              ).returncode == 0

          def try_ensure_commit(sha, remote_url=None, ref=None):
              if git_ok(["cat-file", "-e", f"{sha}^{{commit}}"]):
                  return True
              if git_ok(["fetch", "--no-tags", "--depth=1", "origin", sha]):
                  return True
              if remote_url and ref:
                  dst = f"refs/remotes/authorship-pr/{sha}"
                  if git_ok(["fetch", "--no-tags", "--depth=1", remote_url, f"+refs/heads/{ref}:{dst}"]):
                      return True
              return False

          def ensure_commit(sha, remote_url=None, ref=None):
              if try_ensure_commit(sha, remote_url, ref):
                  return
              print(f"cannot fetch commit {sha}", file=sys.stderr)
              raise SystemExit(1)

          def ensure_origin_branch(branch):
              if not branch:
                  return None
              ref = f"origin/{branch}"
              if git_ok(["rev-parse", "--verify", ref]):
                  return ref
              if git_ok(["fetch", "--no-tags", "--depth=1", "origin", f"{branch}:refs/remotes/{ref}"]):
                  return ref
              return None

          def push_rev_args(before, after, default_branch):
              ensure_commit(after)
              if before != ZEROS and try_ensure_commit(before):
                  return [f"{before}..{after}"]
              base = ensure_origin_branch(default_branch)
              if base:
                  if before != ZEROS:
                      print(
                          f"push before {before[:12]} unavailable; scanning {base}..{after[:12]}",
                          file=sys.stderr,
                      )
                  return [f"{base}..{after}"]
              if before != ZEROS:
                  print(
                      f"push before {before[:12]} unavailable and default branch unavailable; scanning {after[:12]}",
                      file=sys.stderr,
                  )
              return [after]

          def scan_commits(args):
              bad = []
              for record in git_log(args).split("\0"):
                  if not record.strip():
                      continue
                  fields = record.split(FS)
                  if len(fields) < 6:
                      continue
                  sha, an, ae, cn, ce, body = fields[:6]
                  finding = []
                  if bad_identity(an, ae):
                      finding.append(f"author {an} <{ae}>")
                  if bad_identity(cn, ce):
                      finding.append(f"committer {cn} <{ce}>")
                  for line in classify_text(body):
                      finding.append(f"message line {line}")
                  if finding:
                      bad.append((sha, finding))
              return bad

          def event_json():
              with open(os.environ["GITHUB_EVENT_PATH"], "r", encoding="utf-8") as handle:
                  return json.load(handle)

          event_name = os.environ.get("GITHUB_EVENT_NAME", "")
          event = event_json()
          pr_bad = []
          if event_name == "pull_request":
              pull = event["pull_request"]
              base = pull["base"]["sha"]
              head = pull["head"]["sha"]
              ensure_commit(base)
              ensure_commit(
                  head,
                  (pull.get("head", {}).get("repo") or {}).get("clone_url"),
                  pull.get("head", {}).get("ref"),
              )
              rev_args = [f"{base}..{head}"]
              for field in ("title", "body"):
                  for line in classify_text(pull.get(field) or ""):
                      pr_bad.append(f"{field} line: {line}")
          elif event_name == "push":
              before = event.get("before") or ZEROS
              after = event.get("after") or ZEROS
              if after == ZEROS:
                  raise SystemExit(0)
              default_branch = (event.get("repository") or {}).get("default_branch")
              rev_args = push_rev_args(before, after, default_branch)
          else:
              rev_args = ["HEAD"]

          failures = []
          for sha, finding in scan_commits(rev_args):
              for item in finding:
                  failures.append(f"{sha[:12]}: {item}")
          for line in pr_bad:
              failures.append(f"pull request {line}")
          if failures:
              print("authorship metadata check failed", file=sys.stderr)
              for item in failures:
                  print(item, file=sys.stderr)
              raise SystemExit(1)
          print("authorship metadata check passed")
          PY