sb-mig 6.2.0 → 6.3.0-beta.2
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/README.md +15 -4
- package/dist/api/assets/asset-folders.d.ts +3 -0
- package/dist/api/assets/asset-folders.js +56 -0
- package/dist/api/assets/asset-folders.types.d.ts +29 -0
- package/dist/api/assets/asset-folders.types.js +1 -0
- package/dist/api/assets/assets.d.ts +4 -1
- package/dist/api/assets/assets.js +61 -16
- package/dist/api/assets/assets.types.d.ts +12 -1
- package/dist/api/assets/index.d.ts +4 -2
- package/dist/api/assets/index.js +2 -1
- package/dist/api/copy/assets.d.ts +12 -0
- package/dist/api/copy/assets.js +84 -0
- package/dist/api/copy/graph.d.ts +8 -0
- package/dist/api/copy/graph.js +27 -0
- package/dist/api/copy/index.d.ts +6 -0
- package/dist/api/copy/index.js +6 -0
- package/dist/api/copy/manifest.d.ts +23 -0
- package/dist/api/copy/manifest.js +115 -0
- package/dist/api/copy/reference-rewriter.d.ts +6 -0
- package/dist/api/copy/reference-rewriter.js +153 -0
- package/dist/api/copy/reference-scanner.d.ts +23 -0
- package/dist/api/copy/reference-scanner.js +322 -0
- package/dist/api/copy/types.d.ts +196 -0
- package/dist/api/copy/types.js +1 -0
- package/dist/api/data-migration/component-data-migration.d.ts +81 -0
- package/dist/api/data-migration/component-data-migration.js +391 -95
- package/dist/api/data-migration/migration-run-log.d.ts +3 -1
- package/dist/api/data-migration/migration-run-log.js +2 -1
- package/dist/api/managementApi.d.ts +6 -0
- package/dist/api/stories/stories.js +3 -3
- package/dist/api/stories/stories.types.d.ts +3 -1
- package/dist/api/testApi.d.ts +6 -0
- package/dist/api-v2/requestConfig.d.ts +17 -1
- package/dist/api-v2/requestConfig.js +28 -0
- package/dist/cli/cli-descriptions.d.ts +3 -3
- package/dist/cli/cli-descriptions.js +92 -18
- package/dist/cli/commands/copy.js +2091 -145
- package/dist/cli/commands/migrate.js +27 -1
- package/dist/cli/index.js +3 -0
- package/dist/utils/files.d.ts +2 -0
- package/dist/utils/files.js +86 -3
- package/dist-cjs/api/stories/stories.js +3 -3
- package/dist-cjs/api-v2/requestConfig.js +29 -0
- package/package.json +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { PublishLanguagesOption } from "../stories/stories.types.js";
|
|
2
2
|
import type { RequestBaseConfig } from "../utils/request.js";
|
|
3
|
+
import { type LanguagePublishStateMap } from "../stories/language-publish-state.js";
|
|
3
4
|
import { type MigrationComponentAliasesByMigration, type MigrationComponentOverridesByMigration } from "./migration-component-scope.js";
|
|
4
5
|
import { type PreparedMigrationValidator } from "./migration-validation.js";
|
|
6
|
+
import { type PublishedLayerContext } from "./published-layer.js";
|
|
5
7
|
export type MigrateFrom = "file" | "space";
|
|
6
8
|
export type PublicationMode = "preserve-layers" | "collapse-draft" | "save-only";
|
|
7
9
|
export interface PreparedMigrationConfig {
|
|
@@ -73,6 +75,60 @@ export declare const runMigrationPipelineInMemory: ({ itemType, itemsToMigrate,
|
|
|
73
75
|
preparedMigrationConfigs: PreparedMigrationConfig[];
|
|
74
76
|
}) => MigrationPipelineResult;
|
|
75
77
|
export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publicationMode, publicationLanguages, fromFilePath, fileName, languagePublishStatePath, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
|
|
78
|
+
export interface ContinueManifest {
|
|
79
|
+
manifestVersion: 1;
|
|
80
|
+
createdAt?: string;
|
|
81
|
+
kind: "migrate-content-continue-manifest";
|
|
82
|
+
itemType: "story" | "preset";
|
|
83
|
+
from: string;
|
|
84
|
+
to: string;
|
|
85
|
+
migrateFrom: MigrateFrom;
|
|
86
|
+
fromFilePath?: string | null;
|
|
87
|
+
publicationMode: PublicationMode;
|
|
88
|
+
migrationConfigNames: string[];
|
|
89
|
+
publishLanguages?: {
|
|
90
|
+
requested?: PublishLanguagesOption;
|
|
91
|
+
} | null;
|
|
92
|
+
resolvedPublishLanguages?: string[] | null;
|
|
93
|
+
artifactBaseName: string;
|
|
94
|
+
useDatestamp: boolean;
|
|
95
|
+
artifacts: {
|
|
96
|
+
changedItems: string;
|
|
97
|
+
pipelineSummary: string;
|
|
98
|
+
inputFull?: string | null;
|
|
99
|
+
languageStateMap?: string | null;
|
|
100
|
+
draftAfterFull?: string | null;
|
|
101
|
+
publishedAfterFull?: string | null;
|
|
102
|
+
dirtyPublishedRecords?: string | null;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
interface FinalizeMigrationArgs {
|
|
106
|
+
itemType: "story" | "preset";
|
|
107
|
+
from: string;
|
|
108
|
+
to: string;
|
|
109
|
+
migrateFrom: MigrateFrom;
|
|
110
|
+
fromFilePath?: string;
|
|
111
|
+
languagePublishStatePath?: string;
|
|
112
|
+
publicationMode: PublicationMode;
|
|
113
|
+
effectivePublicationLanguages?: PublishLanguagesOption;
|
|
114
|
+
resolvedPublishLanguages?: string[];
|
|
115
|
+
languagePublishStateMap?: LanguagePublishStateMap;
|
|
116
|
+
appliedMigrationNames: string[];
|
|
117
|
+
pipelineResult: MigrationPipelineResult;
|
|
118
|
+
publishedLayerContext?: PublishedLayerContext;
|
|
119
|
+
publishedLayerPipelineResult: MigrationPipelineResult | null;
|
|
120
|
+
artifactBaseName: string;
|
|
121
|
+
useDatestamp: boolean;
|
|
122
|
+
continuedFromManifest?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Everything `doTheMigration` does AFTER the dry-run preview returns:
|
|
126
|
+
* mark migrations as applied, write the changes to Storyblok, and write the
|
|
127
|
+
* run log. Extracted so `migrate continue` can run the exact same path against
|
|
128
|
+
* inputs reconstructed from dry-run artifacts (instead of re-pulling + re-transforming).
|
|
129
|
+
*/
|
|
130
|
+
export declare const finalizeMigration: ({ itemType, from, to, migrateFrom, fromFilePath, languagePublishStatePath, publicationMode, effectivePublicationLanguages, resolvedPublishLanguages, languagePublishStateMap, appliedMigrationNames, pipelineResult, publishedLayerContext, publishedLayerPipelineResult, artifactBaseName, useDatestamp, continuedFromManifest, }: FinalizeMigrationArgs, config: RequestBaseConfig) => Promise<void>;
|
|
131
|
+
export declare const findArtifactFilename: (files: string[], stem: string) => string | null;
|
|
76
132
|
export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publicationMode, publicationLanguages, migrateFrom, fromFilePath, fileName, languagePublishStatePath, }: {
|
|
77
133
|
itemType?: "story" | "preset";
|
|
78
134
|
from: string;
|
|
@@ -89,4 +145,29 @@ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migratio
|
|
|
89
145
|
languagePublishStatePath?: string;
|
|
90
146
|
}, config: RequestBaseConfig) => Promise<void>;
|
|
91
147
|
export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publicationMode, publicationLanguages, fromFilePath, fileName, languagePublishStatePath, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
|
|
148
|
+
export interface ContinuePlanSummary {
|
|
149
|
+
manifestFileName: string;
|
|
150
|
+
from: string;
|
|
151
|
+
to: string;
|
|
152
|
+
itemType: "story" | "preset";
|
|
153
|
+
publicationMode: PublicationMode;
|
|
154
|
+
migrationConfigNames: string[];
|
|
155
|
+
resolvedPublishLanguages: string[];
|
|
156
|
+
changedCount: number;
|
|
157
|
+
dirtyPublishedCount: number;
|
|
158
|
+
artifactFiles: string[];
|
|
159
|
+
}
|
|
160
|
+
export interface ContinuePlan {
|
|
161
|
+
summary: ContinuePlanSummary;
|
|
162
|
+
run: () => Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Discover the dry-run `continue` manifest in the migrations folder, load the
|
|
166
|
+
* artifacts it points at, and reconstruct the exact inputs `finalizeMigration`
|
|
167
|
+
* needs. Returns a summary (for the confirmation prompt) and a `run()` thunk that
|
|
168
|
+
* performs the write. Does NOT pull stories, transform, or fetch published layers.
|
|
169
|
+
*/
|
|
170
|
+
export declare const prepareContinueMigration: ({ manifestFileName }: {
|
|
171
|
+
manifestFileName?: string;
|
|
172
|
+
}, config: RequestBaseConfig) => Promise<ContinuePlan>;
|
|
92
173
|
export {};
|
|
@@ -2,7 +2,7 @@ import path from "path";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { discoverMigrationConfig, discoverStories, LOOKUP_TYPE, SCOPE, } from "../../cli/utils/discover.js";
|
|
4
4
|
import storyblokConfig from "../../config/config.js";
|
|
5
|
-
import { createAndSaveToFile, getFileContentWithRequire, getFilesContentWithRequire, } from "../../utils/files.js";
|
|
5
|
+
import { createAndSaveToFile, getFileContentWithRequire, getFilesContentWithRequire, listFilesInDir, readFile, } from "../../utils/files.js";
|
|
6
6
|
import Logger from "../../utils/logger.js";
|
|
7
7
|
import { modifyOrCreateAppliedMigrationsFile } from "../../utils/migrations.js";
|
|
8
8
|
import { isObjectEmpty } from "../../utils/object-utils.js";
|
|
@@ -810,6 +810,222 @@ export const migrateAllComponentsDataInStories = async ({ itemType, migrationCon
|
|
|
810
810
|
preparedMigrationConfigs,
|
|
811
811
|
}, config);
|
|
812
812
|
};
|
|
813
|
+
/**
|
|
814
|
+
* Everything `doTheMigration` does AFTER the dry-run preview returns:
|
|
815
|
+
* mark migrations as applied, write the changes to Storyblok, and write the
|
|
816
|
+
* run log. Extracted so `migrate continue` can run the exact same path against
|
|
817
|
+
* inputs reconstructed from dry-run artifacts (instead of re-pulling + re-transforming).
|
|
818
|
+
*/
|
|
819
|
+
export const finalizeMigration = async ({ itemType, from, to, migrateFrom, fromFilePath, languagePublishStatePath, publicationMode, effectivePublicationLanguages, resolvedPublishLanguages, languagePublishStateMap, appliedMigrationNames, pipelineResult, publishedLayerContext, publishedLayerPipelineResult, artifactBaseName, useDatestamp, continuedFromManifest, }, config) => {
|
|
820
|
+
for (const migrationConfigName of appliedMigrationNames) {
|
|
821
|
+
await modifyOrCreateAppliedMigrationsFile(migrationConfigName, itemType);
|
|
822
|
+
}
|
|
823
|
+
const publishedLayerChangedCount = publishedLayerPipelineResult?.changedItems.length ?? 0;
|
|
824
|
+
if (pipelineResult.changedItems.length === 0 &&
|
|
825
|
+
publishedLayerChangedCount === 0) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
let writeResults = [];
|
|
829
|
+
if (itemType === "story") {
|
|
830
|
+
if (shouldUsePublishedLayerMode(publicationMode) &&
|
|
831
|
+
publishedLayerContext) {
|
|
832
|
+
const dirtyStoryIds = new Set(publishedLayerContext.dirtyPublishedRecords.map((record) => String(record.storyId)));
|
|
833
|
+
const nonDirtyChangedItems = pipelineResult.changedItems.filter((item) => !dirtyStoryIds.has(storyIdOf(item)));
|
|
834
|
+
const nonDirtyWriteResults = nonDirtyChangedItems.length > 0
|
|
835
|
+
? await managementApi.stories.updateStories({
|
|
836
|
+
stories: nonDirtyChangedItems,
|
|
837
|
+
spaceId: to,
|
|
838
|
+
options: {
|
|
839
|
+
publish: shouldPublishForPublicationMode(publicationMode),
|
|
840
|
+
publishLanguages: resolvedPublishLanguages,
|
|
841
|
+
preservePublishState: shouldPublishForPublicationMode(publicationMode),
|
|
842
|
+
languagePublishStateMap,
|
|
843
|
+
},
|
|
844
|
+
}, config)
|
|
845
|
+
: [];
|
|
846
|
+
const dirtyWriteResults = publishedLayerPipelineResult
|
|
847
|
+
? await writeDirtyPublishedLayerStories({
|
|
848
|
+
context: publishedLayerContext,
|
|
849
|
+
draftPipelineResult: pipelineResult,
|
|
850
|
+
publishedPipelineResult: publishedLayerPipelineResult,
|
|
851
|
+
to,
|
|
852
|
+
resolvedPublishLanguages,
|
|
853
|
+
languagePublishStateMap,
|
|
854
|
+
}, config)
|
|
855
|
+
: [];
|
|
856
|
+
writeResults = [...nonDirtyWriteResults, ...dirtyWriteResults];
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
writeResults = await managementApi.stories.updateStories({
|
|
860
|
+
stories: pipelineResult.changedItems,
|
|
861
|
+
spaceId: to,
|
|
862
|
+
options: {
|
|
863
|
+
publish: shouldPublishForPublicationMode(publicationMode),
|
|
864
|
+
publishLanguages: resolvedPublishLanguages,
|
|
865
|
+
preservePublishState: shouldPublishForPublicationMode(publicationMode),
|
|
866
|
+
publishDirtyPublishedStories: shouldPublishDirtyDrafts(publicationMode),
|
|
867
|
+
languagePublishStateMap,
|
|
868
|
+
},
|
|
869
|
+
}, config);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else if (itemType === "preset") {
|
|
873
|
+
writeResults = await managementApi.presets.updatePresets({
|
|
874
|
+
presets: pipelineResult.changedItems,
|
|
875
|
+
spaceId: to,
|
|
876
|
+
options: {},
|
|
877
|
+
}, config);
|
|
878
|
+
}
|
|
879
|
+
const writeSummary = summarizeMutationWriteResults(writeResults);
|
|
880
|
+
try {
|
|
881
|
+
await saveMigrationRunLog({
|
|
882
|
+
artifactBaseName,
|
|
883
|
+
useDatestamp,
|
|
884
|
+
from,
|
|
885
|
+
to,
|
|
886
|
+
itemType,
|
|
887
|
+
dryRun: false,
|
|
888
|
+
publish: shouldPublishForPublicationMode(publicationMode),
|
|
889
|
+
publishLanguages: effectivePublicationLanguages,
|
|
890
|
+
resolvedPublishLanguages,
|
|
891
|
+
migrateFrom,
|
|
892
|
+
fromFilePath,
|
|
893
|
+
languagePublishStatePath,
|
|
894
|
+
pipelineResult,
|
|
895
|
+
writeResults,
|
|
896
|
+
writeSummary,
|
|
897
|
+
continuedFromManifest,
|
|
898
|
+
}, config);
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
Logger.warning(`[MIGRATION] Could not write migration run log: ${error instanceof Error ? error.message : String(error)}`);
|
|
902
|
+
}
|
|
903
|
+
if (writeSummary.failed === 0) {
|
|
904
|
+
Logger.success(`[MIGRATION] Update complete. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully.`);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
Logger.warning(`[MIGRATION] Update complete with partial failures. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully, ${writeSummary.failed} failed.`);
|
|
908
|
+
writeSummary.failedItems.slice(0, 10).forEach((item) => {
|
|
909
|
+
const label = item.slug || item.name || item.id || "unknown";
|
|
910
|
+
Logger.error(`[MIGRATION] Failed ${itemType}: ${String(label)}`);
|
|
911
|
+
});
|
|
912
|
+
if (writeSummary.failedItems.length > 10) {
|
|
913
|
+
Logger.warning(`[MIGRATION] Showing first 10 failed ${itemType}(s) only.`);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
/**
|
|
917
|
+
* Sortable key for an artifact's datestamp suffix.
|
|
918
|
+
*
|
|
919
|
+
* Datestamps use the non-zero-padded `YYYY-M-D_H-M` format from
|
|
920
|
+
* generateDatestamp, so a plain string sort is NOT chronological
|
|
921
|
+
* (e.g. "2026-1-15_9-30" sorts before "2026-1-9_9-30"). Parse the parts and
|
|
922
|
+
* compare numerically instead. Returns -1 for the non-datestamped variant
|
|
923
|
+
* (`${stem}.json`), which only exists when --fileName is used (single match,
|
|
924
|
+
* never mixed with datestamped files for the same stem).
|
|
925
|
+
*/
|
|
926
|
+
const artifactDatestampKey = (filename, stem) => {
|
|
927
|
+
const prefix = `${stem}__`;
|
|
928
|
+
if (!filename.startsWith(prefix) || !filename.endsWith(".json")) {
|
|
929
|
+
return -1;
|
|
930
|
+
}
|
|
931
|
+
const stamp = filename.slice(prefix.length, -".json".length);
|
|
932
|
+
const match = /^(\d+)-(\d+)-(\d+)_(\d+)-(\d+)$/.exec(stamp);
|
|
933
|
+
if (!match) {
|
|
934
|
+
return -1;
|
|
935
|
+
}
|
|
936
|
+
const [, year, month, day, hour, minute] = match;
|
|
937
|
+
// month/day/hour/minute each fit in two decimal digits, so this packs the
|
|
938
|
+
// parts into one monotonically increasing key.
|
|
939
|
+
return (Number(year) * 100_000_000 +
|
|
940
|
+
Number(month) * 1_000_000 +
|
|
941
|
+
Number(day) * 10_000 +
|
|
942
|
+
Number(hour) * 100 +
|
|
943
|
+
Number(minute));
|
|
944
|
+
};
|
|
945
|
+
export const findArtifactFilename = (files, stem) => {
|
|
946
|
+
const matches = files.filter((file) => file === `${stem}.json` || file.startsWith(`${stem}__`));
|
|
947
|
+
if (matches.length === 0) {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
// Pick the chronologically newest match. Do not lexicographically sort:
|
|
951
|
+
// the datestamp format is not zero-padded, so string order is not time order.
|
|
952
|
+
return matches.reduce((newest, candidate) => artifactDatestampKey(candidate, stem) >=
|
|
953
|
+
artifactDatestampKey(newest, stem)
|
|
954
|
+
? candidate
|
|
955
|
+
: newest);
|
|
956
|
+
};
|
|
957
|
+
/**
|
|
958
|
+
* Written at the end of a dry-run only. A small machine file that lets
|
|
959
|
+
* `migrate continue` finish the migration without re-pulling/re-transforming:
|
|
960
|
+
* carries the scalars that aren't otherwise saved (target space, exact migration
|
|
961
|
+
* names, run-log metadata) and references the heavy dry-run artifacts by filename.
|
|
962
|
+
*/
|
|
963
|
+
const saveContinueManifest = async ({ artifactBaseName, useDatestamp, dryRun, itemType, from, to, migrateFrom, fromFilePath, publicationMode, effectivePublicationLanguages, resolvedPublishLanguages, appliedMigrationNames, publishedLayerContext, publishedLayerPipelineResult, }, config) => {
|
|
964
|
+
if (!dryRun) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const isPreserveLayers = shouldUsePublishedLayerMode(publicationMode) &&
|
|
968
|
+
Boolean(publishedLayerContext);
|
|
969
|
+
if (isPreserveLayers && publishedLayerContext) {
|
|
970
|
+
const publishedChangedIds = (publishedLayerPipelineResult?.changedItems ?? []).map((item) => storyIdOf(item));
|
|
971
|
+
await createAndSaveToFile({
|
|
972
|
+
datestamp: useDatestamp,
|
|
973
|
+
ext: "json",
|
|
974
|
+
filename: `dry-run--${artifactBaseName}---dirty-published-records`,
|
|
975
|
+
folder: "migrations",
|
|
976
|
+
res: {
|
|
977
|
+
dirtyPublishedRecords: publishedLayerContext.dirtyPublishedRecords,
|
|
978
|
+
publishedChangedIds,
|
|
979
|
+
},
|
|
980
|
+
}, config);
|
|
981
|
+
}
|
|
982
|
+
// Resolve the real sibling filenames from disk. The datestamp suffix has
|
|
983
|
+
// minute granularity, so sibling artifacts can in rare cases differ; reading
|
|
984
|
+
// the folder avoids guessing the exact name.
|
|
985
|
+
const files = await listFilesInDir(`${config.sbmigWorkingDirectory}/migrations`);
|
|
986
|
+
const resolve = (stem) => findArtifactFilename(files, `dry-run--${artifactBaseName}---${stem}`);
|
|
987
|
+
const artifacts = {
|
|
988
|
+
changedItems: resolve(`${itemType}-to-migrate`) ?? "",
|
|
989
|
+
pipelineSummary: resolve(`${itemType}-migration-pipeline-summary`) ?? "",
|
|
990
|
+
inputFull: resolve(`${itemType}-input-full`),
|
|
991
|
+
languageStateMap: resolve("language-publish-state-map"),
|
|
992
|
+
draftAfterFull: isPreserveLayers
|
|
993
|
+
? resolve("draft-current-after-full")
|
|
994
|
+
: null,
|
|
995
|
+
publishedAfterFull: isPreserveLayers
|
|
996
|
+
? resolve("published-layer-after-full")
|
|
997
|
+
: null,
|
|
998
|
+
dirtyPublishedRecords: isPreserveLayers
|
|
999
|
+
? resolve("dirty-published-records")
|
|
1000
|
+
: null,
|
|
1001
|
+
};
|
|
1002
|
+
const manifest = {
|
|
1003
|
+
manifestVersion: 1,
|
|
1004
|
+
createdAt: new Date().toISOString(),
|
|
1005
|
+
kind: "migrate-content-continue-manifest",
|
|
1006
|
+
itemType,
|
|
1007
|
+
from,
|
|
1008
|
+
to,
|
|
1009
|
+
migrateFrom,
|
|
1010
|
+
fromFilePath: fromFilePath ?? null,
|
|
1011
|
+
publicationMode,
|
|
1012
|
+
migrationConfigNames: appliedMigrationNames,
|
|
1013
|
+
publishLanguages: publicationMode === "save-only"
|
|
1014
|
+
? null
|
|
1015
|
+
: { requested: effectivePublicationLanguages },
|
|
1016
|
+
resolvedPublishLanguages: resolvedPublishLanguages ?? null,
|
|
1017
|
+
artifactBaseName,
|
|
1018
|
+
useDatestamp,
|
|
1019
|
+
artifacts,
|
|
1020
|
+
};
|
|
1021
|
+
await createAndSaveToFile({
|
|
1022
|
+
datestamp: useDatestamp,
|
|
1023
|
+
ext: "json",
|
|
1024
|
+
filename: `dry-run--${artifactBaseName}---${itemType}-continue-manifest`,
|
|
1025
|
+
folder: "migrations",
|
|
1026
|
+
res: manifest,
|
|
1027
|
+
}, config);
|
|
1028
|
+
};
|
|
813
1029
|
export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publicationMode = DEFAULT_PUBLICATION_MODE, publicationLanguages, migrateFrom, fromFilePath, fileName, languagePublishStatePath, }, config) => {
|
|
814
1030
|
const preparedMigrationConfigs = migrationConfigs ||
|
|
815
1031
|
prepareMigrationConfigs({
|
|
@@ -952,6 +1168,23 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
|
|
|
952
1168
|
languagePublishStatePath,
|
|
953
1169
|
pipelineResult,
|
|
954
1170
|
}, config);
|
|
1171
|
+
await saveContinueManifest({
|
|
1172
|
+
artifactBaseName,
|
|
1173
|
+
useDatestamp,
|
|
1174
|
+
dryRun,
|
|
1175
|
+
itemType,
|
|
1176
|
+
from,
|
|
1177
|
+
to,
|
|
1178
|
+
migrateFrom,
|
|
1179
|
+
fromFilePath,
|
|
1180
|
+
publicationMode,
|
|
1181
|
+
effectivePublicationLanguages,
|
|
1182
|
+
resolvedPublishLanguages,
|
|
1183
|
+
appliedMigrationNames: preparedMigrationConfigs.map((preparedMigrationConfig) => preparedMigrationConfig.migrationConfigName),
|
|
1184
|
+
pipelineResult,
|
|
1185
|
+
publishedLayerContext,
|
|
1186
|
+
publishedLayerPipelineResult,
|
|
1187
|
+
}, config);
|
|
955
1188
|
if (dryRun) {
|
|
956
1189
|
console.log(" ");
|
|
957
1190
|
if (publishedLayerPipelineResult) {
|
|
@@ -961,100 +1194,24 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
|
|
|
961
1194
|
Logger.success(`[DRY RUN] No API changes were made. Review the saved migration file for details.`);
|
|
962
1195
|
return;
|
|
963
1196
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
options: {
|
|
983
|
-
publish: shouldPublishForPublicationMode(publicationMode),
|
|
984
|
-
publishLanguages: resolvedPublishLanguages,
|
|
985
|
-
preservePublishState: shouldPublishForPublicationMode(publicationMode),
|
|
986
|
-
languagePublishStateMap,
|
|
987
|
-
},
|
|
988
|
-
}, config)
|
|
989
|
-
: [];
|
|
990
|
-
const dirtyWriteResults = publishedLayerPipelineResult
|
|
991
|
-
? await writeDirtyPublishedLayerStories({
|
|
992
|
-
context: publishedLayerContext,
|
|
993
|
-
draftPipelineResult: pipelineResult,
|
|
994
|
-
publishedPipelineResult: publishedLayerPipelineResult,
|
|
995
|
-
to,
|
|
996
|
-
resolvedPublishLanguages,
|
|
997
|
-
languagePublishStateMap,
|
|
998
|
-
}, config)
|
|
999
|
-
: [];
|
|
1000
|
-
writeResults = [...nonDirtyWriteResults, ...dirtyWriteResults];
|
|
1001
|
-
}
|
|
1002
|
-
else {
|
|
1003
|
-
writeResults = await managementApi.stories.updateStories({
|
|
1004
|
-
stories: pipelineResult.changedItems,
|
|
1005
|
-
spaceId: to,
|
|
1006
|
-
options: {
|
|
1007
|
-
publish: shouldPublishForPublicationMode(publicationMode),
|
|
1008
|
-
publishLanguages: resolvedPublishLanguages,
|
|
1009
|
-
preservePublishState: shouldPublishForPublicationMode(publicationMode),
|
|
1010
|
-
publishDirtyPublishedStories: shouldPublishDirtyDrafts(publicationMode),
|
|
1011
|
-
languagePublishStateMap,
|
|
1012
|
-
},
|
|
1013
|
-
}, config);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
else if (itemType === "preset") {
|
|
1017
|
-
writeResults = await managementApi.presets.updatePresets({
|
|
1018
|
-
presets: pipelineResult.changedItems,
|
|
1019
|
-
spaceId: to,
|
|
1020
|
-
options: {},
|
|
1021
|
-
}, config);
|
|
1022
|
-
}
|
|
1023
|
-
const writeSummary = summarizeMutationWriteResults(writeResults);
|
|
1024
|
-
try {
|
|
1025
|
-
await saveMigrationRunLog({
|
|
1026
|
-
artifactBaseName,
|
|
1027
|
-
useDatestamp,
|
|
1028
|
-
from,
|
|
1029
|
-
to,
|
|
1030
|
-
itemType,
|
|
1031
|
-
dryRun,
|
|
1032
|
-
publish: shouldPublishForPublicationMode(publicationMode),
|
|
1033
|
-
publishLanguages: effectivePublicationLanguages,
|
|
1034
|
-
resolvedPublishLanguages,
|
|
1035
|
-
migrateFrom,
|
|
1036
|
-
fromFilePath,
|
|
1037
|
-
languagePublishStatePath,
|
|
1038
|
-
pipelineResult,
|
|
1039
|
-
writeResults,
|
|
1040
|
-
writeSummary,
|
|
1041
|
-
}, config);
|
|
1042
|
-
}
|
|
1043
|
-
catch (error) {
|
|
1044
|
-
Logger.warning(`[MIGRATION] Could not write migration run log: ${error instanceof Error ? error.message : String(error)}`);
|
|
1045
|
-
}
|
|
1046
|
-
if (writeSummary.failed === 0) {
|
|
1047
|
-
Logger.success(`[MIGRATION] Update complete. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully.`);
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
Logger.warning(`[MIGRATION] Update complete with partial failures. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully, ${writeSummary.failed} failed.`);
|
|
1051
|
-
writeSummary.failedItems.slice(0, 10).forEach((item) => {
|
|
1052
|
-
const label = item.slug || item.name || item.id || "unknown";
|
|
1053
|
-
Logger.error(`[MIGRATION] Failed ${itemType}: ${String(label)}`);
|
|
1054
|
-
});
|
|
1055
|
-
if (writeSummary.failedItems.length > 10) {
|
|
1056
|
-
Logger.warning(`[MIGRATION] Showing first 10 failed ${itemType}(s) only.`);
|
|
1057
|
-
}
|
|
1197
|
+
await finalizeMigration({
|
|
1198
|
+
itemType,
|
|
1199
|
+
from,
|
|
1200
|
+
to,
|
|
1201
|
+
migrateFrom,
|
|
1202
|
+
fromFilePath,
|
|
1203
|
+
languagePublishStatePath,
|
|
1204
|
+
publicationMode,
|
|
1205
|
+
effectivePublicationLanguages,
|
|
1206
|
+
resolvedPublishLanguages,
|
|
1207
|
+
languagePublishStateMap,
|
|
1208
|
+
appliedMigrationNames: preparedMigrationConfigs.map((preparedMigrationConfig) => preparedMigrationConfig.migrationConfigName),
|
|
1209
|
+
pipelineResult,
|
|
1210
|
+
publishedLayerContext,
|
|
1211
|
+
publishedLayerPipelineResult,
|
|
1212
|
+
artifactBaseName,
|
|
1213
|
+
useDatestamp,
|
|
1214
|
+
}, config);
|
|
1058
1215
|
};
|
|
1059
1216
|
const saveBackupToFile = async ({ itemType, res, folder, filename }, config) => {
|
|
1060
1217
|
await createAndSaveToFile({
|
|
@@ -1109,3 +1266,142 @@ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrati
|
|
|
1109
1266
|
languagePublishStatePath,
|
|
1110
1267
|
}, config);
|
|
1111
1268
|
};
|
|
1269
|
+
const CONTINUE_MANIFEST_RE = /---(?:story|preset)-continue-manifest(?:__.*)?\.json$/;
|
|
1270
|
+
const readMigrationJson = async (config, filename) => {
|
|
1271
|
+
const content = await readFile(`${config.sbmigWorkingDirectory}/migrations/${filename}`);
|
|
1272
|
+
if (content === undefined) {
|
|
1273
|
+
throw new Error(`Could not read migration artifact: migrations/${filename}`);
|
|
1274
|
+
}
|
|
1275
|
+
try {
|
|
1276
|
+
return JSON.parse(content);
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
throw new Error(`Migration artifact is not valid JSON: migrations/${filename}`);
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
/**
|
|
1283
|
+
* Discover the dry-run `continue` manifest in the migrations folder, load the
|
|
1284
|
+
* artifacts it points at, and reconstruct the exact inputs `finalizeMigration`
|
|
1285
|
+
* needs. Returns a summary (for the confirmation prompt) and a `run()` thunk that
|
|
1286
|
+
* performs the write. Does NOT pull stories, transform, or fetch published layers.
|
|
1287
|
+
*/
|
|
1288
|
+
export const prepareContinueMigration = async ({ manifestFileName }, config) => {
|
|
1289
|
+
const files = await listFilesInDir(`${config.sbmigWorkingDirectory}/migrations`);
|
|
1290
|
+
const manifestFiles = files.filter((file) => CONTINUE_MANIFEST_RE.test(file));
|
|
1291
|
+
let chosen;
|
|
1292
|
+
if (manifestFileName) {
|
|
1293
|
+
if (!files.includes(manifestFileName)) {
|
|
1294
|
+
throw new Error(`Manifest not found in migrations folder: ${manifestFileName}`);
|
|
1295
|
+
}
|
|
1296
|
+
chosen = manifestFileName;
|
|
1297
|
+
}
|
|
1298
|
+
else if (manifestFiles.length === 1) {
|
|
1299
|
+
chosen = manifestFiles[0];
|
|
1300
|
+
}
|
|
1301
|
+
else if (manifestFiles.length === 0) {
|
|
1302
|
+
throw new Error("No dry-run found to continue. Run `sb-mig migrate content … --dryRun` first.");
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
throw new Error(`Multiple dry-run manifests found. Re-run with --manifest <filename>:\n${manifestFiles
|
|
1306
|
+
.map((file) => ` - ${file}`)
|
|
1307
|
+
.join("\n")}`);
|
|
1308
|
+
}
|
|
1309
|
+
const manifest = (await readMigrationJson(config, chosen));
|
|
1310
|
+
if (manifest.kind !== "migrate-content-continue-manifest") {
|
|
1311
|
+
throw new Error(`Not a continue manifest: ${chosen}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (manifest.manifestVersion !== 1) {
|
|
1314
|
+
throw new Error(`Unsupported continue manifest version (${manifest.manifestVersion}) in ${chosen}.`);
|
|
1315
|
+
}
|
|
1316
|
+
const { artifacts } = manifest;
|
|
1317
|
+
if (!artifacts?.changedItems) {
|
|
1318
|
+
throw new Error(`Manifest ${chosen} is missing the changed-items artifact reference.`);
|
|
1319
|
+
}
|
|
1320
|
+
const changedItems = (await readMigrationJson(config, artifacts.changedItems));
|
|
1321
|
+
const pipelineSummary = artifacts.pipelineSummary
|
|
1322
|
+
? await readMigrationJson(config, artifacts.pipelineSummary)
|
|
1323
|
+
: null;
|
|
1324
|
+
const languagePublishStateMap = artifacts.languageStateMap
|
|
1325
|
+
? (await readMigrationJson(config, artifacts.languageStateMap))
|
|
1326
|
+
: undefined;
|
|
1327
|
+
const isPreserveLayers = shouldUsePublishedLayerMode(manifest.publicationMode) &&
|
|
1328
|
+
Boolean(artifacts.dirtyPublishedRecords);
|
|
1329
|
+
let publishedLayerContext;
|
|
1330
|
+
let publishedLayerPipelineResult = null;
|
|
1331
|
+
if (isPreserveLayers && artifacts.dirtyPublishedRecords) {
|
|
1332
|
+
const dirty = (await readMigrationJson(config, artifacts.dirtyPublishedRecords));
|
|
1333
|
+
publishedLayerContext = {
|
|
1334
|
+
records: [],
|
|
1335
|
+
publishedLayerInputItems: [],
|
|
1336
|
+
dirtyPublishedRecords: dirty.dirtyPublishedRecords ?? [],
|
|
1337
|
+
missingPublishedLayerRecords: [],
|
|
1338
|
+
};
|
|
1339
|
+
const publishedFinalItems = artifacts.publishedAfterFull
|
|
1340
|
+
? (await readMigrationJson(config, artifacts.publishedAfterFull))
|
|
1341
|
+
: [];
|
|
1342
|
+
const publishedChangedIdSet = new Set((dirty.publishedChangedIds ?? []).map(String));
|
|
1343
|
+
publishedLayerPipelineResult = {
|
|
1344
|
+
changedItems: publishedFinalItems.filter((item) => publishedChangedIdSet.has(storyIdOf(item))),
|
|
1345
|
+
finalItems: publishedFinalItems,
|
|
1346
|
+
stepReports: [],
|
|
1347
|
+
totalItems: publishedFinalItems.length,
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
const draftFinalItems = artifacts.draftAfterFull
|
|
1351
|
+
? (await readMigrationJson(config, artifacts.draftAfterFull))
|
|
1352
|
+
: changedItems;
|
|
1353
|
+
const pipelineResult = {
|
|
1354
|
+
changedItems,
|
|
1355
|
+
finalItems: draftFinalItems,
|
|
1356
|
+
stepReports: Array.isArray(pipelineSummary?.steps)
|
|
1357
|
+
? pipelineSummary.steps
|
|
1358
|
+
: [],
|
|
1359
|
+
totalItems: typeof pipelineSummary?.totalItems === "number"
|
|
1360
|
+
? pipelineSummary.totalItems
|
|
1361
|
+
: changedItems.length,
|
|
1362
|
+
};
|
|
1363
|
+
const resolvedPublishLanguages = manifest.resolvedPublishLanguages ?? undefined;
|
|
1364
|
+
const effectivePublicationLanguages = manifest.publishLanguages?.requested;
|
|
1365
|
+
const artifactFiles = [
|
|
1366
|
+
artifacts.changedItems,
|
|
1367
|
+
artifacts.pipelineSummary,
|
|
1368
|
+
artifacts.draftAfterFull,
|
|
1369
|
+
artifacts.publishedAfterFull,
|
|
1370
|
+
artifacts.dirtyPublishedRecords,
|
|
1371
|
+
artifacts.languageStateMap,
|
|
1372
|
+
artifacts.inputFull,
|
|
1373
|
+
].filter((file) => Boolean(file));
|
|
1374
|
+
const summary = {
|
|
1375
|
+
manifestFileName: chosen,
|
|
1376
|
+
from: manifest.from,
|
|
1377
|
+
to: manifest.to,
|
|
1378
|
+
itemType: manifest.itemType,
|
|
1379
|
+
publicationMode: manifest.publicationMode,
|
|
1380
|
+
migrationConfigNames: manifest.migrationConfigNames ?? [],
|
|
1381
|
+
resolvedPublishLanguages: resolvedPublishLanguages ?? [],
|
|
1382
|
+
changedCount: changedItems.length,
|
|
1383
|
+
dirtyPublishedCount: publishedLayerContext?.dirtyPublishedRecords.length ?? 0,
|
|
1384
|
+
artifactFiles,
|
|
1385
|
+
};
|
|
1386
|
+
const run = async () => {
|
|
1387
|
+
await finalizeMigration({
|
|
1388
|
+
itemType: manifest.itemType,
|
|
1389
|
+
from: manifest.from,
|
|
1390
|
+
to: manifest.to,
|
|
1391
|
+
migrateFrom: manifest.migrateFrom,
|
|
1392
|
+
fromFilePath: manifest.fromFilePath ?? undefined,
|
|
1393
|
+
publicationMode: manifest.publicationMode,
|
|
1394
|
+
effectivePublicationLanguages,
|
|
1395
|
+
resolvedPublishLanguages,
|
|
1396
|
+
languagePublishStateMap,
|
|
1397
|
+
appliedMigrationNames: manifest.migrationConfigNames ?? [],
|
|
1398
|
+
pipelineResult,
|
|
1399
|
+
publishedLayerContext,
|
|
1400
|
+
publishedLayerPipelineResult,
|
|
1401
|
+
artifactBaseName: manifest.artifactBaseName,
|
|
1402
|
+
useDatestamp: manifest.useDatestamp,
|
|
1403
|
+
continuedFromManifest: chosen,
|
|
1404
|
+
}, config);
|
|
1405
|
+
};
|
|
1406
|
+
return { summary, run };
|
|
1407
|
+
};
|
|
@@ -23,6 +23,7 @@ export interface MigrationRunLogRecord {
|
|
|
23
23
|
resolved?: string[];
|
|
24
24
|
};
|
|
25
25
|
dryRun: boolean;
|
|
26
|
+
continuedFromManifest?: string;
|
|
26
27
|
migrationConfigs: string[];
|
|
27
28
|
totalItems: number;
|
|
28
29
|
totalChangedItems: number;
|
|
@@ -76,8 +77,9 @@ interface SaveMigrationRunLogArgs {
|
|
|
76
77
|
pipelineResult: MigrationPipelineResult;
|
|
77
78
|
writeResults: PromiseSettledResult<MutationWriteResult>[];
|
|
78
79
|
writeSummary: MutationWriteSummary;
|
|
80
|
+
continuedFromManifest?: string;
|
|
79
81
|
}
|
|
80
|
-
export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
|
|
82
|
+
export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, continuedFromManifest, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
|
|
81
83
|
export declare const recordsToJsonl: (records: MigrationRunLogRecord[]) => string;
|
|
82
84
|
export declare const saveMigrationRunLog: (args: SaveMigrationRunLogArgs, config: RequestBaseConfig) => Promise<void>;
|
|
83
85
|
export {};
|
|
@@ -31,7 +31,7 @@ const resolveWriteResultValue = (result) => {
|
|
|
31
31
|
error: result.reason,
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
|
-
export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, }) => {
|
|
34
|
+
export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, continuedFromManifest, }) => {
|
|
35
35
|
const timestamp = new Date().toISOString();
|
|
36
36
|
const runId = `${itemType}-${timestamp}`;
|
|
37
37
|
const baseRecord = {
|
|
@@ -57,6 +57,7 @@ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publis
|
|
|
57
57
|
}
|
|
58
58
|
: {}),
|
|
59
59
|
dryRun: Boolean(dryRun),
|
|
60
|
+
...(continuedFromManifest ? { continuedFromManifest } : {}),
|
|
60
61
|
migrationConfigs: pipelineResult.stepReports.map((step) => step.migrationConfig),
|
|
61
62
|
totalItems: pipelineResult.totalItems,
|
|
62
63
|
totalChangedItems: pipelineResult.changedItems.length,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as assets from "./assets/index.js";
|
|
1
2
|
import * as inspect from "./inspect/index.js";
|
|
2
3
|
import * as stories from "./stories/index.js";
|
|
3
4
|
export declare const managementApi: {
|
|
@@ -8,7 +9,12 @@ export declare const managementApi: {
|
|
|
8
9
|
getAsset: (assetName: string | undefined) => Promise<void>;
|
|
9
10
|
getAssetById: import("./assets/assets.types.js").GetAssetById;
|
|
10
11
|
getAssetByName: import("./assets/assets.types.js").GetAssetByName;
|
|
12
|
+
createAssetAndFinalize: assets.CreateAssetAndFinalize;
|
|
13
|
+
downloadAsset: import("./assets/assets.types.js").DownloadAsset;
|
|
14
|
+
finishAssetUpload: assets.FinishAssetUpload;
|
|
11
15
|
updateAsset: import("./assets/assets.types.js").UpdateAsset;
|
|
16
|
+
createAssetFolder: assets.CreateAssetFolder;
|
|
17
|
+
getAllAssetFolders: import("./assets/asset-folders.types.js").GetAllAssetFolders;
|
|
12
18
|
};
|
|
13
19
|
auth: {
|
|
14
20
|
getCurrentUser: import("./auth/auth.types.js").GetCurrentUser;
|