Adds signed commit policy and devel manual
This commit is contained in:
3 files changed
+381
-11
No files matched your search
+309
@@ -0,0 +1,309 @@
|
||||
# Entwicklerhinweise: Anforderungen bei Push und Pull Request
|
||||
|
||||
Dieses Repository verwendet Jenkins-basierte PR- und Branch-Policies.
|
||||
Ziel ist, dass Code bereits **vor dem Merge** bestimmte Qualitätsanforderungen erfüllt.
|
||||
|
||||
Diese Datei beschreibt, **was bei einem Push erwartet wird**, **was in Jenkins geprüft wird** und **wie du Fehler lokal vermeiden kannst**.
|
||||
|
||||
---
|
||||
|
||||
## Überblick
|
||||
|
||||
Bei Pushes und Pull Requests werden automatisierte Policies ausgeführt.
|
||||
|
||||
Je nach Build-Typ gelten leicht unterschiedliche Vergleichsregeln:
|
||||
|
||||
- **bei Pull Requests** wird gegen den Zielbranch verglichen, z. B. `origin/main`
|
||||
- **bei normalen Branch-Builds** wird auf eine lokale Vergleichsbasis zurückgefallen, z. B. `HEAD~1`, falls kein Zielbranch-Ref verfügbar ist
|
||||
|
||||
Die eigentlichen Qualitätsanforderungen bleiben gleich.
|
||||
|
||||
---
|
||||
|
||||
## Was beim Push / PR geprüft wird
|
||||
|
||||
### 1. `.clang-format` muss vorhanden sein
|
||||
Im Repository-Root muss eine Datei `.clang-format` liegen.
|
||||
|
||||
Ohne diese Datei schlägt der Policy-Check fehl.
|
||||
|
||||
---
|
||||
|
||||
### 2. Geänderte C/C++-Dateien müssen korrekt formatiert sein
|
||||
Für geänderte C/C++-Dateien wird `clang-format` geprüft.
|
||||
|
||||
Betroffen sind insbesondere Dateien mit diesen Endungen:
|
||||
|
||||
- `.c`
|
||||
- `.cc`
|
||||
- `.cpp`
|
||||
- `.cxx`
|
||||
- `.h`
|
||||
- `.hh`
|
||||
- `.hpp`
|
||||
- `.hxx`
|
||||
|
||||
Wenn eine Datei nicht korrekt formatiert ist, schlägt der Build fehl.
|
||||
|
||||
#### Lokal prüfen
|
||||
```bash
|
||||
clang-format --dry-run --Werror <datei>
|
||||
```
|
||||
|
||||
#### Lokal automatisch formatieren
|
||||
```bash
|
||||
clang-format -i <datei>
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
```bash
|
||||
clang-format -i main.cpp tests/test_main.cpp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Zu geänderten C/C++-Dateien muss ein passender Test existieren
|
||||
Wenn du eine C/C++-Datei änderst, muss ein passender Test vorhanden sein.
|
||||
|
||||
Es wird nach Tests in diesen Verzeichnissen gesucht:
|
||||
|
||||
- `tests/`
|
||||
- `test/`
|
||||
- `qt6-after/qt6-migrated-benchmark/tests/`
|
||||
|
||||
Unterstützte typische Testnamen sind zum Beispiel:
|
||||
|
||||
- `test_<name>.cpp`
|
||||
- `<name>_test.cpp`
|
||||
- `<name>test.cpp`
|
||||
- `<name>gtest.cpp`
|
||||
|
||||
Zusätzlich erkennt die Policy auch ähnliche Dateinamen heuristisch, solange `test` oder `gtest` im Dateinamen vorkommt.
|
||||
|
||||
#### Beispiel
|
||||
Wenn du `main.cpp` änderst, ist z. B. passend:
|
||||
|
||||
```text
|
||||
tests/test_main.cpp
|
||||
```
|
||||
|
||||
Wenn kein passender Test gefunden wird, schlägt der Build fehl.
|
||||
|
||||
---
|
||||
|
||||
### 4. Nur gültig SSH-signierte Commits sind erlaubt
|
||||
Neue Commits im geprüften Bereich müssen gültig **mit SSH signiert** sein.
|
||||
|
||||
Die Jenkins-Policy prüft Commit-Signaturen gegen eine erlaubte Signiererdatei:
|
||||
|
||||
```text
|
||||
ci/allowed_signers
|
||||
```
|
||||
|
||||
Wenn ein Commit:
|
||||
|
||||
- nicht signiert ist
|
||||
- nicht per SSH signiert ist
|
||||
- mit einem nicht erlaubten SSH-Schlüssel signiert ist
|
||||
|
||||
dann schlägt der Build fehl.
|
||||
|
||||
---
|
||||
|
||||
## SSH-signierte Commits einrichten
|
||||
|
||||
### 1. SSH-Signing-Key erzeugen
|
||||
Falls du noch keinen dedizierten Signing-Key hast:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_signing -C "dein.name@example.com"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Git für SSH-Signing konfigurieren
|
||||
|
||||
```bash
|
||||
git config --global gpg.format ssh
|
||||
git config --global user.signingkey ~/.ssh/id_ed25519_signing.pub
|
||||
git config --global commit.gpgsign true
|
||||
```
|
||||
|
||||
Damit werden neue Commits automatisch mit deinem SSH-Key signiert.
|
||||
|
||||
---
|
||||
|
||||
### 3. Öffentlichen Schlüssel zur Allowlist hinzufügen
|
||||
Dein öffentlicher Schlüssel muss in `ci/allowed_signers` eingetragen sein.
|
||||
|
||||
Format:
|
||||
|
||||
```text
|
||||
<principal> <public-key>
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
max.mustermann@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
|
||||
```
|
||||
|
||||
Ohne Eintrag in `ci/allowed_signers` wird dein signierter Commit in Jenkins trotzdem abgelehnt.
|
||||
|
||||
---
|
||||
|
||||
### 4. Optional: lokale Verifikation aktivieren
|
||||
Wenn du lokal dieselbe Vertrauenskette wie Jenkins verwenden willst:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/git
|
||||
cp ci/allowed_signers ~/.config/git/allowed_signers
|
||||
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Signierten Commit erstellen
|
||||
```bash
|
||||
git add .
|
||||
git commit -S -m "Meine Änderung"
|
||||
git push
|
||||
```
|
||||
|
||||
Wenn `commit.gpgsign=true` gesetzt ist, signiert Git Commits in der Regel automatisch.
|
||||
|
||||
---
|
||||
|
||||
### 6. Signatur lokal prüfen
|
||||
```bash
|
||||
git log --show-signature -1
|
||||
git verify-commit HEAD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lokaler Minimal-Check vor dem Push
|
||||
|
||||
Vor dem Push solltest du mindestens diese Schritte ausführen:
|
||||
|
||||
### Format anwenden
|
||||
```bash
|
||||
clang-format -i main.cpp tests/test_main.cpp
|
||||
```
|
||||
|
||||
### Policy lokal ausführen
|
||||
```bash
|
||||
python3 ci/check_pr_policies.py
|
||||
```
|
||||
|
||||
### Projekt lokal bauen
|
||||
```bash
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build build --parallel
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verhalten in Jenkins
|
||||
|
||||
Die Jenkins-Pipeline umfasst typischerweise diese Stages:
|
||||
|
||||
1. **Checkout**
|
||||
2. **Policy Checks**
|
||||
3. **Configure**
|
||||
4. **Build**
|
||||
5. **Test**
|
||||
6. **Cleanup**
|
||||
|
||||
Wenn die Policy-Checks fehlschlagen, werden die späteren Build- und Test-Stages nicht mehr ausgeführt.
|
||||
|
||||
---
|
||||
|
||||
## Typische Fehlerursachen
|
||||
|
||||
### Formatfehler
|
||||
Beispiel:
|
||||
```text
|
||||
ERROR: Formatfehler in tests/test_main.cpp
|
||||
```
|
||||
|
||||
#### Lösung
|
||||
```bash
|
||||
clang-format -i tests/test_main.cpp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Kein passender Test gefunden
|
||||
Beispiel:
|
||||
```text
|
||||
ERROR: Kein passender Test unter tests/, test/ oder qt6-after/qt6-migrated-benchmark/tests für main.cpp gefunden.
|
||||
```
|
||||
|
||||
#### Lösung
|
||||
- passenden Test anlegen
|
||||
- Testdatei passend benennen
|
||||
- Test in einem der erlaubten Testverzeichnisse ablegen
|
||||
|
||||
---
|
||||
|
||||
### Commit nicht gültig SSH-signiert
|
||||
Beispiel:
|
||||
```text
|
||||
ERROR: Commit ist nicht gültig mit erlaubtem SSH-Signierer signiert: <sha>
|
||||
```
|
||||
|
||||
#### Lösung
|
||||
- SSH-Signing lokal korrekt konfigurieren
|
||||
- prüfen, ob der richtige Schlüssel verwendet wird
|
||||
- prüfen, ob der öffentliche Schlüssel in `ci/allowed_signers` eingetragen ist
|
||||
- Commit neu erstellen oder neu signieren
|
||||
|
||||
---
|
||||
|
||||
### `.clang-format` fehlt
|
||||
Beispiel:
|
||||
```text
|
||||
ERROR: .clang-format fehlt im Repository-Root.
|
||||
```
|
||||
|
||||
#### Lösung
|
||||
- `.clang-format` im Repository-Root anlegen oder wiederherstellen
|
||||
|
||||
---
|
||||
|
||||
## Empfohlener Workflow
|
||||
|
||||
1. Feature-Branch erstellen
|
||||
2. Änderungen implementieren
|
||||
3. Test ergänzen oder anpassen
|
||||
4. Code formatieren
|
||||
5. Commit SSH-signiert erstellen
|
||||
6. lokal Policy, Build und Tests ausführen
|
||||
7. pushen
|
||||
8. Pull Request öffnen
|
||||
9. Jenkins-Ergebnis prüfen
|
||||
10. erst bei grünem Status mergen
|
||||
|
||||
---
|
||||
|
||||
## Kurz-Checkliste vor jedem Push
|
||||
|
||||
- [ ] C/C++-Dateien sind formatiert
|
||||
- [ ] passender Test existiert
|
||||
- [ ] Commit ist SSH-signiert
|
||||
- [ ] öffentlicher Schlüssel steht in `ci/allowed_signers`
|
||||
- [ ] `python3 ci/check_pr_policies.py` läuft erfolgreich
|
||||
- [ ] Build und Tests laufen lokal
|
||||
|
||||
---
|
||||
|
||||
## Ziel der Regeln
|
||||
|
||||
Die Policies sollen sicherstellen, dass:
|
||||
|
||||
- Code konsistent formatiert ist
|
||||
- Änderungen nachvollziehbar getestet sind
|
||||
- nur autorisierte Signierer Commits einbringen
|
||||
- PRs frühzeitig automatisiert validiert werden
|
||||
@@ -0,0 +1 @@
|
||||
torsten.wierschin@life-maps.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG+sdfrCTkAHvZCwBGPGUaT4VFkX8YvnqprCpCrLajSH
|
||||
Executable → Regular
+71
-11
@@ -11,6 +11,7 @@ TEST_DIRS = [
|
||||
ROOT / "qt6-after" / "qt6-migrated-benchmark" / "tests",
|
||||
]
|
||||
CPP_EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"}
|
||||
ALLOWED_SIGNERS_FILE = ROOT / "ci" / "allowed_signers"
|
||||
|
||||
TEST_PATTERNS = [
|
||||
"test_{name}.cpp",
|
||||
@@ -23,9 +24,11 @@ TEST_PATTERNS = [
|
||||
"{name}gtest.h",
|
||||
]
|
||||
|
||||
|
||||
def run(cmd: str) -> str:
|
||||
return subprocess.check_output(cmd, shell=True, text=True).strip()
|
||||
|
||||
|
||||
def git_ref_exists(ref: str) -> bool:
|
||||
try:
|
||||
subprocess.check_call(
|
||||
@@ -38,9 +41,11 @@ def git_ref_exists(ref: str) -> bool:
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def is_pull_request_build() -> bool:
|
||||
return bool(os.environ.get("CHANGE_ID"))
|
||||
|
||||
|
||||
def resolve_target_ref() -> str | None:
|
||||
raw = os.environ.get("CHANGE_TARGET", "").strip()
|
||||
|
||||
@@ -61,13 +66,15 @@ def resolve_target_ref() -> str | None:
|
||||
|
||||
return None
|
||||
|
||||
def changed_files():
|
||||
|
||||
def changed_files_and_base() -> tuple[list[str], str]:
|
||||
target = resolve_target_ref()
|
||||
|
||||
if target:
|
||||
print(f"Vergleiche gegen Ziel-Ref: {target}")
|
||||
out = run(f"git diff --name-only {target}...HEAD")
|
||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||
files = [line.strip() for line in out.splitlines() if line.strip()]
|
||||
return files, target
|
||||
|
||||
if is_pull_request_build():
|
||||
print("ERROR: Kein gültiger Ziel-Branch-Ref für PR-Build gefunden.")
|
||||
@@ -76,23 +83,46 @@ def changed_files():
|
||||
if git_ref_exists("HEAD~1"):
|
||||
print("Kein Ziel-Branch-Ref gefunden, verwende HEAD~1 für Branch-Build.")
|
||||
out = run("git diff --name-only HEAD~1..HEAD")
|
||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||
files = [line.strip() for line in out.splitlines() if line.strip()]
|
||||
return files, "HEAD~1"
|
||||
|
||||
print("Keine Vergleichsbasis gefunden, behandle alle versionierten Dateien als geändert.")
|
||||
out = run("git ls-files")
|
||||
files = [line.strip() for line in out.splitlines() if line.strip()]
|
||||
return files, ""
|
||||
|
||||
|
||||
def commits_in_range(base: str) -> list[str]:
|
||||
if base:
|
||||
if base == "HEAD~1":
|
||||
out = run("git rev-list HEAD~1..HEAD")
|
||||
else:
|
||||
out = run(f"git rev-list {base}..HEAD")
|
||||
else:
|
||||
out = run("git rev-list HEAD")
|
||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def is_cpp_file(path: Path) -> bool:
|
||||
return path.suffix.lower() in CPP_EXTENSIONS
|
||||
|
||||
|
||||
def require_clang_format():
|
||||
if not (ROOT / ".clang-format").exists():
|
||||
print("ERROR: .clang-format fehlt im Repository-Root.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def require_allowed_signers():
|
||||
if not ALLOWED_SIGNERS_FILE.exists():
|
||||
print(f"ERROR: {ALLOWED_SIGNERS_FILE} fehlt.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_format(path: Path):
|
||||
subprocess.check_call(f"clang-format --dry-run --Werror '{path}'", shell=True)
|
||||
|
||||
|
||||
def candidate_names(path: Path):
|
||||
stem = path.stem.lower()
|
||||
names = {stem}
|
||||
@@ -101,11 +131,13 @@ def candidate_names(path: Path):
|
||||
names.add(stem[:-len(suffix)])
|
||||
return sorted(n for n in names if n)
|
||||
|
||||
|
||||
def iter_test_files():
|
||||
for test_dir in TEST_DIRS:
|
||||
if test_dir.exists():
|
||||
yield from (p for p in test_dir.rglob("*") if p.is_file())
|
||||
|
||||
|
||||
def has_related_test(path: Path) -> bool:
|
||||
test_files = list(iter_test_files())
|
||||
if not test_files:
|
||||
@@ -122,14 +154,46 @@ def has_related_test(path: Path) -> bool:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def verify_commit_signature(commit: str):
|
||||
env = os.environ.copy()
|
||||
env["GIT_CONFIG_COUNT"] = "1"
|
||||
env["GIT_CONFIG_KEY_0"] = "gpg.ssh.allowedSignersFile"
|
||||
env["GIT_CONFIG_VALUE_0"] = str(ALLOWED_SIGNERS_FILE)
|
||||
|
||||
subprocess.check_call(
|
||||
f"git verify-commit {commit}",
|
||||
shell=True,
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
require_clang_format()
|
||||
require_allowed_signers()
|
||||
|
||||
files = changed_files()
|
||||
files, base = changed_files_and_base()
|
||||
if not files:
|
||||
print("Keine geänderten Dateien erkannt.")
|
||||
return
|
||||
|
||||
commits = commits_in_range(base)
|
||||
if not commits:
|
||||
print("Keine neuen Commits im Vergleichsbereich erkannt.")
|
||||
else:
|
||||
for commit in commits:
|
||||
print(f"Prüfe SSH-Signatur: {commit}")
|
||||
try:
|
||||
verify_commit_signature(commit)
|
||||
except subprocess.CalledProcessError:
|
||||
print(
|
||||
"ERROR: Commit ist nicht gültig mit erlaubtem SSH-Signierer signiert: "
|
||||
f"{commit}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
for rel in files:
|
||||
path = ROOT / rel
|
||||
if not path.exists():
|
||||
@@ -143,10 +207,6 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
if not has_related_test(path):
|
||||
print(f"ERROR: Kein passender Test unter tests/, test/ oder qt6-after/qt6-migrated-benchmark/tests für {rel} gefunden.")
|
||||
sys.exit(1)
|
||||
|
||||
print("Policy-Checks erfolgreich.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
print(
|
||||
"ERROR: Kein passender Test unter tests/, test/ oder "
|
||||
f
|
||||
Reference in new issue
Block a user