Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8b99ea8
fix(deps): update platform while preserving existing profiles
lklimek Sep 10, 2026
9cbf464
fix(deps): pin platform to 63cf57f
lklimek Sep 10, 2026
3057fe3
docs(migration): add fixture contract, capture recipe and matrix design
lklimek Sep 10, 2026
8ff28cf
feat(mcp): add read-only identity_list and app_storage_status tools
lklimek Sep 11, 2026
931dc29
test(migration): add cross-version migration matrix harness
lklimek Sep 11, 2026
b1db9ce
Merge branch 'worktree-agent-a2fcb5657487e74bb' into feat/migration-t…
lklimek Sep 11, 2026
7b31feb
Merge branch 'worktree-agent-a4b3674e1b8943e08' into feat/migration-t…
lklimek Sep 11, 2026
474e440
docs(migration): record real v0.9.3 fixture capture and its proof-for…
lklimek Sep 11, 2026
e502a57
ci(migration): add migration-matrix workflow, fixture scripts and pin…
lklimek Sep 11, 2026
c236e4f
fix(migration): resolve fixture archive by documented manifest key
lklimek Sep 11, 2026
c127508
ci(migration): add fixture bootstrap upload and make the matrix fail …
lklimek Sep 11, 2026
f82f38e
test(migration): record v0.9.3 fixture artifact run
lklimek Sep 11, 2026
a213fe7
fix(migration-matrix): stop a test-env leak from clobbering DET_CLI_BIN
lklimek Sep 11, 2026
ac06bd2
docs(ai-design): add platform_compatibility bridge pattern reference
lklimek Sep 11, 2026
b197b1b
fix(mcp): restore the legacy network before the det-cli boot picks one
lklimek Sep 11, 2026
c9848f8
test(migration-matrix): assert the read-only data.db contract and uma…
lklimek Sep 11, 2026
4f0b519
test(migration-matrix): expect StorageUpdateNeedsDesktop for password…
lklimek Sep 11, 2026
d8f2522
fix(logging): cap rmcp logging so raw MCP tool arguments never reach …
lklimek Sep 11, 2026
94512c7
feat(storage-update): finish a protected-wallet storage update non-in…
lklimek Sep 11, 2026
3cb16a5
test(migration): boot the v0.9.3 fixture again with its password supp…
lklimek Sep 11, 2026
d4ec6aa
docs(storage-update): record the non-interactive password decision
lklimek Sep 11, 2026
eded628
fix(det-cli): keep wallet passwords off plain HTTP and out of stdin's…
lklimek Sep 11, 2026
b977562
fix(migration-matrix): skip SPV-gated address derivation for legacy-d…
lklimek Sep 11, 2026
3f46f12
test(migration): real-data platform_compatibility fixture from v1.0.0…
lklimek Sep 11, 2026
4d74032
ci(migration): relay large fixture archives through a draft release a…
lklimek Sep 11, 2026
6faaf8f
chore(migration-fixtures): record the weekly-20260908 fixture's uploa…
lklimek Sep 11, 2026
b82ccd6
Merge origin/v1.0-dev into feat/migration-test-matrix
lklimek Sep 11, 2026
d718695
Merge branch 'v1.0-dev' into feat/migration-test-matrix
lklimek Sep 14, 2026
e855f4f
ci(migration): run the matrix on every PR 983 branch push
lklimek Sep 14, 2026
0280335
test: harden migration matrix outcomes and password handling
lklimek Sep 14, 2026
579b0bd
fix(migration): enforce fixture integrity and credential isolation
lklimek Sep 14, 2026
c76de0d
fix(mcp): inspect storage without initialization and report wallet ow…
lklimek Sep 14, 2026
25b491b
ci(migration): validate fixture scripts before building
lklimek Sep 14, 2026
d74a66f
test(migration): use runner-provided grep for script assertions
lklimek Sep 14, 2026
a8a826b
test(cli): check request read in redirect regression
lklimek Sep 14, 2026
d9c297c
Merge remote-tracking branch 'origin/v1.0-dev' into feat/migration-te…
lklimek Sep 14, 2026
4e65659
fix(migration): address retry and matrix review findings
lklimek Sep 15, 2026
d0d1d37
Merge remote-tracking branch 'origin/v1.0-dev' into feat/migration-te…
lklimek Sep 15, 2026
d1d032a
Merge branch 'v1.0-dev' into feat/migration-test-matrix
lklimek Sep 16, 2026
883df2a
fix(migration): preserve captured data and secure CLI transport
lklimek Sep 16, 2026
24d2a59
fix(cli): preserve active context when network settings fail
lklimek Sep 16, 2026
1ebefee
Merge branch 'v1.0-dev' into feat/migration-test-matrix
lklimek Sep 17, 2026
e4aea27
fix(ci): run migration matrix for PR 983
lklimek Sep 17, 2026
43982fa
fix(test): update migration fixture RNG import for rand 0.10
lklimek Sep 17, 2026
3e6f29e
fix: keep passphrase-limit import in single-key tests
lklimek Sep 17, 2026
3c0679c
test(migration): cover historical public identities and DPNS
lklimek Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 295 additions & 0 deletions .github/workflows/migration-fixture-bootstrap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
name: Migration Fixture Bootstrap

# Manual upload path for a migration fixture archive captured OUTSIDE CI — the
# v0.9.3 baseline needs a GUI capture against a released binary, see
# tests/migration-fixtures/README.md. It turns local bytes into a GitHub
# Actions build artifact without them ever entering the git tree: git history
# is permanent, while an artifact can be deleted or left to expire.
#
# The archive reaches the runner in one of two ways. Everything else — artifact
# name, file name, size, sha256 — comes from the COMMITTED manifest entry named
# by `fixture_id`, so a dispatch can only publish bytes a reviewed manifest
# already describes.
#
# - `archive_b64`: the archive base64-encoded in a workflow_dispatch input. The
# inputs payload limit is 65,535 characters, so archives up to ~48 KB fit;
# the v0.9.3 baseline is 6 KB.
# - `release_tag`: the archive attached to a temporary DRAFT release under that
# tag, for anything larger (a current-era profile carries its SQLite WAL).
# Draft releases are listed only to tokens with push access, so the one job
# that fetches the archive holds `contents: write` and nothing else. It runs
# nothing but jq and the GitHub API, and hands the bytes to the verifying job
# as a same-run artifact. Delete the draft once the run has succeeded.
#
# This is not capture automation: it captures nothing, runs on no schedule and
# opens no PR. A human dispatches it and records the run id in the manifest.
#
# USAGE, small archive:
# gh workflow run migration-fixture-bootstrap.yml \
# --repo dashpay/dash-evo-tool --ref <branch-carrying-the-manifest-entry> \
# -f fixture_id=v0.9.3-wallet-only \
# -f archive_b64="$(base64 -w0 dash-evo-tool-v093-fixture.tar.zst)"
#
# USAGE, large archive (draft-release relay):
# gh release create fixture-relay-<fixture_id> <archive_filename> \
# --repo dashpay/dash-evo-tool --draft --title "Fixture relay (temporary)" \
# --notes "Temporary: deleted once migration-fixture-bootstrap.yml has uploaded it."
# gh workflow run migration-fixture-bootstrap.yml \
# --repo dashpay/dash-evo-tool --ref <branch-carrying-the-manifest-entry> \
# -f fixture_id=<fixture_id> -f release_tag=fixture-relay-<fixture_id>
# # once the run has succeeded:
# gh release delete fixture-relay-<fixture_id> --repo dashpay/dash-evo-tool \
# --cleanup-tag --yes
#
# Either way, copy the values from the run's step summary into the entry's
# `artifact` block in tests/migration-fixtures/manifest.json.
#
# RETENTION HAZARD: artifacts expire, and 90 days is the ceiling for a public
# repository. Before the manifest's `expires_at`, re-dispatch with the same
# bytes and record the new `workflow_run_id`/`expires_at`. Once the artifact
# has expired, the migration matrix fails (download-fixtures.sh --strict)
# until that is done.
#
# REVOCATION: `gh run delete <run-id>` removes the artifact together with the
# run's inputs payload.

on:
workflow_dispatch:
inputs:
fixture_id:
description: "Manifest entry id (tests/migration-fixtures/manifest.json .fixtures[].id)"
required: true
type: string
archive_b64:
description: "The fixture archive, base64-encoded without line wraps (base64 -w0). Leave empty when using release_tag."
required: false
type: string
release_tag:
description: "Tag of a temporary draft release carrying the archive, for archives over ~48 KB. Leave empty when using archive_b64."
required: false
type: string
# Registration only. GitHub dispatches a workflow only once it exists on the
# default branch or has already run; this trigger makes the pull request that
# adds or edits this file register it, so it can be dispatched with `--ref`
# against that PR's branch before it reaches the default branch. The jobs
# below never run for it.
pull_request:
paths:
- ".github/workflows/migration-fixture-bootstrap.yml"

concurrency:
group: ${{ github.workflow }}-${{ inputs.fixture_id || github.ref }}
cancel-in-progress: false

