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 +4 -4
- data/README.md +14 -1
- data/lib/tebako_release/adapter.rb +13 -0
- data/lib/tebako_release/blksum.rb +98 -0
- data/lib/tebako_release/uploader.rb +209 -43
- data/lib/tebako_release/version.rb +1 -1
- data/lib/tebako_release.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e91116fb240ffb5e0cb7cec20a1753fbd4177b7bd8f3c78adb8c3e260acf718
|
|
4
|
+
data.tar.gz: c5c886163f60c1ef021829015d95a1f3c50b6fc239a22a39cb890324041353d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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,
|
|
490
|
-
|
|
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
|
-
|
|
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
|
|
518
|
-
#
|
|
519
|
-
#
|
|
520
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1452
|
-
#
|
|
1453
|
-
#
|
|
1454
|
-
# would be a
|
|
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
|
|
1590
|
-
#
|
|
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
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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)
|
|
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)
|
|
1630
|
-
#
|
|
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
|
data/lib/tebako_release.rb
CHANGED
|
@@ -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.
|
|
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
|