diff --git a/scripts/stage-release-bundle.sh b/scripts/stage-release-bundle.sh index 9d8b39b..0e543b5 100755 --- a/scripts/stage-release-bundle.sh +++ b/scripts/stage-release-bundle.sh @@ -43,6 +43,35 @@ cp \ "$source_root/.dockerignore" \ "$destination/" +# Promotion manifests hash-bind an eligibility policy that lives under docs/, +# which is otherwise outside the staged tree. A bundle has to contain every +# artifact its manifests bind or freeze-manifest cannot canonicalize them, so +# copy exactly those files. Not all of docs/: that also carries node_modules +# and build output, which bloats the bundle and gives the analyzer a large +# unrelated tree to index. +promotion_root="$source_root/benchmarks/promotion" +if [[ -d "$promotion_root" ]]; then + bound_list="$(mktemp)" + trap 'rm -f "$bound_list"' EXIT + find "$promotion_root" -type f -name '*.json' -print | sort | while IFS= read -r promotion_manifest; do + jq -r '.. | objects | select(has("file")) | .file' "$promotion_manifest" + done | sort -u | grep -Ev '^(benchmarks|fixtures|adapters|schema|src|containers|scripts)/' > "$bound_list" || true + while IFS= read -r bound_artifact; do + [[ -n "$bound_artifact" ]] || continue + [[ "$bound_artifact" != /* && "$bound_artifact" != *..* ]] || { + echo "promotion manifest binds an unsafe artifact path: $bound_artifact" >&2 + exit 1 + } + [[ -f "$source_root/$bound_artifact" ]] || { + echo "promotion manifest binds a missing artifact: $bound_artifact" >&2 + exit 1 + } + mkdir -p "$destination/$(dirname "$bound_artifact")" + cp -- "$source_root/$bound_artifact" "$destination/$bound_artifact" + echo "staged bound promotion artifact: $bound_artifact" + done < "$bound_list" +fi + jq -n \ --arg releaseTag "$release_tag" \ --arg releaseVersion "${release_tag#v}" \ diff --git a/tests/test_freeze_shards.py b/tests/test_freeze_shards.py index 0dc12e1..beccf51 100644 --- a/tests/test_freeze_shards.py +++ b/tests/test_freeze_shards.py @@ -160,6 +160,53 @@ def test_ruby_lsp_native_shard_provisions_ruby_three_four_keg(self): self.assertNotIn('brew list ruby >/dev/null', workflow) self.assertNotIn('ruby\\ 3\\.[4-9]', workflow) + def test_release_bundle_stages_every_artifact_a_promotion_manifest_binds(self): + """A bundle must contain what its manifests hash-bind. + + The v0.3.1 freeze reached aggregation and failed on `canonicalize + promotion artifact docs/legacy-promotion-selection-policy.md`: the + legacy manifest binds an eligibility policy under docs/, and the + staging step copies benchmarks, fixtures, adapters, schema, src, + containers, and scripts but not docs. + """ + + staging = (ROOT / "scripts/stage-release-bundle.sh").read_text() + # Derived from the manifests rather than a hardcoded docs/ path, so a + # newly bound artifact is staged without editing this script. + self.assertIn( + "jq -r '.. | objects | select(has(\"file\")) | .file' \"$promotion_manifest\"", + staging, + ) + self.assertIn('cp -- "$source_root/$bound_artifact" "$destination/$bound_artifact"', staging) + # All of docs/ would drag in node_modules and build output. + self.assertNotIn('"$source_root/docs" \\', staging) + + promotion_root = ROOT / "benchmarks/promotion" + staged_prefixes = ( + "benchmarks/", "fixtures/", "adapters/", "schema/", "src/", + "containers/", "scripts/", + ) + bound = set() + + def walk(node): + if isinstance(node, dict): + value = node.get("file") + if isinstance(value, str): + bound.add(value) + for child in node.values(): + walk(child) + elif isinstance(node, list): + for child in node: + walk(child) + + for manifest in sorted(promotion_root.rglob("*.json")): + walk(json.loads(manifest.read_text())) + outside = sorted(p for p in bound if not p.startswith(staged_prefixes)) + # Every such artifact must exist, or the freeze fails at aggregation. + for relative in outside: + self.assertTrue((ROOT / relative).is_file(), f"bound artifact missing: {relative}") + self.assertIn("docs/legacy-promotion-selection-policy.md", outside) + def test_legacy_scope_is_bound_to_the_110_case_manifest(self): scope = subprocess.run( [ROOT / "scripts/resolve-freeze-scope.sh", "legacy-promoted"],