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
57 changes: 40 additions & 17 deletions .github/workflows/techapi-pr-validation-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,22 @@ jobs:
repository: GetTechAPI/TechAPI
ref: ${{ env.TECHAPI_HEAD_SHA }}
path: TechAPI
fetch-depth: 0

- name: Checkout TechAPI main
- name: Checkout TechAPI PR base
uses: actions/checkout@v4
with:
repository: GetTechAPI/TechAPI
ref: main
path: TechAPI-main
ref: develop
path: TechAPI-base
fetch-depth: 0

- name: Pin PR base to the merge base
shell: bash
run: |
git -C TechAPI fetch --no-tags origin develop
base_sha="$(git -C TechAPI merge-base origin/develop HEAD)"
git -C TechAPI-base checkout --detach "$base_sha"

- uses: actions/setup-python@v6
with:
Expand Down Expand Up @@ -100,7 +109,7 @@ jobs:
return files

head = rel_site_files(Path("TechAPI"))
base = rel_site_files(Path("TechAPI-main"))
base = rel_site_files(Path("TechAPI-base"))
added = sorted(set(head) - set(base))
deleted = sorted(set(base) - set(head))
modified = sorted(key for key in set(head) & set(base) if digest(head[key]) != digest(base[key]))
Expand Down Expand Up @@ -135,11 +144,26 @@ jobs:

{
echo
echo "## integrity_check.py --strict"
python integrity_check.py TechAPI/data --strict
echo "integrity_status=$?"
echo "## integrity_check.py (PR head compared with PR base)"
python integrity_check.py TechAPI/data --hard-report head-hard.json
echo "head_integrity_scan_status=$?"
python integrity_check.py TechAPI-base/data --hard-report base-hard.json > baseline-integrity.log
echo "base_integrity_scan_status=$?"
} >> validation.log 2>&1
integrity_status=$(grep "integrity_status=" validation.log | tail -n 1 | cut -d= -f2)
integrity_status=$(python - <<'PY'
import json
from pathlib import Path

head = set(json.loads(Path("head-hard.json").read_text(encoding="utf-8")))
base = set(json.loads(Path("base-hard.json").read_text(encoding="utf-8")))
introduced = sorted(head - base)
with Path("validation.log").open("a", encoding="utf-8") as log:
log.write(f"\nBaseline hard anomalies: {len(base)}; PR head: {len(head)}; introduced: {len(introduced)}\n")
for anomaly in introduced:
log.write(f"NEW HARD ANOMALY: {anomaly}\n")
print("1" if introduced else "0")
PY
)

sed -i '/_status=/d' validation.log

Expand Down Expand Up @@ -173,7 +197,6 @@ jobs:
- name: Build data quality summary
shell: bash
run: |
git -C TechAPI fetch --no-tags --depth=1 origin main
python - <<'PY'
from __future__ import annotations

Expand All @@ -186,7 +209,7 @@ jobs:
from typing import Any

