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
13 changes: 13 additions & 0 deletions .github/contracts/linker_relink_timeouts_v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"contractVersion": 1,
"workflow": "relink.yml",
"timeouts": {
"relink": {
"kaggle": 90,
"local": 1440,
"server": 480
},
"resolve": 480,
"publish": 30
}
}
355 changes: 355 additions & 0 deletions .github/scripts/patch_fan_lib.sh

Large diffs are not rendered by default.

529 changes: 479 additions & 50 deletions .github/scripts/prefetch_patch_anchors.sh

Large diffs are not rendered by default.

72 changes: 65 additions & 7 deletions .github/scripts/release_draft.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,23 +179,72 @@ else:
PY
}

# One summary line per asset, and one closing line per step.
#
# The publish step spends 20-27 minutes of a 2h11m job pushing ~3.4 GiB up a
# ~2.2 MB/s uplink and, before this, said nothing at all between "Adopting this
# build's existing draft release." and the final release URL: a stalled upload,
# a retried one and a fast one were indistinguishable, and the dedupe that skips
# an early-uploaded asset left no trace that it had run. These lines are pure
# reporting — no upload decision, no retry budget and no failure path changes.
#
# `uploaded` = this call sent the bytes. `reused` = the asset was already on the
# draft with a matching name+size+digest (the early upload or a lost-response
# reconcile put it there) and this call verified it instead of re-sending.
RELEASE_UPLOAD_ASSETS=0
RELEASE_UPLOAD_BYTES=0
RELEASE_UPLOAD_SECONDS=0

_release_upload_note() {
local verb="$1" name="$2" size="$3" digest="$4" started="$5" elapsed rate
elapsed=$(( SECONDS - started ))
# Guard the rate against a sub-second asset: report it over 1s rather than
# dividing by zero. Reporting only — nothing reads this number back.
rate=$(awk -v bytes="$size" -v secs="$elapsed" \
'BEGIN { if (secs < 1) secs = 1; printf "%.2f", bytes / 1048576 / secs }')
RELEASE_UPLOAD_ASSETS=$(( RELEASE_UPLOAD_ASSETS + 1 ))
RELEASE_UPLOAD_BYTES=$(( RELEASE_UPLOAD_BYTES + size ))
RELEASE_UPLOAD_SECONDS=$(( RELEASE_UPLOAD_SECONDS + elapsed ))
echo "$verb $name $size sha256=${digest:0:12} in ${elapsed}s (${rate} MB/s)"
}

# Closing line for a step that uploaded assets. Counts every asset upload_asset
# handled in THIS shell, uploaded or reused, so the total is the step's whole
# release payload rather than only what happened to be missing.
release_upload_summary() {
echo "release ${RELEASE_TAG}: ${RELEASE_UPLOAD_ASSETS} assets, ${RELEASE_UPLOAD_BYTES} bytes, ${RELEASE_UPLOAD_SECONDS}s"
}

# GNU coreutils spells this `stat --format`, while macOS/BSD stat uses `-f`.
# Release verification must calculate the same byte count on a developer's Mac
# as on the Ubuntu runner; probing the implementation is less ambiguous than
# trying one format string and accepting an implementation-specific result.
file_size_bytes() {
if stat --version >/dev/null 2>&1; then
stat --format='%s' "$1"
else
stat -f '%z' "$1"
fi
}

