Repository navigation
CI input verification #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: AlphaBridge CI input verification | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| probe: | |
| type: string | |
| required: true | |
| jobs: | |
| check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: echo 'Changed inputs reached the consumer' | |
| needs: | |
| - alpha_bridge_inputs | |
| if: ${{ (success()) && needs.alpha_bridge_inputs.outputs.changed == 'true' | |
| }} | |
| alpha_bridge_inputs: | |
| name: AlphaBridge input check | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: read | |
| concurrency: | |
| group: alpha-bridge-inputs-${{ github.workflow }}-${{ github.sha }}-${{ | |
| github.event.schedule || github.event_name }} | |
| cancel-in-progress: false | |
| outputs: | |
| changed: ${{ steps.identity.outputs.changed }} | |
| steps: | |
| - id: identity | |
| name: Check source, workflow, variables, and runner inputs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| AB_REPOSITORY: ${{ github.repository }} | |
| AB_RUN_ID: ${{ github.run_id }} | |
| AB_HEAD_SHA: ${{ github.sha }} | |
| AB_WORKFLOW_DIGEST: | |
| f2545f7bff2d631118302e97f27dd11878f9bc9967348ae138d23306e7baf973 | |
| AB_EVENT: schedule | |
| AB_SCHEDULE: alpha-bridge verification | |
| AB_VARIABLES: ${{ toJSON(vars) }} | |
| AB_INPUTS: ${{ toJSON(inputs) }} | |
| AB_WORKFLOW_RUN: ${{ toJSON(github.event.workflow_run) }} | |
| shell: bash | |
| run: "python3 - <<'ALPHA_BRIDGE_CI_PY'\n\"\"\"Native scheduled-CI input check, | |
| embedded in the owned fork configuration.\"\"\"\n\nfrom __future__ import | |
| annotations\n\nimport base64\nimport hashlib\nimport json\nimport os\nimport | |
| re\nimport urllib.parse\nimport urllib.request\n\n\ndef parent_has_work(get, | |
| repository, parent):\n if (\n not parent\n or parent.get(\"\ | |
| head_branch\") != \"alpha-bridge-ci\"\n or parent.get(\"status\") != | |
| \"completed\"\n ):\n return True\n sha = parent[\"head_sha\"\ | |
| ]\n if re.fullmatch(r\"[0-9a-f]{40}\", sha) is None:\n raise ValueError(\"\ | |
| parent workflow has no exact configuration commit\")\n record = get(f\"\ | |
| /repos/{repository}\")\n marker_file = get(f\"/repos/{repository}/contents/.github/alpha-bridge-ci.json?ref={sha}\"\ | |
| )\n marker = json.loads(base64.b64decode(marker_file[\"content\"]))\n \ | |
| \ if (\n record.get(\"fork\") is not True\n or marker.get(\"\ | |
| schema\") != \"alpha-bridge/fork-ci\"\n or marker.get(\"source_repository\"\ | |
| ) != record.get(\"parent\", {}).get(\"full_name\")\n ):\n raise | |
| ValueError(\"parent CI configuration has no verified fork ownership\")\n \ | |
| \ path = parent[\"path\"].split(\"@\", 1)[0]\n if not path.startswith(\"\ | |
| .github/workflows/\") or \"/\" in path.removeprefix(\".github/workflows/\"\ | |
| ):\n raise ValueError(\"parent workflow path is outside its configuration | |
| owner\")\n definition = get(f\"/repos/{repository}/contents/{urllib.parse.quote(path, | |
| safe='/')}?ref={sha}\")\n if hashlib.sha256(base64.b64decode(definition[\"\ | |
| content\"])).hexdigest() != marker.get(\n \"definition_sha256\", {}\n\ | |
| \ ).get(path):\n raise ValueError(\"parent workflow differs from | |
| its owned definition\")\n if path == \".github/workflows/alpha-bridge-ci-verify.yml\"\ | |
| :\n return False\n jobs = []\n page = 1\n while True:\n \ | |
| \ values = get(\n f\"/repos/{repository}/actions/runs/{parent['id']}/jobs?per_page=100&page={page}\"\ | |
| \n )[\"jobs\"]\n jobs.extend(values)\n if len(values) | |
| < 100:\n break\n page += 1\n guards = [\n job\n\ | |
| \ for job in jobs\n if job.get(\"name\") == \"AlphaBridge input | |
| check\"\n and job.get(\"conclusion\") == \"success\"\n and any(\n\ | |
| \ re.fullmatch(r\"AlphaBridge input identity [0-9a-f]{64}\", step.get(\"\ | |
| name\", \"\"))\n and step.get(\"conclusion\") == \"success\"\n\ | |
| \ for step in job.get(\"steps\", [])\n )\n ]\n return | |
| not guards or any(\n job not in guards and job.get(\"conclusion\") | |
| != \"skipped\" for job in jobs\n )\n\n\ndef changed(get, *, repository, | |
| run_id, identity, job_name):\n current = get(f\"/repos/{repository}/actions/runs/{run_id}\"\ | |
| )\n event = current[\"event\"]\n query = urllib.parse.urlencode(\n \ | |
| \ {\n \"branch\": current[\"head_branch\"],\n \ | |
| \ \"event\": event,\n \"per_page\": 100,\n }\n )\n \ | |
| \ runs = get(f\"/repos/{repository}/actions/workflows/{current['workflow_id']}/runs?{query}\"\ | |
| )\n for run in runs[\"workflow_runs\"]:\n if run[\"id\"] == run_id:\n\ | |
| \ continue\n if run.get(\"head_branch\") != current[\"head_branch\"\ | |
| ] or run.get(\"event\") != event:\n raise ValueError(\"provider | |
| returned a different CI input identity\")\n page = 1\n while | |
| True:\n jobs = get(\n f\"/repos/{repository}/actions/runs/{run['id']}/jobs?per_page=100&page={page}\"\ | |
| \n )[\"jobs\"]\n for job in jobs:\n if | |
| (\n job[\"name\"] == job_name\n and | |
| job.get(\"status\") == \"completed\"\n and job.get(\"conclusion\"\ | |
| ) == \"success\"\n and any(\n step[\"\ | |
| name\"] == f\"AlphaBridge input identity {identity}\"\n \ | |
| \ and step.get(\"conclusion\") == \"success\"\n \ | |
| \ for step in job.get(\"steps\", [])\n )\n \ | |
| \ ):\n return False\n if len(jobs) < 100:\n\ | |
| \ break\n page += 1\n return True\n\n\ndef main():\n\ | |
| \ inputs = {\n \"source_commit\": os.environ[\"AB_HEAD_SHA\"],\n\ | |
| \ \"workflow_definition\": os.environ[\"AB_WORKFLOW_DIGEST\"],\n \ | |
| \ \"event\": os.environ[\"AB_EVENT\"],\n \"schedule\": os.environ[\"\ | |
| AB_SCHEDULE\"],\n \"variables\": json.loads(os.environ[\"AB_VARIABLES\"\ | |
| ]),\n \"inputs\": json.loads(os.environ[\"AB_INPUTS\"]),\n \"\ | |
| runner_image\": [os.environ.get(\"ImageOS\", \"\"), os.environ.get(\"ImageVersion\"\ | |
| , \"\")],\n \"producer\": json.loads(os.environ.get(\"AB_WORKFLOW_RUN\"\ | |
| , \"null\")),\n }\n identity = hashlib.sha256(json.dumps(inputs, sort_keys=True).encode()).hexdigest()\n\ | |
| \n def get(path):\n request = urllib.request.Request(\n \ | |
| \ \"https://api.github.com\" + path,\n headers={\n \ | |
| \ \"Authorization\": \"Bearer \" + os.environ[\"GH_TOKEN\"],\n \ | |
| \ \"Accept\": \"application/vnd.github+json\",\n \ | |
| \ \"X-GitHub-Api-Version\": \"2026-03-10\",\n },\n )\n \ | |
| \ with urllib.request.urlopen(request, timeout=60) as response:\n \ | |
| \ return json.load(response)\n\n should_run = inputs[\"event\"\ | |
| ] not in {\"schedule\", \"workflow_run\"} or (\n (\n inputs[\"\ | |
| event\"] != \"workflow_run\"\n or parent_has_work(get, os.environ[\"\ | |
| AB_REPOSITORY\"], inputs[\"producer\"])\n )\n and changed(\n\ | |
| \ get,\n repository=os.environ[\"AB_REPOSITORY\"],\n\ | |
| \ run_id=int(os.environ[\"AB_RUN_ID\"]),\n identity=identity,\n\ | |
| \ job_name=\"AlphaBridge input check\",\n )\n )\n \ | |
| \ with open(os.environ[\"GITHUB_OUTPUT\"], \"a\", encoding=\"utf-8\") as output:\n\ | |
| \ output.write(f\"digest={identity}\\nchanged={str(should_run).lower()}\\\ | |
| n\")\n print(\"CI inputs changed\" if should_run else \"CI inputs unchanged; | |
| build and test jobs skipped\")\n\n\nif __name__ == \"__main__\":\n main()\n\ | |
| \nALPHA_BRIDGE_CI_PY\n" | |
| - name: AlphaBridge input identity ${{ steps.identity.outputs.digest }} | |
| run: echo 'Input identity recorded' |