#!/usr/bin/env python3 """check-wai-release-quickstart.py: the release runs the quickstart from each archive before it attaches it, and the Windows twin keeps the script's steps. wai-release.yml builds the quickstart archive on each platform and attaches it to the release. Before it is attached, each archive runs the quickstart as a new user runs it, from the archive alone: wai/quickstart/test/clean-machine.sh on macOS and Linux, its PowerShell twin clean-machine.ps1 on Windows. Each step of the twin is a step of the script, with the same `wai` command. This holds the three files to that: - each of the jobs macos, linux and windows runs its script in a step's command (a comment or a step's name that names it is not a run of it); that step has no `if:` and no `continue-on-error:`, and neither has its job, so it can neither be skipped nor fail without failing the job; and it comes before the job's first step that attaches anything to the release (one that runs wai/quickstart/publish.sh or uploads to a release's assets); - clean-machine.sh's steps are unpack, contents, hello, verify, create, dev-check and two-tabs (the browser's), and clean-machine.ps1's are the same steps in the same order, the browser's aside, each running the same `wai` command (the command alone, up to the first `;`, `}`, `'`, `|`, `>` or `&&` after it); - the unpack step of each checks the archive against the SHA-256 beside it ("$ARCHIVE.sha256", "$Archive.sha256") and fails when they differ, before it unpacks anything. check-wai-release-quickstart.py [--root DIR] Exit 0 when all of it holds, 1 with one line a finding otherwise. Standard library only. The selftest (check-wai-release-quickstart-selftest.sh) breaks each rule in a copy and requires a finding. """ import os import re import sys WORKFLOW = ".github/workflows/wai-release.yml" SH = "wai/quickstart/test/clean-machine.sh" PS = "wai/quickstart/test/clean-machine.ps1" # The job, and the script it runs on its archive. JOBS = [("macos", "clean-machine.sh"), ("linux", "clean-machine.sh"), ("windows", "clean-machine.ps1")] SH_STEPS = ["unpack", "contents", "hello", "verify", "create", "dev-check", "two-tabs"] BROWSER_STEP = "two-tabs" def indent(line): return len(line) - len(line.lstrip(" ")) def is_comment(line): return line.strip().startswith("#") def jobs(text): """{job name: its lines}, the jobs under the top-level `jobs:` key.""" out = {} lines = text.splitlines() try: start = next(i for i, l in enumerate(lines) if l.rstrip() == "jobs:") except StopIteration: return out name = None for l in lines[start + 1:]: if l.strip() and not is_comment(l) and indent(l) == 0: break m = re.match(r"^ ([A-Za-z0-9_-]+):\s*$", l) if m: name = m.group(1) out[name] = [] elif name is not None: out[name].append(l) return out def job_keys(lines): """The job's own keys (indented four spaces), comments aside.""" return [re.match(r"^\s*([A-Za-z0-9_-]+):", l).group(1) for l in lines if indent(l) == 4 and not is_comment(l) and re.match(r"^\s*([A-Za-z0-9_-]+):", l)] def steps(lines): """The job's steps, in order: each {"keys": [its keys], "name": …, "run": [its command's lines, comments aside]}. A step is a list item of `steps:`; its keys are the item's first key and those at the indent of the item's content.""" out = [] in_steps = False item_indent = None cur = None run_indent = None for l in lines: if not l.strip(): continue if indent(l) == 4 and not is_comment(l): in_steps = l.strip() == "steps:" cur = None continue if not in_steps: continue s = l.lstrip(" ") if s.startswith("- ") and not is_comment(l) and (item_indent is None or indent(l) == item_indent): item_indent = indent(l) cur = {"keys": [], "name": "", "run": []} out.append(cur) run_indent = None l = " " * (item_indent + 2) + s[2:] if cur is None: continue key_indent = item_indent + 2 if run_indent is not None: if indent(l) > key_indent: if not is_comment(l): cur["run"].append(l.strip()) continue run_indent = None if is_comment(l) or indent(l) != key_indent: continue m = re.match(r"^\s*([A-Za-z0-9_-]+):\s*(.*)$", l) if not m: continue key, value = m.group(1), m.group(2).strip() cur["keys"].append(key) if key == "name": cur["name"] = value.strip("'\"") elif key == "run": if value in ("|", ">", "|-", ">-", "|+", ">+"): run_indent = key_indent elif value and not value.startswith("#"): cur["run"].append(value) return out def runs_script(step, script): """Whether the step's command runs `script` (wai/quickstart/test/