tebako-release 0.4.0 → 0.4.2

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: 6e91116fb240ffb5e0cb7cec20a1753fbd4177b7bd8f3c78adb8c3e260acf718
4
- data.tar.gz: c5c886163f60c1ef021829015d95a1f3c50b6fc239a22a39cb890324041353d1
3
+ metadata.gz: 704f713348ac797af821eb4ad5a18d8a4e0df36e2cdba2367539322a766a9c73
4
+ data.tar.gz: 34a74c0de836562a84ff77f9f287025b01e8a8ca1049bd0ed1e123b3e31e37da
5
5
  SHA512:
6
- metadata.gz: 78d61711350a83bc076b26472fab4b468e68368ca6f16f9f4c7bb613149d0a7c9be68821e6646e7469958e24424f59b3b88afd3d45c7b1f01db0b806550d9c69
7
- data.tar.gz: 44d1ee55d6bf22f2aa8d6dfdacaa27f26a77170faa2d26a45669425879d6b5ece851d0d5e1072855c6f9fa6a7b2269578f1b112a0d62b34fecf2510bd82fa6a8
6
+ metadata.gz: faf9b0a2eadb2e3555676e9430d94f029429e39da686c96065eb0b44ed226a63dc9eed22fc0018278cff0c0b9c38eab67543401a8047b16c68f259c6aede7d69
7
+ data.tar.gz: 1f5347adb990a2165ea736575d7f98374cd2fcdf3f339838abe9004bf1826bb5918946b4e06b3d661b8a32e986299645a7d0d8276464c54d88fb4dcef9bc2d2f
@@ -76,9 +76,20 @@ module TebakoRelease
76
76
  # elsewhere (openjdk's out/<flavor>-<triplet>/) at this run's dir.
77
77
  LOCAL_PACKAGES_DIR = "runtime-packages"
78
78
 
79
- # upload convergence: a tiny metadata asset either lands or cycles;
80
- # three bounded polls then a named failure.
81
- CONVERGENCE_DELAYS = [5, 15, 30].freeze
79
+ # upload convergence: a tiny metadata asset either lands or cycles.
80
+ # Campaign-scale concurrency — dozens of sign legs replacing .asc
81
+ # assets on one release object — makes the 422 delete-propagation
82
+ # race routine, so the budget is ten jittered cycles over ~4–5
83
+ # minutes (tebako-release#15); the jitter keeps concurrently failing
84
+ # legs from re-colliding on the same propagation windows in lockstep.
85
+ CONVERGENCE_DELAYS = [5, 10, 15, 20, 30, 30, 30, 30, 40, 40].freeze
86
+ CONVERGENCE_JITTER = 0.4
87
+
88
+ # A convergence pause scaled by 1 ± CONVERGENCE_JITTER. A zero pause
89
+ # stays zero, so spec-stubbed delay lists keep their determinism.
90
+ def self.jittered(pause)
91
+ pause * (1 + (((rand * 2) - 1) * CONVERGENCE_JITTER))
92
+ end
82
93
 
83
94
  # served-bytes convergence: a young release object lists an asset before
84
95
  # the byte store serves it (the runtime-ruby 0.16.28 republish's
@@ -310,8 +321,9 @@ module TebakoRelease
310
321
  converged = asc_converged?(release, asc_file, sha)
311
322
  break if converged
312
323
 
313
- puts "#{asc_file.basename} has not converged on the release yet; cycling in #{pause}s"
314
- sleep pause
324
+ wait = Signer.jittered(pause)
325
+ puts "#{asc_file.basename} has not converged on the release yet; cycling in #{wait.round}s"
326
+ sleep wait
315
327
  end
316
328
  raise SigningGateError, "NAMED FAILURE: #{asc_file.basename} did not converge on #{@tag}" unless converged
317
329
  end
@@ -29,6 +29,7 @@ require "octokit"
29
29
  require "digest"
30
30
  require "json"
31
31
  require "pathname"
32
+ require "stringio"
32
33
  require "tmpdir"
33
34
  require "yaml"
34
35
 
@@ -956,7 +957,7 @@ module TebakoRelease
956
957
  # metadata describes what the release serves, never the fresh bytes
957
958
  # that did not land.
958
959
  def ensure_package_metadata(release, entry)
959
- effective = effective_entry(entry)
960
+ effective = effective_entry(release, entry)
960
961
  ensure_metadata_asset(release, shard_name_for(effective), "#{JSON.pretty_generate(effective)}\n")
961
962
  metadata_assets(effective).each do |name, sha|