HEAD = Path("TechAPI/data")
BASE = Path("TechAPI-main/data")
BASE = Path("TechAPI-base/data")
CATEGORIES = (
"brand",
"soc",
Expand Down Expand Up @@ -227,8 +250,7 @@ jobs:
"diff",
"--name-status",
"--no-renames",
"FETCH_HEAD",
"HEAD",
"origin/develop...HEAD",
"--",
"data",
],
Expand Down Expand Up @@ -469,7 +491,7 @@ jobs:
site_lines.append("## Changed site")
site_lines.append("")
head_site = rel_site_files(Path("TechAPI"))
base_site = rel_site_files(Path("TechAPI-main"))
base_site = rel_site_files(Path("TechAPI-base"))
site_added = sorted(set(head_site) - set(base_site))
site_deleted = sorted(set(base_site) - set(head_site))
site_modified = sorted(
Expand Down Expand Up @@ -573,12 +595,13 @@ jobs:
continue
if current_section and line.startswith(" "):
section_counts[current_section] += 1
if any(token in line for token in ("DUP ", "slug!=file", " > ")):
hard_lines.append(line)
if line.startswith("NEW HARD ANOMALY: "):
hard_lines.append(line.removeprefix("NEW HARD ANOMALY: "))

out: list[str] = []
out.append("## Validation notes")
out.append("")
out.append("- The integrity gate blocks only hard anomalies introduced by this PR relative to its merge base.")
out.append("- Full advisory outlier listings are suppressed on successful runs because they are dataset-wide and mostly stable between PRs.")
out.append("- Failure runs still include a detailed log excerpt for debugging.")
if key_lines:
Expand All @@ -596,7 +619,7 @@ jobs:
out.append(f"| {name} | {count} |")
if hard_lines:
out.append("")
out.append("Potential blocking lines:")
out.append("New blocking anomalies:")
out.append("")
out.append("```text")
out.extend(hard_lines[:80])
Expand Down Expand Up @@ -654,7 +677,7 @@ jobs:
echo "| Check | Result |"
echo "| --- | --- |"
echo "| \`python -m app.validate\` | $([ "${{ steps.validate.outputs.app_status }}" = "0" ] && echo PASS || echo FAIL) |"
echo "| \`python integrity_check.py TechAPI/data --strict\` | $([ "${{ steps.validate.outputs.integrity_status }}" = "0" ] && echo PASS || echo FAIL) |"
echo "| New hard integrity anomalies vs PR base | $([ "${{ steps.validate.outputs.integrity_status }}" = "0" ] && echo PASS || echo FAIL) |"
if [ "${site_changed}" = "true" ]; then
echo "| \`cd TechAPI/site && npm ci && npm run build\` | $([ "${site_build_status}" = "0" ] && echo PASS || echo FAIL) |"
fi
Expand Down
23 changes: 17 additions & 6 deletions .github/workflows/weekly-refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,21 @@ jobs:
env:
GH_TOKEN: ${{ env.TECHAPI_WRITE_TOKEN }}
run: |
pr_count="$(gh pr list --repo GetTechAPI/TechAPI --head refresh/${{ steps.meta.outputs.date }} --base main --state open --json number --jq length)"
if [ "$pr_count" -gt 0 ]; then
exit 0
fi

echo "::error::Refresh PR refresh/${{ steps.meta.outputs.date }} was not created."
set -euo pipefail
branch="refresh/${{ steps.meta.outputs.date }}"
title="chore(data): weekly refresh ${{ steps.meta.outputs.date }}"
for attempt in 1 2 3; do
if gh pr list --repo GetTechAPI/TechAPI --head "$branch" --base main \
--state open --json number --jq 'length' | grep -qx '[1-9][0-9]*'; then
exit 0
fi
# create-pull-request may have pushed the branch before its API call
# failed. Retry just the PR API operation without repeating the scrape.
if gh pr create --repo GetTechAPI/TechAPI --head "$branch" --base main \
--title "$title" --body-file pr-body.md; then
exit 0
fi
if [ "$attempt" -lt 3 ]; then sleep 15; fi
done
echo "::error::Refresh PR $branch was not created after three attempts."
exit 1
15 changes: 12 additions & 3 deletions integrity_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

Usage::

python integrity_check.py [DATA_ROOT] [--strict]
python integrity_check.py [DATA_ROOT] [--strict] [--hard-report PATH]

By default it prints every flagged item and exits 0 (human-review mode). With
``--strict`` it additionally exits non-zero when any *hard* anomaly is found —
Expand All @@ -16,6 +16,7 @@
The statistical cross-source/era outliers stay advisory (a heterogeneous catalog
of server + desktop + mobile parts legitimately produces many ratio outliers), so
they are printed for review but never fail the gate.
``--hard-report`` writes a JSON list of hard anomalies for baseline comparison.
"""
from __future__ import annotations
import os, json, math, re, statistics, sys
Expand All @@ -28,6 +29,10 @@

_argv = sys.argv[1:]
STRICT = "--strict" in _argv
_report_index = _argv.index("--hard-report") if "--hard-report" in _argv else -1
HARD_REPORT = _argv[_report_index + 1] if _report_index >= 0 else None
if _report_index >= 0:
del _argv[_report_index:_report_index + 2]
_positional = [a for a in _argv if not a.startswith("-")]
ROOT = _positional[0] if _positional else r"C:\Users\29\Desktop\TechAPI\data"

Expand Down Expand Up @@ -70,9 +75,9 @@ def section(t): print(f"\n### {t}")
if d.get("slug") != fn:
hard(f" [{comp}] slug!=file: {fn} slug={d.get('slug')}")
for s, fl in slugs.items():
if len(fl) > 1: hard(f" [{comp}] DUP slug {s}: {fl}")
if len(fl) > 1: hard(f" [{comp}] DUP slug {s}: {sorted(fl)}")
for n, fl in names.items():
if len(fl) > 1: hard(f" [{comp}] DUP name {n!r}: {fl}")
if len(fl) > 1: hard(f" [{comp}] DUP name {n!r}: {sorted(fl)}")

# --- 2. AMD Ryzen line vs DESKTOP model tier-digit (2nd digit); APU/mobile excepted ---
section("CPU name/tier consistency (desktop mainstream only)")
Expand Down Expand Up @@ -130,6 +135,10 @@ def collect(recs, fa, fb):

print("\n(no lines under a section = clean)")

if HARD_REPORT:
with open(HARD_REPORT, "w", encoding="utf-8") as report:
json.dump(sorted(set(HARD)), report, ensure_ascii=False, indent=2)

if STRICT and HARD:
print(f"\n❌ integrity gate: {len(HARD)} hard anomaly(ies) — blocking refresh.")
sys.exit(1)
Expand Down
Loading