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
22 changes: 21 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@
- [ ] Ran existing tests with `uv sync && uv run pytest`
- [ ] Tested with a sample project (if applicable)

## Security Review Evidence

<!-- Identify the changed scope, review performed, findings/fixes, and residual risk. -->

- Changed files reviewed:
- Security impact:
- Findings and fixes:
- Residual risk:

## Lessons Learned

<!-- Cite applicable LL-000x controls, or state "No applicable central lesson" with a rationale. -->

- Central register reviewed: `SoloSentryOrg/github-enterprise-management-solosentry/docs/lessons-learned/register.json`
- Active RCA files reviewed:
- Applicable lesson IDs and controls:
- Considered but not applicable:
- If no central lesson applies, rationale:
- Complexity removed or justified:
- Tracking issue or project item:

## AI Disclosure

<!-- Per our Contributing guidelines, AI assistance must be disclosed. -->
Expand All @@ -19,4 +40,3 @@
- [ ] I **did** use AI assistance (describe below)

<!-- If you used AI, briefly describe how (e.g., "Code generated by Copilot", "Consulted ChatGPT for approach"): -->

15 changes: 14 additions & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
# Keep advanced setup active so the org security configuration does not
# replace it with default setup after 90 days without an analysis.
- cron: "17 4 1 * *"

concurrency:
group: codeql-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
security-events: write # Upload CodeQL SARIF results for this repository.
contents: read
strategy:
fail-fast: false
Expand All @@ -20,6 +31,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/lessons-evidence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Lessons Evidence

on:
pull_request:

permissions:
contents: read

concurrency:
group: lessons-evidence-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
validate:
name: lessons-evidence
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false

- name: Validate lessons-learned PR evidence
run: python3 scripts/check-lessons-evidence.py
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their

---

## SoloSentry Governance Controls

- Before changing workflows, security controls, repository governance, release behavior, or cross-repository contracts, review the central register at `SoloSentryOrg/github-enterprise-management-solosentry` under `docs/lessons-learned/register.json` and any referenced active RCA.
- Carry applicable `LL-000x` controls into the implementation plan, pull-request body, verification evidence, and final status. If no lesson applies, state `No applicable central lesson`, give a rationale, and assess whether a new recurring failure mode or RCA follow-up is needed.
- After two failures in one cluster, record the root cause before further fix-forward. After three failed fixes, stop and perform an architecture and simplification review.
- Keep one authoritative CodeQL setup. This repository uses `.github/workflows/codeql.yml` as advanced setup; verify GitHub default-setup state and live check names before changing CodeQL or required checks.
- Use pull requests for `main`, require current-head checks to pass, resolve review findings, and perform a secure review before merge. Do not merge with open security findings or failed required checks.
- Track organization governance delivery through central issue `SoloSentryOrg/github-enterprise-management-solosentry#12` and Project 9 until rollout acceptance criteria and verification are complete.
- Include a short `Complexity removed or justified` statement for workflow, security-control, or governance changes.

---

## Integration Architecture

Each AI agent is a self-contained **integration subpackage** under `src/specify_cli/integrations/<key>/`. The subpackage exposes a single class that declares all metadata and inherits setup/teardown logic from a base class. Built-in integrations are then instantiated and added to the global `INTEGRATION_REGISTRY` by `src/specify_cli/integrations/__init__.py` via `_register_builtins()`.
Expand Down
182 changes: 182 additions & 0 deletions scripts/check-lessons-evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT

"""Reference CI guard for central lessons-learned evidence in pull request bodies."""

from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Any


ROOT = Path.cwd()
LESSON_ID_RE = re.compile(r"\bLL-[0-9]{4}\b")
LESSONS_HEADING_RE = re.compile(r"(?im)^##+\s+(central\s+)?lessons?(\s+learned)?\b.*$")
NOT_APPLICABLE_RE = re.compile(
r"\b(no|none)\s+applicable\s+(central\s+)?lessons?\b|\bcentral\s+lessons?\s*:\s*(n/a|not applicable|none)\b",
re.IGNORECASE,
)
RATIONALE_RE = re.compile(r"\b(rationale|reason|why)\s*:", re.IGNORECASE)
DEFAULT_SCOPED_PATTERNS = (
r"^AGENTS[.]md$",
r"^[.]github/(workflows|ISSUE_TEMPLATE)/",
r"^[.]github/pull_request_template[.]md$",
r"^catalog/",
r"^factory-contracts/",
r"^factory-lifecycle-actions/",
r"^factory-requests/",
r"^images/",
r"^packages/",
r"^policies/",
r"^schemas/",
r"^scripts/",
r"^security/",
r"^docs/",
r"^(README|SECURITY|CONTRIBUTING)[.]md$",
)