permissions: {}

jobs:
fetch-draft-release:
name: Fetch archive from draft release
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
runs-on: ubuntu-latest
permissions:
# Draft releases are listed only to tokens with push access. This job
# holds nothing else, and it only reads the manifest and the API.
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# The write-scoped token must not linger in .git/config.
persist-credentials: false

- name: Download the draft release asset
env:
GH_TOKEN: ${{ github.token }}
FIXTURE_ID: ${{ inputs.fixture_id }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
manifest=tests/migration-fixtures/manifest.json
archive_filename="$(jq -r --arg id "$FIXTURE_ID" \
'[.fixtures[] | select(.id == $id)] | if length == 1 then (.[0].artifact.archive_filename // empty) else empty end' "$manifest")"
if ! [[ "$archive_filename" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*\.tar\.(zst|gz)$ ]]; then
echo "::error::$manifest has no single entry '$FIXTURE_ID' with a well-formed artifact.archive_filename."
exit 1
fi
# A draft has no published tag, so it is found by listing. Only a
# draft qualifies: a published release is not this relay.
releases="$(gh api --paginate "repos/$GITHUB_REPOSITORY/releases?per_page=100" |
jq -c --arg tag "$RELEASE_TAG" '.[] | select(.draft and .tag_name == $tag)')"
if [ "$(jq -s 'length' <<<"$releases")" != "1" ]; then
echo "::error::Expected exactly one draft release tagged '$RELEASE_TAG'."
exit 1
fi
asset_id="$(jq -r --arg name "$archive_filename" \
'[.assets[] | select(.name == $name)] | if length == 1 then .[0].id else empty end' <<<"$releases")"
if [ -z "$asset_id" ]; then
echo "::error::The draft release '$RELEASE_TAG' has no single asset named $archive_filename."
exit 1
fi
dir="$RUNNER_TEMP/relay"
mkdir -p "$dir"
gh api -H 'Accept: application/octet-stream' \
"repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" >"$dir/$archive_filename"
echo "Fetched $archive_filename ($(stat -c %s "$dir/$archive_filename") bytes); the upload job verifies it against the manifest."

- name: Hand the archive to the upload job
uses: actions/upload-artifact@v4
with:
name: fixture-relay
path: ${{ runner.temp }}/relay
retention-days: 1
if-no-files-found: error
compression-level: 0

upload:
name: Upload fixture artifact
needs: fetch-draft-release
# `always()` lets the archive_b64 path run although the relay job was
# skipped. The relay path needs that job to have succeeded; the input check
# below rejects a dispatch that gives neither or both inputs.
if: >-
always() && github.event_name == 'workflow_dispatch' &&
needs.fetch-draft-release.result != 'failure' &&
needs.fetch-draft-release.result != 'cancelled'
runs-on: ubuntu-latest
permissions:
contents: read
# Reads back the uploaded artifact's expiry for the step summary.
actions: read
env:
# Platform ceiling for a public repository; see RETENTION HAZARD above.
RETENTION_DAYS: 90
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Resolve manifest entry
id: entry
env:
FIXTURE_ID: ${{ inputs.fixture_id }}
HAS_ARCHIVE_B64: ${{ inputs.archive_b64 != '' }}
HAS_RELEASE_TAG: ${{ inputs.release_tag != '' }}
run: |
set -euo pipefail
if [ "$HAS_ARCHIVE_B64" = "$HAS_RELEASE_TAG" ]; then
echo "::error::Give exactly one of archive_b64 (archives up to ~48 KB) or release_tag (a draft release carrying the archive)."
exit 1
fi
manifest=tests/migration-fixtures/manifest.json
entry="$(jq -c --arg id "$FIXTURE_ID" \
'[.fixtures[] | select(.id == $id)] | if length == 1 then .[0] else empty end' "$manifest")"
if [ -z "$entry" ]; then
echo "::error::$manifest has no single fixture entry with id '$FIXTURE_ID'."
exit 1
fi
# Each value lands in $GITHUB_OUTPUT and later in a file path, so it
# is held to a strict shape rather than trusted as free text.
check() {
local key="$1" pattern="$2" value
value="$(jq -r --arg k "$key" '.artifact[$k] // empty' <<<"$entry")"
if ! [[ "$value" =~ $pattern ]]; then
echo "::error::Manifest entry '$FIXTURE_ID' has a missing or malformed artifact.$key."
exit 1
fi
echo "$key=$value" >>"$GITHUB_OUTPUT"
}
check artifact_name '^[A-Za-z0-9][A-Za-z0-9._-]*$'
check archive_filename '^[A-Za-z0-9][A-Za-z0-9._-]*\.tar\.(zst|gz)$'
check sha256 '^[0-9a-f]{64}$'
check bytes '^[1-9][0-9]*$'

