Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/migrate-runs-undone-unconfirmed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": patch
---

`clerk migrate runs <id>` no longer lists a user as "Not confirmed" once an undo of the run deleted it. Undo deletes a user whose create was cut off when it finds it by the run's marker. A user no undo deleted is still listed, since Clerk may hold it. `clerk migrate` is still experimental and needs `CLERK_EXPERIMENTAL=migrate`.
7 changes: 4 additions & 3 deletions packages/cli-core/src/commands/migrate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,9 +1027,10 @@ clerk migrate runs --json
It prints the runs folder first. The listing shows each run's ID, date, kind,
status, target, file and counts. `runs <id>` adds the error breakdown and the
users that failed, were skipped, or are `creating` with no answer to confirm the
create (`unconfirmed` in `--json`), with the path to the full record, then what
to run next: for an interrupted or partial import, the import that continues it,
with the flags it ran with, then the undo. The continue is left out when
create (`unconfirmed` in `--json`, leaving out any an undo of the run deleted:
it finds an in-flight user by the run's marker), with the path to the full
record, then what to run next: for an interrupted or partial import,
the import that continues it, with the flags it ran with, then the undo. The continue is left out when
importing the file again might not reach this run: a newer run of the same file
and source that may be for the same instance (a run under a key's stand-in ID
could be for any instance), a file changed since, or any undo of it. It is also
Expand Down
32 changes: 32 additions & 0 deletions packages/cli-core/src/commands/migrate/runs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { quoteArg } from "../../lib/json-body.ts";
import { captureTelemetryPayload, useCaptureLog } from "../../test/lib/stubs.ts";
import {
continueRun,
patchRun,
type RunRecord,
sha256File,
startRun,
Expand Down Expand Up @@ -172,6 +173,37 @@ describe("runs <id>", () => {
expect(err).toContain("1 user: bad gateway (a re-run checks)");
});

// Undo finds an in-flight user by the run's marker and deletes it, writing
// the delete to its own run. One it didn't delete may still be in Clerk.
test("lists no unconfirmed user an undo deleted, and keeps the rest", async () => {
fs.writeFileSync(usersFile(), "[]");
const run = startRun(runsDir, {
kind: "import",
target: TARGET,
source: "clerk",
file: { path: usersFile(), sha256: sha256File(usersFile()) },
});
run.append({ sourceId: "a", status: "creating" });
run.append({ sourceId: "b", status: "creating" });
run.release();
const undo = startRun(runsDir, { kind: "undo", target: TARGET, undoes: run.record.id });
undo.append({ sourceId: "a", clerkId: "user_a", status: "deleted" });
undo.finish();
patchRun(runsDir, run.record.id, { status: "undone" });

await runs(run.record.id, { runsDir, json: true });
expect(JSON.parse(captured.out)).toMatchObject({
run: { state: "undone" },
unconfirmed: [{ sourceId: "b" }],
});

captured.clear();
await runs(run.record.id, { runsDir });
const err = Bun.stripANSI(captured.err);
expect(err).toContain("Not confirmed (Clerk may have created them; a continue checks) (1)");
expect(err).not.toMatch(/^ {2}a\b/m);
});

// A stopped import's unsent users have no lines: only the count says so.
test("shows the users a stopped import never sent, in the list and the run", async () => {
fs.writeFileSync(usersFile(), "[]");
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/commands/migrate/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,23 @@ function printList(runsDir: string, records: RunRecord[]): void {
function showJson(runsDir: string, record: RunRecord, carry: string[]) {
const lines = [...latestUserLines(runsDir, record.id).values()];
const state = runState(runsDir, record);
// Undo writes its `deleted` lines to its own run: a user one of them
// deleted is gone, whatever the import's last line for it says.
const deleted = new Set(
undosOf(listRuns(runsDir), record.id).flatMap((undo) =>
[...latestUserLines(runsDir, undo.id).values()]
.filter((line) => line.status === "deleted")
.map((line) => line.sourceId),
),
);
return {
runsDir,
run: { ...record, state },
next: nextCommands(runsDir, record, state, carry, lines),
errors: errorBreakdown(lines),
failed: lines.filter((line) => line.status === "failed"),
skipped: lines.filter((line) => line.status === "skipped"),
unconfirmed: lines.filter((line) => line.status === "creating"),
unconfirmed: lines.filter((line) => line.status === "creating" && !deleted.has(line.sourceId)),
};
}

Expand Down
Loading