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 +4 -4
- data/lib/tebako_release/signer.rb +17 -5
- data/lib/tebako_release/uploader.rb +205 -4
- data/lib/tebako_release/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 704f713348ac797af821eb4ad5a18d8a4e0df36e2cdba2367539322a766a9c73
|
|
4
|
+
data.tar.gz: 34a74c0de836562a84ff77f9f287025b01e8a8ca1049bd0ed1e123b3e31e37da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
81
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|