- name: Download relayed archive
if: inputs.release_tag != ''
uses: actions/download-artifact@v4
with:
name: fixture-relay
path: ${{ runner.temp }}/relay

- name: Decode and verify archive
id: archive
env:
# Passed through the environment, never interpolated into the
# script, and never echoed: it is dispatch input.
ARCHIVE_B64: ${{ inputs.archive_b64 }}
RELEASE_TAG: ${{ inputs.release_tag }}
ARCHIVE_FILENAME: ${{ steps.entry.outputs.archive_filename }}
EXPECTED_SHA256: ${{ steps.entry.outputs.sha256 }}
EXPECTED_BYTES: ${{ steps.entry.outputs.bytes }}
run: |
set -euo pipefail
dir="$RUNNER_TEMP/fixture"
mkdir -p "$dir"
archive="$dir/$ARCHIVE_FILENAME"
if [ -n "$RELEASE_TAG" ]; then
# Fetched from the draft release by the job above.
relayed="$RUNNER_TEMP/relay/$ARCHIVE_FILENAME"
if [ ! -f "$relayed" ]; then
echo "::error::The relay job handed over no $ARCHIVE_FILENAME."
exit 1
fi
mv "$relayed" "$archive"
elif ! printf '%s' "$ARCHIVE_B64" | base64 --decode >"$archive"; then
echo "::error::archive_b64 is not valid base64."
exit 1
fi
actual_bytes="$(stat -c %s "$archive")"
actual_sha="$(sha256sum "$archive" | cut -d' ' -f1)"
if [ "$actual_bytes" != "$EXPECTED_BYTES" ] || [ "$actual_sha" != "$EXPECTED_SHA256" ]; then
echo "::error::The dispatched archive does not match the manifest entry (manifest: $EXPECTED_BYTES bytes, sha256 $EXPECTED_SHA256; received: $actual_bytes bytes, sha256 $actual_sha). Nothing was uploaded."
exit 1
fi
# The checksum proves these are the bytes the manifest describes; this
# proves the manifest describes a readable archive.
tar -tf "$archive" >/dev/null
# Same sidecar format pack.sh writes, so the artifact matches what a
# CI capture would have uploaded.
printf '%s %s\n' "$actual_sha" "$ARCHIVE_FILENAME" >"$archive.sha256"
echo "dir=$dir" >>"$GITHUB_OUTPUT"
echo "Verified $ARCHIVE_FILENAME: $actual_bytes bytes, sha256 $actual_sha."

- name: Upload fixture artifact
id: upload
uses: actions/upload-artifact@v4
with:
name: ${{ steps.entry.outputs.artifact_name }}
path: ${{ steps.archive.outputs.dir }}
retention-days: ${{ env.RETENTION_DAYS }}
if-no-files-found: error
# A name clash inside one run is a mis-dispatch, not a refresh.
overwrite: false
# Already zstd/gzip-compressed; zipping it again gains nothing.
compression-level: 0

- name: Record manifest values
env:
GH_TOKEN: ${{ github.token }}
ARTIFACT_ID: ${{ steps.upload.outputs.artifact-id }}
FIXTURE_ID: ${{ inputs.fixture_id }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
# The expiry actually granted: a repository-level retention setting
# can cap what RETENTION_DAYS asked for.
expires_at="$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$ARTIFACT_ID" --jq .expires_at)"
workflow="${GITHUB_WORKFLOW_REF%@*}"
workflow="${workflow##*/}"
{
echo "### Fixture \`$FIXTURE_ID\` uploaded"
echo
echo "Set these in the fixture's \`artifact\` block of \`tests/migration-fixtures/manifest.json\` and drop \`staging_note\`:"
echo
echo '```json'
jq -n --arg workflow "$workflow" --argjson run "$GITHUB_RUN_ID" \
--argjson days "$RETENTION_DAYS" --arg expires "$expires_at" \
'{workflow_name: $workflow, workflow_run_id: $run, retention_days: $days, expires_at: $expires}'
echo '```'
if [ -n "$RELEASE_TAG" ]; then
echo
echo "The archive came through the draft release \`$RELEASE_TAG\`. Delete it now:"
echo
echo '```sh'
echo "gh release delete '$RELEASE_TAG' --repo $GITHUB_REPOSITORY --cleanup-tag --yes"
echo '```'
fi
} >>"$GITHUB_STEP_SUMMARY"
Loading
Loading