diff --git a/docs/releasing.md b/docs/releasing.md index 9db47f8e..34bb1ba7 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -57,7 +57,12 @@ is safe and is how `latest` is restored if something else overwrote it. Notes an operator needs when upgrading, newest first. `bin/docker-entrypoint` runs `bin/rails db:prepare` on boot, so every migration listed here runs unattended. - **Proxies (spec 068).** One migration, `CreateDeckProxies`, runs on boot; it creates the `deck_proxies` table and changes nothing else. A collector records the cards a deck's zone needs and they don't own as proxies; an export now carries them as `proxies` in format version 4, which no release had shipped before, so a version 4 file written by a development build before this change (an `edge` image) has no `proxies` and is refused as not well-formed. Nothing is required of the operator; the backup includes the table with the primary database. +- **Decks (spec 062).** One migration, `AddDecks`, runs on boot. It adds `kind`, `parent_id` and `zone` to `locations`, where every existing location becomes a place. It replaces the unique index on a location's name with one for top-level locations and one for a deck's zones, and creates the `decks` table. Exports written by this release are export format version 4 (with `decks`, a lot's zone and `proxies`). This release reads versions 1 to 4, and v0.2.x has no import, so restore a 0.3.0 export only on 0.3.0 or later. Nothing is required of the operator. - **Mana value (spec 058).** One migration, `AddManaValueToMTGCards`, runs on boot; it adds a nullable `mana_value` column to `mtg_cards` and changes nothing else. Existing cards have no mana value until the next catalog refresh, which applies even if Scryfall's file hasn't changed since the last one (the weekly refresh, or **Refresh now** on the admin catalog page, or `bin/rails "catalog:refresh[mtg]"`); nothing reads it yet. Nothing is required of the operator, and the export format is unchanged (version 3). +- **Instance settings hold one row (#229).** Two migrations run on boot. If the `instance_settings` table holds more than one row, the first keeps the oldest and deletes the rest. The second adds a check that only one row can exist. Nothing is required of the operator. +- **Session idle expiry (spec 049).** Three migrations run on boot. They add `last_used_at` to `sessions`, set it to the time of the upgrade for every existing session, then drop the sessions' stored IP address and user agent. That drop is on purpose, and the upgrade proof expects it. A session unused for 30 days now ends, and a nightly job deletes it; nobody is signed out by the upgrade itself. Nothing is required of the operator. +- **Each account's currency (spec 038).** Two migrations run on boot. They add `currency` to `accounts`, then give every existing account the instance's `COLLECTOR_CURRENCY` as set at upgrade time (`USD` when unset). **Set `COLLECTOR_CURRENCY` to your instance's currency before upgrading** if it isn't USD. An unsupported code fails the migration, and the app doesn't start until it's fixed. After the upgrade, each collector changes their own currency from More → Currency, and the setting only decides where new accounts start. +- **Smaller additions.** These migrations only add tables or nullable columns and change no existing data: catalog refresh progress (`catalog_refresh_runs`, spec 015), Add by number (`number_entries`, `number_entry_lines`, spec 026), a lot's location (`lots.location_id`, spec 032), the scanner sitting's place and condition (`scanner_sittings`, specs 040 and 042), and the bulk mode location filter (`bulk_selections.location_filter`, spec 037). Nothing is required of the operator. - **Upgrade proof (spec 031).** No migration, nothing required of the operator. CI now boots the previous release's published image, fills it through its own code, starts each build on that volume and fails unless the database upgraded unattended, lost nothing and the app served the collection as it was; a release tag whose build fails it publishes no image. `bin/ci` also fails when a released migration is edited, deleted or renamed. The proof's seeds, its list of expected changes and the release steps that keep them current are in "Cutting a release". - **Backup and restore (spec 030).** No migration. Two new commands, `bin/rails "backup:create[DIRECTORY]"` and `bin/rails "backup:restore[FILE]"`, back up a running instance into one file and restore it onto a new, empty volume; see the README's "Backup and restore". `config/deploy.yml` gains a `backup` alias and a commented backup-directory volume; `compose.yaml` is unchanged. Nothing is required of the operator, but take a backup before this upgrade and keep your `SECRET_KEY_BASE` and `COLLECTOR_*` settings somewhere apart from it. - **Locations (spec 029).** One new migration, `locations`, runs on boot; it adds a table and changes nothing else. Exports written by this release are export format version 2 (they carry the collector's `locations`), and an earlier release refuses one as written by a newer version of Collector, so upgrade every instance an export is restored on before restoring. This release reads export files of version 1 and version 2 (a version 1 file leaves the account's locations as they are). Nothing is required of the operator. diff --git a/script/upgrade/seed_exceptions.yml b/script/upgrade/seed_exceptions.yml index a7ec3f25..406370d1 100644 --- a/script/upgrade/seed_exceptions.yml +++ b/script/upgrade/seed_exceptions.yml @@ -2,4 +2,5 @@ # Each entry is { table: , reason: }. Cutting a release renames next.rb and the `next` key to # the version (docs/releasing.md). "0.2.1": [] +"0.3.0": [] next: [] diff --git a/script/upgrade/seeds/0.3.0.rb b/script/upgrade/seeds/0.3.0.rb new file mode 100644 index 00000000..5267b1d0 --- /dev/null +++ b/script/upgrade/seeds/0.3.0.rb @@ -0,0 +1,70 @@ +# Seed for the upgrade proof when v0.3.0 is the previous release (spec 031 decision 4). Run by `bin/rails runner -` in a +# container of v0.3.0's published image, so it can use only what that release has, through that release's own models. +# It fills every table of v0.3.0 (seed_exceptions.yml lists none) and never changes after v0.3.0 is tagged. The last line it +# prints is what the harness reads. +password = "upgrade-password-1234" +marker = "upgrade-marker" +now = Time.current + +sets = %w[ups upt].each_with_index.map do |code, index| + Catalog::Set.create!(collectible_type: "mtg", code:, name: "Upgrade Set #{code.upcase}", released_on: Date.new(2020 + index, 1, 1), + content_digest: "upgrade-seed") +end + +names = Array.new(50) { |index| format("Upgrade Dragon %02d", index + 1) } +entries = names.each_with_index.map do |name, index| + set = sets[index % 2] + identity = Catalog::Identity.create!(collectible_type: "mtg", external_key: "upgrade-identity-#{index}", name:, content_digest: "upgrade-seed") + entry = Catalog::Entry.create!(collectible_type: "mtg", external_key: "upgrade-entry-#{index}", set:, identity:, number: (index + 1).to_s, + language: "en", name:, kind: "card", released_on: set.released_on, content_digest: "upgrade-seed") + MTG::Card.create!(identity:, mana_cost: "{R}", type_line: "Creature", oracle_text: "Seed text.") + MTG::Printing.create!(entry:, rarity: "common", finishes: %w[nonfoil foil], layout: "normal", illustration_id: "upgrade-illustration-#{index}", + scryfall_uri: "https://scryfall.com/card/#{set.code}/#{index + 1}", + faces: [ { "name" => name, "mana_cost" => "{R}", "type_line" => "Creature", "oracle_text" => "Seed text.", "artist" => "Nobody", "image_uris" => {} } ]) + entry +end +Catalog::NameIndex.new("mtg").rebuild + +ada, = User::Command.call(email: "ada@upgrade.example", password:) +grace = User.create!(name: "Grace", email_address: "grace@upgrade.example", password:) +grace.account.update!(currency: "EUR") # a non-default currency, so the next release's proof shows the column survives (spec 038 AC-4.7) +InstanceSetting.current.update!(sign_up_open: true) +[ ada, grace ].each { |user| user.sessions.create! } + +# 50 printings x 2 finishes x 5 conditions x 4 prices = 2,000 lots for Ada; Grace's collection stays empty. +variants = %w[nonfoil foil].product(%w[near_mint lightly_played moderately_played heavily_played damaged], [ nil, 100, 250, 1999 ]) +Lot.insert_all(entries.each_with_index.flat_map do |entry, index| + variants.each_with_index.map do |(finish, condition, price), position| + { account_id: ada.account_id, catalog_entry_id: entry.id, quantity: 1 + (index + position) % 7, finish:, condition:, price_paid_cents: price, + lot_key: Lot.key_for(finish, condition, price), created_at: now, updated_at: now } + end +end) +Scanner::Sitting.add!(account: ada.account, printing: entries.first, finish: "nonfoil", reading_key: SecureRandom.hex(16)) + +session = ada.sessions.first +selection = BulkSelection.for(session) +ada.account.lots.limit(3).pluck(:id).each { |lot_id| selection.marks.create!(lot_id:, account: ada.account) } +BulkRemoval.create!(session:, account: ada.account, copies: 2, + lots_data: [ { "catalog_entry_id" => entries.first.id, "finish" => "foil", "condition" => "damaged", "price_paid_cents" => nil, "quantity" => 2 } ]) + +Catalog::RefreshRun.create!(collectible_type: "mtg", trigger: "manual", status: "applied", source_version: "upgrade-seed", languages: "en", + started_at: now - 60, finished_at: now, seen_count: 50, inserted_count: 50) +MTG::ArtBuild.create!(status: "finished", job_id: "upgrade-seed", catalog_version: "upgrade-seed", settings_digest: "upgrade-seed", + index_file: "art-index-upgrade.bin.gz", started_at: now - 60, heartbeat_at: now, finished_at: now, total_count: 50, indexed_count: 50) +MTG::Artwork.create!(entry: entries.first, illustration_id: "upgrade-illustration-0", fingerprint: "\x00".b * 128, settings_digest: "upgrade-seed") +locations = [ "Binder A", "Shelf B" ].map { |location| Location.create!(account: ada.account, name: location) } +# A deck with a lot in a zone, so the upgrade proof carries decks, their zones and a placed lot across (spec 062 AC-9.2). +deck = Deck::Creation.new(account: ada.account, name: "Upgrade Deck", format: "mtg/modern").tap(&:save!).deck +Lot.add!(account: ada.account, entry: entries.first, quantity: 3, finish: "nonfoil", location: deck.zone("main")) +# A proxy beside the deck's lot, so the next release's upgrade proof carries `deck_proxies` across (spec 068 AC-8.6, spec 031's coverage rule). +Deck::Proxy.add!(account: ada.account, zone: deck.zone("sideboard"), entry: entries.first, finish: "foil", quantity: 2) +NumberEntry.add!(account: ada.account, entry: entries.first, quantity: 2, finish: "foil", line_key: SecureRandom.hex(16)) +plan = Object.new +def plan.fingerprint(checksum) = "upgrade-seed-#{checksum}" +upload = ImportUpload.store!(account: grace.account, bytes: "{}", plan:) +ActiveStorage::VariantRecord.create!(blob: upload.file.blob, variation_digest: "upgrade-seed") +Catalog::RefreshJob.set(wait: 1.year).perform_later("mtg", marker) + +facts = { release: "next", password:, marker:, locations: locations.map(&:name), + users: [ { email: ada.email_address, lots: ada.account.lots.count, names: }, { email: grace.email_address, lots: 0, names: [] } ] } +puts "UPGRADE-SEED-COMPLETE #{facts.to_json}"