Module: Everywhere::UpdateManifest
- Defined in:
- lib/everywhere/update_manifest.rb
Overview
The update-feed contract: bucket key layout + latest.json / index.json
bodies. This is THE interface between every publish, the shell's Rust
updater, and the Platform's release publishing — all three speak exactly
these shapes, so keep changes here schema-versioned and additive.
Layout (channel is a release channel — stable/beta — never the receipt's distribution_channel):
{prefix?}/{channel}/{os}/{arch}/
latest.json mutable pointer, written last, Cache-Control: no-cache
index.json append-only publish history (rollback + stats source)
My-App-1.2.0.zip immutable + .minisig sibling
My-App-latest.zip convenience alias (CopyObject) for download links
The shell only ever does:
GET {updates.url}/{channel}/{os}/{arch}/latest.json?current={version}
— a raw bucket/CDN or a dynamic Platform endpoint serve the identical JSON.
Constant Summary collapse
- SCHEMA =
1- IMMUTABLE_CACHE =
"public, max-age=31536000, immutable"- MUTABLE_CACHE =
"no-cache"
Class Method Summary collapse
- .artifact_key(filename, **loc) ⇒ Object
- .dir(channel:, os:, arch:, prefix: nil) ⇒ Object
-
.entry(receipt:, url:, signature:, channel:, notes: nil, notes_html: nil, published_at: Time.now.utc) ⇒ Object
One latest.json body (also an index.json entry), built from a build receipt (dist/release.json) + publish-time facts.
-
.index_append(existing_json, entry) ⇒ Object
index.json: append-only history, newest first.
- .index_find(index_json, version) ⇒ Object
- .index_key(**loc) ⇒ Object
-
.latest_alias(filename, version) ⇒ Object
"My-App-1.2.0.zip" -> "My-App-latest.zip" (keeps the extension).
- .latest_key(**loc) ⇒ Object
-
.notes_asset_key(filename, channel:, version:, prefix: nil) ⇒ Object
Where images embedded in release notes live: channel-scoped (shared by every os/arch target of the release), versioned so they're immutable and rollback-safe.
-
.render_notes_html(notes) ⇒ Object
Markdown -> HTML for notes_html.
- .to_json(entry) ⇒ Object
Class Method Details
.artifact_key(filename, **loc) ⇒ Object
36 |
# File 'lib/everywhere/update_manifest.rb', line 36 def artifact_key(filename, **loc) = "#{dir(**loc)}/#{filename}" |
.dir(channel:, os:, arch:, prefix: nil) ⇒ Object
32 33 34 |
# File 'lib/everywhere/update_manifest.rb', line 32 def dir(channel:, os:, arch:, prefix: nil) [prefix, channel, os, arch].compact.reject(&:empty?).join("/") end |
.entry(receipt:, url:, signature:, channel:, notes: nil, notes_html: nil, published_at: Time.now.utc) ⇒ Object
One latest.json body (also an index.json entry), built from a build receipt (dist/release.json) + publish-time facts.
Release notes ship in two forms: notes is the human-authored source
(markdown; plain text is valid markdown), notes_html is pre-rendered
sanitized-by-construction HTML the app can inject straight into a
changelog modal via the bridge. Pass notes_html explicitly to override
the default markdown rendering (the Platform does — its notes are
authored rich).
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/everywhere/update_manifest.rb', line 63 def entry(receipt:, url:, signature:, channel:, notes: nil, notes_html: nil, published_at: Time.now.utc) artifact = receipt.fetch("artifact") sha256 = artifact.fetch("checksum").to_s.delete_prefix("sha256:") notes_html ||= render_notes_html(notes) { "schema" => SCHEMA, "app" => { "name" => receipt.dig("app", "name"), "bundle_id" => receipt.dig("app", "bundle_id") }, "channel" => channel, "os" => receipt.dig("target", "os"), "arch" => receipt.dig("target", "arch"), "version" => receipt.dig("app", "version"), "published_at" => published_at.iso8601, "notes" => notes, "notes_html" => notes_html, "url" => url, "sha256" => sha256, "size_bytes" => artifact.fetch("size_bytes"), "signature" => signature, "manifest_checksum" => receipt["manifest_checksum"] }.compact end |
.index_append(existing_json, entry) ⇒ Object
index.json: append-only history, newest first. Re-publishing the same version replaces its entry (idempotent re-runs don't duplicate).
91 92 93 94 95 |
# File 'lib/everywhere/update_manifest.rb', line 91 def index_append(existing_json, entry) entries = existing_json ? JSON.parse(existing_json).fetch("entries", []) : [] entries = entries.reject { |e| e["version"] == entry["version"] } JSON.pretty_generate({ "schema" => SCHEMA, "entries" => [entry] + entries }) << "\n" end |
.index_find(index_json, version) ⇒ Object
97 98 99 100 101 |
# File 'lib/everywhere/update_manifest.rb', line 97 def index_find(index_json, version) return nil unless index_json JSON.parse(index_json).fetch("entries", []).find { |e| e["version"] == version } end |
.index_key(**loc) ⇒ Object
38 |
# File 'lib/everywhere/update_manifest.rb', line 38 def index_key(**loc) = "#{dir(**loc)}/index.json" |
.latest_alias(filename, version) ⇒ Object
"My-App-1.2.0.zip" -> "My-App-latest.zip" (keeps the extension).
49 50 51 52 |
# File 'lib/everywhere/update_manifest.rb', line 49 def latest_alias(filename, version) aliased = filename.sub("-#{version}", "-latest") aliased == filename ? nil : aliased end |
.latest_key(**loc) ⇒ Object
37 |
# File 'lib/everywhere/update_manifest.rb', line 37 def latest_key(**loc) = "#{dir(**loc)}/latest.json" |
.notes_asset_key(filename, channel:, version:, prefix: nil) ⇒ Object
Where images embedded in release notes live: channel-scoped (shared by every os/arch target of the release), versioned so they're immutable and rollback-safe. notes_html rewrites embedded images to these URLs at publish time — signed/expiring storage URLs must never leak into a feed.
44 45 46 |
# File 'lib/everywhere/update_manifest.rb', line 44 def notes_asset_key(filename, channel:, version:, prefix: nil) "#{[ prefix, channel ].compact.reject(&:empty?).join("/")}/notes/#{version}/#{filename}" end |
.render_notes_html(notes) ⇒ Object
Markdown -> HTML for notes_html. Kramdown with GFM-ish niceties off the shelf; raw HTML in the source passes through (the feed is the app developer's own signed content — same trust as the artifact itself).
108 109 110 111 112 113 |
# File 'lib/everywhere/update_manifest.rb', line 108 def render_notes_html(notes) return nil if notes.nil? || notes.strip.empty? require "kramdown" Kramdown::Document.new(notes, auto_ids: false, entity_output: :as_char).to_html.strip end |
.to_json(entry) ⇒ Object
103 |
# File 'lib/everywhere/update_manifest.rb', line 103 def to_json(entry) = JSON.pretty_generate(entry) << "\n" |