tebako-release 0.3.0 → 0.4.1
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/README.md +6 -1
- data/lib/tebako_release/adapter.rb +13 -0
- data/lib/tebako_release/uploader.rb +287 -32
- 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: 5382811aa28e5730bd9a513c18f61cd0619d6586ec0089de1822d8283313399f
|
|
4
|
+
data.tar.gz: da87f7c9358a2c1b69e38f0986b42ddbe73c29cd835814657e1f8ade793f402b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 451fa6d51c2392f4739c0236dd5d72dfce5788cd07dc9e240d72d61a59a47e265dbc0d009f3e7f1b7175d15c371096490b9ff852999b3aab1116e9eaf7732aa9
|
|
7
|
+
data.tar.gz: 5ba106db23aa707ec41abe0629d06e16a6dd7a61156495bff9dc6e3134f6cdcbab9724d19fc5996d746f864519c6cef91c543b6de16ebc14cb6762e64d026ff6
|
data/README.md
CHANGED
|
@@ -23,7 +23,12 @@ 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.
|
|
27
32
|
- **`TebakoRelease::Blksum`** — the spec 39 §3 block-group digest sidecar
|
|
28
33
|
(`<image>.blksum.json`: one sha256 per 4 MiB group of env-image bytes,
|
|
29
34
|
the lazy mount's range-GET trust anchor). The uploader derives it
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -437,7 +438,7 @@ module TebakoRelease
|
|
|
437
438
|
nil
|
|
438
439
|
end
|
|
439
440
|
|
|
440
|
-
def manifest_entry(package, image = nil, dll = nil, bundle: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
441
|
+
def manifest_entry(package, image = nil, dll = nil, bundle: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
441
442
|
runtime_version, platform = parse_package_filename(package.basename.to_s)
|
|
442
443
|
contract = contract_sidecar(package)
|
|
443
444
|
filename = package.basename.to_s
|
|
@@ -470,6 +471,11 @@ module TebakoRelease
|
|
|
470
471
|
entry[:image] = image_entry(image, blksum) if image
|
|
471
472
|
entry[:dll] = dll_entry(dll, runtime_version, platform) if dll
|
|
472
473
|
entry[:bundle] = bundle_entry(bundle) if bundle
|
|
474
|
+
# Spec 36 §3's co-publish witness (runtime-manifest MINOR 2):
|
|
475
|
+
# true declares the per-file assets are served standalone beside
|
|
476
|
+
# the bundle — the lazy arm's gate on bundle-declaring shards
|
|
477
|
+
# (spec 39 §7). Never emitted on bundle-only lines.
|
|
478
|
+
entry[:per_file_assets] = true if bundle && copublish?
|
|
473
479
|
declare_signatures(entry)
|
|
474
480
|
end
|
|
475
481
|
end
|
|
@@ -530,7 +536,13 @@ module TebakoRelease
|
|
|
530
536
|
return unless signing_enabled?
|
|
531
537
|
|
|
532
538
|
keyid = signing_keyid
|
|
533
|
-
|
|
539
|
+
declare_bundle_signature(entry, keyid) if entry[:bundle]
|
|
540
|
+
# Bundle-only lines serve no other payload asset — declaring the
|
|
541
|
+
# members' .ascs there would be an invalid signing state (spec 09
|
|
542
|
+
# §4). A co-published line (spec 36 §3, the shard's per_file_assets
|
|
543
|
+
# witness) serves the members standalone, so the every-served-name
|
|
544
|
+
# rule covers the exe/image/dll set again.
|
|
545
|
+
return if entry[:bundle] && !entry[:per_file_assets]
|
|
534
546
|
|
|
535
547
|
entry[:signature] = { keyid: keyid, asc: "#{entry[:filename]}.asc" }
|
|
536
548
|
%i[image dll].each do |facet|
|
|
@@ -539,10 +551,10 @@ module TebakoRelease
|
|
|
539
551
|
end
|
|
540
552
|
end
|
|
541
553
|
|
|
542
|
-
# Bundle-era (spec 36 §3): the
|
|
543
|
-
#
|
|
544
|
-
#
|
|
545
|
-
#
|
|
554
|
+
# Bundle-era (spec 36 §3): the bundle's own signature declaration.
|
|
555
|
+
# On bundle-only lines it is the ONLY payload signature — three
|
|
556
|
+
# signatures per leg (bundle, its sidecar, the shard); co-published
|
|
557
|
+
# lines add the per-file declarations on top (declare_signatures).
|
|
546
558
|
def declare_bundle_signature(entry, keyid)
|
|
547
559
|
entry[:bundle][:signature] = { keyid: keyid, asc: "#{entry[:bundle][:filename]}.asc" }
|
|
548
560
|
end
|
|
@@ -832,11 +844,16 @@ module TebakoRelease
|
|
|
832
844
|
# feeds the entry's bundle block and the idempotent skip), upload the
|
|
833
845
|
# bundles as the legs' only payload assets, then the metadata (shard
|
|
834
846
|
# with the bundle block + the bundle's sidecar). The member files
|
|
835
|
-
# themselves never become release assets — the bundle is the unit
|
|
847
|
+
# themselves never become release assets — the bundle is the unit —
|
|
848
|
+
# UNLESS the factory co-publishes (spec 36 §3): then the members are
|
|
849
|
+
# standalone served names beside the bundle (the lazy arm
|
|
850
|
+
# range-fetches the image), still ahead of the shard that witnesses
|
|
851
|
+
# them (a witnessed shard without its assets is the invalid publish).
|
|
836
852
|
def process_bundle_release(release, packages)
|
|
837
853
|
bundles = build_bundles(packages)
|
|
838
854
|
entries = build_manifest_entries(packages, bundles: bundles)
|
|
839
855
|
bundles.each_value { |bundle| upload_package(release, bundle) }
|
|
856
|
+
packages.each { |package| upload_package(release, package) } if copublish?
|
|
840
857
|
upload_blksum_sidecars(release, entries)
|
|
841
858
|
entries.each { |entry| ensure_package_metadata(release, entry) }
|
|
842
859
|
print_settled_summary
|
|
@@ -877,6 +894,14 @@ module TebakoRelease
|
|
|
877
894
|
@config.adapter.bundle_publish?
|
|
878
895
|
end
|
|
879
896
|
|
|
897
|
+
# Spec 36 §3's co-publish mode: a bundle-era line that ALSO serves
|
|
898
|
+
# the per-file assets as standalone release assets (the lazy arm's
|
|
899
|
+
# serving requirement — spec 39 §7). Factory-declared through the
|
|
900
|
+
# adapter, like the era.
|
|
901
|
+
def copublish?
|
|
902
|
+
bundle_publish? && @config.adapter.per_file_alongside_bundle?
|
|
903
|
+
end
|
|
904
|
+
|
|
880
905
|
# AUDIT_ONLY (the coordinator's release job / a publish dry run):
|
|
881
906
|
# strictly read-only — finds the release (never creates one), needs no
|
|
882
907
|
# local packages, uploads nothing, touches no notes; the release's
|
|
@@ -932,7 +957,7 @@ module TebakoRelease
|
|
|
932
957
|
# metadata describes what the release serves, never the fresh bytes
|
|
933
958
|
# that did not land.
|
|
934
959
|
def ensure_package_metadata(release, entry)
|
|
935
|
-
effective = effective_entry(entry)
|
|
960
|
+
effective = effective_entry(release, entry)
|
|
936
961
|
ensure_metadata_asset(release, shard_name_for(effective), "#{JSON.pretty_generate(effective)}\n")
|
|
937
962
|
metadata_assets(effective).each do |name, sha|
|
|
938
963
|
ensure_metadata_asset(release, "#{name}#{SIDECAR_SUFFIX}", "#{sha} #{name}\n")
|
|
@@ -943,20 +968,27 @@ module TebakoRelease
|
|
|
943
968
|
# era: the exe, its .tfs image and its .dll facet. Bundle era (spec 36
|
|
944
969
|
# §3): the bundle is the ONE served payload asset — the entry's
|
|
945
970
|
# exe/image/dll fields are member pins, not served names, so they earn
|
|
946
|
-
# no sidecar
|
|
947
|
-
# served standalone
|
|
971
|
+
# no sidecar — UNLESS the entry carries the co-publish witness, when
|
|
972
|
+
# the members ARE served standalone beside the bundle and earn theirs.
|
|
973
|
+
# Both eras: the derived blksum sidecar (spec 39 §3) IS a served
|
|
974
|
+
# standalone asset, so it earns its own .sha256 sidecar like
|
|
948
975
|
# every served name.
|
|
949
976
|
def metadata_assets(entry)
|
|
950
|
-
served =
|
|
951
|
-
|
|
952
|
-
else
|
|
953
|
-
{ entry[:filename] => entry[:sha256] }
|
|
954
|
-
.merge(facet_metadata(entry, :image))
|
|
955
|
-
.merge(facet_metadata(entry, :dll))
|
|
956
|
-
end
|
|
977
|
+
served = bundle_metadata(entry)
|
|
978
|
+
served.merge!(per_file_metadata(entry)) unless entry[:bundle] && !entry[:per_file_assets]
|
|
957
979
|
served.merge(blksum_metadata(entry))
|
|
958
980
|
end
|
|
959
981
|
|
|
982
|
+
def bundle_metadata(entry)
|
|
983
|
+
entry[:bundle] ? { entry[:bundle][:filename] => entry[:bundle][:sha256] } : {}
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
def per_file_metadata(entry)
|
|
987
|
+
{ entry[:filename] => entry[:sha256] }
|
|
988
|
+
.merge(facet_metadata(entry, :image))
|
|
989
|
+
.merge(facet_metadata(entry, :dll))
|
|
990
|
+
end
|
|
991
|
+
|
|
960
992
|
def facet_metadata(entry, facet)
|
|
961
993
|
block = entry[facet]
|
|
962
994
|
block ? { block[:filename] => block[:sha256] } : {}
|
|
@@ -977,12 +1009,57 @@ module TebakoRelease
|
|
|
977
1009
|
# A settled package (any of its assets kept their previous bytes) speaks
|
|
978
1010
|
# with the previous entry's voice; a never-published settle has nothing
|
|
979
1011
|
# truthful to keep and never reaches here (upload_package re-raised).
|
|
980
|
-
|
|
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)
|
|
981
1019
|
names = [entry[:filename], entry.dig(:image, :filename), entry.dig(:dll, :filename),
|
|
982
1020
|
entry.dig(:image, :blksum, :filename)].compact
|
|
983
1021
|
return entry unless names.any? { |name| settled?(name) }
|
|
984
1022
|
|
|
985
|
-
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
|
|
986
1063
|
end
|
|
987
1064
|
|
|
988
1065
|
# One metadata asset, converged: already-current bytes (the listing's
|
|
@@ -1183,7 +1260,7 @@ module TebakoRelease
|
|
|
1183
1260
|
|
|
1184
1261
|
exe = [name, "#{name}.exe"].find { |candidate| present.include?(candidate) }
|
|
1185
1262
|
landed, missing = expected_facets(name).partition { |facet| present.include?(facet) }
|
|
1186
|
-
landed, missing = split_blksum_expectation(release, present, name, landed, missing)
|
|
1263
|
+
landed, missing = split_blksum_expectation(served_shard(release, present, name), present, landed, missing)
|
|
1187
1264
|
missing.unshift(name) unless exe
|
|
1188
1265
|
return missing if exe.nil?
|
|
1189
1266
|
|
|
@@ -1194,12 +1271,17 @@ module TebakoRelease
|
|
|
1194
1271
|
# asset; a landed bundle owes its sidecar and the package's shard
|
|
1195
1272
|
# (plus an .asc of each on signing-enabled audits). The declared
|
|
1196
1273
|
# blksum sidecar (spec 39 §3) is a standalone served asset in this
|
|
1197
|
-
# era too — expected, with its own metadata, when the shard pins it
|
|
1274
|
+
# era too — expected, with its own metadata, when the shard pins it —
|
|
1275
|
+
# and a shard carrying the co-publish witness (spec 36 §3's
|
|
1276
|
+
# per_file_assets) owes the standalone per-file assets beside the
|
|
1277
|
+
# bundle.
|
|
1198
1278
|
def missing_bundle_assets(release, present, name, require_signatures: false)
|
|
1199
1279
|
bundle = "#{name}#{Bundler::BUNDLE_SUFFIX}"
|
|
1200
1280
|
return [bundle] unless present.include?(bundle)
|
|
1201
1281
|
|
|
1202
|
-
|
|
1282
|
+
shard = served_shard(release, present, name)
|
|
1283
|
+
landed, missing = split_blksum_expectation(shard, present, [bundle], [])
|
|
1284
|
+
landed, missing = split_copublish_expectation(shard, present, name, landed, missing)
|
|
1203
1285
|
missing + missing_metadata(present, name, landed, require_signatures: require_signatures)
|
|
1204
1286
|
end
|
|
1205
1287
|
|
|
@@ -1212,8 +1294,8 @@ module TebakoRelease
|
|
|
1212
1294
|
# the shard's own gap is the gate's business). A DECLARED sidecar that
|
|
1213
1295
|
# never landed reports its own name only — its sidecar/.asc cascade
|
|
1214
1296
|
# rides the landed set, one error per gap.
|
|
1215
|
-
def split_blksum_expectation(
|
|
1216
|
-
declared =
|
|
1297
|
+
def split_blksum_expectation(shard, present, landed, missing)
|
|
1298
|
+
declared = shard&.dig("image", "blksum", "filename")
|
|
1217
1299
|
return [landed, missing] unless declared
|
|
1218
1300
|
|
|
1219
1301
|
if present.include?(declared)
|
|
@@ -1223,17 +1305,35 @@ module TebakoRelease
|
|
|
1223
1305
|
end
|
|
1224
1306
|
end
|
|
1225
1307
|
|
|
1226
|
-
# The
|
|
1227
|
-
#
|
|
1228
|
-
#
|
|
1229
|
-
|
|
1308
|
+
# The shard-witnessed co-publish expectation (spec 36 §3): a shard
|
|
1309
|
+
# carrying per_file_assets: true owes the standalone per-file assets —
|
|
1310
|
+
# the exe (either spelling), the image, the windows DLL — beside the
|
|
1311
|
+
# bundle; each landed member earns its metadata through the landed
|
|
1312
|
+
# set. An unwitnessed shard expects nothing (bundle-only lines audit
|
|
1313
|
+
# green), and a missing exe reports its own gap only, never a
|
|
1314
|
+
# cascade (the one-error-per-gap rule).
|
|
1315
|
+
def split_copublish_expectation(shard, present, name, landed, missing)
|
|
1316
|
+
return [landed, missing] unless shard && shard["per_file_assets"] == true
|
|
1317
|
+
|
|
1318
|
+
exe = [name, "#{name}.exe"].find { |candidate| present.include?(candidate) }
|
|
1319
|
+
facets_landed, facets_missing = expected_facets(name).partition { |facet| present.include?(facet) }
|
|
1320
|
+
gaps = facets_missing
|
|
1321
|
+
gaps = [name] + gaps unless exe
|
|
1322
|
+
[landed + [exe, *facets_landed].compact, missing + gaps]
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
# The package's served shard as parsed JSON — the declarations' only
|
|
1326
|
+
# authority (the additive keys gate the audit's expectations). nil
|
|
1327
|
+
# when the shard never landed (its own gap is the gate's business) or
|
|
1328
|
+
# cannot be read (loudly — the audit never guesses an expectation).
|
|
1329
|
+
def served_shard(release, present, name)
|
|
1230
1330
|
shard = "#{name}#{SHARD_SUFFIX}"
|
|
1231
1331
|
return nil unless present.include?(shard)
|
|
1232
1332
|
|
|
1233
|
-
download_asset_json(find_asset(release, shard))
|
|
1333
|
+
download_asset_json(find_asset(release, shard))
|
|
1234
1334
|
rescue StandardError => e
|
|
1235
|
-
puts "::warning::could not read #{shard} for the
|
|
1236
|
-
"no
|
|
1335
|
+
puts "::warning::could not read #{shard} for the additive expectations (#{e.class}: #{e.message}) — " \
|
|
1336
|
+
"no additive assets are expected of this package"
|
|
1237
1337
|
nil
|
|
1238
1338
|
end
|
|
1239
1339
|
|
|
@@ -1497,10 +1597,16 @@ module TebakoRelease
|
|
|
1497
1597
|
exit 1
|
|
1498
1598
|
end
|
|
1499
1599
|
|
|
1500
|
-
def upload_package(release, package) # rubocop:disable Metrics/MethodLength
|
|
1600
|
+
def upload_package(release, package) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
1501
1601
|
filename = package.basename.to_s
|
|
1502
1602
|
puts "Processing #{filename}..."
|
|
1503
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
|
+
|
|
1504
1610
|
puts "#{filename} kept its previous bytes earlier in this publish run — " \
|
|
1505
1611
|
"the settled asset is never re-attempted; the refresh lands on a republish onto a recreated release object"
|
|
1506
1612
|
return nil
|
|
@@ -1535,6 +1641,155 @@ module TebakoRelease
|
|
|
1535
1641
|
nil
|
|
1536
1642
|
end
|
|
1537
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
|
+
|
|
1538
1793
|
# The settled ledger — the durable half of warn-and-keep-previous. The
|
|
1539
1794
|
# publish step runs the uploader once per platform (sequential
|
|
1540
1795
|
# processes, one workspace); an asset settled in one invocation (the
|