deepspace 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +18 -7
- package/dist/cli.js +325 -13
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# deepspace
|
|
2
2
|
|
|
3
|
+
## 0.9.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Keep `deepspace app migrate` as the permanent ordered app-upgrade runner and
|
|
8
|
+
repair obsolete `x-app-name` platform identity wiring before a canonical app
|
|
9
|
+
deployment, without changing retained `APP_NAME` data namespaces. Track
|
|
10
|
+
source-only migration commits against live release lineage so both GitHub and
|
|
11
|
+
DeepSpace apps are guided through the required deploy.
|
|
12
|
+
|
|
3
13
|
## 0.9.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -90,9 +90,9 @@ npx deepspace deploy # deploy to *.app.space
|
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
The [normative CLI hierarchy](../../docs/platform/cli-contract.md#public-command-hierarchy)
|
|
93
|
-
keeps durable app
|
|
93
|
+
keeps durable app lifecycle and version migrations under `deepspace app`,
|
|
94
94
|
while checkout-oriented Git, workspace, release, and deploy operations stay
|
|
95
|
-
top-level. In particular,
|
|
95
|
+
top-level. In particular, app migration is
|
|
96
96
|
`deepspace app migrate`; there is no top-level `deepspace migrate` alias.
|
|
97
97
|
|
|
98
98
|
Every app has one authoritative Git repository. DeepSpace source is the packaged
|
|
@@ -118,17 +118,28 @@ atomic authority change; switching back uses the same commands. Commands support
|
|
|
118
118
|
[repository guide](https://github.com/deepdotspace/deepspace/blob/main/docs/platform/repo-store-git.md)
|
|
119
119
|
for workspaces, releases, and rollback.
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
`deepspace app migrate` is the permanent upgrade runner for breaking app
|
|
122
|
+
changes. It contains an ordered set of structural, idempotent migrations, so
|
|
123
|
+
agents and developers keep using the same command across releases:
|
|
123
124
|
|
|
124
125
|
```bash
|
|
125
126
|
npx deepspace app migrate --dry-run
|
|
126
127
|
npx deepspace app migrate
|
|
127
128
|
```
|
|
128
129
|
|
|
129
|
-
The dry-run lists
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
The dry-run lists pending source migrations without changing files. For a
|
|
131
|
+
legacy GitHub app whose `DEEPSPACE_APP_ID` is still name-shaped, it also lists
|
|
132
|
+
the exact registry rows that will be re-keyed and the physical stores that
|
|
133
|
+
remain at the existing resource id. The command applies only transformations
|
|
134
|
+
it recognizes safely, pauses for normal commit/push, and finishes with one
|
|
135
|
+
deploy. Source migration commits carry a machine-readable Git trailer, so the
|
|
136
|
+
next run can compare them with the live release and return the deploy action
|
|
137
|
+
without a local state file. Rerun after each returned action; when nothing is
|
|
138
|
+
pending it reports `up_to_date`. This is the same workflow for GitHub and
|
|
139
|
+
DeepSpace source; only their normal push behavior differs. `APP_NAME`-based
|
|
140
|
+
legacy room and storage addresses are retained; canonical `DEEPSPACE_APP_ID`
|
|
141
|
+
is used only for logical identity and platform authentication.
|
|
142
|
+
|
|
132
143
|
Keep deploy, release rollback, and undeploy idle from the mutating command until
|
|
133
144
|
that returned deploy begins; normal app traffic continues throughout.
|
|
134
145
|
Before that deploy starts, `--cancel` reverses a prepared migration and
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/cli/cli.ts
|
|
4
4
|
import { defineCommand as defineCommand14, runMain, runCommand } from "citty";
|
|
5
5
|
import { sync as spawnSync8 } from "cross-spawn";
|
|
6
|
-
import { readFileSync as
|
|
7
|
-
import { dirname as dirname10, join as
|
|
6
|
+
import { readFileSync as readFileSync26, existsSync as existsSync24 } from "fs";
|
|
7
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9
9
|
|
|
10
10
|
// src/cli/lib/spinner.ts
|
|
@@ -11783,6 +11783,177 @@ function sameRefs(left, right) {
|
|
|
11783
11783
|
return left.every((ref) => expected.get(ref.name) === ref.oid);
|
|
11784
11784
|
}
|
|
11785
11785
|
|
|
11786
|
+
// src/cli/lib/app-migrations.ts
|
|
11787
|
+
import { lstatSync, readFileSync as readFileSync25, writeFileSync as writeFileSync10 } from "fs";
|
|
11788
|
+
import { join as join23 } from "path";
|
|
11789
|
+
var CANONICAL_IDENTITY_WIRE_ID = "2026-08-canonical-app-identity-wire";
|
|
11790
|
+
var SOURCE_MIGRATIONS = [
|
|
11791
|
+
{
|
|
11792
|
+
id: CANONICAL_IDENTITY_WIRE_ID,
|
|
11793
|
+
description: "Use the immutable app id for authenticated platform requests",
|
|
11794
|
+
transform: migrateCanonicalIdentityWire,
|
|
11795
|
+
findBlockers: findLegacyIdentityWireBlockers
|
|
11796
|
+
}
|
|
11797
|
+
];
|
|
11798
|
+
var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
|
|
11799
|
+
({ id, description }) => ({ id, description })
|
|
11800
|
+
);
|
|
11801
|
+
function planAppMigrations(appDir) {
|
|
11802
|
+
const repo = repoToplevel(appDir);
|
|
11803
|
+
const appPrefix = gitLine(appDir, ["rev-parse", "--show-prefix"]);
|
|
11804
|
+
const tracked = splitNulFields(runGit(repo, ["ls-files", "-z"]).stdout);
|
|
11805
|
+
const perMigration = /* @__PURE__ */ new Map();
|
|
11806
|
+
const blockers = [];
|
|
11807
|
+
const edits = [];
|
|
11808
|
+
for (const repoPath of tracked) {
|
|
11809
|
+
if (!isSourcePath(repoPath, appPrefix)) continue;
|
|
11810
|
+
const absolutePath = join23(repo, repoPath);
|
|
11811
|
+
let stat;
|
|
11812
|
+
try {
|
|
11813
|
+
stat = lstatSync(absolutePath);
|
|
11814
|
+
} catch {
|
|
11815
|
+
continue;
|
|
11816
|
+
}
|
|
11817
|
+
if (!stat.isFile() || stat.isSymbolicLink()) continue;
|
|
11818
|
+
const before = readFileSync25(absolutePath, "utf-8");
|
|
11819
|
+
let after = before;
|
|
11820
|
+
for (const migration of SOURCE_MIGRATIONS) {
|
|
11821
|
+
const result = migration.transform(after);
|
|
11822
|
+
after = result.source;
|
|
11823
|
+
if (result.replacements > 0) {
|
|
11824
|
+
const planned = perMigration.get(migration.id) ?? {
|
|
11825
|
+
id: migration.id,
|
|
11826
|
+
description: migration.description,
|
|
11827
|
+
files: [],
|
|
11828
|
+
replacements: 0
|
|
11829
|
+
};
|
|
11830
|
+
planned.files.push(repoPath);
|
|
11831
|
+
planned.replacements += result.replacements;
|
|
11832
|
+
perMigration.set(migration.id, planned);
|
|
11833
|
+
}
|
|
11834
|
+
blockers.push(...migration.findBlockers(after, repoPath));
|
|
11835
|
+
}
|
|
11836
|
+
if (after !== before) edits.push({ path: repoPath, before, after });
|
|
11837
|
+
}
|
|
11838
|
+
return {
|
|
11839
|
+
pending: SOURCE_MIGRATIONS.flatMap((migration) => {
|
|
11840
|
+
const planned = perMigration.get(migration.id);
|
|
11841
|
+
return planned ? [{ ...planned, files: [...new Set(planned.files)].sort() }] : [];
|
|
11842
|
+
}),
|
|
11843
|
+
changedFiles: edits.map((edit) => edit.path).sort(),
|
|
11844
|
+
blockers,
|
|
11845
|
+
edits
|
|
11846
|
+
};
|
|
11847
|
+
}
|
|
11848
|
+
function applyAppMigrationPlan(appDir, plan) {
|
|
11849
|
+
if (plan.blockers.length > 0) {
|
|
11850
|
+
throw new Error("Cannot apply an app migration plan with unresolved blockers.");
|
|
11851
|
+
}
|
|
11852
|
+
const repo = repoToplevel(appDir);
|
|
11853
|
+
for (const edit of plan.edits) {
|
|
11854
|
+
const absolutePath = join23(repo, edit.path);
|
|
11855
|
+
if (readFileSync25(absolutePath, "utf-8") !== edit.before) {
|
|
11856
|
+
throw new Error(`Migration input changed after planning: ${edit.path}`);
|
|
11857
|
+
}
|
|
11858
|
+
}
|
|
11859
|
+
for (const edit of plan.edits) writeFileSync10(join23(repo, edit.path), edit.after);
|
|
11860
|
+
}
|
|
11861
|
+
function isSourcePath(repoPath, appPrefix) {
|
|
11862
|
+
if (appPrefix && !repoPath.startsWith(appPrefix)) return false;
|
|
11863
|
+
return /\.(?:[cm]?[jt]sx?)$/.test(repoPath);
|
|
11864
|
+
}
|
|
11865
|
+
function migrateCanonicalIdentityWire(source) {
|
|
11866
|
+
let replacements = 0;
|
|
11867
|
+
const lines = source.split("\n");
|
|
11868
|
+
for (let index = 0; index < lines.length; index++) {
|
|
11869
|
+
let line = lines[index];
|
|
11870
|
+
line = line.replace(
|
|
11871
|
+
/(\.set\(\s*)(['"])x-app-name\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
|
|
11872
|
+
(_match, open, quote, separator, receiver, close) => {
|
|
11873
|
+
replacements++;
|
|
11874
|
+
return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
|
|
11875
|
+
}
|
|
11876
|
+
);
|
|
11877
|
+
line = line.replace(
|
|
11878
|
+
/(['"])x-app-name\1(\s*:\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
|
|
11879
|
+
(_match, quote, separator, receiver) => {
|
|
11880
|
+
replacements++;
|
|
11881
|
+
return `${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
|
|
11882
|
+
}
|
|
11883
|
+
);
|
|
11884
|
+
line = line.replace(
|
|
11885
|
+
/(\[\s*)(['"])x-app-name\2(\s*\]\s*=\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
|
|
11886
|
+
(_match, open, quote, separator, receiver) => {
|
|
11887
|
+
replacements++;
|
|
11888
|
+
return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
|
|
11889
|
+
}
|
|
11890
|
+
);
|
|
11891
|
+
line = line.replace(
|
|
11892
|
+
/(forwardedParams\.set\(\s*)(['"])appName\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
|
|
11893
|
+
(_match, open, quote, separator, receiver, close) => {
|
|
11894
|
+
replacements++;
|
|
11895
|
+
return `${open}${quote}appId${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
|
|
11896
|
+
}
|
|
11897
|
+
);
|
|
11898
|
+
lines[index] = line;
|
|
11899
|
+
}
|
|
11900
|
+
if (replacements === 0) return { source, replacements };
|
|
11901
|
+
let migrated = lines.join("\n");
|
|
11902
|
+
if (/forwardedParams\.set\(\s*['"]appId['"]/.test(migrated)) {
|
|
11903
|
+
migrated = migrated.replaceAll("injectAppName", "injectAppId").replaceAll("`?appName=...`", "`?appId=...`").replaceAll("Inject appName into the query string", "Inject appId into the query string").replaceAll("caller-supplied appName", "caller-supplied appId").replaceAll(
|
|
11904
|
+
"/_deepspace/subscriptions/plans?appName=",
|
|
11905
|
+
"/_deepspace/subscriptions/plans?appId="
|
|
11906
|
+
);
|
|
11907
|
+
}
|
|
11908
|
+
migrated = migrated.replaceAll(
|
|
11909
|
+
"APP_IDENTITY_TOKEN + APP_NAME",
|
|
11910
|
+
"APP_IDENTITY_TOKEN + DEEPSPACE_APP_ID"
|
|
11911
|
+
);
|
|
11912
|
+
return {
|
|
11913
|
+
source: ensureCanonicalIdentityProperties(migrated),
|
|
11914
|
+
replacements
|
|
11915
|
+
};
|
|
11916
|
+
}
|
|
11917
|
+
function ensureCanonicalIdentityProperties(source) {
|
|
11918
|
+
const lines = source.split("\n");
|
|
11919
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
11920
|
+
const match = /^(\s*)(?:readonly\s+)?APP_NAME\??\s*:\s*string\s*([;,]?)\s*$/.exec(lines[index]);
|
|
11921
|
+
if (!match || blockAlreadyDeclaresCanonicalId(lines, index, match[1].length)) continue;
|
|
11922
|
+
const suffix = match[2] ?? "";
|
|
11923
|
+
lines.splice(index + 1, 0, `${match[1]}DEEPSPACE_APP_ID: string${suffix}`);
|
|
11924
|
+
}
|
|
11925
|
+
return lines.join("\n");
|
|
11926
|
+
}
|
|
11927
|
+
function blockAlreadyDeclaresCanonicalId(lines, propertyIndex, propertyIndent) {
|
|
11928
|
+
for (const direction of [-1, 1]) {
|
|
11929
|
+
for (let index = propertyIndex + direction; index >= 0 && index < lines.length; index += direction) {
|
|
11930
|
+
const line = lines[index];
|
|
11931
|
+
if (/^\s*(?:readonly\s+)?DEEPSPACE_APP_ID\??\s*:/.test(line)) return true;
|
|
11932
|
+
const trimmed = line.trimStart();
|
|
11933
|
+
if (!trimmed) continue;
|
|
11934
|
+
const indent = line.length - trimmed.length;
|
|
11935
|
+
if (indent < propertyIndent && (trimmed.includes("{") || trimmed.startsWith("}"))) break;
|
|
11936
|
+
}
|
|
11937
|
+
}
|
|
11938
|
+
return false;
|
|
11939
|
+
}
|
|
11940
|
+
function findLegacyIdentityWireBlockers(source, file) {
|
|
11941
|
+
const blockers = [];
|
|
11942
|
+
for (const [index, line] of source.split("\n").entries()) {
|
|
11943
|
+
const trimmed = line.trimStart();
|
|
11944
|
+
if (!/['"]x-app-name['"]/.test(line) || /\.delete\(\s*['"]x-app-name['"]/.test(line) || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
|
|
11945
|
+
continue;
|
|
11946
|
+
}
|
|
11947
|
+
blockers.push({
|
|
11948
|
+
migrationId: CANONICAL_IDENTITY_WIRE_ID,
|
|
11949
|
+
file,
|
|
11950
|
+
line: index + 1,
|
|
11951
|
+
message: "This app still sends the removed x-app-name identity header in a form the safe migration cannot rewrite."
|
|
11952
|
+
});
|
|
11953
|
+
}
|
|
11954
|
+
return blockers;
|
|
11955
|
+
}
|
|
11956
|
+
|
|
11786
11957
|
// src/cli/lib/identity-migration-api.ts
|
|
11787
11958
|
async function getIdentityMigration(deployUrl, token, appId) {
|
|
11788
11959
|
try {
|
|
@@ -11847,7 +12018,7 @@ var STRICT_APP_ID_RE2 = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
|
11847
12018
|
var migrate_default = defineDeepspaceCommand({
|
|
11848
12019
|
meta: {
|
|
11849
12020
|
name: "migrate",
|
|
11850
|
-
description: "
|
|
12021
|
+
description: "Apply pending DeepSpace app migrations without moving retained data"
|
|
11851
12022
|
},
|
|
11852
12023
|
args: {
|
|
11853
12024
|
remote: {
|
|
@@ -11920,14 +12091,54 @@ var migrate_default = defineDeepspaceCommand({
|
|
|
11920
12091
|
json: args.json
|
|
11921
12092
|
});
|
|
11922
12093
|
}
|
|
12094
|
+
const sourcePlan = planAppMigrations(appDir);
|
|
12095
|
+
if (sourcePlan.blockers.length > 0) {
|
|
12096
|
+
const first = sourcePlan.blockers[0];
|
|
12097
|
+
throw new Refusal(
|
|
12098
|
+
`Migration ${first.migrationId} needs a manual source update at ${first.file}:${first.line}. ${first.message}`,
|
|
12099
|
+
"source_migration_blocked",
|
|
12100
|
+
{ extra: { blockers: sourcePlan.blockers } }
|
|
12101
|
+
);
|
|
12102
|
+
}
|
|
11923
12103
|
if (STRICT_APP_ID_RE2.test(currentAppId)) {
|
|
11924
12104
|
if (!existingMigration) {
|
|
11925
|
-
|
|
11926
|
-
|
|
12105
|
+
if (args["dry-run"] && sourcePlan.pending.length > 0) {
|
|
12106
|
+
reportSourcePlan(sourcePlan, args.json);
|
|
12107
|
+
return {
|
|
12108
|
+
data: {
|
|
12109
|
+
status: "ready",
|
|
12110
|
+
appId: currentAppId,
|
|
12111
|
+
sourceMigrations: sourcePlan.pending
|
|
12112
|
+
}
|
|
12113
|
+
};
|
|
12114
|
+
}
|
|
12115
|
+
if (sourcePlan.pending.length > 0) {
|
|
12116
|
+
requireSourceMigrationWorktree(appDir);
|
|
12117
|
+
applyAppMigrationPlan(appDir, sourcePlan);
|
|
12118
|
+
throw sourceMigrationCommitRequired(appDir, sourcePlan);
|
|
12119
|
+
}
|
|
12120
|
+
await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token);
|
|
12121
|
+
reportUpToDate(currentAppId, args.json);
|
|
12122
|
+
return { data: { status: "up_to_date", appId: currentAppId } };
|
|
12123
|
+
}
|
|
12124
|
+
if (sourcePlan.pending.length === 0 && existingMigration.status === "verified") {
|
|
12125
|
+
await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token);
|
|
11927
12126
|
}
|
|
11928
12127
|
if (args["dry-run"]) {
|
|
11929
12128
|
reportMigration(existingMigration, args.json);
|
|
11930
|
-
|
|
12129
|
+
reportSourcePlan(sourcePlan, args.json);
|
|
12130
|
+
return {
|
|
12131
|
+
data: {
|
|
12132
|
+
status: existingMigration.status,
|
|
12133
|
+
migration: existingMigration,
|
|
12134
|
+
sourceMigrations: sourcePlan.pending
|
|
12135
|
+
}
|
|
12136
|
+
};
|
|
12137
|
+
}
|
|
12138
|
+
if (sourcePlan.pending.length > 0) {
|
|
12139
|
+
requireSourceMigrationWorktree(appDir, existingMigration.status === "prepared");
|
|
12140
|
+
applyAppMigrationPlan(appDir, sourcePlan);
|
|
12141
|
+
throw existingMigration.status === "prepared" ? commitRequired(appDir, existingMigration, envName, sourcePlan.changedFiles) : sourceMigrationCommitRequired(appDir, sourcePlan);
|
|
11931
12142
|
}
|
|
11932
12143
|
return continuePreparedMigration({
|
|
11933
12144
|
appDir,
|
|
@@ -11978,6 +12189,7 @@ var migrate_default = defineDeepspaceCommand({
|
|
|
11978
12189
|
);
|
|
11979
12190
|
console.log("No changes made.");
|
|
11980
12191
|
}
|
|
12192
|
+
reportSourcePlan(sourcePlan, args.json);
|
|
11981
12193
|
return {
|
|
11982
12194
|
data: {
|
|
11983
12195
|
status: "ready",
|
|
@@ -11988,6 +12200,7 @@ var migrate_default = defineDeepspaceCommand({
|
|
|
11988
12200
|
branch: head.branch,
|
|
11989
12201
|
commitOid: head.oid,
|
|
11990
12202
|
sourceClaimRequired: inventory.app.sourceClaimRequired,
|
|
12203
|
+
sourceMigrations: sourcePlan.pending,
|
|
11991
12204
|
inventory
|
|
11992
12205
|
}
|
|
11993
12206
|
};
|
|
@@ -11997,8 +12210,9 @@ var migrate_default = defineDeepspaceCommand({
|
|
|
11997
12210
|
repository: remote.repository,
|
|
11998
12211
|
expectedSourceRevision: inventory.app.sourceRevision
|
|
11999
12212
|
});
|
|
12213
|
+
applyAppMigrationPlan(appDir, sourcePlan);
|
|
12000
12214
|
writeAppId(appDir, migration.appId, { wranglerEnv: envName, force: true });
|
|
12001
|
-
throw commitRequired(appDir, migration, envName);
|
|
12215
|
+
throw commitRequired(appDir, migration, envName, sourcePlan.changedFiles);
|
|
12002
12216
|
}
|
|
12003
12217
|
});
|
|
12004
12218
|
async function recoverMigration(input) {
|
|
@@ -12128,10 +12342,11 @@ function requirePublishedHead2(appDir, remote) {
|
|
|
12128
12342
|
}
|
|
12129
12343
|
return head;
|
|
12130
12344
|
}
|
|
12131
|
-
function commitRequired(appDir, migration, envName) {
|
|
12345
|
+
function commitRequired(appDir, migration, envName, additionalPaths = []) {
|
|
12132
12346
|
const wranglerPath = wranglerRepoPath(appDir);
|
|
12347
|
+
const migrationPaths = [.../* @__PURE__ */ new Set([wranglerPath, ...additionalPaths])].sort();
|
|
12133
12348
|
return new Refusal(
|
|
12134
|
-
`Prepared ${migration.legacyAppId} \u2192 ${migration.appId}. Commit the deterministic
|
|
12349
|
+
`Prepared ${migration.legacyAppId} \u2192 ${migration.appId}. Commit the deterministic app migration changes, then rerun.`,
|
|
12135
12350
|
"migration_commit_required",
|
|
12136
12351
|
{
|
|
12137
12352
|
actionRequired: true,
|
|
@@ -12144,13 +12359,102 @@ function commitRequired(appDir, migration, envName) {
|
|
|
12144
12359
|
"--message",
|
|
12145
12360
|
`Migrate DeepSpace app identity to ${migration.appId}`,
|
|
12146
12361
|
"--",
|
|
12147
|
-
|
|
12362
|
+
...migrationPaths
|
|
12148
12363
|
]
|
|
12149
12364
|
},
|
|
12150
12365
|
extra: { migration, env: envName ?? null }
|
|
12151
12366
|
}
|
|
12152
12367
|
);
|
|
12153
12368
|
}
|
|
12369
|
+
function sourceMigrationCommitRequired(appDir, plan) {
|
|
12370
|
+
const migrationIds = plan.pending.map((migration) => migration.id);
|
|
12371
|
+
return new Refusal(
|
|
12372
|
+
`Applied ${migrationIds.join(", ")}. Commit the deterministic app migration changes, then rerun.`,
|
|
12373
|
+
"migration_commit_required",
|
|
12374
|
+
{
|
|
12375
|
+
actionRequired: true,
|
|
12376
|
+
action: {
|
|
12377
|
+
cwd: repoToplevel(appDir),
|
|
12378
|
+
argv: [
|
|
12379
|
+
"git",
|
|
12380
|
+
"commit",
|
|
12381
|
+
"--only",
|
|
12382
|
+
"--message",
|
|
12383
|
+
"Apply DeepSpace app migrations",
|
|
12384
|
+
"--message",
|
|
12385
|
+
`DeepSpace-Migrations: ${migrationIds.join(",")}`,
|
|
12386
|
+
"--",
|
|
12387
|
+
...plan.changedFiles
|
|
12388
|
+
]
|
|
12389
|
+
},
|
|
12390
|
+
extra: { sourceMigrations: plan.pending }
|
|
12391
|
+
}
|
|
12392
|
+
);
|
|
12393
|
+
}
|
|
12394
|
+
async function requireSourceMigrationDeploy(appDir, appId, deployUrl, token) {
|
|
12395
|
+
if (!isWorkTreeClean(appDir)) {
|
|
12396
|
+
throw new Refusal(
|
|
12397
|
+
"Commit or discard local changes before checking app migration deployment state.",
|
|
12398
|
+
"dirty_worktree"
|
|
12399
|
+
);
|
|
12400
|
+
}
|
|
12401
|
+
const head = resolveCommit(appDir, "HEAD");
|
|
12402
|
+
if (!head) return;
|
|
12403
|
+
const repo = repoToplevel(appDir);
|
|
12404
|
+
const knownIds = new Set(APP_MIGRATION_DEFINITIONS.map((migration) => migration.id));
|
|
12405
|
+
const migrationCommits = splitNulFields(
|
|
12406
|
+
runGit(repo, [
|
|
12407
|
+
"log",
|
|
12408
|
+
"--format=%H%x00",
|
|
12409
|
+
"--extended-regexp",
|
|
12410
|
+
"--grep",
|
|
12411
|
+
`^DeepSpace-Migrations:.*(${[...knownIds].join("|")})`
|
|
12412
|
+
]).stdout
|
|
12413
|
+
).map((oid) => oid.trim()).filter((oid) => /^[0-9a-f]{40}$/.test(oid));
|
|
12414
|
+
if (migrationCommits.length === 0) return;
|
|
12415
|
+
const { release: release2 } = await repoApi(deployUrl, token, appId).latestRelease();
|
|
12416
|
+
if (release2?.commitOid === head) return;
|
|
12417
|
+
const liveCommit = release2?.commitOid ?? null;
|
|
12418
|
+
if (liveCommit && (resolveCommit(appDir, liveCommit) === null || !isAncestor(appDir, liveCommit, head))) {
|
|
12419
|
+
throw new Refusal(
|
|
12420
|
+
`The live release commit ${liveCommit.slice(0, 10)} is not an ancestor of this checkout, so migration deployment state cannot be proven. Fetch and integrate the live source history first.`,
|
|
12421
|
+
"migration_lineage_unknown",
|
|
12422
|
+
{ extra: { appId, liveCommitOid: liveCommit, commitOid: head } }
|
|
12423
|
+
);
|
|
12424
|
+
}
|
|
12425
|
+
const undeployedMigrationCommits = migrationCommits.filter(
|
|
12426
|
+
(oid) => !liveCommit || oid !== liveCommit && isAncestor(appDir, liveCommit, oid)
|
|
12427
|
+
);
|
|
12428
|
+
const pendingIds = [
|
|
12429
|
+
...new Set(
|
|
12430
|
+
undeployedMigrationCommits.flatMap((oid) => {
|
|
12431
|
+
const message = runGit(repo, ["show", "-s", "--format=%B", oid]).stdout.toString("utf-8");
|
|
12432
|
+
return [...message.matchAll(/^DeepSpace-Migrations:\s*(.+)$/gm)].flatMap(
|
|
12433
|
+
(match) => (match[1] ?? "").split(",").map((id) => id.trim()).filter((id) => knownIds.has(id))
|
|
12434
|
+
);
|
|
12435
|
+
})
|
|
12436
|
+
)
|
|
12437
|
+
];
|
|
12438
|
+
if (pendingIds.length === 0) return;
|
|
12439
|
+
throw new Refusal(
|
|
12440
|
+
`App migrations ${pendingIds.join(", ")} are committed but are not in the live release. Deploy them, then rerun.`,
|
|
12441
|
+
"migration_deploy_required",
|
|
12442
|
+
{
|
|
12443
|
+
actionRequired: true,
|
|
12444
|
+
action: { cwd: appDir, argv: ["deepspace", "deploy"] },
|
|
12445
|
+
extra: { appId, sourceMigrations: pendingIds, commitOid: head }
|
|
12446
|
+
}
|
|
12447
|
+
);
|
|
12448
|
+
}
|
|
12449
|
+
function requireSourceMigrationWorktree(appDir, allowDirtyWrangler = false) {
|
|
12450
|
+
if (isWorkTreeClean(appDir)) return;
|
|
12451
|
+
const changed = statusFiles(repoToplevel(appDir));
|
|
12452
|
+
if (allowDirtyWrangler && changed.every((path) => path === wranglerRepoPath(appDir))) return;
|
|
12453
|
+
throw new Refusal(
|
|
12454
|
+
"Commit or discard unrelated working-tree changes before applying app migrations.",
|
|
12455
|
+
"dirty_worktree"
|
|
12456
|
+
);
|
|
12457
|
+
}
|
|
12154
12458
|
function restoreLegacyCommitRequired(appDir, migration, envName) {
|
|
12155
12459
|
const wranglerPath = wranglerRepoPath(appDir);
|
|
12156
12460
|
return new Refusal(
|
|
@@ -12200,8 +12504,16 @@ function deployRequired(appDir, migration, envName) {
|
|
|
12200
12504
|
}
|
|
12201
12505
|
);
|
|
12202
12506
|
}
|
|
12203
|
-
function
|
|
12204
|
-
if (!json) console.log(
|
|
12507
|
+
function reportUpToDate(appId, json) {
|
|
12508
|
+
if (!json) console.log(`No pending DeepSpace app migrations for ${appId}.`);
|
|
12509
|
+
}
|
|
12510
|
+
function reportSourcePlan(plan, json) {
|
|
12511
|
+
if (json || plan.pending.length === 0) return;
|
|
12512
|
+
for (const migration of plan.pending) {
|
|
12513
|
+
console.log(
|
|
12514
|
+
`Pending source migration: ${migration.id} (${migration.files.length} file${migration.files.length === 1 ? "" : "s"})`
|
|
12515
|
+
);
|
|
12516
|
+
}
|
|
12205
12517
|
}
|
|
12206
12518
|
function reportMigration(migration, json) {
|
|
12207
12519
|
if (json) return;
|
|
@@ -12213,7 +12525,7 @@ function reportMigration(migration, json) {
|
|
|
12213
12525
|
|
|
12214
12526
|
// src/cli/cli.ts
|
|
12215
12527
|
var __dirname = dirname10(fileURLToPath3(import.meta.url));
|
|
12216
|
-
var pkg = JSON.parse(
|
|
12528
|
+
var pkg = JSON.parse(readFileSync26(join24(__dirname, "..", "package.json"), "utf-8"));
|
|
12217
12529
|
if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync24("/etc/ssl/certs/ca-certificates.crt")) {
|
|
12218
12530
|
process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
|
|
12219
12531
|
}
|