962
963
  ensure_metadata_asset(release, "#{name}#{SIDECAR_SUFFIX}", "#{sha} #{name}\n")
@@ -1008,12 +1009,57 @@ module TebakoRelease
1008
1009
  # A settled package (any of its assets kept their previous bytes) speaks
1009
1010
  # with the previous entry's voice; a never-published settle has nothing
1010
1011
  # truthful to keep and never reaches here (upload_package re-raised).
1011
- def effective_entry(entry)
1012
+ # The co-publish adoption exception (spec 36 §3): the previous bundle-era
1013
+ # entry predates the witness, and this run repaired the standalone
1014
+ # members out of the settled bundle — the entry keeps the previous pins
1015
+ # (they pin exactly those bytes) and gains the witness, the served
1016
+ # blksum pin, and the members' signature declarations, each only when
1017
+ # the asset it names verifiably landed.
1018
+ def effective_entry(release, entry)
1012
1019
  names = [entry[:filename], entry.dig(:image, :filename), entry.dig(:dll, :filename),
1013
1020
  entry.dig(:image, :blksum, :filename)].compact
1014
1021
  return entry unless names.any? { |name| settled?(name) }
1015
1022
 
1016
- previous_entry_for(entry[:filename]) || entry
1023
+ previous = previous_entry_for(entry[:filename]) || entry
1024
+ return previous unless entry[:per_file_assets] && !previous[:per_file_assets]
1025
+ return previous unless copublish_members_landed?(release, previous)
1026
+
1027
+ witnessed_repair_entry(release, previous, entry)
1028
+ end
1029
+
1030
+ # The witness gate: the standalone members the previous entry pins are
1031
+ # all on the release (landed this run through the repair, or served all
1032
+ # along). A witness without its assets is the invalid publish.
1033
+ def copublish_members_landed?(release, entry)
1034
+ names = [entry[:filename], entry.dig(:image, :filename), entry.dig(:dll, :filename)].compact
1035
+ names.all? { |name| find_asset(release, name) }
1036
+ end
1037
+
1038
+ # The adoption shard: the previous entry verbatim (its pins describe
1039
+ # the served bytes exactly) plus the co-publish witness, the blksum pin
1040
+ # read off the landed sidecar's listing digest (byte-truthful by
1041
+ # construction — never the fresh build's derivation), and the members'
1042
+ # signature declarations on signing-enabled lines (spec 09 §5's
1043
+ # every-served-name rule; the in-leg sign pass fulfills them).
1044
+ def witnessed_repair_entry(release, previous, fresh)
1045
+ repaired = Marshal.load(Marshal.dump(previous))
1046
+ repaired[:per_file_assets] = true
1047
+ blksum_name = fresh.dig(:image, :blksum, :filename)
1048
+ blksum_sha = blksum_name && listed_digest(release, blksum_name)
1049
+ repaired[:image][:blksum] = { filename: blksum_name, sha256: blksum_sha } if blksum_sha && repaired[:image]
1050
+ declare_repaired_member_signatures(repaired)
1051
+ repaired
1052
+ end
1053
+
1054
+ def declare_repaired_member_signatures(entry)
1055
+ return unless signing_enabled?
1056
+
1057
+ keyid = signing_keyid
1058
+ entry[:signature] = { keyid: keyid, asc: "#{entry[:filename]}.asc" }
1059
+ %i[image dll].each do |facet|
1060
+ block = entry[facet]
1061
+ block[:signature] = { keyid: keyid, asc: "#{block[:filename]}.asc" } if block
1062
+ end
1017
1063
  end
1018
1064
 
1019
1065
  # One metadata asset, converged: already-current bytes (the listing's
@@ -1551,10 +1597,16 @@ module TebakoRelease
1551
1597
  exit 1
1552
1598
  end
1553
1599
 
1554
- def upload_package(release, package) # rubocop:disable Metrics/MethodLength
1600
+ def upload_package(release, package) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
1555
1601
  filename = package.basename.to_s
1556
1602
  puts "Processing #{filename}..."
1557
1603
  if settled?(filename)
1604
+ # A settled stem stands its facets down ONLY when they are on the
1605
+ # release — a facet that never landed has nothing to keep, and on a
1606
+ # co-publish line the byte-truthful source is the settled bundle
1607
+ # itself (its members ARE the previous entry's pins).
1608
+ return repair_settled_absent_asset(release, filename) if copublish? && !find_asset(release, filename)
1609
+
1558
1610
  puts "#{filename} kept its previous bytes earlier in this publish run — " \
