Skip to content

Let stacklets ship migrations the framework runs once, in order #220

Open

Description

@arthware-dev

Story

As a stacklet author, I want to ship a change to existing data as a migration that the framework runs once per instance, in order, records, and runs safely, so that moving files, renaming fields or rewriting records happens exactly once, the way database migrations work.

Context

Each data change today invents its own trigger and its own record of having run:

  • stack up memory moves each topic's old to-do file to the lists (move_todos_to_lists in stacklets/memory/lib.py, called from stacklets/memory/hooks/on_start_ready.py). It runs on every start and finds nothing to do after the first.
  • The archivist moves photos filed as bookmarks to notes (stacklets/docs/bot/migration_photos_to_notes.py, fix(archivist): keep what the family writes about a capture #219). It runs a minute after the bot starts and leaves a marker file in its session directory.

None of them is ordered against another, none shows up in stack doctor, and a failure is a log line. Coming changes need the same: #217 (kind), #228 (tags), and the start of ADR-015 (a change to a record is a person's commit).

Proposal

  • Files. A stacklet ships migrations as stacklets/<id>/migrations/NNNN_<name>.py, each with def run(ctx) and a docstring saying what it changes. Picked up by convention, like hooks, and given the same ctx. Work that needs a container goes through the existing seams: ctx.run_in_container, or a bot-runner command (the photo migration becomes one).
  • When. stack up <id> runs the pending ones after on_start_ready, only when the health checks passed, in number order. A command that starts several stacklets runs them in dependency order. stack update never runs them. A fresh install runs them all; they find nothing to do.
  • Safe. A migration is idempotent and either writes in one step or can resume. One that changes the family memory writes one Forgejo commit that checks each file's sha, so a concurrent writer makes it fail rather than overwrite; the bots keep running. Git history is the undo for the family memory. No down migrations, no dry run: stack doctor listing what is pending is the preview.
  • Record. The record lives with the data it describes, so restoring an older backup brings the old record back and the migration runs again:
    • a migration of the family memory records itself in the commit that applied it, with a Migration: <id>/<NNNN> trailer;
    • any other migration records itself in <data_dir>/<id>/.migrations.json.
  • Failure. The first failure stops the run, prints the error, and stack up exits 1 with the containers left running. The rest wait for the next stack up.
  • Visibility. With nothing pending, stack up prints nothing extra. stack doctor lists pending and failed migrations, and data migrated by a newer release. stack update to an older release names the migrations applied since and asks first. A commit that adds a migration carries an Upgrade: footer.
  • Lifetime. Migrations stay until 1.0. Then a minimum release to upgrade from is named, and older migrations are deleted.
  • The two existing migrations move over and keep their behaviour.

Out of scope: down migrations, versioned data formats.

Definition of done

  • A test: two migrations of one stacklet run once, in order, on stack up; a second stack up runs neither and prints nothing extra
  • A test: a failing migration stops the run, stack up exits 1, and it runs again on the next stack up; the one after it waits
  • A test: a family memory migration's record is the Migration: trailer of its commit; with that commit gone from the history, it runs again
  • A test: a migration whose Forgejo commit meets a file changed underneath fails and changes nothing
  • The to-do and photo migrations run through it, with their existing tests green
  • stack doctor names a pending or failed migration
  • docs/stack-reference.md describes the migration contract

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions