tebako-release 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34b2e7ff86b5dcf3a3a56600ad664e32a7b2a7e114e234cc5b2a0f094ffe9010
4
- data.tar.gz: 866f3b422839db8d8dcee027cb840acadef89430e874e538946565939a7a954c
3
+ metadata.gz: 6e91116fb240ffb5e0cb7cec20a1753fbd4177b7bd8f3c78adb8c3e260acf718
4
+ data.tar.gz: c5c886163f60c1ef021829015d95a1f3c50b6fc239a22a39cb890324041353d1
5
5
  SHA512:
6
- metadata.gz: 7a138c918d4387006dcf59e2b4771d0f39b3a49baffcf0c01505daacf08a608af24c1413195c123db112f764c5a8e25cf341188d5910f23ca4f909fd4ed7f5e7
7
- data.tar.gz: e9e9d4aae48992f997f6d2095ed7cedddfb1e37a27dd050117ae50b447d356b875df7aaa4ee36aa83e2de470e8939ed07860c57aa0211631802e27a39e285c22
6
+ metadata.gz: 78d61711350a83bc076b26472fab4b468e68368ca6f16f9f4c7bb613149d0a7c9be68821e6646e7469958e24424f59b3b88afd3d45c7b1f01db0b806550d9c69
7
+ data.tar.gz: 44d1ee55d6bf22f2aa8d6dfdacaa27f26a77170faa2d26a45669425879d6b5ece851d0d5e1072855c6f9fa6a7b2269578f1b112a0d62b34fecf2510bd82fa6a8
data/README.md CHANGED
@@ -23,7 +23,20 @@ through a small adapter file and pin this gem — they never carry copies.
23
23
  in-bundle SHA256SUMS; fixed member order, mtime=0 — identical inputs
24
24
  yield identical bytes). A factory opts into the bundle-era publish shape
25
25
  through its adapter (`bundle_publish? → true`); the per-file shape stays
26
- the default for pre-bundle lines.
26
+ the default for pre-bundle lines. Bundle-era lines whose consumers
27
+ include the lazy arm additionally opt into co-publish
28
+ (`per_file_alongside_bundle? → true`, spec 36 §3): the staged members
29
+ are served as standalone assets beside the bundle, the shard carries
30
+ the `per_file_assets` witness (runtime-manifest MINOR 2), the facet
31
+ signature declarations return, and the audit expects the union.
32
+ - **`TebakoRelease::Blksum`** — the spec 39 §3 block-group digest sidecar
33
+ (`<image>.blksum.json`: one sha256 per 4 MiB group of env-image bytes,
34
+ the lazy mount's range-GET trust anchor). The uploader derives it
35
+ in-process from the staged image at entry-build time — a pure function
36
+ of the served bytes, the same class as the `.sha256` sidecars — pins it
37
+ in the shard (`image.blksum {filename, sha256}`), and uploads it as a
38
+ standalone asset in both publish eras. The render is byte-exact with
39
+ `tpkg::lazy::Blksum::render`, golden-pinned in `spec/blksum_spec.rb`.
27
40
  - **`TebakoRelease::Platform`** — the release-side platform vocabulary
28
41
  (the `(os, arch) → host_id` table; mirrors `tpkg::Platform`, drift fails
29
42
  loudly in factory CI).
@@ -55,5 +55,18 @@ module TebakoRelease
55
55
  def bundle_publish?
56
56
  false
57
57
  end
58
+
59
+ # Spec 36 §3's co-publish: when true (and bundle_publish? holds), each
60
+ # leg ALSO serves the per-file assets — the exe under its historical
61
+ # spelling, the env image, the windows DLL — as standalone release
62
+ # assets beside the bundle, each with its .sha256 sidecar. The lazy
63
+ # arm (spec 39 §7) range-fetches the image's groups over HTTP, which
64
+ # is impossible inside the gzip stream; the shard gains the additive
65
+ # per_file_assets witness (runtime-manifest MINOR 2) that gates the
66
+ # loaders' lazy arm on bundle-declaring shards. Default off: a
67
+ # factory opts in when its consumers include the lazy arm.
68
+ def per_file_alongside_bundle?
69
+ false
70
+ end
58
71
  end
59
72
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2026 [Ribose Inc](https://www.ribose.com).
4
+ # All rights reserved.
5
+ # This file is a part of tamatebako
6
+
7
+ require "digest"
8
+ require "json"
9
+ require "pathname"
10
+
11
+ module TebakoRelease
12
+ # The spec 39 §3 block-group digest sidecar (`<image>.blksum.json`): one
13
+ # sha256 per 4 MiB group of image bytes plus the whole-image sha256,
14
+ # authored by the PUBLISHER in-process (a resolver never derives one).
15
+ # For the factories the publisher invocation is `tebako-release upload`,
16
+ # so the derivation lives here — a pure function of the staged image
17
+ # bytes, the same derivable-metadata class as the `.sha256` sidecars.
18
+ #
19
+ # The render is byte-exact with tpkg::lazy::Blksum::render (the product's
20
+ # single owner of the grammar): compact machine JSON, keys in schema
21
+ # order, lowercase hex, no trailing newline. spec/blksum_spec.rb pins
22
+ # tpkg-minted golden vectors against this render — a drift fails the
23
+ # suite, never ships.
24
+ class Blksum
25
+ # The locked fetch unit (spec 39 §11.3): 4 MiB of image bytes; the
26
+ # final group is short. Tunable only by a measured product follow-up.
27
+ GROUP_SIZE = 4 * 1024 * 1024
28
+
29
+ # `schema_version` of the v1 document.
30
+ SCHEMA_VERSION = 1
31
+
32
+ # The served asset name is the image's name plus this suffix.
33
+ SIDECAR_SUFFIX = ".blksum.json"
34
+
35
+ # The sidecar asset name for an image name (or an image path).
36
+ def self.sidecar_name(image)
37
+ "#{Pathname.new(image).basename}#{SIDECAR_SUFFIX}"
38
+ end
39
+
40
+ def self.blksum_file?(path)
41
+ path.basename.to_s.end_with?(SIDECAR_SUFFIX)
42
+ end
43
+
44
+ # Derive the sidecar of a staged image: streams the file in GROUP_SIZE
45
+ # chunks (the runtime env images are 50–200 MB — never held whole),
46
+ # one sha256 per group plus the whole-image sha256. An absent or empty
47
+ # image is a named refusal (spec 39 §3: size_bytes >= 1).
48
+ def self.for_image(image_path)
49
+ path = Pathname.new(image_path)
50
+ assert_derivable!(path)
51
+ whole, groups = hash_stream(path)
52
+ new(size_bytes: path.size, sha256: whole.hexdigest, groups: groups)
53
+ end
54
+
55
+ def self.assert_derivable!(path)
56
+ raise Error, "the blksum sidecar cannot be derived: #{path} does not exist" unless path.file?
57
+ raise Error, "the blksum sidecar cannot be derived: #{path} is empty (size_bytes >= 1)" if path.empty?
58
+ end
59
+
60
+ def self.hash_stream(path)
61
+ whole = Digest::SHA256.new
62
+ groups = []
63
+ path.open("rb") do |io|
64
+ while (chunk = io.read(GROUP_SIZE))
65
+ whole.update(chunk)
66
+ groups << Digest::SHA256.hexdigest(chunk)
67
+ end
68
+ end
69
+ [whole, groups]
70
+ end
71
+
72
+ attr_reader :size_bytes, :sha256, :groups
73
+
74
+ def initialize(size_bytes:, sha256:, groups:)
75
+ @size_bytes = size_bytes
76
+ @sha256 = sha256
77
+ @groups = groups
78
+ end
79
+
80
+ # The machine JSON form (keys in schema order) — byte-exact with
81
+ # tpkg::lazy::Blksum::render, golden-pinned in spec/blksum_spec.rb.
82
+ def render
83
+ JSON.generate(
84
+ schema_version: SCHEMA_VERSION,
85
+ group_size: GROUP_SIZE,
86
+ size_bytes: size_bytes,
87
+ sha256: sha256,
88
+ groups: groups
89
+ )
90
+ end
91
+
92
+ # The sidecar document's own sha256 (the manifest's
93
+ # `image.blksum.sha256` pin — spec 39 §3).
94
+ def digest
95
+ Digest::SHA256.hexdigest(render)
96
+ end
97
+ end
98
+ end
@@ -437,12 +437,13 @@ module TebakoRelease
437
437
  nil
438
438
  end
439
439
 
440
- def manifest_entry(package, image = nil, dll = nil, bundle: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
440
+ def manifest_entry(package, image = nil, dll = nil, bundle: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
441
441
  runtime_version, platform = parse_package_filename(package.basename.to_s)
442
442
  contract = contract_sidecar(package)
443
443
  filename = package.basename.to_s
444
444
  sha256 = Digest::SHA256.file(package).hexdigest
445
- register_current_shas(package, image, dll, bundle)
445
+ blksum = derive_blksum(image) if image
446
+ register_current_shas(package, image, dll, bundle, blksum)
446
447
  {
447
448
  tebako_version: @version,
448
449
  contract_era: contract.fetch("contract_era"),
@@ -466,9 +467,14 @@ module TebakoRelease
466
467
  # selector axis. Sourced from the factory's adapter (the same truth
467
468
  # its boot smoke asserts), so manifest and smoke can never disagree.
468
469
  entry[:capabilities] = @config.adapter.capabilities(version: runtime_version, platform_id: platform)
469
- entry[:image] = image_entry(image) if image
470
+ entry[:image] = image_entry(image, blksum) if image
470
471
  entry[:dll] = dll_entry(dll, runtime_version, platform) if dll
471
472
  entry[:bundle] = bundle_entry(bundle) if bundle
473
+ # Spec 36 §3's co-publish witness (runtime-manifest MINOR 2):
474
+ # true declares the per-file assets are served standalone beside
475
+ # the bundle — the lazy arm's gate on bundle-declaring shards
476
+ # (spec 39 §7). Never emitted on bundle-only lines.
477
+ entry[:per_file_assets] = true if bundle && copublish?
472
478
  declare_signatures(entry)
473
479
  end
474
480
  end
@@ -486,12 +492,36 @@ module TebakoRelease
486
492
  end
487
493
 
488
494
  # The idempotent upload skip reads these (same name + same sha = kept):
489
- # the exe, its facets, and the bundle (spec 36's bundle-era asset).
490
- def register_current_shas(package, image, dll, bundle) # rubocop:disable Metrics/AbcSize
495
+ # the exe, its facets, the derived blksum sidecar (spec 39 §3), and the
496
+ # bundle (spec 36's bundle-era asset).
497
+ def register_current_shas(package, image, dll, bundle, blksum = nil) # rubocop:disable Metrics/AbcSize
491
498
  current_shas[package.basename.to_s] = Digest::SHA256.file(package).hexdigest
492
499
  current_shas[image_name_for(package)] = Digest::SHA256.file(image).hexdigest if image
493
500
  current_shas[dll_name_for(package)] = Digest::SHA256.file(dll).hexdigest if dll
494
501
  current_shas[bundle.basename.to_s] = Digest::SHA256.file(bundle).hexdigest if bundle
502
+ current_shas[Blksum.sidecar_name(image)] = blksum.digest if blksum
503
+ end
504
+
505
+ # The spec 39 §3 derivation: the publisher authors the block-group
506
+ # digest sidecar in-process from the staged image bytes (a pure
507
+ # function of them — the same derivable-metadata class as the .sha256
508
+ # sidecars), stages it BESIDE the image so the leg's attestation glob
509
+ # (runtime-packages/*) covers it, and the entry's `image.blksum` block
510
+ # pins the document's own sha256. support_file? keeps a staged sidecar
511
+ # from classifying as a package on a re-run validation.
512
+ def derive_blksum(image)
513
+ blksum = Blksum.for_image(image)
514
+ path = Pathname.new("#{image}#{Blksum::SIDECAR_SUFFIX}")
515
+ path.write(blksum.render)
516
+ staged_blksums[path.basename.to_s] = path
517
+ blksum
518
+ end
519
+
520
+ # The derived sidecar paths this invocation staged, keyed by asset
521
+ # name — the upload leg's source (entries are built before uploads in
522
+ # every publish shape, so a declared sidecar is always staged).
523
+ def staged_blksums
524
+ @staged_blksums ||= {}
495
525
  end
496
526
 
497
527
  # Spec 13 §2a / spec 09 §5: on signing-enabled lines every artifact the
@@ -505,7 +535,13 @@ module TebakoRelease
505
535
  return unless signing_enabled?
506
536
 
507
537
  keyid = signing_keyid
508
- return declare_bundle_signature(entry, keyid) if entry[:bundle]
538
+ declare_bundle_signature(entry, keyid) if entry[:bundle]
539
+ # Bundle-only lines serve no other payload asset — declaring the
540
+ # members' .ascs there would be an invalid signing state (spec 09
541
+ # §4). A co-published line (spec 36 §3, the shard's per_file_assets
542
+ # witness) serves the members standalone, so the every-served-name
543
+ # rule covers the exe/image/dll set again.
544
+ return if entry[:bundle] && !entry[:per_file_assets]
509
545
 
510
546
  entry[:signature] = { keyid: keyid, asc: "#{entry[:filename]}.asc" }
511
547
  %i[image dll].each do |facet|
@@ -514,10 +550,10 @@ module TebakoRelease
514
550
  end
515
551
  end
516
552
 
517
- # Bundle-era (spec 36 §3): the only served payload is the bundle —
518
- # three signatures per leg (bundle, its sidecar, the shard). The
519
- # exe/image/dll member pins are NOT served assets; declaring their
520
- # .asc would be an invalid signing state (spec 09 §4).
553
+ # Bundle-era (spec 36 §3): the bundle's own signature declaration.
554
+ # On bundle-only lines it is the ONLY payload signature — three
555
+ # signatures per leg (bundle, its sidecar, the shard); co-published
556
+ # lines add the per-file declarations on top (declare_signatures).
521
557
  def declare_bundle_signature(entry, keyid)
522
558
  entry[:bundle][:signature] = { keyid: keyid, asc: "#{entry[:bundle][:filename]}.asc" }
523
559
  end
@@ -573,12 +609,19 @@ module TebakoRelease
573
609
  end
574
610
 
575
611
  # The additive image metadata: name, sha256, size (consumers ignoring the
576
- # `image` key keep working; item 30's compat rule).
577
- def image_entry(image)
612
+ # `image` key keep working; item 30's compat rule) — plus the additive
613
+ # `blksum` pin (spec 39 §3: {filename, sha256} of the derived block-group
614
+ # digest sidecar; consumers that predate the key ignore it, and the
615
+ # loader's lazy arm falls back to eager loudly when it is absent).
616
+ def image_entry(image, blksum)
578
617
  {
579
618
  filename: image.basename.to_s,
580
619
  sha256: Digest::SHA256.file(image).hexdigest,
581
- size_bytes: image.size
620
+ size_bytes: image.size,
621
+ blksum: {
622
+ filename: Blksum.sidecar_name(image),
623
+ sha256: blksum.digest
624
+ }
582
625
  }
583
626
  end
584
627
 
@@ -800,11 +843,17 @@ module TebakoRelease
800
843
  # feeds the entry's bundle block and the idempotent skip), upload the
801
844
  # bundles as the legs' only payload assets, then the metadata (shard
802
845
  # with the bundle block + the bundle's sidecar). The member files
803
- # themselves never become release assets — the bundle is the unit.
846
+ # themselves never become release assets — the bundle is the unit —
847
+ # UNLESS the factory co-publishes (spec 36 §3): then the members are
848
+ # standalone served names beside the bundle (the lazy arm
849
+ # range-fetches the image), still ahead of the shard that witnesses
850
+ # them (a witnessed shard without its assets is the invalid publish).
804
851
  def process_bundle_release(release, packages)
805
852
  bundles = build_bundles(packages)
806
853
  entries = build_manifest_entries(packages, bundles: bundles)
807
854
  bundles.each_value { |bundle| upload_package(release, bundle) }
855
+ packages.each { |package| upload_package(release, package) } if copublish?
856
+ upload_blksum_sidecars(release, entries)
808
857
  entries.each { |entry| ensure_package_metadata(release, entry) }
809
858
  print_settled_summary
810
859
  end
@@ -844,6 +893,14 @@ module TebakoRelease
844
893
  @config.adapter.bundle_publish?
845
894
  end
846
895
 
896
+ # Spec 36 §3's co-publish mode: a bundle-era line that ALSO serves
897
+ # the per-file assets as standalone release assets (the lazy arm's
898
+ # serving requirement — spec 39 §7). Factory-declared through the
899
+ # adapter, like the era.
900
+ def copublish?
901
+ bundle_publish? && @config.adapter.per_file_alongside_bundle?
902
+ end
903
+
847
904
  # AUDIT_ONLY (the coordinator's release job / a publish dry run):
848
905
  # strictly read-only — finds the release (never creates one), needs no
849
906
  # local packages, uploads nothing, touches no notes; the release's
@@ -867,10 +924,32 @@ module TebakoRelease
867
924
  # the release notes are written once at creation.
868
925
  def publish_release(release, packages, entries)
869
926
  packages.each { |package| upload_package(release, package) }
927
+ upload_blksum_sidecars(release, entries)
870
928
  entries.each { |entry| ensure_package_metadata(release, entry) }
871
929
  print_settled_summary
872
930
  end
873
931
 
932
+ # The spec 39 §3 sidecars ride along in BOTH publish eras as STANDALONE
933
+ # assets — never bundle members: the lazy resolver fetches the sidecar
934
+ # with a plain GET against its own release-asset URL
935
+ # (`{dir_url}/{image.blksum.filename}`), in the bundle era too. Derived
936
+ # at entry-build time and staged beside the images; uploaded before the
937
+ # metadata so the shard that pins them never precedes them.
938
+ def upload_blksum_sidecars(release, entries)
939
+ entries.each do |entry|
940
+ name = entry.dig(:image, :blksum, :filename)
941
+ next unless name
942
+
943
+ path = staged_blksums[name]
944
+ unless path
945
+ raise Error, "the blksum sidecar #{name} was declared but never staged — " \
946
+ "the derivation runs at entry-build time; this is a machinery bug"
947
+ end
948
+
949
+ upload_package(release, path)
950
+ end
951
+ end
952
+
874
953
  # The package's metadata assets, byte-truthful for the SERVED bytes: a
875
954
  # settled package (the byte-immutable keep, or a wedged recovery
876
955
  # replace) publishes the PREVIOUS entry's shard and sidecars — the
@@ -888,10 +967,22 @@ module TebakoRelease
888
967
  # era: the exe, its .tfs image and its .dll facet. Bundle era (spec 36
889
968
  # §3): the bundle is the ONE served payload asset — the entry's
890
969
  # exe/image/dll fields are member pins, not served names, so they earn
891
- # no sidecar.
970
+ # no sidecar — UNLESS the entry carries the co-publish witness, when
971
+ # the members ARE served standalone beside the bundle and earn theirs.
972
+ # Both eras: the derived blksum sidecar (spec 39 §3) IS a served
973
+ # standalone asset, so it earns its own .sha256 sidecar like
974
+ # every served name.
892
975
  def metadata_assets(entry)
893
- return { entry[:bundle][:filename] => entry[:bundle][:sha256] } if entry[:bundle]
976
+ served = bundle_metadata(entry)
977
+ served.merge!(per_file_metadata(entry)) unless entry[:bundle] && !entry[:per_file_assets]
978
+ served.merge(blksum_metadata(entry))
979
+ end
980
+
981
+ def bundle_metadata(entry)
982
+ entry[:bundle] ? { entry[:bundle][:filename] => entry[:bundle][:sha256] } : {}
983
+ end
894
984
 
985
+ def per_file_metadata(entry)
895
986
  { entry[:filename] => entry[:sha256] }
896
987
  .merge(facet_metadata(entry, :image))
897
988
  .merge(facet_metadata(entry, :dll))
@@ -902,6 +993,14 @@ module TebakoRelease
902
993
  block ? { block[:filename] => block[:sha256] } : {}
903
994
  end
904
995
 
996
+ # The blksum sidecar's name -> sha256 pair when the entry declares it
997
+ # (spec 39 §3's `image.blksum` pin); a pre-blksum previous entry
998
+ # declares nothing and earns nothing (byte-truthful metadata).
999
+ def blksum_metadata(entry)
1000
+ block = entry.dig(:image, :blksum)
1001
+ block ? { block[:filename] => block[:sha256] } : {}
1002
+ end
1003
+
905
1004
  def shard_name_for(entry)
906
1005
  "#{package_stem(entry[:filename])}#{SHARD_SUFFIX}"
907
1006
  end
@@ -910,7 +1009,8 @@ module TebakoRelease
910
1009
  # with the previous entry's voice; a never-published settle has nothing
911
1010
  # truthful to keep and never reaches here (upload_package re-raised).
912
1011
  def effective_entry(entry)
913
- names = [entry[:filename], entry.dig(:image, :filename), entry.dig(:dll, :filename)].compact
1012
+ names = [entry[:filename], entry.dig(:image, :filename), entry.dig(:dll, :filename),
1013
+ entry.dig(:image, :blksum, :filename)].compact
914
1014
  return entry unless names.any? { |name| settled?(name) }
915
1015
 
916
1016
  previous_entry_for(entry[:filename]) || entry
@@ -1095,7 +1195,9 @@ module TebakoRelease
1095
1195
 
1096
1196
  def missing_assets(release, packages, require_signatures: false)
1097
1197
  present = all_assets(release).map(&:name)
1098
- packages.flat_map { |name| missing_package_assets(present, name, require_signatures: require_signatures) }
1198
+ packages.flat_map do |name|
1199
+ missing_package_assets(release, present, name, require_signatures: require_signatures)
1200
+ end
1099
1201
  end
1100
1202
 
1101
1203
  # One expected package's gaps. Windows executables may or may not carry
@@ -1103,11 +1205,16 @@ module TebakoRelease
1103
1205
  # expectation matches both. Metadata expectations ride on what actually
1104
1206
  # landed — a package whose exe never landed reports its own name only,
1105
1207
  # never a cascade of secondary sidecar/shard gaps (one error per gap).
1106
- def missing_package_assets(present, name, require_signatures: false)
1107
- return missing_bundle_assets(present, name, require_signatures: require_signatures) if bundle_publish?
1208
+ # The blksum sidecar (spec 39 §3) is expected ONLY when the package's
1209
+ # shard declares the `image.blksum` pin — a pre-blksum release's audit
1210
+ # stays green (the additive-key compat rule), and a missing shard
1211
+ # reports its own gap only.
1212
+ def missing_package_assets(release, present, name, require_signatures: false)
1213
+ return missing_bundle_assets(release, present, name, require_signatures: require_signatures) if bundle_publish?
1108
1214
 
1109
1215
  exe = [name, "#{name}.exe"].find { |candidate| present.include?(candidate) }
1110
1216
  landed, missing = expected_facets(name).partition { |facet| present.include?(facet) }
1217
+ landed, missing = split_blksum_expectation(served_shard(release, present, name), present, landed, missing)
1111
1218
  missing.unshift(name) unless exe
1112
1219
  return missing if exe.nil?
1113
1220
 
@@ -1116,12 +1223,72 @@ module TebakoRelease
1116
1223
 
1117
1224
  # Spec 36's completeness shape: the bundle is the leg's ONE payload
1118
1225
  # asset; a landed bundle owes its sidecar and the package's shard
1119
- # (plus an .asc of each on signing-enabled audits).
1120
- def missing_bundle_assets(present, name, require_signatures: false)
1226
+ # (plus an .asc of each on signing-enabled audits). The declared
1227
+ # blksum sidecar (spec 39 §3) is a standalone served asset in this
1228
+ # era too — expected, with its own metadata, when the shard pins it —
1229
+ # and a shard carrying the co-publish witness (spec 36 §3's
1230
+ # per_file_assets) owes the standalone per-file assets beside the
1231
+ # bundle.
1232
+ def missing_bundle_assets(release, present, name, require_signatures: false)
1121
1233
  bundle = "#{name}#{Bundler::BUNDLE_SUFFIX}"
1122
1234
  return [bundle] unless present.include?(bundle)
1123
1235
 
1124
- missing_metadata(present, name, [bundle], require_signatures: require_signatures)
1236
+ shard = served_shard(release, present, name)
1237
+ landed, missing = split_blksum_expectation(shard, present, [bundle], [])
1238
+ landed, missing = split_copublish_expectation(shard, present, name, landed, missing)
1239
+ missing + missing_metadata(present, name, landed, require_signatures: require_signatures)
1240
+ end
1241
+
1242
+ # The shard-declared blksum expectation (spec 39 §3): the shard is the
1243
+ # declaration's only authority (the additive `image.blksum` key), so
1244
+ # the audit reads it. A shard that declares no pin expects nothing (a
1245
+ # pre-blksum release audits green); a shard that never landed already
1246
+ # reports its own gap, so it adds no secondary blksum gaps; an
1247
+ # unreadable shard warns and expects nothing (never a silent failure —
1248
+ # the shard's own gap is the gate's business). A DECLARED sidecar that
1249
+ # never landed reports its own name only — its sidecar/.asc cascade
1250
+ # rides the landed set, one error per gap.
1251
+ def split_blksum_expectation(shard, present, landed, missing)
1252
+ declared = shard&.dig("image", "blksum", "filename")
1253
+ return [landed, missing] unless declared
1254
+
1255
+ if present.include?(declared)
1256
+ [landed + [declared], missing]
1257
+ else
1258
+ [landed, missing + [declared]]
1259
+ end
1260
+ end
1261
+
1262
+ # The shard-witnessed co-publish expectation (spec 36 §3): a shard
1263
+ # carrying per_file_assets: true owes the standalone per-file assets —
1264
+ # the exe (either spelling), the image, the windows DLL — beside the
1265
+ # bundle; each landed member earns its metadata through the landed
1266
+ # set. An unwitnessed shard expects nothing (bundle-only lines audit
1267
+ # green), and a missing exe reports its own gap only, never a
1268
+ # cascade (the one-error-per-gap rule).
1269
+ def split_copublish_expectation(shard, present, name, landed, missing)
1270
+ return [landed, missing] unless shard && shard["per_file_assets"] == true
1271
+
1272
+ exe = [name, "#{name}.exe"].find { |candidate| present.include?(candidate) }
1273
+ facets_landed, facets_missing = expected_facets(name).partition { |facet| present.include?(facet) }
1274
+ gaps = facets_missing
1275
+ gaps = [name] + gaps unless exe
1276
+ [landed + [exe, *facets_landed].compact, missing + gaps]
1277
+ end
1278
+
1279
+ # The package's served shard as parsed JSON — the declarations' only
1280
+ # authority (the additive keys gate the audit's expectations). nil
1281
+ # when the shard never landed (its own gap is the gate's business) or
1282
+ # cannot be read (loudly — the audit never guesses an expectation).
1283
+ def served_shard(release, present, name)
1284
+ shard = "#{name}#{SHARD_SUFFIX}"
1285
+ return nil unless present.include?(shard)
1286
+
1287
+ download_asset_json(find_asset(release, shard))
1288
+ rescue StandardError => e
1289
+ puts "::warning::could not read #{shard} for the additive expectations (#{e.class}: #{e.message}) — " \
1290
+ "no additive assets are expected of this package"
1291
+ nil
1125
1292
  end
1126
1293
 
1127
1294
  # The non-executable artifacts a package is expected to carry: the
@@ -1448,12 +1615,13 @@ module TebakoRelease
1448
1615
  end
1449
1616
 
1450
1617
  # The package stem an asset name belongs to: the exe (with or without
1451
- # .exe), its .tfs image, its .dll facet and its .tar.gz bundle (spec 36)
1452
- # share one stem — settling is package-scoped so a wedged exe also
1453
- # stands down its facets (a fresh facet over previous package bytes
1454
- # would be a mixed-version package).
1618
+ # .exe), its .tfs image, its .dll facet, the derived blksum sidecar
1619
+ # (spec 39 §3) and its .tar.gz bundle (spec 36) share one stem —
1620
+ # settling is package-scoped so a wedged exe also stands down its
1621
+ # facets (a fresh facet over previous package bytes would be a
1622
+ # mixed-version package). Longest suffix first.
1455
1623
  def package_stem(filename)
1456
- filename.sub(/\.tar\.gz\z/, "").sub(/\.(exe|tfs|dll)\z/, "")
1624
+ filename.sub(/\.tar\.gz\z/, "").sub(/\.tfs\.blksum\.json\z/, "").sub(/\.(exe|tfs|dll)\z/, "")
1457
1625
  end
1458
1626
 
1459
1627
  def settled?(filename)
@@ -1586,18 +1754,13 @@ module TebakoRelease
1586
1754
 
1587
1755
  # The sha256 the previous manifest records for this asset: the entry's
1588
1756
  # own sha for the executable, the image/dll facet block's sha for a
1589
- # facet (facets key under their package's entry), the bundle block's
1590
- # sha for the bundle (spec 36 — the bundle-era served asset).
1757
+ # facet (facets key under their package's entry), the image's blksum
1758
+ # pin for the derived sidecar (spec 39 §3), the bundle block's sha for
1759
+ # the bundle (spec 36 — the bundle-era served asset).
1591
1760
  def previous_sha_for(entry, filename)
1592
- if entry.dig(:image, :filename) == filename
1593
- entry.dig(:image, :sha256)
1594
- elsif entry.dig(:dll, :filename) == filename
1595
- entry.dig(:dll, :sha256)
1596
- elsif entry.dig(:bundle, :filename) == filename
1597
- entry.dig(:bundle, :sha256)
1598
- else
1599
- entry[:sha256]
1600
- end
1761
+ blocks = [entry.dig(:image, :blksum), entry[:image], entry[:dll], entry[:bundle]]
1762
+ block = blocks.find { |candidate| candidate && candidate[:filename] == filename }
1763
+ block ? block[:sha256] : entry[:sha256]
1601
1764
  end
1602
1765
 
1603
1766
  def validate_environment
@@ -1624,13 +1787,16 @@ module TebakoRelease
1624
1787
  end
1625
1788
 
1626
1789
  # `.abi` sidecars (the runtime's platform string), `.contract.yaml`
1627
- # sidecars (the era-2 release card provenance) and `.sha256` markers
1790
+ # sidecars (the era-2 release card provenance), `.sha256` markers
1628
1791
  # (the image's store-layout trust anchor, spec 22 §6 — the boot
1629
- # smoke's image-key input) are manifest inputs / build outputs read
1630
- # in place — never packages of their own.
1792
+ # smoke's image-key input) and `.blksum.json` sidecars (the spec 39 §3
1793
+ # derivation staged beside its image at entry-build time — still in
1794
+ # the directory when a re-run validates) are manifest inputs / build
1795
+ # outputs read in place — never packages of their own.
1631
1796
  def support_file?(path)
1632
1797
  path.extname == ".abi" || path.extname == ".sha256" ||
1633
- path.basename.to_s.end_with?(CONTRACT_SIDECAR_SUFFIX)
1798
+ path.basename.to_s.end_with?(CONTRACT_SIDECAR_SUFFIX) ||
1799
+ Blksum.blksum_file?(path)
1634
1800
  end
1635
1801
  end
1636
1802
  end
@@ -5,5 +5,5 @@
5
5
  # This file is a part of tamatebako
6
6
 
7
7
  module TebakoRelease
8
- VERSION = "0.2.1"
8
+ VERSION = "0.4.0"
9
9
  end
@@ -10,6 +10,7 @@ require_relative "tebako_release/adapter"
10
10
  require_relative "tebako_release/config"
11
11
  require_relative "tebako_release/platform"
12
12
  require_relative "tebako_release/bundler"
13
+ require_relative "tebako_release/blksum"
13
14
  require_relative "tebako_release/uploader"
14
15
  require_relative "tebako_release/signer"
15
16
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tebako-release
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc
@@ -39,6 +39,7 @@ files:
39
39
  - exe/tebako-release
40
40
  - lib/tebako_release.rb
41
41
  - lib/tebako_release/adapter.rb
42
+ - lib/tebako_release/blksum.rb
42
43
  - lib/tebako_release/bundler.rb
43
44
  - lib/tebako_release/config.rb
44
45
  - lib/tebako_release/error.rb