1559
1611
  "the settled asset is never re-attempted; the refresh lands on a republish onto a recreated release object"
1560
1612
  return nil
@@ -1589,6 +1641,155 @@ module TebakoRelease
1589
1641
  nil
1590
1642
  end
1591
1643
 
1644
+ # The settled-stem co-publish repair (spec 36 §3 adoption on a
1645
+ # bundle-era release): the stem settled (the byte-immutable keep — the
1646
+ # build is not bit-reproducible), so the package speaks with the
1647
+ # previous entry's voice — but the previous bundle-era publish never
1648
+ # served the standalone members. The settled bundle on the release
1649
+ # holds exactly the bytes the previous entry pins (that is what the
1650
+ # bundle IS), so the members are served from it: extract, verify
1651
+ # against the pin, upload. A member absent with nothing truthful to
1652
+ # repair from (no previous entry, no pin, no bundle asset) is a named
1653
+ # error — never a silent skip, never a lying upload.
1654
+ def repair_settled_absent_asset(release, filename)
1655
+ if Blksum.blksum_file?(Pathname.new(filename))
1656
+ repair_settled_blksum(release, filename)
1657
+ else
1658
+ repair_settled_member(release, filename)
1659
+ end
1660
+ end
1661
+
1662
+ def repair_settled_member(release, filename)
1663
+ entry = previous_entry_for_stem(filename)
1664
+ bundle_asset = repair_bundle_asset(release, entry, filename)
1665
+ member = verified_bundle_member(release, bundle_asset, entry, filename)
1666
+ puts "Repairing #{filename} from the settled bundle #{bundle_asset.name} " \
1667
+ "(the co-publish adoption serves the bundle's own members standalone — byte-identical with the pin)"
1668
+ perform_upload(release, member, filename)
1669
+ filename
1670
+ end
1671
+
1672
+ # The settled bundle asset the previous entry pins this member over, or
1673
+ # a named error: an absent member with nothing truthful to repair from
1674
+ # (no previous entry, no pin, no bundle asset) is never silently
1675
+ # skipped and never freshly uploaded over a settled voice.
1676
+ def repair_bundle_asset(release, entry, filename)
1677
+ pin = entry && member_pin_for(entry, filename)
1678
+ bundle_asset = entry && find_asset(release, entry&.dig(:bundle, :filename))
1679
+ return bundle_asset if pin && bundle_asset
1680
+
1681
+ raise Error, "#{filename} never landed on the release and its settled package has nothing truthful " \
1682
+ "to repair from (no previous entry pinning it over a served bundle) — " \
1683
+ "the refresh lands on a republish onto a recreated release object"
1684
+ end
1685
+
1686
+ # The member's extraction, verified against the previous entry's pin —
1687
+ # the bundle's contents and the shard must agree, or neither is served.
1688
+ def verified_bundle_member(release, bundle_asset, entry, filename)
1689
+ member = extracted_bundle_members(release, bundle_asset)[filename]
1690
+ actual = member && Digest::SHA256.file(member).hexdigest
1691
+ return member if member && actual == member_pin_for(entry, filename)
1692
+
1693
+ raise Error, "#{filename} extracted from the settled bundle #{bundle_asset.name} does not hash to the " \
1694
+ "package's published pin (#{actual || "not a member"}) — the bundle and the shard " \
1695
+ "disagree; refusing to serve either reading (byte-truthful, never a guess)"
1696
+ end
1697
+
1698
+ # The derived blksum sidecar of a settled package derives from the
1699
+ # SERVED image bytes — never the fresh build's (they did not land).
1700
+ # The served bytes are the just-repaired extraction when this run
1701
+ # landed the image, else the release's own image asset.
1702
+ def repair_settled_blksum(release, filename)
1703
+ image_name = filename.delete_suffix(Blksum::SIDECAR_SUFFIX)
1704
+ image = served_image_path(release, image_name)
1705
+ blksum = Blksum.for_image(image)
1706
+ sidecar = Pathname.new(Dir.mktmpdir).join(filename).tap { |path| path.write(blksum.render) }
1707
+ puts "Repairing #{filename} derived from the served image #{image_name} (spec 39 §3)"
1708
+ perform_upload(release, sidecar, filename)
1709
+ filename
1710
+ end
1711
+
1712
+ def served_image_path(release, image_name)
1713
+ extracted = repaired_image_extraction(release, image_name)
1714
+ return extracted if extracted
1715
+
1716
+ asset = find_asset(release, image_name)
1717
+ unless asset
1718
+ raise Error, "the served image #{image_name} is not on the release — cannot derive its blksum sidecar"
1719
+ end
1720
+
1721
+ bytes = with_transient_retries { @client.get(asset.browser_download_url) }.to_s
1722
+ Pathname.new(Dir.mktmpdir).join(image_name).tap { |path| path.binwrite(bytes) }
1723
+ end
1724
+
1725
+ # The image's extraction when this run repaired it (the just-landed
1726
+ # bytes), nil when the image was never extracted here.
1727
+ def repaired_image_extraction(release, image_name)
1728
+ entry = previous_entry_for_stem(image_name)
1729
+ bundle_asset = entry && find_asset(release, entry&.dig(:bundle, :filename))
1730
+ bundle_asset && extracted_bundle_members(release, bundle_asset)[image_name]
1731
+ end
1732
+
1733
+ # The previous manifest entry of the package stem an asset name belongs
1734
+ # to (the exe, its facets, the derived sidecars, the bundle — one
1735
+ # stem). previous_entry_covering's metadata-assets walk does not serve
1736
+ # here: an UNWITNESSED bundle-era entry's metadata covers the bundle
1737
+ # alone, while the repair needs the member pins it still carries.
1738
+ def previous_entry_for_stem(filename)
1739
+ stem = package_stem(filename)
1740
+ previous_manifest_entries.find { |entry| package_stem(entry[:filename]) == stem }
1741
+ end
1742
+
1743
+ # The previous entry's pin for one member name: the exe at the top
1744
+ # level, the image/dll in their facet blocks. nil when the entry does
1745
+ # not pin the name (nothing truthful to verify an extraction against).
1746
+ def member_pin_for(entry, filename)
1747
+ return entry[:sha256] if entry[:filename] == filename
1748
+
1749
+ %i[image dll].each do |facet|
1750
+ block = entry[facet]
1751
+ return block[:sha256] if block && block[:filename] == filename
1752
+ end
1753
+ nil
1754
+ end
1755
+
1756
+ # The settled bundle's members, extracted once per bundle into a
1757
+ # process-lifetime scratch dir (the exe/image/dll repairs of one stem
1758
+ # share the one download). The bundle grammar is flat bare basenames
1759
+ # (Bundler writes them so); anything else is refused, never extracted.
1760
+ def extracted_bundle_members(release, bundle_asset)
1761
+ @extracted_bundles ||= {}
1762
+ @extracted_bundles[bundle_asset.name] ||= extract_bundle(release, bundle_asset)
1763
+ end
1764
+
1765
+ def extract_bundle(_release, bundle_asset)
1766
+ bytes = with_transient_retries { @client.get(bundle_asset.browser_download_url) }.to_s
1767
+ dir = Pathname.new(Dir.mktmpdir("tebako-release-bundle-"))
1768
+ Zlib::GzipReader.wrap(StringIO.new(bytes)) { |gzip| extract_tar_members(bundle_asset, gzip, dir) }
1769
+ end
1770
+
1771
+ def extract_tar_members(bundle_asset, gzip, dir)
1772
+ members = {}
1773
+ Gem::Package::TarReader.new(gzip) do |tar|
1774
+ tar.each do |tar_entry|
1775
+ next unless tar_entry.file?
1776
+
1777
+ name = flat_member_name(bundle_asset, tar_entry.full_name)
1778
+ members[name] = dir.join(name).tap { |path| path.binwrite(tar_entry.read) }
1779
+ end
1780
+ end
1781
+ members
1782
+ end
1783
+
1784
+ # The bundle grammar is flat bare basenames (Bundler writes them so);
1785
+ # anything else is refused, never extracted.
1786
+ def flat_member_name(bundle_asset, name)
1787
+ return name if name == File.basename(name)
1788
+
1789
+ raise Error, "bundle #{bundle_asset.name} carries the non-flat member #{name} — " \
1790
+ "the spec 36 §2 grammar is bare basenames only; refusing to extract"
1791
+ end
1792
+
1592
1793
  # The settled ledger — the durable half of warn-and-keep-previous. The
1593
1794
  # publish step runs the uploader once per platform (sequential
1594
1795
  # processes, one workspace); an asset settled in one invocation (the
@@ -5,5 +5,5 @@
5
5
  # This file is a part of tamatebako
6
6
 
7
7
  module TebakoRelease
8
- VERSION = "0.4.0"
8
+ VERSION = "0.4.2"
9
9
  end
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.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc