From 2e10722aba99258352d6ea526c007ebd83d86bc5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:48:40 +0000 Subject: [PATCH 1/3] Add timestamps and machine info to all generated artifacts - api_reference.md footer now shows when and from which machine it was generated - openapi_baseline.json wrapped with _meta.saved_at and _meta.saved_by_machine - drift_summary.md includes detection timestamp and machine - audit_report.md includes generation timestamp and machine - Backward compatible: check command handles both old (raw spec) and new (_meta wrapper) baseline formats Co-Authored-By: unknown <> --- artifacts/openapi_baseline.json | 532 ++++++++++++++++---------------- docs/api_reference.md | 2 +- drift_guard/guard.py | 47 ++- drift_guard/openapi_to_md.py | 11 +- 4 files changed, 323 insertions(+), 269 deletions(-) diff --git a/artifacts/openapi_baseline.json b/artifacts/openapi_baseline.json index b738e51..0cecea7 100644 --- a/artifacts/openapi_baseline.json +++ b/artifacts/openapi_baseline.json @@ -1,297 +1,303 @@ { - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "title": "Detail", - "type": "array" - } - }, - "title": "HTTPValidationError", - "type": "object" - }, - "HealthResponse": { - "description": "Health-check response.", - "properties": { - "status": { - "description": "Service status", - "examples": [ - "ok" - ], - "title": "Status", - "type": "string" - } - }, - "required": [ - "status" - ], - "title": "HealthResponse", - "type": "object" - }, - "User": { - "description": "Representation of a user returned by the API.", - "properties": { - "email": { - "description": "Email address", - "title": "Email", - "type": "string" - }, - "id": { - "description": "Unique user identifier", - "title": "Id", - "type": "integer" - }, - "name": { - "description": "Full name of the user", - "title": "Name", - "type": "string" + "_meta": { + "saved_at": "2026-02-23 19:48:05 UTC", + "saved_by_machine": "devin-box" + }, + "spec": { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } }, - "role": { - "default": "member", - "description": "User role", - "title": "Role", - "type": "string" - } + "title": "HTTPValidationError", + "type": "object" }, - "required": [ - "id", - "name", - "email" - ], - "title": "User", - "type": "object" - }, - "UserCreate": { - "description": "Request body for creating a new user.", - "properties": { - "email": { - "description": "Email address", - "title": "Email", - "type": "string" - }, - "name": { - "description": "Full name of the user", - "title": "Name", - "type": "string" + "HealthResponse": { + "description": "Health-check response.", + "properties": { + "status": { + "description": "Service status", + "examples": [ + "ok" + ], + "title": "Status", + "type": "string" + } }, - "role": { - "default": "member", - "description": "User role", - "title": "Role", - "type": "string" - } + "required": [ + "status" + ], + "title": "HealthResponse", + "type": "object" }, - "required": [ - "name", - "email" - ], - "title": "UserCreate", - "type": "object" - }, - "ValidationError": { - "properties": { - "ctx": { - "title": "Context", - "type": "object" - }, - "input": { - "title": "Input" - }, - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] + "User": { + "description": "Representation of a user returned by the API.", + "properties": { + "email": { + "description": "Email address", + "title": "Email", + "type": "string" }, - "title": "Location", - "type": "array" - }, - "msg": { - "title": "Message", - "type": "string" + "id": { + "description": "Unique user identifier", + "title": "Id", + "type": "integer" + }, + "name": { + "description": "Full name of the user", + "title": "Name", + "type": "string" + }, + "role": { + "default": "member", + "description": "User role", + "title": "Role", + "type": "string" + } }, - "type": { - "title": "Error Type", - "type": "string" - } + "required": [ + "id", + "name", + "email" + ], + "title": "User", + "type": "object" }, - "required": [ - "loc", - "msg", - "type" - ], - "title": "ValidationError", - "type": "object" - } - } - }, - "info": { - "description": "A minimal enterprise API used to demonstrate Docs Drift Guard.", - "title": "Enterprise User API", - "version": "1.0.0" - }, - "openapi": "3.1.0", - "paths": { - "/health": { - "get": { - "description": "Return the current health status of the service.", - "operationId": "health_check_health_get", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthResponse" - } - } + "UserCreate": { + "description": "Request body for creating a new user.", + "properties": { + "email": { + "description": "Email address", + "title": "Email", + "type": "string" + }, + "name": { + "description": "Full name of the user", + "title": "Name", + "type": "string" }, - "description": "Successful Response" - } + "role": { + "default": "member", + "description": "User role", + "title": "Role", + "type": "string" + } + }, + "required": [ + "name", + "email" + ], + "title": "UserCreate", + "type": "object" }, - "summary": "Health check", - "tags": [ - "health" - ] + "ValidationError": { + "properties": { + "ctx": { + "title": "Context", + "type": "object" + }, + "input": { + "title": "Input" + }, + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "title": "Location", + "type": "array" + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + }, + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError", + "type": "object" + } } }, - "/v1/audit": { - "delete": { - "description": "Clear all recorded mismatches.", - "operationId": "clear_audit_log_v1_audit_delete", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": true, - "title": "Response Clear Audit Log V1 Audit Delete", - "type": "object" + "info": { + "description": "A minimal enterprise API used to demonstrate Docs Drift Guard.", + "title": "Enterprise User API", + "version": "1.0.0" + }, + "openapi": "3.1.0", + "paths": { + "/health": { + "get": { + "description": "Return the current health status of the service.", + "operationId": "health_check_health_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } } - } - }, - "description": "Successful Response" - } - }, - "summary": "Clear the audit log", - "tags": [ - "audit" - ] + }, + "description": "Successful Response" + } + }, + "summary": "Health check", + "tags": [ + "health" + ] + } }, - "get": { - "description": "Return all API call mismatches recorded by the middleware.\nUseful for identifying clients that are using stale field names\nor calling endpoints that don't exist.", - "operationId": "get_audit_log_v1_audit_get", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": true, - "title": "Response Get Audit Log V1 Audit Get", - "type": "object" + "/v1/audit": { + "delete": { + "description": "Clear all recorded mismatches.", + "operationId": "clear_audit_log_v1_audit_delete", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "title": "Response Clear Audit Log V1 Audit Delete", + "type": "object" + } } - } - }, - "description": "Successful Response" - } - }, - "summary": "View API call mismatch audit log", - "tags": [ - "audit" - ] - } - }, - "/v1/users": { - "post": { - "description": "Create a new user and return the created resource.", - "operationId": "create_user_v1_users_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserCreate" - } + }, + "description": "Successful Response" } }, - "required": true + "summary": "Clear the audit log", + "tags": [ + "audit" + ] }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" + "get": { + "description": "Return all API call mismatches recorded by the middleware.\nUseful for identifying clients that are using stale field names\nor calling endpoints that don't exist.", + "operationId": "get_audit_log_v1_audit_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "title": "Response Get Audit Log V1 Audit Get", + "type": "object" + } } - } - }, - "description": "Successful Response" + }, + "description": "Successful Response" + } }, - "422": { + "summary": "View API call mismatch audit log", + "tags": [ + "audit" + ] + } + }, + "/v1/users": { + "post": { + "description": "Create a new user and return the created resource.", + "operationId": "create_user_v1_users_post", + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/UserCreate" } } }, - "description": "Validation Error" - } - }, - "summary": "Create a new user", - "tags": [ - "users" - ] - } - }, - "/v1/users/{user_id}": { - "get": { - "description": "Retrieve a single user by their unique ID.", - "operationId": "get_user_v1_users__user_id__get", - "parameters": [ - { - "in": "path", - "name": "user_id", - "required": true, - "schema": { - "title": "User Id", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } } - } + }, + "description": "Successful Response" }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } + }, + "description": "Validation Error" + } + }, + "summary": "Create a new user", + "tags": [ + "users" + ] + } + }, + "/v1/users/{user_id}": { + "get": { + "description": "Retrieve a single user by their unique ID.", + "operationId": "get_user_v1_users__user_id__get", + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "title": "User Id", + "type": "integer" } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "Successful Response" }, - "description": "Validation Error" - } - }, - "summary": "Get a user by ID", - "tags": [ - "users" - ] + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Get a user by ID", + "tags": [ + "users" + ] + } } } } diff --git a/docs/api_reference.md b/docs/api_reference.md index 21cdd4b..21eeebc 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -160,4 +160,4 @@ curl -s "http://localhost:8000/v1/users/1" | python -m json.tool --- -_This file was auto-generated by [Docs Drift Guard](../drift_guard/guard.py). Do not edit manually._ +_This file was auto-generated by [Docs Drift Guard](../drift_guard/guard.py) on **2026-02-23 19:48:05 UTC** from **devin-box**. Do not edit manually._ diff --git a/drift_guard/guard.py b/drift_guard/guard.py index 4bd9270..071bcdb 100644 --- a/drift_guard/guard.py +++ b/drift_guard/guard.py @@ -18,7 +18,9 @@ from __future__ import annotations import json +import platform import sys +from datetime import datetime, timezone from pathlib import Path import requests @@ -77,13 +79,34 @@ def _write_text(path: Path, text: str) -> None: # Commands # --------------------------------------------------------------------------- +def _timestamp() -> str: + """Return a human-readable UTC timestamp.""" + return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + + +def _machine() -> str: + """Return the hostname of the machine running this command.""" + return platform.node() or "unknown" + + def cmd_baseline(url: str = DEFAULT_OPENAPI_URL) -> None: """Save current OpenAPI as baseline and regenerate the Markdown docs.""" console.rule("[bold blue]Drift Guard – Baseline[/bold blue]") spec = _fetch_openapi(url) - _write_json(BASELINE_PATH, spec) + + # Wrap the spec with metadata so we know when/where it was saved + baseline_data = { + "_meta": { + "saved_at": _timestamp(), + "saved_by_machine": _machine(), + }, + "spec": spec, + } + _write_json(BASELINE_PATH, baseline_data) console.print(f" Saved baseline to [cyan]{BASELINE_PATH}[/cyan]") + console.print(f" Timestamp: [dim]{baseline_data['_meta']['saved_at']}[/dim]") + console.print(f" Machine: [dim]{baseline_data['_meta']['saved_by_machine']}[/dim]") md = openapi_to_markdown(spec) _write_text(API_REF_PATH, md) @@ -104,7 +127,19 @@ def cmd_check(url: str = DEFAULT_OPENAPI_URL) -> None: ) sys.exit(1) - baseline = json.loads(BASELINE_PATH.read_text()) + baseline_data = json.loads(BASELINE_PATH.read_text()) + + # Support both old format (raw spec) and new format (with _meta wrapper) + if "_meta" in baseline_data: + meta = baseline_data["_meta"] + baseline = baseline_data["spec"] + console.print( + f" Baseline from: [dim]{meta.get('saved_at', 'unknown')}[/dim] " + f"on [dim]{meta.get('saved_by_machine', 'unknown')}[/dim]" + ) + else: + baseline = baseline_data + current = _fetch_openapi(url) report = detect_drift(baseline, current) @@ -123,8 +158,12 @@ def cmd_check(url: str = DEFAULT_OPENAPI_URL) -> None: console.print(f"\n Regenerated [cyan]{API_REF_PATH}[/cyan]") # Write summary artifact + now = _timestamp() + machine = _machine() summary_md = ( "# Drift Summary\n\n" + f"**Detected:** {now} \n" + f"**Machine:** {machine}\n\n" f"{report.as_bullet_list()}\n\n" f"---\n\n" f"`docs/api_reference.md` has been regenerated.\n" @@ -191,8 +230,12 @@ def cmd_audit(url: str = DEFAULT_AUDIT_URL) -> None: console.print(summary) # Write audit report + now = _timestamp() + machine = _machine() audit_report = ( "# Runtime Audit Report\n\n" + f"**Generated:** {now} \n" + f"**Machine:** {machine}\n\n" f"Total mismatches: {total}\n\n" f"{summary}\n\n" "---\n\n" diff --git a/drift_guard/openapi_to_md.py b/drift_guard/openapi_to_md.py index 2cffcba..e3b3dab 100644 --- a/drift_guard/openapi_to_md.py +++ b/drift_guard/openapi_to_md.py @@ -9,6 +9,8 @@ from __future__ import annotations import json +import platform +from datetime import datetime, timezone from typing import Any @@ -180,10 +182,13 @@ def openapi_to_markdown(spec: dict[str, Any]) -> str: lines.append("---") lines.append("") + now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + machine = platform.node() or "unknown" lines.append( - "_This file was auto-generated by " - "[Docs Drift Guard](../drift_guard/guard.py). " - "Do not edit manually._" + f"_This file was auto-generated by " + f"[Docs Drift Guard](../drift_guard/guard.py) " + f"on **{now}** from **{machine}**. " + f"Do not edit manually._" ) lines.append("") From a92436c6ff3b1b883786d55f81aa690f9f812818 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:50:04 +0000 Subject: [PATCH 2/3] Fix: wrap updated baseline with _meta in cmd_check When drift is detected, cmd_check now writes the baseline with the _meta wrapper (saved_at, saved_by_machine) instead of the raw spec, keeping provenance info consistent with cmd_baseline. Co-Authored-By: unknown <> --- drift_guard/guard.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/drift_guard/guard.py b/drift_guard/guard.py index 071bcdb..d8210db 100644 --- a/drift_guard/guard.py +++ b/drift_guard/guard.py @@ -171,8 +171,15 @@ def cmd_check(url: str = DEFAULT_OPENAPI_URL) -> None: _write_text(SUMMARY_PATH, summary_md) console.print(f" Wrote change summary to [cyan]{SUMMARY_PATH}[/cyan]") - # Update baseline to the current spec - _write_json(BASELINE_PATH, current) + # Update baseline to the current spec (with metadata wrapper) + updated_baseline = { + "_meta": { + "saved_at": now, + "saved_by_machine": machine, + }, + "spec": current, + } + _write_json(BASELINE_PATH, updated_baseline) console.print(f" Updated baseline at [cyan]{BASELINE_PATH}[/cyan]") # Slack notification From 030c542cd631ce99ed67f6c4554853691ea6780c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:51:59 +0000 Subject: [PATCH 3/3] Auto-update baseline in pre-commit hook and CI instead of blocking - Pre-commit hook now runs 'drift_guard baseline' automatically when drift is detected, stages the updated files, and lets the commit proceed - CI workflow also auto-updates baseline instead of failing on drift - No more manual 'python -m drift_guard.guard baseline' step needed Co-Authored-By: unknown <> --- .github/workflows/drift-guard.yml | 4 +++- scripts/pre-commit-drift-check.sh | 28 +++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/drift-guard.yml b/.github/workflows/drift-guard.yml index 934ae30..5e1d037 100644 --- a/.github/workflows/drift-guard.yml +++ b/.github/workflows/drift-guard.yml @@ -50,7 +50,9 @@ jobs: EXIT_CODE=$? echo "drift_exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" if [ "$EXIT_CODE" -eq 2 ]; then - echo "::warning::API drift detected — docs are out of sync with the code" + echo "::warning::API drift detected — auto-updating baseline and docs" + python -m drift_guard.guard baseline + echo "drift_exit_code=0" >> "$GITHUB_OUTPUT" fi # Don't fail yet — collect all results first exit 0 diff --git a/scripts/pre-commit-drift-check.sh b/scripts/pre-commit-drift-check.sh index 0a2e639..e5a87ea 100755 --- a/scripts/pre-commit-drift-check.sh +++ b/scripts/pre-commit-drift-check.sh @@ -3,8 +3,8 @@ # # A git pre-commit hook that checks whether staged changes to app/ # introduce API drift. It starts a temporary server, compares the -# live OpenAPI spec to the committed baseline, and blocks the commit -# if drift is found. +# live OpenAPI spec to the committed baseline, and auto-updates the +# baseline and docs if drift is found. # # Install: # cp scripts/pre-commit-drift-check.sh .git/hooks/pre-commit @@ -57,17 +57,23 @@ set -e if [ "$EXIT_CODE" -eq 2 ]; then echo "" - echo "[drift-guard] API drift detected!" - echo "[drift-guard] The OpenAPI spec has changed but the baseline hasn't been updated." + echo "[drift-guard] API drift detected — auto-updating baseline and docs..." echo "" - echo " To fix, run:" - echo " python -m drift_guard.guard baseline" - echo " git add artifacts/openapi_baseline.json docs/api_reference.md" - echo "" - echo " To skip this check (not recommended):" - echo " git commit --no-verify" + + # Regenerate baseline using the temporary server + python -m drift_guard.guard baseline --url "http://127.0.0.1:${PORT}/openapi.json" + BASELINE_EXIT=$? + + if [ "$BASELINE_EXIT" -ne 0 ]; then + echo "[drift-guard] Failed to regenerate baseline (exit code $BASELINE_EXIT)." + echo "[drift-guard] Please run manually: python -m drift_guard.guard baseline" + exit 1 + fi + + # Stage the updated baseline and docs so they're included in this commit + git add artifacts/openapi_baseline.json docs/api_reference.md echo "" - exit 1 + echo "[drift-guard] Baseline and docs updated and staged. Commit will proceed." elif [ "$EXIT_CODE" -eq 1 ]; then echo "[drift-guard] Warning: drift check encountered an error (exit code 1)." echo "[drift-guard] Allowing commit to proceed — investigate manually."