class LessonsEvidenceError(Exception):
"""Raised when lessons evidence is missing or malformed."""


def fail(message: str) -> None:
print(f"lessons evidence validation failed: {message}", file=sys.stderr)
raise SystemExit(1)


def load_json(path: Path) -> dict[str, Any]:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise LessonsEvidenceError(f"{path}: JSON parse failed: {exc}") from exc
if not isinstance(data, dict):
raise LessonsEvidenceError(f"{path}: top-level JSON value must be an object")
return data


def run_git_diff(base_sha: str, head_sha: str) -> list[str]:
result = subprocess.run(
["git", "diff", "--name-only", f"{base_sha}...{head_sha}"],
cwd=ROOT,
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if result.returncode != 0:
raise LessonsEvidenceError(result.stderr.strip() or "git diff failed")
return [line.strip() for line in result.stdout.splitlines() if line.strip()]


def event_context(event_path: Path | None) -> tuple[str, list[str], bool]:
if event_path is None:
return "", [], False
event = load_json(event_path)
pull_request = event.get("pull_request")
if not isinstance(pull_request, dict):
return "", [], False
body = pull_request.get("body") or ""
if not isinstance(body, str):
raise LessonsEvidenceError("pull_request.body must be a string when present")
base = pull_request.get("base")
head = pull_request.get("head")
if not isinstance(base, dict) or not isinstance(head, dict):
raise LessonsEvidenceError("pull_request.base and pull_request.head are required")
base_sha = base.get("sha")
head_sha = head.get("sha")
if not isinstance(base_sha, str) or not isinstance(head_sha, str):
raise LessonsEvidenceError("pull_request base/head sha values are required")
return body, run_git_diff(base_sha, head_sha), True


def read_changed_files(args: argparse.Namespace) -> list[str]:
changed: list[str] = []
changed.extend(args.changed_file or [])
if args.changed_files_file:
changed.extend(
line.strip()
for line in args.changed_files_file.read_text(encoding="utf-8").splitlines()
if line.strip()
)
return sorted({path for path in changed if path})


def scoped_files(changed_files: list[str], patterns: list[re.Pattern[str]]) -> list[str]:
return sorted(
path
for path in changed_files
if any(pattern.search(path) for pattern in patterns)
)


def lessons_section(body: str) -> str | None:
match = LESSONS_HEADING_RE.search(body)
if not match:
return None
next_heading = re.search(r"(?m)^##+\s+", body[match.end() :])
if next_heading:
return body[match.start() : match.end() + next_heading.start()]
return body[match.start() :]


def validate_body(body: str) -> None:
section = lessons_section(body)
if section is None:
raise LessonsEvidenceError(
"scoped changes require a 'Lessons Learned' PR section citing lesson IDs or a not-applicable rationale"
)
ids = sorted(set(LESSON_ID_RE.findall(section)))
if ids:
print(f"lessons evidence: cited {', '.join(ids)}")
return
if NOT_APPLICABLE_RE.search(section) and RATIONALE_RE.search(section):
print("lessons evidence: no applicable central lesson recorded with rationale")
return
raise LessonsEvidenceError(
"Lessons Learned section must cite at least one LL-000x ID or state no applicable central lesson with a rationale"
)


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--event-path", type=Path, default=os.environ.get("GITHUB_EVENT_PATH"))
parser.add_argument("--body-file", type=Path)
parser.add_argument("--changed-file", action="append")
parser.add_argument("--changed-files-file", type=Path)
parser.add_argument("--scoped-pattern", action="append", default=[])
return parser.parse_args()


def main() -> int:
args = parse_args()
try:
body = args.body_file.read_text(encoding="utf-8") if args.body_file else ""
changed = read_changed_files(args)
is_pr_event = False
if args.event_path and not body and not changed:
body, changed, is_pr_event = event_context(args.event_path)
if args.event_path and not is_pr_event and not body and not changed:
print("lessons evidence check skipped: event is not a pull_request")
return 0
raw_patterns = args.scoped_pattern or list(DEFAULT_SCOPED_PATTERNS)
patterns = [re.compile(pattern) for pattern in raw_patterns]
scoped = scoped_files(changed, patterns)
if not scoped:
print("lessons evidence check passed: no scoped files changed")
return 0
validate_body(body)
except LessonsEvidenceError as exc:
fail(str(exc))
print(f"lessons evidence check passed for {len(scoped)} scoped file(s)")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading