The shortcut fix frome844ff3has been live in git and absent from the floor since 2026-08-06. site-config.json exists TWICE on the share, from one repo source: enrollment/shopfloor-setup/ and enrollment/config/. Only the second one is staged to a bay - startnet copies Y:\config\site-config.json to W:\Enrollment\site-config.json - and it was the stale one. So every bay imaged in the last two weeks came up without the Plant Apps startup item and without the Defect_Tracker taskbar pin, while the drift report showed site-config.json in sync, because it was reading the copy nothing consumes. A green check on the wrong file is worse than no check. Deployed the repo copy over it (backup on the server at ~/backups/site-config.json.bak-20260819) and marked BOTH destinations git-owned, so neither can go stale behind the other. The shopfloor unattend is reconciled the other way round. Live was 87 lines ahead of the repo - the default-user startup-delay removal, the Windows Update disables, the removable-media block that stops PPKG auto-detection at OOBE, and the run-enrollment.ps1 path fix from C:\ to C:\Enrollment. The repo copy was a 201-line fossil. LIVE WINS: these files boot machines, and pushing the repo copy over them is exactly the 2026-08-06 outage that prompted this tool. Adopted live into the repo verbatim (lint clean) rather than merging by hand. Also fixed a pair that could never pass: the engineer unattend was compared against playbook/FlatUnattendW10.xml, which is the STANDARD answer file, so it reported DIFFERS permanently.6f86c81added FlatUnattendW10-engineer.xml but did not repoint the pair at it. A permanently red row is one nobody reads, which is how the site-config gap stayed invisible next to it. All twelve pairs are now git-owned and in sync, and the gate has nothing left classified as "known bad" to hide behind.
163 lines
6.6 KiB
Python
Executable File
163 lines
6.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
share-drift.py - report where the repo and the live share disagree.
|
|
|
|
WHY
|
|
The share is production. The repo is meant to describe it. On 2026-08-06 an
|
|
unattend edit made directly on the share broke every shopfloor, standard and
|
|
engineer build for a day - while the repo copy was fine the whole time. Nobody
|
|
could have known, because nothing compared them.
|
|
|
|
Worse, drift runs both directions. Some live files are AHEAD of the repo
|
|
(hand-edits nobody committed) and some repo files are ahead of live (fixes never
|
|
deployed). Blindly pushing the repo over the share would have overwritten a
|
|
working 17 KB unattend with a stale 12 KB one.
|
|
|
|
So this tool REPORTS by default and never writes. Each pair is classified:
|
|
|
|
git-owned the repo is the source of truth. Safe to push.
|
|
unreconciled the two have diverged and nobody has decided which wins. NOT safe
|
|
to push. Reconcile by hand, then move the pair to git-owned.
|
|
|
|
Usage:
|
|
./share-drift.py # status table
|
|
./share-drift.py --diff # show the actual differences
|
|
./share-drift.py --only unattend # filter by substring
|
|
|
|
Exit non-zero if any git-owned pair differs, so it can gate a deploy. An
|
|
unreconciled pair that differs is reported but does not fail - that is a known
|
|
state, not a regression.
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import difflib
|
|
import hashlib
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
PXE_HOST = "172.16.9.1"
|
|
PXE_USER = "pxe"
|
|
PXE_PASS = "pxe"
|
|
|
|
GIT_OWNED, UNRECONCILED = "git-owned", "unreconciled"
|
|
|
|
# (repo path, live path, ownership)
|
|
PAIRS = [
|
|
("playbook/shopfloor-setup/run-enrollment.ps1",
|
|
"/srv/samba/enrollment/scripts/run-enrollment.ps1", GIT_OWNED),
|
|
("playbook/scripts/preflight.ps1",
|
|
"/srv/samba/enrollment/scripts/preflight.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Run-ShopfloorSetup.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Run-ShopfloorSetup.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Verify-And-Heal-Staging.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Verify-And-Heal-Staging.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Fetch-StagingPayload.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Fetch-StagingPayload.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/site-config.json",
|
|
"/srv/samba/enrollment/shopfloor-setup/site-config.json", GIT_OWNED),
|
|
("playbook/shopfloor-setup/BIOS/models.txt",
|
|
"/srv/samba/winpeapps/_shared/BIOS/models.txt", GIT_OWNED),
|
|
("playbook/shopfloor-setup/BIOS/check-bios.cmd",
|
|
"/srv/samba/winpeapps/_shared/BIOS/check-bios.cmd", GIT_OWNED),
|
|
|
|
# The config/ copy is the one startnet actually stages to
|
|
# C:\Enrollment\site-config.json, so it is the copy every bay reads. It had
|
|
# its own edit history and sat stale since 2026-08-06, which meant bays
|
|
# imaged after e844ff3 quietly lost the Plant Apps startup item and the
|
|
# Defect_Tracker pin - while the sibling copy under shopfloor-setup/ showed
|
|
# perfectly in sync and made the pair look healthy. Reconciled 2026-08-19 by
|
|
# deploying the repo copy over it. One source file, two destinations, both
|
|
# git-owned now, so a stale one cannot hide behind the other again.
|
|
("playbook/shopfloor-setup/site-config.json",
|
|
"/srv/samba/enrollment/config/site-config.json", GIT_OWNED),
|
|
|
|
# All three unattends are reconciled. The live files were ahead of the repo
|
|
# and the LIVE side won, because these are what boot machines - pushing the
|
|
# repo over them is precisely the 2026-08-06 outage.
|
|
#
|
|
# Engineer has its own repo file. It used to be compared against the
|
|
# standard unattend, which is a different answer file, so the pair reported
|
|
# DIFFERS permanently - and a light that is always red is one nobody reads.
|
|
("playbook/FlatUnattendW10-shopfloor.xml",
|
|
"/srv/samba/winpeapps/gea-shopfloor/Deploy/FlatUnattendW10.xml", GIT_OWNED),
|
|
("playbook/FlatUnattendW10.xml",
|
|
"/srv/samba/winpeapps/gea-standard/Deploy/FlatUnattendW10.xml", GIT_OWNED),
|
|
("playbook/FlatUnattendW10-engineer.xml",
|
|
"/srv/samba/winpeapps/gea-engineer/Deploy/FlatUnattendW10.xml", GIT_OWNED),
|
|
]
|
|
|
|
|
|
def ssh(cmd):
|
|
return subprocess.run(
|
|
["sshpass", "-p", PXE_PASS, "ssh", "-o", "StrictHostKeyChecking=no",
|
|
"-o", "LogLevel=ERROR", f"{PXE_USER}@{PXE_HOST}", cmd],
|
|
capture_output=True, text=True)
|
|
|
|
|
|
def live_bytes(path):
|
|
"""base64 in transit so BOM and CRLF survive the hop unchanged."""
|
|
r = ssh("base64 -w0 '%s' 2>/dev/null" % path)
|
|
if r.returncode != 0 or not r.stdout.strip():
|
|
return None
|
|
return base64.b64decode(r.stdout.strip())
|
|
|
|
|
|
def repo_bytes(path):
|
|
try:
|
|
with open(os.path.join(REPO, path), "rb") as f:
|
|
return f.read()
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def short(b):
|
|
return hashlib.md5(b).hexdigest()[:10] if b is not None else "MISSING"
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Report repo vs live share drift.")
|
|
ap.add_argument("--diff", action="store_true", help="show the actual differences")
|
|
ap.add_argument("--only", help="only pairs whose paths contain this substring")
|
|
args = ap.parse_args()
|
|
|
|
blocking = 0
|
|
known = 0
|
|
print("%-13s %-44s %-11s %-11s" % ("OWNERSHIP", "LIVE PATH", "REPO", "LIVE"))
|
|
print("-" * 84)
|
|
|
|
for repo_path, live_path, owner in PAIRS:
|
|
if args.only and args.only not in repo_path and args.only not in live_path:
|
|
continue
|
|
rb, lb = repo_bytes(repo_path), live_bytes(live_path)
|
|
same = rb is not None and lb is not None and rb == lb
|
|
tag = "same" if same else "DIFFERS"
|
|
if not same:
|
|
if owner == GIT_OWNED:
|
|
blocking += 1
|
|
else:
|
|
known += 1
|
|
print("%-13s %-44s %-11s %-11s %s"
|
|
% (owner, live_path.replace("/srv/samba/", ""), short(rb), short(lb), tag))
|
|
|
|
if args.diff and not same and rb is not None and lb is not None:
|
|
a = rb.decode("utf-8", "replace").replace("\r\n", "\n").splitlines()
|
|
b = lb.decode("utf-8", "replace").replace("\r\n", "\n").splitlines()
|
|
for line in list(difflib.unified_diff(a, b, "repo", "live", lineterm="", n=1))[:40]:
|
|
print(" " + line)
|
|
print()
|
|
|
|
print("-" * 84)
|
|
print("%d git-owned pair(s) out of sync, %d known-unreconciled" % (blocking, known))
|
|
if blocking:
|
|
print("FAILED: a git-owned file differs from the share. Deploy it or commit the live version.")
|
|
return 1
|
|
print("PASSED: everything git-owned matches the share.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|