# Upload one asset. If an upload response is lost, reconcile by exact
# name+size+digest and continue. Never use --clobber.
upload_asset() {
local asset_path="$1" expected_size expected_digest state attempt
expected_size=$(stat --format='%s' "$asset_path")
local name="${asset_path##*/}" started="$SECONDS"
expected_size=$(file_size_bytes "$asset_path")
expected_digest=$(sha256sum "$asset_path" | cut -d ' ' -f1)
state=$(asset_state "$asset_path" "$expected_size" "$expected_digest") || return 1
case "$state" in
exact) return 0 ;;
exact) _release_upload_note reused "$name" "$expected_size" "$expected_digest" "$started"; return 0 ;;
conflict) echo "::error::Conflicting remote asset: ${asset_path##*/}"; return 1 ;;
absent) ;;
pending)
for attempt in $(seq 1 12); do
sleep 5
state=$(asset_state "$asset_path" "$expected_size" "$expected_digest") || state=query-error
case "$state" in
exact) return 0 ;;
exact) _release_upload_note reused "$name" "$expected_size" "$expected_digest" "$started"; return 0 ;;
conflict|absent) return 1 ;;
esac
done
Expand All @@ -204,12 +253,15 @@ upload_asset() {
;;
*) echo "::error::Unknown asset state: $state"; return 1 ;;
esac
if gh release upload "$RELEASE_TAG" "$asset_path"; then return 0; fi
if gh release upload "$RELEASE_TAG" "$asset_path"; then
_release_upload_note uploaded "$name" "$expected_size" "$expected_digest" "$started"
return 0
fi
echo "::warning::Upload failed for ${asset_path##*/}; reconciling before retry."
for attempt in $(seq 1 12); do
state=$(asset_state "$asset_path" "$expected_size" "$expected_digest") || state=query-error
case "$state" in
exact) return 0 ;;
exact) _release_upload_note uploaded "$name" "$expected_size" "$expected_digest" "$started"; return 0 ;;
conflict) return 1 ;;
esac
sleep 5
Expand All @@ -219,11 +271,17 @@ upload_asset() {
return 1
fi
switch_token || return 1
if gh release upload "$RELEASE_TAG" "$asset_path"; then return 0; fi
if gh release upload "$RELEASE_TAG" "$asset_path"; then
_release_upload_note uploaded "$name" "$expected_size" "$expected_digest" "$started"
return 0
fi
for attempt in $(seq 1 6); do
state=$(asset_state "$asset_path" "$expected_size" "$expected_digest") || state=query-error
[ "$state" != conflict ] || return 1
if [ "$state" = exact ]; then return 0; fi
if [ "$state" = exact ]; then
_release_upload_note uploaded "$name" "$expected_size" "$expected_digest" "$started"
return 0
fi
sleep 2
done
return 1
Expand Down
33 changes: 33 additions & 0 deletions .github/scripts/test_build_provenance.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

Expand Down Expand Up @@ -201,6 +203,37 @@ def test_db_schema_block_is_strict(self):
with self.assertRaises(ValueError):
contract.validate(contract.load(self.write(tmp, value)))

def test_cli_prints_one_positive_line_and_keeps_its_failure_contract(self):
# A pipeline whose whole premise is provenance logged ZERO evidence that
# ~190 lines of validation had run: a silently stubbed validator and a
# working one were the same green step (audit of run 34024655297). The
# pass now says what it checked; the failure contract is unchanged —
# stderr, exit 2, nothing on stdout.
with tempfile.TemporaryDirectory() as tmp:
good = self.write(tmp, self.value())
done = subprocess.run(
[sys.executable, str(SCRIPT), str(good)], capture_output=True, text=True
)
self.assertEqual(done.returncode, 0, done.stderr)
lines = [line for line in done.stdout.splitlines() if line.strip()]
self.assertEqual(len(lines), 1, lines)
self.assertRegex(
lines[0],
r"^ok: build_provenance v5, \d+ fields, 2 assets, "
r"source_commit=[0-9a-f]{12} \(.*build_provenance\.json\)$",
)

broken = self.value()
broken["assets"][0]["size"] = 0
done = subprocess.run(
[sys.executable, str(SCRIPT), str(self.write(tmp, broken))],
capture_output=True,
text=True,
)
self.assertEqual(done.returncode, 2)
self.assertEqual(done.stdout, "")
self.assertIn("build provenance contract error:", done.stderr)

def test_attempt_and_asset_order_are_strict(self):
with tempfile.TemporaryDirectory() as tmp:
value = self.value()
Expand Down
Loading