From 23f782ab32a2bc7b0d05ab0eb64039b835d77fb0 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:30:54 -0400 Subject: [PATCH 1/7] Add inactive GitHub Actions CI normalizer --- adapters/github-actions-ci/v1/normalize.jq | 216 +++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 adapters/github-actions-ci/v1/normalize.jq diff --git a/adapters/github-actions-ci/v1/normalize.jq b/adapters/github-actions-ci/v1/normalize.jq new file mode 100644 index 0000000..692ad34 --- /dev/null +++ b/adapters/github-actions-ci/v1/normalize.jq @@ -0,0 +1,216 @@ +def exact_fields($required; $optional): + . as $value | + type == "object" and + ((keys_unsorted - ($required + $optional)) | length) == 0 and + all($required[]; . as $key | $value | has($key)); +def id_ok: + type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); +def provider_id_ok: + type == "string" and test("\\A[1-9][0-9]{0,19}\\z"); +def sha256_ok: + type == "string" and test("\\A[0-9a-f]{64}\\z"); +def time_ok: + type == "string" and + test("\\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\\z") and + (capture("\\A(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})T(?[0-9]{2}):(?[0-9]{2}):(?[0-9]{2})Z\\z") | + map_values(tonumber)) as $t | + ($t.y % 4 == 0 and ($t.y % 100 != 0 or $t.y % 400 == 0)) as $leap | + [31,(if $leap then 29 else 28 end),31,30,31,30,31,31,30,31,30,31] as $days | + $t.m >= 1 and $t.m <= 12 and $t.d >= 1 and $t.d <= $days[$t.m - 1] and + $t.h >= 0 and $t.h <= 23 and $t.i >= 0 and $t.i <= 59 and + $t.s >= 0 and $t.s <= 59; +def revision_ok: + exact_fields(["repository_id","hash_algorithm","commit_id"];[]) and + (.repository_id | id_ok) and + (.hash_algorithm == "sha1" or .hash_algorithm == "sha256") and + (if .hash_algorithm == "sha1" then (.commit_id | test("\\A[0-9a-f]{40}\\z")) + else (.commit_id | test("\\A[0-9a-f]{64}\\z")) end); +def content_ref_ok: + exact_fields(["content_id","media_type","sha256"];[]) and + (.content_id | id_ok and (contains(":") or contains("/") | not)) and + (.media_type | type == "string" and + test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z")) and + (.sha256 | sha256_ok); +def job_identity_ok: + exact_fields(["job_id","check_run_id"];[]) and + (.job_id | provider_id_ok) and (.check_run_id | provider_id_ok); +def job_key: [(.job_id | length),.job_id,(.check_run_id | length),.check_run_id]; +def ordered_unique: + map(job_key) as $keys | + $keys == ($keys | sort) and ($keys | length) == ($keys | unique | length); +def expected_jobs_ok: + type == "array" and length >= 1 and length <= 128 and + all(.[]; job_identity_ok) and ordered_unique; +def trust_context_ok: + exact_fields( + ["expected_repository_id","expected_check_suite_id","expected_workflow_id", + "expected_run_id","expected_github_app_id","expected_head","expected_base", + "expected_jobs","observation_time","instruction_ref","config_ref", + "execution_boundary_id"]; + []) and + all([.expected_repository_id,.expected_check_suite_id,.expected_workflow_id, + .expected_run_id,.expected_github_app_id][]; provider_id_ok) and + (.expected_head | revision_ok) and (.expected_base | revision_ok) and + .expected_head.repository_id == .expected_base.repository_id and + (.expected_jobs | expected_jobs_ok) and (.observation_time | time_ok) and + (.instruction_ref | content_ref_ok) and (.config_ref | content_ref_ok) and + (.execution_boundary_id | id_ok); +def opaque_text_ok: . == null or (type == "string" and utf8bytelength <= 8192); +def provider_data_ok: + exact_fields(["name","text","details_url"];[]) and + all([.name,.text,.details_url][]; opaque_text_ok); +def queued_status: IN("queued","pending","requested","waiting"); +def status_ok: queued_status or . == "in_progress" or . == "completed"; +def conclusion_ok: + IN("success","failure","neutral","cancelled","skipped","timed_out", + "action_required","stale"); +def status_conclusion_ok: + if .status == "completed" then (.conclusion | conclusion_ok) + else .conclusion == null end; +def job_time_ok($observed): + if (.status | queued_status) then .started_at == null and .completed_at == null + elif .status == "in_progress" then + (.started_at | time_ok) and .started_at <= $observed and .completed_at == null + else + (.started_at | time_ok) and (.completed_at | time_ok) and + .started_at <= .completed_at and .completed_at <= $observed + end; +def job_ok($observed): + exact_fields( + ["job_id","check_run_id","status","conclusion","started_at","completed_at", + "payload_sha256","provider_data"]; + []) and (.job_id | provider_id_ok) and (.check_run_id | provider_id_ok) and + (.status | status_ok) and status_conclusion_ok and + job_time_ok($observed) and (.payload_sha256 | sha256_ok) and + (.provider_data | provider_data_ok); +def run_time_ok: + (.created_at | time_ok) and (.updated_at | time_ok) and + .created_at <= .updated_at and .updated_at <= .observed_at and + if (.status | queued_status) then .started_at == null and .completed_at == null + elif .status == "in_progress" then + (.started_at | time_ok) and .created_at <= .started_at and + .started_at <= .updated_at and .completed_at == null + else + (.started_at | time_ok) and (.completed_at | time_ok) and + .created_at <= .started_at and .started_at <= .completed_at and + .completed_at <= .updated_at + end; +def count_ok: type == "number" and . == floor and . >= 0 and . <= 128; +def jobs_ok($snapshot): + type == "array" and length <= 128 and + all(.[]; job_ok($snapshot.observed_at)) and ordered_unique and + $snapshot.reported_job_count == length + $snapshot.hidden_job_count and + (if $snapshot.complete then $snapshot.hidden_job_count == 0 else true end); +def snapshot_ok: + . as $snapshot | + exact_fields( + ["repository_id","check_suite_id","workflow_id","run_id","github_app_id", + "head","base","observed_at","complete","reported_job_count", + "hidden_job_count","status","conclusion","created_at","updated_at", + "started_at","completed_at","jobs","payload_sha256","provider_data"]; + []) and + all([.repository_id,.check_suite_id,.workflow_id,.run_id,.github_app_id][]; + provider_id_ok) and + (.head | revision_ok) and (.base | revision_ok) and + .head.repository_id == .base.repository_id and (.observed_at | time_ok) and + (.complete | type == "boolean") and (.reported_job_count | count_ok) and + (.hidden_job_count | count_ok) and (.status | status_ok) and + status_conclusion_ok and run_time_ok and (.jobs | jobs_ok($snapshot)) and + (.payload_sha256 | sha256_ok) and (.provider_data | provider_data_ok); +def jobs_bound_ok($context; $snapshot): + ($context.expected_jobs) as $expected | + ($snapshot.jobs | map({job_id,check_run_id})) as $actual | + all($actual[]; . as $identity | $expected | index($identity) != null) and + (if $snapshot.complete then $actual == $expected else true end); +def children_agree($snapshot): + ($snapshot.jobs) as $jobs | + if ($snapshot.status | queued_status) then all($jobs[]; .status | queued_status) + elif $snapshot.status == "in_progress" then + all($jobs[]; .status | status_ok) and any($jobs[]; .status != "completed") + elif $snapshot.complete == false then all($jobs[]; .status == "completed") + elif $snapshot.conclusion == "success" then + any($jobs[]; .conclusion == "success") and + all($jobs[]; .conclusion | IN("success","neutral","skipped")) + elif $snapshot.conclusion == "failure" then any($jobs[]; .conclusion == "failure") + elif $snapshot.conclusion == "cancelled" then any($jobs[]; .conclusion == "cancelled") + elif $snapshot.conclusion == "timed_out" then any($jobs[]; .conclusion == "timed_out") + elif $snapshot.conclusion == "action_required" then + any($jobs[]; .conclusion == "action_required") + elif $snapshot.conclusion == "stale" then any($jobs[]; .conclusion == "stale") + elif $snapshot.conclusion == "neutral" then any($jobs[]; .conclusion == "neutral") + else all($jobs[]; .conclusion == "skipped") + end; +def stale_bindings($context; $snapshot): + [ + if $snapshot.github_app_id != $context.expected_github_app_id then "app" else empty end, + if $snapshot.base != $context.expected_base then "base" else empty end, + if $snapshot.check_suite_id != $context.expected_check_suite_id then "check-suite" else empty end, + if $snapshot.head != $context.expected_head then "head" else empty end, + if $snapshot.observed_at != $context.observation_time then "observation-time" else empty end, + if $snapshot.repository_id != $context.expected_repository_id then "repository" else empty end, + if $snapshot.run_id != $context.expected_run_id then "run" else empty end, + if $snapshot.workflow_id != $context.expected_workflow_id then "workflow" else empty end + ]; +def normalized($context; $snapshot; $stale): + if ($stale | length) > 0 then ["stale","ci.binding-stale"] + elif $snapshot.complete == false or + ($snapshot.jobs | length) != ($context.expected_jobs | length) then + ["inconclusive","ci.observation-incomplete"] + elif ($snapshot.status | queued_status) then ["queued","ci.queued"] + elif $snapshot.status == "in_progress" then ["in-progress","ci.in-progress"] + elif $snapshot.conclusion == "success" then ["passed","ci.passed"] + elif $snapshot.conclusion == "failure" then ["failed","ci.failed"] + elif $snapshot.conclusion == "cancelled" then ["cancelled","ci.cancelled"] + elif $snapshot.conclusion == "timed_out" then ["timed-out","ci.timed-out"] + elif $snapshot.conclusion == "action_required" then ["action-required","ci.action-required"] + elif $snapshot.conclusion == "stale" then ["stale","ci.provider-stale"] + else ["inconclusive","ci.provider-inconclusive"] + end; +def fact_state: + if (.status | queued_status) then "queued" + elif .status == "in_progress" then "in-progress" + elif .conclusion == "success" then "passed" + elif .conclusion == "failure" then "failed" + elif .conclusion == "cancelled" then "cancelled" + elif .conclusion == "timed_out" then "timed-out" + elif .conclusion == "action_required" then "action-required" + elif .conclusion == "stale" then "stale" + else "inconclusive" end; +if (exact_fields(["trust_context","snapshot"];[]) | not) then + error("github-actions-ci.invalid-envelope") +elif (.trust_context | trust_context_ok) == false then + error("github-actions-ci.invalid-trust-context") +elif (.snapshot | snapshot_ok) == false then + error("github-actions-ci.invalid-snapshot") +elif jobs_bound_ok(.trust_context;.snapshot) == false or children_agree(.snapshot) == false then + error("github-actions-ci.provider-contradiction") +else + .trust_context as $context | .snapshot as $snapshot | + stale_bindings($context;$snapshot) as $stale | + normalized($context;$snapshot;$stale) as $normalized | + { + schema_version:1, + kind:"adapter_observation", + adapter:{id:"adapter.github-actions-ci.v1",version:"v1",status:"inactive"}, + state:$normalized[0], + reason_id:$normalized[1], + stale_bindings:$stale, + trust_context:$context, + observation:$snapshot, + result:{ + state:$normalized[0], + observed_at:$snapshot.observed_at, + subject:{head:$snapshot.head,base:$snapshot.base}, + source_sha256:$snapshot.payload_sha256, + facts:($snapshot.jobs | map({ + fact_id:("ci.job." + .job_id + ".check." + .check_run_id), + state:fact_state, + source_sha256:.payload_sha256, + provider_data:.provider_data + })) + }, + authority:"none", + qualification:{state:"unavailable",reason_id:"adapter.unqualified"}, + effects:[] + } +end From c04805c8add9183ca8a8230b502647b9a63a1ec0 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:33:45 -0400 Subject: [PATCH 2/7] Add GitHub Actions CI adapter contract proof --- adapters/github-actions-ci/v1/manifest.json | 1 + .../default-github-actions-ci-adapter.test.sh | 247 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 adapters/github-actions-ci/v1/manifest.json create mode 100755 scripts/test/default-github-actions-ci-adapter.test.sh diff --git a/adapters/github-actions-ci/v1/manifest.json b/adapters/github-actions-ci/v1/manifest.json new file mode 100644 index 0000000..dee3342 --- /dev/null +++ b/adapters/github-actions-ci/v1/manifest.json @@ -0,0 +1 @@ +{"body":{"adapter_version":"v1","offered_capabilities":[],"offered_execution_kinds":["deterministic"],"offered_permissions":[],"offered_roles":["ci"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-actions-ci/v1/normalize.jq"},"mode":"100644","object_id":"692ad34601fbf3cca00c1661da258f1545a31688","object_type":"blob","revision":{"commit_id":"23f782ab32a2bc7b0d05ab0eb64039b835d77fb0","hash_algorithm":"sha1","repository_id":"repo.ystack"}}},"id":"adapter.github-actions-ci.v1","kind":"adapter_manifest","schema_version":2} diff --git a/scripts/test/default-github-actions-ci-adapter.test.sh b/scripts/test/default-github-actions-ci-adapter.test.sh new file mode 100755 index 0000000..900c8ba --- /dev/null +++ b/scripts/test/default-github-actions-ci-adapter.test.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +normalizer="$root/adapters/github-actions-ci/v1/normalize.jq" +manifest="$root/adapters/github-actions-ci/v1/manifest.json" +registry="$root/core/v2/generation-registry.json" +tmp="$(mktemp -d "${TMPDIR:-/tmp}/ystack-github-actions-ci.XXXXXX")" +trap 'rm -rf -- "$tmp"' EXIT + +sha256_path() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +platform="$(uname -s):$(uname -m)" +case "$platform" in + Linux:x86_64) + asset=jq-linux64 + asset_sha256=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 + ;; + Darwin:x86_64|Darwin:arm64) + asset=jq-osx-amd64 + asset_sha256=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef + ;; + *) printf 'FAIL: unsupported jq 1.6 proof platform: %s\n' "$platform" >&2; exit 1 ;; +esac + +candidate="${TMPDIR:-/tmp}/ystack-portable-core-jq16/$asset" +if [ -f "$candidate" ] && [ "$(sha256_path "$candidate")" = "$asset_sha256" ]; then + jq_command=("$candidate") + [ "$platform" != Darwin:arm64 ] || jq_command=(/usr/bin/arch -x86_64 "$candidate") +elif candidate="$(command -v jq 2>/dev/null)" && + [ "$("$candidate" --version 2>/dev/null)" = jq-1.6 ]; then + jq_command=("$candidate") +else + echo 'FAIL: verified jq 1.6 is required' >&2 + exit 1 +fi +[ "$("${jq_command[@]}" --version)" = jq-1.6 ] || { + echo 'FAIL: jq 1.6 identity' >&2 + exit 1 +} + +total=0 +pass() { total=$((total + 1)); } +fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +run() { "${jq_command[@]}" -S -c -f "$normalizer" "$1"; } +mutate() { "${jq_command[@]}" -S -c "$2" "$root_input" >"$1"; } + +expect_state() { + local name=$1 state=$2 filter=$3 + local input="$tmp/$name.json" output="$tmp/$name.out" + mutate "$input" "$filter" + if ! run "$input" >"$output" 2>"$tmp/$name.err"; then + cat "$tmp/$name.err" >&2 + fail "$name rejected" + fi + [ ! -s "$tmp/$name.err" ] || fail "$name stderr" + "${jq_command[@]}" -e --arg state "$state" '.state == $state and .result.state == $state' \ + "$output" >/dev/null || fail "$name state" + pass +} + +expect_stale() { + local name=$1 binding=$2 filter=$3 + local input="$tmp/$name.json" output="$tmp/$name.out" + mutate "$input" "$filter" + if ! run "$input" >"$output" 2>"$tmp/$name.err"; then + cat "$tmp/$name.err" >&2 + fail "$name rejected" + fi + "${jq_command[@]}" -e --arg binding "$binding" \ + '.state == "stale" and .stale_bindings == [$binding]' "$output" >/dev/null || + fail "$name stale binding" + pass +} + +expect_reject() { + local name=$1 filter=$2 + local input="$tmp/$name.json" + mutate "$input" "$filter" + [ -s "$input" ] || fail "$name mutation" + if run "$input" >"$tmp/$name.out" 2>"$tmp/$name.err"; then + cat "$tmp/$name.out" >&2 + cat "$tmp/$name.err" >&2 + fail "$name accepted" + fi + [ ! -s "$tmp/$name.out" ] && [ -s "$tmp/$name.err" ] || fail "$name channel" + pass +} + +root_input="$tmp/base.json" +"${jq_command[@]}" -S -c -n ' + def rev($digit): + {repository_id:"repo.target",hash_algorithm:"sha1",commit_id:($digit * 40)}; + def ref($id;$digit): + {content_id:$id,media_type:"application/json",sha256:($digit * 64)}; + def identity($job;$check): {job_id:$job,check_run_id:$check}; + def data($name): {name:$name,text:null,details_url:"https://example.invalid/detail"}; + { + trust_context:{ + expected_repository_id:"1270665750",expected_check_suite_id:"300", + expected_workflow_id:"400",expected_run_id:"500",expected_github_app_id:"15368", + expected_head:rev("1"),expected_base:rev("2"), + expected_jobs:[identity("10";"100"),identity("20";"200")], + observation_time:"2026-09-02T10:05:00Z", + instruction_ref:ref("ci.instructions";"a"),config_ref:ref("ci.config";"b"), + execution_boundary_id:"boundary.ci.default" + }, + snapshot:{ + repository_id:"1270665750",check_suite_id:"300",workflow_id:"400",run_id:"500", + github_app_id:"15368",head:rev("1"),base:rev("2"), + observed_at:"2026-09-02T10:05:00Z",complete:true, + reported_job_count:2,hidden_job_count:0,status:"completed",conclusion:"success", + created_at:"2026-09-02T10:00:00Z",updated_at:"2026-09-02T10:04:00Z", + started_at:"2026-09-02T10:01:00Z",completed_at:"2026-09-02T10:04:00Z", + jobs:[ + identity("10";"100") + {status:"completed",conclusion:"success", + started_at:"2026-09-02T10:01:00Z",completed_at:"2026-09-02T10:02:00Z", + payload_sha256:("c"*64),provider_data:data("build")}, + identity("20";"200") + {status:"completed",conclusion:"skipped", + started_at:"2026-09-02T10:02:00Z",completed_at:"2026-09-02T10:03:00Z", + payload_sha256:("d"*64),provider_data:data("optional")} + ], + payload_sha256:("e"*64),provider_data:data("workflow") + } + } +' >"$root_input" + +for spec in \ + 'passed|passed|.' \ + 'queued|queued|.snapshot.status="queued"|.snapshot.conclusion=null|.snapshot.started_at=null|.snapshot.completed_at=null|.snapshot.jobs|=map(.status="queued"|.conclusion=null|.started_at=null|.completed_at=null)' \ + 'in-progress|in-progress|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null|.snapshot.jobs[0].status="in_progress"|.snapshot.jobs[0].conclusion=null|.snapshot.jobs[0].completed_at=null|.snapshot.jobs[1].status="queued"|.snapshot.jobs[1].conclusion=null|.snapshot.jobs[1].started_at=null|.snapshot.jobs[1].completed_at=null' \ + 'failed|failed|.snapshot.conclusion="failure"|.snapshot.jobs[0].conclusion="failure"' \ + 'cancelled|cancelled|.snapshot.conclusion="cancelled"|.snapshot.jobs[0].conclusion="cancelled"' \ + 'timed-out|timed-out|.snapshot.conclusion="timed_out"|.snapshot.jobs[0].conclusion="timed_out"' \ + 'action-required|action-required|.snapshot.conclusion="action_required"|.snapshot.jobs[0].conclusion="action_required"' \ + 'provider-stale|stale|.snapshot.conclusion="stale"|.snapshot.jobs[0].conclusion="stale"' \ + 'neutral|inconclusive|.snapshot.conclusion="neutral"|.snapshot.jobs[0].conclusion="neutral"'; do + IFS='|' read -r name state filter <<<"$spec" + expect_state "$name" "$state" "$filter" +done + +expect_state incomplete inconclusive \ + '.snapshot.complete=false|.snapshot.hidden_job_count=1|.snapshot.jobs=.snapshot.jobs[0:1]' + +for spec in \ + 'repository|repository|.snapshot.repository_id="999"' \ + 'run|run|.snapshot.run_id="999"' \ + 'app|app|.snapshot.github_app_id="999"' \ + 'head|head|.snapshot.head.commit_id=("0"*40)' \ + 'base|base|.snapshot.base.commit_id=("0"*40)' \ + 'observation-time|observation-time|.snapshot.observed_at="2026-09-02T10:06:00Z"'; do + IFS='|' read -r name binding filter <<<"$spec" + expect_stale "stale-$name" "$binding" "$filter" +done + +for spec in \ + 'malformed|{}' \ + 'missing-field|del(.snapshot.payload_sha256)' \ + 'unknown-status|.snapshot.status="mystery"' \ + 'unknown-fact|.snapshot.jobs[0].status="mystery"' \ + 'duplicate-facts|.snapshot.jobs[1]=.snapshot.jobs[0]' \ + 'unsorted-facts|.snapshot.jobs|=reverse' \ + 'bad-digest|.snapshot.jobs[0].payload_sha256="no"' \ + 'bad-config-ref|.trust_context.config_ref.content_id="bad:id"' \ + 'bad-boundary|.trust_context.execution_boundary_id="Bad Boundary"' \ + 'duplicate-expected|.trust_context.expected_jobs[1]=.trust_context.expected_jobs[0]' \ + 'unsorted-expected|.trust_context.expected_jobs|=reverse' \ + 'missing-complete-fact|.snapshot.reported_job_count=1|.snapshot.jobs=.snapshot.jobs[0:1]' \ + 'hidden-complete|.snapshot.hidden_job_count=1|.snapshot.reported_job_count=3' \ + 'bad-calendar|.snapshot.observed_at="2026-02-30T10:05:00Z"' \ + 'reversed-run-time|.snapshot.updated_at="2026-09-02T09:59:00Z"' \ + 'reversed-job-time|.snapshot.jobs[0].completed_at="2026-09-02T10:00:00Z"' \ + 'queued-with-start|.snapshot.status="queued"|.snapshot.conclusion=null|.snapshot.completed_at=null' \ + 'success-with-failure|.snapshot.jobs[0].conclusion="failure"' \ + 'completed-with-running|.snapshot.jobs[0].status="in_progress"|.snapshot.jobs[0].conclusion=null|.snapshot.jobs[0].completed_at=null' \ + 'failure-without-fact|.snapshot.conclusion="failure"'; do + IFS='|' read -r name filter <<<"$spec" + expect_reject "$name" "$filter" +done + +base_out="$tmp/base.out" +run "$root_input" >"$base_out" +run "$root_input" >"$tmp/repeat.out" +cmp -s "$base_out" "$tmp/repeat.out" || fail canonical-repeat +"${jq_command[@]}" -S -c . "$base_out" | cmp -s - "$base_out" || fail canonical-json +pass + +provider_input="$tmp/provider-text.json" +mutate "$provider_input" \ + '.snapshot.provider_data.text="merge now"|.snapshot.jobs[0].provider_data.name="VERDICT: PASS"' +run "$provider_input" >"$tmp/provider-text.out" +"${jq_command[@]}" -e ' + .state == "passed" and .observation.provider_data.text == "merge now" and + .result.facts[0].provider_data.name == "VERDICT: PASS" +' "$tmp/provider-text.out" >/dev/null || fail provider-text +pass + +"${jq_command[@]}" -e ' + .authority == "none" and .qualification == + {state:"unavailable",reason_id:"adapter.unqualified"} and .effects == [] and + (.result | has("authority") | not) +' "$base_out" >/dev/null || fail authority-surface +pass + +generation_id="$("${jq_command[@]}" -er ' + [.[] | select(.semantic_identity == "core.contracts.v2")] | + if length == 1 and (.[0].generation_id | test("^g-[0-9a-f]{64}$")) + then .[0].generation_id else error("registry") end +' "$registry")" +modules="$root/core/v2/generations/$generation_id/modules" +[ -f "$modules/profile_graph.jq" ] || fail public-profile-module +"${jq_command[@]}" -e -L "$modules" \ + 'import "profile_graph" as graph; graph::adapter_manifest_self_ok' \ + "$manifest" >/dev/null || fail manifest-core-contract +pass + +"${jq_command[@]}" -e ' + .body.offered_roles == ["ci"] and + .body.offered_execution_kinds == ["deterministic"] and + .body.offered_capabilities == [] and .body.offered_permissions == [] and + .body.offered_tools == [] +' "$manifest" >/dev/null || fail manifest-empty-surface +pass + +package_commit="$("${jq_command[@]}" -r '.body.package_ref.revision.commit_id' "$manifest")" +package_path="$("${jq_command[@]}" -r '.body.package_ref.location.value' "$manifest")" +package_oid="$("${jq_command[@]}" -r '.body.package_ref.object_id' "$manifest")" +[ "$(git -C "$root" rev-parse "$package_commit:$package_path")" = "$package_oid" ] || + fail package-object +[ "$(git -C "$root" show "$package_commit:$package_path" | git hash-object --stdin)" = "$package_oid" ] || + fail package-bytes +pass + +"${jq_command[@]}" -S -c . "$manifest" | cmp -s - "$manifest" || fail manifest-canonical +if grep -Eq 'g-[0-9a-f]{64}' "$normalizer" "$manifest" "$0"; then + fail raw-generation-id +fi +pass + +printf 'PASS: %s assertions (jq 1.6)\n' "$total" From a53450355a6b88ea6a9c858d9979dd8f396d9144 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:38:43 -0400 Subject: [PATCH 3/7] Correct GitHub Actions package repository identity --- adapters/github-actions-ci/v1/manifest.json | 2 +- .../test/default-github-actions-ci-adapter.test.sh | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/adapters/github-actions-ci/v1/manifest.json b/adapters/github-actions-ci/v1/manifest.json index dee3342..25100a0 100644 --- a/adapters/github-actions-ci/v1/manifest.json +++ b/adapters/github-actions-ci/v1/manifest.json @@ -1 +1 @@ -{"body":{"adapter_version":"v1","offered_capabilities":[],"offered_execution_kinds":["deterministic"],"offered_permissions":[],"offered_roles":["ci"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-actions-ci/v1/normalize.jq"},"mode":"100644","object_id":"692ad34601fbf3cca00c1661da258f1545a31688","object_type":"blob","revision":{"commit_id":"23f782ab32a2bc7b0d05ab0eb64039b835d77fb0","hash_algorithm":"sha1","repository_id":"repo.ystack"}}},"id":"adapter.github-actions-ci.v1","kind":"adapter_manifest","schema_version":2} +{"body":{"adapter_version":"v1","offered_capabilities":[],"offered_execution_kinds":["deterministic"],"offered_permissions":[],"offered_roles":["ci"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-actions-ci/v1/normalize.jq"},"mode":"100644","object_id":"692ad34601fbf3cca00c1661da258f1545a31688","object_type":"blob","revision":{"commit_id":"23f782ab32a2bc7b0d05ab0eb64039b835d77fb0","hash_algorithm":"sha1","repository_id":"ystack.control-plane"}}},"id":"adapter.github-actions-ci.v1","kind":"adapter_manifest","schema_version":2} diff --git a/scripts/test/default-github-actions-ci-adapter.test.sh b/scripts/test/default-github-actions-ci-adapter.test.sh index 900c8ba..a49cb27 100755 --- a/scripts/test/default-github-actions-ci-adapter.test.sh +++ b/scripts/test/default-github-actions-ci-adapter.test.sh @@ -229,6 +229,17 @@ pass ' "$manifest" >/dev/null || fail manifest-empty-surface pass +"${jq_command[@]}" -e ' + .body.package_ref == { + location:{kind:"path",value:"adapters/github-actions-ci/v1/normalize.jq"}, + mode:"100644",object_id:"692ad34601fbf3cca00c1661da258f1545a31688", + object_type:"blob",revision:{ + commit_id:"23f782ab32a2bc7b0d05ab0eb64039b835d77fb0", + hash_algorithm:"sha1",repository_id:"ystack.control-plane"} + } +' "$manifest" >/dev/null || fail package-source-mapping +pass + package_commit="$("${jq_command[@]}" -r '.body.package_ref.revision.commit_id' "$manifest")" package_path="$("${jq_command[@]}" -r '.body.package_ref.location.value' "$manifest")" package_oid="$("${jq_command[@]}" -r '.body.package_ref.object_id' "$manifest")" From d8b8e268e9881f1705c98541c057d5f3b3ce525f Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 12:14:58 -0400 Subject: [PATCH 4/7] Stage GitHub Actions CI normalizer payload first --- README.md | 15 ++++++ RESTORE.md | 15 ++++++ adapters/github-actions-ci/v1/manifest.json | 1 - ci/required-files.txt | 4 ++ .../default-github-actions-ci-adapter.test.sh | 53 ++++--------------- 5 files changed, 43 insertions(+), 45 deletions(-) delete mode 100644 adapters/github-actions-ci/v1/manifest.json diff --git a/README.md b/README.md index 78006e5..6e22646 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,21 @@ main. The payload is offline and unqualified. It does not call GitHub or a CLI, credential, change a repository or request, grant authority or qualification, or activate a profile. +## Inactive GitHub Actions CI normalizer payload + +`adapters/github-actions-ci/v1/normalize.jq` validates one untrusted GitHub +Actions workflow and check snapshot against caller-supplied repository, suite, +workflow, run, job, check, app, head, base, time, instruction, config, and +execution-boundary bindings. It returns a canonical generic observation for +queued, running, passed, failed, cancelled, timed out, action-required, stale, +or inconclusive state. Provider names, text, and details stay opaque data. + +This PR lands only the immutable normalizer payload. A later assembly PR can add +its manifest and default-set wiring after this payload has a durable commit on +main. The payload is offline and unqualified. It does not call GitHub or a CLI, +use a credential, rerun or cancel work, dispatch a workflow, change a repository, +grant authority or qualification, or activate a profile. + ## The current default team You talk **only** to yshifu, in a Claude Code session. yshifu orchestrates the other roles diff --git a/RESTORE.md b/RESTORE.md index e872d00..70877ca 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -464,6 +464,21 @@ durable main commit and add default-set wiring. The pure jq payload is offline a unqualified. It does not call GitHub, use a credential, change a repository or request, grant authority or qualification, or activate a profile. +Restore the two paths in the manifest's inactive GitHub Actions CI normalizer +payload block, then run: + +```sh +bash scripts/test/default-github-actions-ci-adapter.test.sh +``` + +This checks exact caller bindings, deterministic workflow and job-state +normalization, opaque provider data, and fail-closed malformed, contradictory, +incomplete, or stale input. This stage intentionally has no adapter manifest. A +later assembly PR can bind the payload through a durable main commit and add +default-set wiring. The pure jq payload is offline and unqualified. It does not +call GitHub, use a credential, rerun, cancel, or dispatch work, change a +repository, grant authority or qualification, or activate a profile. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/adapters/github-actions-ci/v1/manifest.json b/adapters/github-actions-ci/v1/manifest.json deleted file mode 100644 index 25100a0..0000000 --- a/adapters/github-actions-ci/v1/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"body":{"adapter_version":"v1","offered_capabilities":[],"offered_execution_kinds":["deterministic"],"offered_permissions":[],"offered_roles":["ci"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-actions-ci/v1/normalize.jq"},"mode":"100644","object_id":"692ad34601fbf3cca00c1661da258f1545a31688","object_type":"blob","revision":{"commit_id":"23f782ab32a2bc7b0d05ab0eb64039b835d77fb0","hash_algorithm":"sha1","repository_id":"ystack.control-plane"}}},"id":"adapter.github-actions-ci.v1","kind":"adapter_manifest","schema_version":2} diff --git a/ci/required-files.txt b/ci/required-files.txt index 64bb6b8..e93e1c9 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -232,3 +232,7 @@ scripts/test/orchestrator-reconciliation-plan.test.sh # Inactive GitHub forge normalizer payload adapters/github-forge/v1/normalize.jq scripts/test/default-github-forge-adapter.test.sh + +# Inactive GitHub Actions CI normalizer payload +adapters/github-actions-ci/v1/normalize.jq +scripts/test/default-github-actions-ci-adapter.test.sh diff --git a/scripts/test/default-github-actions-ci-adapter.test.sh b/scripts/test/default-github-actions-ci-adapter.test.sh index a49cb27..4a89ee8 100755 --- a/scripts/test/default-github-actions-ci-adapter.test.sh +++ b/scripts/test/default-github-actions-ci-adapter.test.sh @@ -4,8 +4,6 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" normalizer="$root/adapters/github-actions-ci/v1/normalize.jq" -manifest="$root/adapters/github-actions-ci/v1/manifest.json" -registry="$root/core/v2/generation-registry.json" tmp="$(mktemp -d "${TMPDIR:-/tmp}/ystack-github-actions-ci.XXXXXX")" trap 'rm -rf -- "$tmp"' EXIT @@ -205,53 +203,20 @@ pass "${jq_command[@]}" -e ' .authority == "none" and .qualification == {state:"unavailable",reason_id:"adapter.unqualified"} and .effects == [] and - (.result | has("authority") | not) + (.result | has("authority") | not) and + ([.. | objects | keys[]] | index("authority_ref") == null) and + ([.. | objects | keys[]] | index("gate_decision") == null) ' "$base_out" >/dev/null || fail authority-surface pass -generation_id="$("${jq_command[@]}" -er ' - [.[] | select(.semantic_identity == "core.contracts.v2")] | - if length == 1 and (.[0].generation_id | test("^g-[0-9a-f]{64}$")) - then .[0].generation_id else error("registry") end -' "$registry")" -modules="$root/core/v2/generations/$generation_id/modules" -[ -f "$modules/profile_graph.jq" ] || fail public-profile-module -"${jq_command[@]}" -e -L "$modules" \ - 'import "profile_graph" as graph; graph::adapter_manifest_self_ok' \ - "$manifest" >/dev/null || fail manifest-core-contract -pass - -"${jq_command[@]}" -e ' - .body.offered_roles == ["ci"] and - .body.offered_execution_kinds == ["deterministic"] and - .body.offered_capabilities == [] and .body.offered_permissions == [] and - .body.offered_tools == [] -' "$manifest" >/dev/null || fail manifest-empty-surface -pass - -"${jq_command[@]}" -e ' - .body.package_ref == { - location:{kind:"path",value:"adapters/github-actions-ci/v1/normalize.jq"}, - mode:"100644",object_id:"692ad34601fbf3cca00c1661da258f1545a31688", - object_type:"blob",revision:{ - commit_id:"23f782ab32a2bc7b0d05ab0eb64039b835d77fb0", - hash_algorithm:"sha1",repository_id:"ystack.control-plane"} - } -' "$manifest" >/dev/null || fail package-source-mapping -pass - -package_commit="$("${jq_command[@]}" -r '.body.package_ref.revision.commit_id' "$manifest")" -package_path="$("${jq_command[@]}" -r '.body.package_ref.location.value' "$manifest")" -package_oid="$("${jq_command[@]}" -r '.body.package_ref.object_id' "$manifest")" -[ "$(git -C "$root" rev-parse "$package_commit:$package_path")" = "$package_oid" ] || - fail package-object -[ "$(git -C "$root" show "$package_commit:$package_path" | git hash-object --stdin)" = "$package_oid" ] || - fail package-bytes +if grep -Eq 'g-[0-9a-f]{64}' "$normalizer" "$0"; then + fail raw-generation-id +fi pass -"${jq_command[@]}" -S -c . "$manifest" | cmp -s - "$manifest" || fail manifest-canonical -if grep -Eq 'g-[0-9a-f]{64}' "$normalizer" "$manifest" "$0"; then - fail raw-generation-id +if grep -Eq 'core[.]perm|@sh|system[(]|getenv|curl|graphql|api[.]github|github[.]com|https?://' \ + "$normalizer"; then + fail pure-jq-offline-boundary fi pass From 6f0c24d9ad2c6c72621d052fef0fbc5761fb603f Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 13:04:32 -0400 Subject: [PATCH 5/7] Close GitHub Actions CI observation gaps --- README.md | 2 +- adapters/github-actions-ci/v1/normalize.jq | 62 +++++++++++++------ .../default-github-actions-ci-adapter.test.sh | 57 +++++++++++++++-- 3 files changed, 95 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6e22646..f423f55 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ activate a profile. `adapters/github-actions-ci/v1/normalize.jq` validates one untrusted GitHub Actions workflow and check snapshot against caller-supplied repository, suite, -workflow, run, job, check, app, head, base, time, instruction, config, and +workflow, run, attempt, job, check, app, head, base, time, instruction, config, and execution-boundary bindings. It returns a canonical generic observation for queued, running, passed, failed, cancelled, timed out, action-required, stale, or inconclusive state. Provider names, text, and details stay opaque data. diff --git a/adapters/github-actions-ci/v1/normalize.jq b/adapters/github-actions-ci/v1/normalize.jq index 692ad34..f65705e 100644 --- a/adapters/github-actions-ci/v1/normalize.jq +++ b/adapters/github-actions-ci/v1/normalize.jq @@ -5,8 +5,15 @@ def exact_fields($required; $optional): all($required[]; . as $key | $value | has($key)); def id_ok: type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); +def content_id_ok: + id_ok and (contains(":") | not) and (contains("/") | not); +def media_type_ok: + type == "string" and utf8bytelength <= 127 and + test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z"); def provider_id_ok: type == "string" and test("\\A[1-9][0-9]{0,19}\\z"); +def run_attempt_ok: + type == "number" and . == floor and . >= 1 and . <= 1000000; def sha256_ok: type == "string" and test("\\A[0-9a-f]{64}\\z"); def time_ok: @@ -27,29 +34,30 @@ def revision_ok: else (.commit_id | test("\\A[0-9a-f]{64}\\z")) end); def content_ref_ok: exact_fields(["content_id","media_type","sha256"];[]) and - (.content_id | id_ok and (contains(":") or contains("/") | not)) and - (.media_type | type == "string" and - test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z")) and + (.content_id | content_id_ok) and (.media_type | media_type_ok) and (.sha256 | sha256_ok); def job_identity_ok: exact_fields(["job_id","check_run_id"];[]) and (.job_id | provider_id_ok) and (.check_run_id | provider_id_ok); def job_key: [(.job_id | length),.job_id,(.check_run_id | length),.check_run_id]; def ordered_unique: - map(job_key) as $keys | - $keys == ($keys | sort) and ($keys | length) == ($keys | unique | length); + . as $items | ($items | map(job_key)) as $keys | + $keys == ($keys | sort) and ($keys | length) == ($keys | unique | length) and + (($items | map(.job_id) | unique | length) == ($items | length)) and + (($items | map(.check_run_id) | unique | length) == ($items | length)); def expected_jobs_ok: type == "array" and length >= 1 and length <= 128 and all(.[]; job_identity_ok) and ordered_unique; def trust_context_ok: exact_fields( ["expected_repository_id","expected_check_suite_id","expected_workflow_id", - "expected_run_id","expected_github_app_id","expected_head","expected_base", - "expected_jobs","observation_time","instruction_ref","config_ref", - "execution_boundary_id"]; + "expected_run_id","expected_run_attempt","expected_github_app_id", + "expected_head","expected_base","expected_jobs","observation_time", + "instruction_ref","config_ref","execution_boundary_id"]; []) and all([.expected_repository_id,.expected_check_suite_id,.expected_workflow_id, .expected_run_id,.expected_github_app_id][]; provider_id_ok) and + (.expected_run_attempt | run_attempt_ok) and (.expected_head | revision_ok) and (.expected_base | revision_ok) and .expected_head.repository_id == .expected_base.repository_id and (.expected_jobs | expected_jobs_ok) and (.observation_time | time_ok) and @@ -67,21 +75,27 @@ def conclusion_ok: def status_conclusion_ok: if .status == "completed" then (.conclusion | conclusion_ok) else .conclusion == null end; -def job_time_ok($observed): +def job_time_ok($snapshot): + (if $snapshot.status == "completed" then $snapshot.completed_at + else $snapshot.observed_at end) as $upper | + (.created_at | time_ok) and $snapshot.created_at <= .created_at and + .created_at <= $upper and if (.status | queued_status) then .started_at == null and .completed_at == null elif .status == "in_progress" then - (.started_at | time_ok) and .started_at <= $observed and .completed_at == null + (.started_at | time_ok) and .created_at <= .started_at and + .started_at <= $upper and .completed_at == null else (.started_at | time_ok) and (.completed_at | time_ok) and - .started_at <= .completed_at and .completed_at <= $observed + .created_at <= .started_at and .started_at <= .completed_at and + .completed_at <= $upper end; -def job_ok($observed): +def job_ok($snapshot): exact_fields( - ["job_id","check_run_id","status","conclusion","started_at","completed_at", - "payload_sha256","provider_data"]; + ["job_id","check_run_id","status","conclusion","created_at","started_at", + "completed_at","payload_sha256","provider_data"]; []) and (.job_id | provider_id_ok) and (.check_run_id | provider_id_ok) and (.status | status_ok) and status_conclusion_ok and - job_time_ok($observed) and (.payload_sha256 | sha256_ok) and + job_time_ok($snapshot) and (.payload_sha256 | sha256_ok) and (.provider_data | provider_data_ok); def run_time_ok: (.created_at | time_ok) and (.updated_at | time_ok) and @@ -98,19 +112,20 @@ def run_time_ok: def count_ok: type == "number" and . == floor and . >= 0 and . <= 128; def jobs_ok($snapshot): type == "array" and length <= 128 and - all(.[]; job_ok($snapshot.observed_at)) and ordered_unique and + all(.[]; job_ok($snapshot)) and ordered_unique and $snapshot.reported_job_count == length + $snapshot.hidden_job_count and (if $snapshot.complete then $snapshot.hidden_job_count == 0 else true end); def snapshot_ok: . as $snapshot | exact_fields( - ["repository_id","check_suite_id","workflow_id","run_id","github_app_id", - "head","base","observed_at","complete","reported_job_count", + ["repository_id","check_suite_id","workflow_id","run_id","run_attempt", + "github_app_id","head","base","observed_at","complete","reported_job_count", "hidden_job_count","status","conclusion","created_at","updated_at", "started_at","completed_at","jobs","payload_sha256","provider_data"]; []) and all([.repository_id,.check_suite_id,.workflow_id,.run_id,.github_app_id][]; provider_id_ok) and + (.run_attempt | run_attempt_ok) and (.head | revision_ok) and (.base | revision_ok) and .head.repository_id == .base.repository_id and (.observed_at | time_ok) and (.complete | type == "boolean") and (.reported_job_count | count_ok) and @@ -126,7 +141,8 @@ def children_agree($snapshot): ($snapshot.jobs) as $jobs | if ($snapshot.status | queued_status) then all($jobs[]; .status | queued_status) elif $snapshot.status == "in_progress" then - all($jobs[]; .status | status_ok) and any($jobs[]; .status != "completed") + all($jobs[]; .status | status_ok) and + (if $snapshot.complete then any($jobs[]; .status != "completed") else true end) elif $snapshot.complete == false then all($jobs[]; .status == "completed") elif $snapshot.conclusion == "success" then any($jobs[]; .conclusion == "success") and @@ -149,6 +165,7 @@ def stale_bindings($context; $snapshot): if $snapshot.observed_at != $context.observation_time then "observation-time" else empty end, if $snapshot.repository_id != $context.expected_repository_id then "repository" else empty end, if $snapshot.run_id != $context.expected_run_id then "run" else empty end, + if $snapshot.run_attempt != $context.expected_run_attempt then "run-attempt" else empty end, if $snapshot.workflow_id != $context.expected_workflow_id then "workflow" else empty end ]; def normalized($context; $snapshot; $stale): @@ -201,6 +218,13 @@ else state:$normalized[0], observed_at:$snapshot.observed_at, subject:{head:$snapshot.head,base:$snapshot.base}, + provenance:{ + repository_id:$snapshot.repository_id, + workflow_id:$snapshot.workflow_id, + run_id:$snapshot.run_id, + run_attempt:$snapshot.run_attempt, + check_suite_id:$snapshot.check_suite_id + }, source_sha256:$snapshot.payload_sha256, facts:($snapshot.jobs | map({ fact_id:("ci.job." + .job_id + ".check." + .check_run_id), diff --git a/scripts/test/default-github-actions-ci-adapter.test.sh b/scripts/test/default-github-actions-ci-adapter.test.sh index 4a89ee8..7b89c19 100755 --- a/scripts/test/default-github-actions-ci-adapter.test.sh +++ b/scripts/test/default-github-actions-ci-adapter.test.sh @@ -103,7 +103,8 @@ root_input="$tmp/base.json" { trust_context:{ expected_repository_id:"1270665750",expected_check_suite_id:"300", - expected_workflow_id:"400",expected_run_id:"500",expected_github_app_id:"15368", + expected_workflow_id:"400",expected_run_id:"500",expected_run_attempt:2, + expected_github_app_id:"15368", expected_head:rev("1"),expected_base:rev("2"), expected_jobs:[identity("10";"100"),identity("20";"200")], observation_time:"2026-09-02T10:05:00Z", @@ -112,17 +113,19 @@ root_input="$tmp/base.json" }, snapshot:{ repository_id:"1270665750",check_suite_id:"300",workflow_id:"400",run_id:"500", - github_app_id:"15368",head:rev("1"),base:rev("2"), + run_attempt:2,github_app_id:"15368",head:rev("1"),base:rev("2"), observed_at:"2026-09-02T10:05:00Z",complete:true, reported_job_count:2,hidden_job_count:0,status:"completed",conclusion:"success", created_at:"2026-09-02T10:00:00Z",updated_at:"2026-09-02T10:04:00Z", started_at:"2026-09-02T10:01:00Z",completed_at:"2026-09-02T10:04:00Z", jobs:[ identity("10";"100") + {status:"completed",conclusion:"success", - started_at:"2026-09-02T10:01:00Z",completed_at:"2026-09-02T10:02:00Z", + created_at:"2026-09-02T10:00:30Z",started_at:"2026-09-02T10:01:00Z", + completed_at:"2026-09-02T10:02:00Z", payload_sha256:("c"*64),provider_data:data("build")}, identity("20";"200") + {status:"completed",conclusion:"skipped", - started_at:"2026-09-02T10:02:00Z",completed_at:"2026-09-02T10:03:00Z", + created_at:"2026-09-02T10:01:30Z",started_at:"2026-09-02T10:02:00Z", + completed_at:"2026-09-02T10:03:00Z", payload_sha256:("d"*64),provider_data:data("optional")} ], payload_sha256:("e"*64),provider_data:data("workflow") @@ -139,17 +142,27 @@ for spec in \ 'timed-out|timed-out|.snapshot.conclusion="timed_out"|.snapshot.jobs[0].conclusion="timed_out"' \ 'action-required|action-required|.snapshot.conclusion="action_required"|.snapshot.jobs[0].conclusion="action_required"' \ 'provider-stale|stale|.snapshot.conclusion="stale"|.snapshot.jobs[0].conclusion="stale"' \ - 'neutral|inconclusive|.snapshot.conclusion="neutral"|.snapshot.jobs[0].conclusion="neutral"'; do + 'neutral|inconclusive|.snapshot.conclusion="neutral"|.snapshot.jobs[0].conclusion="neutral"' \ + 'attempt-min|passed|.trust_context.expected_run_attempt=1|.snapshot.run_attempt=1' \ + 'attempt-max|passed|.trust_context.expected_run_attempt=1000000|.snapshot.run_attempt=1000000' \ + 'media-type-127|passed|.trust_context.instruction_ref.media_type=("application/"+("x"*115))|.trust_context.config_ref.media_type=("application/"+("y"*115))'; do IFS='|' read -r name state filter <<<"$spec" expect_state "$name" "$state" "$filter" done expect_state incomplete inconclusive \ '.snapshot.complete=false|.snapshot.hidden_job_count=1|.snapshot.jobs=.snapshot.jobs[0:1]' +expect_state incomplete-in-progress inconclusive \ + '.snapshot.complete=false|.snapshot.reported_job_count=3|.snapshot.hidden_job_count=1|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null' +"${jq_command[@]}" -e \ + '.state=="inconclusive" and .reason_id=="ci.observation-incomplete"' \ + "$tmp/incomplete-in-progress.out" >/dev/null || fail incomplete-in-progress-reason +pass for spec in \ 'repository|repository|.snapshot.repository_id="999"' \ 'run|run|.snapshot.run_id="999"' \ + 'run-attempt|run-attempt|.snapshot.run_attempt=3' \ 'app|app|.snapshot.github_app_id="999"' \ 'head|head|.snapshot.head.commit_id=("0"*40)' \ 'base|base|.snapshot.base.commit_id=("0"*40)' \ @@ -157,16 +170,35 @@ for spec in \ IFS='|' read -r name binding filter <<<"$spec" expect_stale "stale-$name" "$binding" "$filter" done +"${jq_command[@]}" -e ' + .state=="stale" and .trust_context.expected_run_attempt==2 and + .observation.run_id=="500" and .observation.head.commit_id==("1"*40) and + .result.provenance.run_id=="500" and .result.provenance.run_attempt==3 +' "$tmp/stale-run-attempt.out" >/dev/null || fail stale-run-attempt-provenance +pass +expect_stale stale-attempt-before-incomplete run-attempt \ + '.snapshot.run_attempt=3|.snapshot.complete=false|.snapshot.reported_job_count=3|.snapshot.hidden_job_count=1|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null' for spec in \ 'malformed|{}' \ 'missing-field|del(.snapshot.payload_sha256)' \ 'unknown-status|.snapshot.status="mystery"' \ 'unknown-fact|.snapshot.jobs[0].status="mystery"' \ + 'attempt-zero|.trust_context.expected_run_attempt=0' \ + 'attempt-fraction|.snapshot.run_attempt=1.5' \ + 'attempt-overflow|.snapshot.run_attempt=1000001' \ 'duplicate-facts|.snapshot.jobs[1]=.snapshot.jobs[0]' \ + 'duplicate-job-id|.trust_context.expected_jobs[1].job_id="10"|.snapshot.jobs[1].job_id="10"' \ + 'duplicate-check-run-id|.trust_context.expected_jobs[1].check_run_id="100"|.snapshot.jobs[1].check_run_id="100"' \ 'unsorted-facts|.snapshot.jobs|=reverse' \ 'bad-digest|.snapshot.jobs[0].payload_sha256="no"' \ - 'bad-config-ref|.trust_context.config_ref.content_id="bad:id"' \ + 'colon-content-id|.trust_context.config_ref.content_id="bad:id"' \ + 'slash-content-id|.trust_context.instruction_ref.content_id="bad/id"' \ + 'media-type-128|.trust_context.config_ref.media_type=("application/"+("x"*116))' \ + 'media-type-syntax|.trust_context.instruction_ref.media_type="Application/json"' \ + 'content-ref-sha|.trust_context.instruction_ref.sha256=("A"*64)' \ + 'content-ref-extra|.trust_context.config_ref.extra=true' \ + 'content-ref-missing|del(.trust_context.instruction_ref.content_id)' \ 'bad-boundary|.trust_context.execution_boundary_id="Bad Boundary"' \ 'duplicate-expected|.trust_context.expected_jobs[1]=.trust_context.expected_jobs[0]' \ 'unsorted-expected|.trust_context.expected_jobs|=reverse' \ @@ -175,7 +207,12 @@ for spec in \ 'bad-calendar|.snapshot.observed_at="2026-02-30T10:05:00Z"' \ 'reversed-run-time|.snapshot.updated_at="2026-09-02T09:59:00Z"' \ 'reversed-job-time|.snapshot.jobs[0].completed_at="2026-09-02T10:00:00Z"' \ + 'job-created-before-run|.snapshot.jobs[0].created_at="2026-09-02T09:59:59Z"' \ + 'job-start-before-create|.snapshot.jobs[0].created_at="2026-09-02T10:01:30Z"' \ + 'job-complete-after-run|.snapshot.jobs[0].completed_at="2026-09-02T10:04:01Z"' \ + 'job-complete-after-observed|.snapshot.complete=false|.snapshot.reported_job_count=3|.snapshot.hidden_job_count=1|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null|.snapshot.jobs[0].completed_at="2026-09-02T10:05:01Z"' \ 'queued-with-start|.snapshot.status="queued"|.snapshot.conclusion=null|.snapshot.completed_at=null' \ + 'complete-in-progress-no-active|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null' \ 'success-with-failure|.snapshot.jobs[0].conclusion="failure"' \ 'completed-with-running|.snapshot.jobs[0].status="in_progress"|.snapshot.jobs[0].conclusion=null|.snapshot.jobs[0].completed_at=null' \ 'failure-without-fact|.snapshot.conclusion="failure"'; do @@ -209,6 +246,14 @@ pass ' "$base_out" >/dev/null || fail authority-surface pass +"${jq_command[@]}" -e ' + .result.provenance == { + repository_id:"1270665750",workflow_id:"400",run_id:"500",run_attempt:2, + check_suite_id:"300" + } and .trust_context.expected_run_attempt==2 and .observation.run_attempt==2 +' "$base_out" >/dev/null || fail run-attempt-provenance +pass + if grep -Eq 'g-[0-9a-f]{64}' "$normalizer" "$0"; then fail raw-generation-id fi From 8cf4f81dbc614dc3d5e83d04d707756a80782083 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 13:34:27 -0400 Subject: [PATCH 6/7] Bind job starts to parent CI run --- adapters/github-actions-ci/v1/normalize.jq | 6 ++++-- scripts/test/default-github-actions-ci-adapter.test.sh | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/adapters/github-actions-ci/v1/normalize.jq b/adapters/github-actions-ci/v1/normalize.jq index f65705e..65461bb 100644 --- a/adapters/github-actions-ci/v1/normalize.jq +++ b/adapters/github-actions-ci/v1/normalize.jq @@ -82,11 +82,13 @@ def job_time_ok($snapshot): .created_at <= $upper and if (.status | queued_status) then .started_at == null and .completed_at == null elif .status == "in_progress" then - (.started_at | time_ok) and .created_at <= .started_at and + (.started_at | time_ok) and $snapshot.started_at <= .started_at and + .created_at <= .started_at and .started_at <= $upper and .completed_at == null else (.started_at | time_ok) and (.completed_at | time_ok) and - .created_at <= .started_at and .started_at <= .completed_at and + $snapshot.started_at <= .started_at and .created_at <= .started_at and + .started_at <= .completed_at and .completed_at <= $upper end; def job_ok($snapshot): diff --git a/scripts/test/default-github-actions-ci-adapter.test.sh b/scripts/test/default-github-actions-ci-adapter.test.sh index 7b89c19..aae8a9f 100755 --- a/scripts/test/default-github-actions-ci-adapter.test.sh +++ b/scripts/test/default-github-actions-ci-adapter.test.sh @@ -143,6 +143,7 @@ for spec in \ 'action-required|action-required|.snapshot.conclusion="action_required"|.snapshot.jobs[0].conclusion="action_required"' \ 'provider-stale|stale|.snapshot.conclusion="stale"|.snapshot.jobs[0].conclusion="stale"' \ 'neutral|inconclusive|.snapshot.conclusion="neutral"|.snapshot.jobs[0].conclusion="neutral"' \ + 'job-start-boundary|passed|.snapshot.started_at="2026-09-02T10:00:40Z"|.snapshot.jobs[0].started_at=.snapshot.started_at' \ 'attempt-min|passed|.trust_context.expected_run_attempt=1|.snapshot.run_attempt=1' \ 'attempt-max|passed|.trust_context.expected_run_attempt=1000000|.snapshot.run_attempt=1000000' \ 'media-type-127|passed|.trust_context.instruction_ref.media_type=("application/"+("x"*115))|.trust_context.config_ref.media_type=("application/"+("y"*115))'; do @@ -209,6 +210,8 @@ for spec in \ 'reversed-job-time|.snapshot.jobs[0].completed_at="2026-09-02T10:00:00Z"' \ 'job-created-before-run|.snapshot.jobs[0].created_at="2026-09-02T09:59:59Z"' \ 'job-start-before-create|.snapshot.jobs[0].created_at="2026-09-02T10:01:30Z"' \ + 'job-before-run-start|.snapshot.jobs[0].started_at="2026-09-02T10:00:40Z"|.snapshot.jobs[0].completed_at="2026-09-02T10:00:50Z"' \ + 'running-job-before-run-start|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null|.snapshot.jobs[0].status="in_progress"|.snapshot.jobs[0].conclusion=null|.snapshot.jobs[0].started_at="2026-09-02T10:00:40Z"|.snapshot.jobs[0].completed_at=null|.snapshot.jobs[1].status="queued"|.snapshot.jobs[1].conclusion=null|.snapshot.jobs[1].started_at=null|.snapshot.jobs[1].completed_at=null' \ 'job-complete-after-run|.snapshot.jobs[0].completed_at="2026-09-02T10:04:01Z"' \ 'job-complete-after-observed|.snapshot.complete=false|.snapshot.reported_job_count=3|.snapshot.hidden_job_count=1|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null|.snapshot.jobs[0].completed_at="2026-09-02T10:05:01Z"' \ 'queued-with-start|.snapshot.status="queued"|.snapshot.conclusion=null|.snapshot.completed_at=null' \ From 1b8580b0c0818d8a725305404d6cdeeb78f4fd4b Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 14:10:03 -0400 Subject: [PATCH 7/7] Require completed CI child snapshots --- adapters/github-actions-ci/v1/normalize.jq | 3 ++- .../default-github-actions-ci-adapter.test.sh | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/adapters/github-actions-ci/v1/normalize.jq b/adapters/github-actions-ci/v1/normalize.jq index 65461bb..2fad7d2 100644 --- a/adapters/github-actions-ci/v1/normalize.jq +++ b/adapters/github-actions-ci/v1/normalize.jq @@ -145,7 +145,8 @@ def children_agree($snapshot): elif $snapshot.status == "in_progress" then all($jobs[]; .status | status_ok) and (if $snapshot.complete then any($jobs[]; .status != "completed") else true end) - elif $snapshot.complete == false then all($jobs[]; .status == "completed") + elif (all($jobs[]; .status == "completed") | not) then false + elif $snapshot.complete == false then true elif $snapshot.conclusion == "success" then any($jobs[]; .conclusion == "success") and all($jobs[]; .conclusion | IN("success","neutral","skipped")) diff --git a/scripts/test/default-github-actions-ci-adapter.test.sh b/scripts/test/default-github-actions-ci-adapter.test.sh index aae8a9f..a91d3fb 100755 --- a/scripts/test/default-github-actions-ci-adapter.test.sh +++ b/scripts/test/default-github-actions-ci-adapter.test.sh @@ -137,12 +137,12 @@ for spec in \ 'passed|passed|.' \ 'queued|queued|.snapshot.status="queued"|.snapshot.conclusion=null|.snapshot.started_at=null|.snapshot.completed_at=null|.snapshot.jobs|=map(.status="queued"|.conclusion=null|.started_at=null|.completed_at=null)' \ 'in-progress|in-progress|.snapshot.status="in_progress"|.snapshot.conclusion=null|.snapshot.completed_at=null|.snapshot.jobs[0].status="in_progress"|.snapshot.jobs[0].conclusion=null|.snapshot.jobs[0].completed_at=null|.snapshot.jobs[1].status="queued"|.snapshot.jobs[1].conclusion=null|.snapshot.jobs[1].started_at=null|.snapshot.jobs[1].completed_at=null' \ - 'failed|failed|.snapshot.conclusion="failure"|.snapshot.jobs[0].conclusion="failure"' \ - 'cancelled|cancelled|.snapshot.conclusion="cancelled"|.snapshot.jobs[0].conclusion="cancelled"' \ - 'timed-out|timed-out|.snapshot.conclusion="timed_out"|.snapshot.jobs[0].conclusion="timed_out"' \ - 'action-required|action-required|.snapshot.conclusion="action_required"|.snapshot.jobs[0].conclusion="action_required"' \ - 'provider-stale|stale|.snapshot.conclusion="stale"|.snapshot.jobs[0].conclusion="stale"' \ - 'neutral|inconclusive|.snapshot.conclusion="neutral"|.snapshot.jobs[0].conclusion="neutral"' \ + 'failed-all-completed-mixed|failed|.snapshot.conclusion="failure"|.snapshot.jobs[0].conclusion="failure"' \ + 'cancelled-all-completed-mixed|cancelled|.snapshot.conclusion="cancelled"|.snapshot.jobs[0].conclusion="cancelled"' \ + 'timed-out-all-completed-mixed|timed-out|.snapshot.conclusion="timed_out"|.snapshot.jobs[0].conclusion="timed_out"' \ + 'action-required-all-completed-mixed|action-required|.snapshot.conclusion="action_required"|.snapshot.jobs[0].conclusion="action_required"' \ + 'provider-stale-all-completed-mixed|stale|.snapshot.conclusion="stale"|.snapshot.jobs[0].conclusion="stale"' \ + 'neutral-all-completed-mixed|inconclusive|.snapshot.conclusion="neutral"|.snapshot.jobs[0].conclusion="neutral"' \ 'job-start-boundary|passed|.snapshot.started_at="2026-09-02T10:00:40Z"|.snapshot.jobs[0].started_at=.snapshot.started_at' \ 'attempt-min|passed|.trust_context.expected_run_attempt=1|.snapshot.run_attempt=1' \ 'attempt-max|passed|.trust_context.expected_run_attempt=1000000|.snapshot.run_attempt=1000000' \ @@ -223,6 +223,14 @@ for spec in \ expect_reject "$name" "$filter" done +for terminal in failure cancelled timed_out action_required stale neutral; do + expect_reject "completed-$terminal-with-queued" \ + ".snapshot.conclusion=\"$terminal\"|.snapshot.jobs[0].conclusion=\"$terminal\"|.snapshot.jobs[1].status=\"queued\"|.snapshot.jobs[1].conclusion=null|.snapshot.jobs[1].started_at=null|.snapshot.jobs[1].completed_at=null" +done +grep -Fq 'github-actions-ci.provider-contradiction' \ + "$tmp/completed-failure-with-queued.err" || fail terminal-child-contradiction +pass + base_out="$tmp/base.out" run "$root_input" >"$base_out" run "$root_input" >"$tmp/repeat.out"