sdx-cli 0.2.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/LICENSE +21 -0
- package/README.md +266 -0
- package/bin/dev.js +11 -0
- package/bin/run.js +3 -0
- package/dist/commands/bootstrap/consumer.js +75 -0
- package/dist/commands/bootstrap/org.js +29 -0
- package/dist/commands/bootstrap/quick.js +82 -0
- package/dist/commands/codex/run.js +36 -0
- package/dist/commands/contracts/extract.js +22 -0
- package/dist/commands/docs/generate.js +22 -0
- package/dist/commands/handoff/draft.js +41 -0
- package/dist/commands/init.js +14 -0
- package/dist/commands/map/build.js +44 -0
- package/dist/commands/map/create.js +40 -0
- package/dist/commands/map/exclude.js +25 -0
- package/dist/commands/map/include.js +25 -0
- package/dist/commands/map/remove-override.js +25 -0
- package/dist/commands/map/status.js +30 -0
- package/dist/commands/migrate/artifacts.js +68 -0
- package/dist/commands/plan/review.js +60 -0
- package/dist/commands/prompt.js +62 -0
- package/dist/commands/publish/notices.js +98 -0
- package/dist/commands/publish/sync.js +67 -0
- package/dist/commands/publish/wiki.js +39 -0
- package/dist/commands/repo/add.js +29 -0
- package/dist/commands/repo/sync.js +30 -0
- package/dist/commands/service/propose.js +40 -0
- package/dist/commands/status.js +37 -0
- package/dist/commands/version.js +16 -0
- package/dist/index.js +10 -0
- package/dist/lib/artifactMigration.js +29 -0
- package/dist/lib/bootstrap.js +43 -0
- package/dist/lib/bootstrapConsumer.js +187 -0
- package/dist/lib/bootstrapQuick.js +27 -0
- package/dist/lib/codex.js +138 -0
- package/dist/lib/config.js +40 -0
- package/dist/lib/constants.js +26 -0
- package/dist/lib/contractChanges.js +347 -0
- package/dist/lib/contracts.js +93 -0
- package/dist/lib/db.js +41 -0
- package/dist/lib/docs.js +46 -0
- package/dist/lib/fileScan.js +34 -0
- package/dist/lib/fs.js +36 -0
- package/dist/lib/github.js +52 -0
- package/dist/lib/githubPublish.js +161 -0
- package/dist/lib/handoff.js +62 -0
- package/dist/lib/mapBuilder.js +182 -0
- package/dist/lib/paths.js +39 -0
- package/dist/lib/planReview.js +88 -0
- package/dist/lib/project.js +65 -0
- package/dist/lib/promptParser.js +88 -0
- package/dist/lib/publishContracts.js +876 -0
- package/dist/lib/repoRegistry.js +92 -0
- package/dist/lib/scope.js +110 -0
- package/dist/lib/serviceNoticePlan.js +130 -0
- package/dist/lib/serviceProposal.js +82 -0
- package/dist/lib/status.js +34 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/version.js +17 -0
- package/dist/lib/workflows.js +70 -0
- package/package.json +50 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.publishNotices = publishNotices;
|
|
7
|
+
exports.publishSync = publishSync;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const constants_1 = require("./constants");
|
|
11
|
+
const contractChanges_1 = require("./contractChanges");
|
|
12
|
+
const fs_1 = require("./fs");
|
|
13
|
+
const githubPublish_1 = require("./githubPublish");
|
|
14
|
+
const repoRegistry_1 = require("./repoRegistry");
|
|
15
|
+
const scope_1 = require("./scope");
|
|
16
|
+
const serviceNoticePlan_1 = require("./serviceNoticePlan");
|
|
17
|
+
const SERVICE_NOTICE_CHANGE_TYPE = 'service_added';
|
|
18
|
+
function nowIso() {
|
|
19
|
+
return new Date().toISOString();
|
|
20
|
+
}
|
|
21
|
+
function today() {
|
|
22
|
+
return nowIso().slice(0, 10);
|
|
23
|
+
}
|
|
24
|
+
function fileStamp() {
|
|
25
|
+
return nowIso().replace(/[:.]/g, '-');
|
|
26
|
+
}
|
|
27
|
+
function slugify(value) {
|
|
28
|
+
return value
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
31
|
+
.replace(/^-+|-+$/g, '');
|
|
32
|
+
}
|
|
33
|
+
function normalizeRepoRef(value) {
|
|
34
|
+
const raw = value.trim();
|
|
35
|
+
if (!raw) {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
const withoutProtocol = raw.replace(/^https?:\/\/github\.com\//i, '');
|
|
39
|
+
return withoutProtocol.replace(/\.git$/i, '').replace(/\/+$/, '');
|
|
40
|
+
}
|
|
41
|
+
function repoParts(record) {
|
|
42
|
+
const fullName = record.fullName.includes('/') ? record.fullName : `${record.org}/${record.name}`;
|
|
43
|
+
const [owner, repo] = fullName.split('/', 2);
|
|
44
|
+
return { owner, repo, fullName };
|
|
45
|
+
}
|
|
46
|
+
function toContractSuffix(contractChangeId) {
|
|
47
|
+
if (!contractChangeId) {
|
|
48
|
+
return 'batch';
|
|
49
|
+
}
|
|
50
|
+
return slugify(contractChangeId) || 'single';
|
|
51
|
+
}
|
|
52
|
+
function createTargetBranch(contractChangeId, targetRepoName) {
|
|
53
|
+
return `sdx/spec-notice/${slugify(contractChangeId)}-${slugify(targetRepoName)}`.slice(0, 120);
|
|
54
|
+
}
|
|
55
|
+
function createSourceSyncBranch(contractChangeId) {
|
|
56
|
+
return `sdx/source-sync/${slugify(contractChangeId)}`.slice(0, 120);
|
|
57
|
+
}
|
|
58
|
+
function splitRepoOwnerAndName(value) {
|
|
59
|
+
const normalized = normalizeRepoRef(value);
|
|
60
|
+
const parts = normalized.split('/');
|
|
61
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
owner: parts[0],
|
|
66
|
+
repo: parts[1],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function parseAliasTokens(raw) {
|
|
70
|
+
const value = raw.trim();
|
|
71
|
+
if (!value) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(value);
|
|
77
|
+
if (Array.isArray(parsed)) {
|
|
78
|
+
return parsed.map((entry) => String(entry).trim()).filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Fall through to delimiter parsing.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return value
|
|
86
|
+
.replace(/^\[/, '')
|
|
87
|
+
.replace(/\]$/, '')
|
|
88
|
+
.split(',')
|
|
89
|
+
.map((entry) => entry.replace(/^"|"$/g, '').trim())
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
function mergeAliasTokens(existing, additions) {
|
|
93
|
+
const merged = [...parseAliasTokens(existing), ...additions];
|
|
94
|
+
const unique = [...new Set(merged)].sort((a, b) => a.localeCompare(b));
|
|
95
|
+
return unique.join(', ');
|
|
96
|
+
}
|
|
97
|
+
function sourceAliasTokens(source, sourceContractId) {
|
|
98
|
+
return [`source_repo:${source.fullName}`, `source_cc:${sourceContractId}`];
|
|
99
|
+
}
|
|
100
|
+
function resolveRepoFromScope(inputRepo, ownerHint, scopedRepos, allRepos) {
|
|
101
|
+
const normalized = normalizeRepoRef(inputRepo);
|
|
102
|
+
if (!normalized) {
|
|
103
|
+
return { error: 'Repository value is empty.' };
|
|
104
|
+
}
|
|
105
|
+
const fullMatch = splitRepoOwnerAndName(normalized);
|
|
106
|
+
if (fullMatch) {
|
|
107
|
+
const scoped = scopedRepos.filter((repo) => {
|
|
108
|
+
const parts = repoParts(repo);
|
|
109
|
+
return parts.owner.toLowerCase() === fullMatch.owner.toLowerCase() && parts.repo.toLowerCase() === fullMatch.repo.toLowerCase();
|
|
110
|
+
});
|
|
111
|
+
if (scoped.length === 1) {
|
|
112
|
+
return { record: scoped[0] };
|
|
113
|
+
}
|
|
114
|
+
const allMatches = allRepos.filter((repo) => {
|
|
115
|
+
const parts = repoParts(repo);
|
|
116
|
+
return parts.owner.toLowerCase() === fullMatch.owner.toLowerCase() && parts.repo.toLowerCase() === fullMatch.repo.toLowerCase();
|
|
117
|
+
});
|
|
118
|
+
if (allMatches.length > 0) {
|
|
119
|
+
return { error: `Repository '${normalized}' is outside the active map scope.` };
|
|
120
|
+
}
|
|
121
|
+
return { error: `Repository '${normalized}' was not found in the repo registry.` };
|
|
122
|
+
}
|
|
123
|
+
let matches = scopedRepos.filter((repo) => repo.name.toLowerCase() === normalized.toLowerCase());
|
|
124
|
+
if (matches.length > 1 && ownerHint) {
|
|
125
|
+
const ownerNormalized = ownerHint.trim().toLowerCase();
|
|
126
|
+
const hinted = matches.filter((repo) => repoParts(repo).owner.toLowerCase() === ownerNormalized);
|
|
127
|
+
if (hinted.length === 1) {
|
|
128
|
+
matches = hinted;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (matches.length === 1) {
|
|
132
|
+
return { record: matches[0] };
|
|
133
|
+
}
|
|
134
|
+
if (matches.length > 1) {
|
|
135
|
+
return {
|
|
136
|
+
error: `Repository '${inputRepo}' is ambiguous in the current scope: ${matches
|
|
137
|
+
.map((repo) => repoParts(repo).fullName)
|
|
138
|
+
.join(', ')}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const outOfScope = allRepos.some((repo) => repo.name.toLowerCase() === normalized.toLowerCase());
|
|
142
|
+
if (outOfScope) {
|
|
143
|
+
return { error: `Repository '${inputRepo}' is outside the active map scope.` };
|
|
144
|
+
}
|
|
145
|
+
return { error: `Repository '${inputRepo}' was not found in the repo registry.` };
|
|
146
|
+
}
|
|
147
|
+
function createPublishContext(input, requireOps) {
|
|
148
|
+
const cwd = input.cwd ?? process.cwd();
|
|
149
|
+
const scope = (0, scope_1.loadScopeManifest)(input.mapId, cwd);
|
|
150
|
+
const allRepos = (0, repoRegistry_1.listAllRepos)(input.db);
|
|
151
|
+
const scopeSet = new Set(scope.effective);
|
|
152
|
+
const scopedRepos = allRepos.filter((repo) => scopeSet.has(repo.name));
|
|
153
|
+
const sourceResolution = resolveRepoFromScope(input.sourceRepo, undefined, scopedRepos, allRepos);
|
|
154
|
+
if (!sourceResolution.record) {
|
|
155
|
+
throw new Error(sourceResolution.error ?? `Unable to resolve source repo '${input.sourceRepo}'.`);
|
|
156
|
+
}
|
|
157
|
+
const sourceRepoRecord = sourceResolution.record;
|
|
158
|
+
if (!sourceRepoRecord.localPath) {
|
|
159
|
+
throw new Error(`Source repository '${repoParts(sourceRepoRecord).fullName}' is missing local path metadata. Register it with 'sdx repo add --name ${sourceRepoRecord.name} --path <local-path>'.`);
|
|
160
|
+
}
|
|
161
|
+
const indexPath = node_path_1.default.join(sourceRepoRecord.localPath, 'docs', 'CONTRACT_CHANGES.md');
|
|
162
|
+
if (!node_fs_1.default.existsSync(indexPath)) {
|
|
163
|
+
throw new Error(`Source repository '${repoParts(sourceRepoRecord).fullName}' is missing docs/CONTRACT_CHANGES.md. spec-system not instantiated (or invalid).`);
|
|
164
|
+
}
|
|
165
|
+
const indexBody = node_fs_1.default.readFileSync(indexPath, 'utf8');
|
|
166
|
+
if (!(0, contractChanges_1.hasContractChangeIndexShape)(indexBody)) {
|
|
167
|
+
throw new Error(`Source repository '${repoParts(sourceRepoRecord).fullName}' has invalid docs/CONTRACT_CHANGES.md. spec-system not instantiated (or invalid).`);
|
|
168
|
+
}
|
|
169
|
+
const { meta, rows } = (0, contractChanges_1.parseContractChangeIndexText)(indexBody);
|
|
170
|
+
const ops = input.githubOps ?? (input.githubToken ? (0, githubPublish_1.createGithubPublishOps)(input.githubToken) : undefined);
|
|
171
|
+
if (requireOps && !ops) {
|
|
172
|
+
throw new Error('GitHub token is required for this command. Set GITHUB_TOKEN (or configured token env).');
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
cwd,
|
|
176
|
+
scopeMapId: input.mapId,
|
|
177
|
+
scopedRepos,
|
|
178
|
+
allRepos,
|
|
179
|
+
sourceRepoParts: repoParts(sourceRepoRecord),
|
|
180
|
+
sourceRepoPath: sourceRepoRecord.localPath,
|
|
181
|
+
indexMeta: meta,
|
|
182
|
+
indexRows: rows,
|
|
183
|
+
ops,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function ensureArtifactPaths(baseDir, category, mapId, contractChangeId) {
|
|
187
|
+
const stamp = fileStamp();
|
|
188
|
+
const suffix = toContractSuffix(contractChangeId);
|
|
189
|
+
const jsonPath = node_path_1.default.join(baseDir, 'publish', category, `${stamp}-${slugify(mapId)}-${suffix}.json`);
|
|
190
|
+
const markdownPath = node_path_1.default.join(baseDir, 'publish', category, `${stamp}-${slugify(mapId)}-${suffix}.md`);
|
|
191
|
+
return { jsonPath, markdownPath };
|
|
192
|
+
}
|
|
193
|
+
function contractFilePath(contractChangeId, name) {
|
|
194
|
+
return `contracts/${contractChangeId}-${slugify(name) || slugify(contractChangeId)}.md`;
|
|
195
|
+
}
|
|
196
|
+
function buildTargetContractDoc(source, sourceDoc, targetContractChangeId, targetOwner) {
|
|
197
|
+
return {
|
|
198
|
+
contractChangeId: targetContractChangeId,
|
|
199
|
+
name: `Respond to ${sourceDoc.contractChangeId}: ${sourceDoc.name}`,
|
|
200
|
+
status: 'draft',
|
|
201
|
+
changeType: sourceDoc.changeType || 'api_contract_changed',
|
|
202
|
+
owner: targetOwner || sourceDoc.owner,
|
|
203
|
+
lastUpdated: today(),
|
|
204
|
+
absolutePath: '',
|
|
205
|
+
relativePath: `docs/${contractFilePath(targetContractChangeId, sourceDoc.name)}`,
|
|
206
|
+
sections: {
|
|
207
|
+
summary: [
|
|
208
|
+
`This contract change was generated by SDX from source contract change ${sourceDoc.contractChangeId}.`,
|
|
209
|
+
'',
|
|
210
|
+
`Source repository: ${source.fullName}`,
|
|
211
|
+
'',
|
|
212
|
+
sourceDoc.sections.summary.trim() || '_No summary provided._',
|
|
213
|
+
].join('\n'),
|
|
214
|
+
contractSurface: sourceDoc.sections.contractSurface.trim() || '_No contract surface details provided._',
|
|
215
|
+
changeDetails: [
|
|
216
|
+
sourceDoc.sections.changeDetails.trim() || '_No change details provided._',
|
|
217
|
+
'',
|
|
218
|
+
`Source reference: ${source.fullName} / ${sourceDoc.contractChangeId}`,
|
|
219
|
+
].join('\n'),
|
|
220
|
+
compatibilityAndMigrationGuidance: sourceDoc.sections.compatibilityAndMigrationGuidance.trim() || '_No migration guidance provided._',
|
|
221
|
+
},
|
|
222
|
+
targets: [],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function buildTargetPrBody(source, sourceDoc, targetContext, targetContractChangeId) {
|
|
226
|
+
return [
|
|
227
|
+
`## SDX Contract Change Assignment: ${targetContractChangeId}`,
|
|
228
|
+
'',
|
|
229
|
+
`Source repository: ${source.fullName}`,
|
|
230
|
+
`Source contract change: ${sourceDoc.contractChangeId}`,
|
|
231
|
+
`Change type: ${sourceDoc.changeType}`,
|
|
232
|
+
'',
|
|
233
|
+
'### Summary',
|
|
234
|
+
sourceDoc.sections.summary.trim() || '_No summary provided._',
|
|
235
|
+
'',
|
|
236
|
+
'### Contract Surface',
|
|
237
|
+
sourceDoc.sections.contractSurface.trim() || '_No contract surface details provided._',
|
|
238
|
+
'',
|
|
239
|
+
'### Migration Guidance',
|
|
240
|
+
sourceDoc.sections.compatibilityAndMigrationGuidance.trim() || '_No migration guidance provided._',
|
|
241
|
+
'',
|
|
242
|
+
'### Target Context',
|
|
243
|
+
targetContext,
|
|
244
|
+
'',
|
|
245
|
+
'### Required Response',
|
|
246
|
+
'- [ ] Confirm impact on this repository.',
|
|
247
|
+
'- [ ] Implement required code or contract adjustments.',
|
|
248
|
+
'- [ ] Add tests and rollout notes.',
|
|
249
|
+
'- [ ] Update CC status in this repo as work progresses.',
|
|
250
|
+
'',
|
|
251
|
+
].join('\n');
|
|
252
|
+
}
|
|
253
|
+
function buildSourceSyncPrBody(doc) {
|
|
254
|
+
return [
|
|
255
|
+
`## Source Sync for ${doc.contractChangeId}`,
|
|
256
|
+
'',
|
|
257
|
+
'This PR updates source contract change artifacts with downstream target PR links and states.',
|
|
258
|
+
'',
|
|
259
|
+
'### Updated Files',
|
|
260
|
+
'- docs/CONTRACT_CHANGES.md',
|
|
261
|
+
`- ${doc.relativePath}`,
|
|
262
|
+
'',
|
|
263
|
+
].join('\n');
|
|
264
|
+
}
|
|
265
|
+
function updateIndexRowFromDoc(row, doc) {
|
|
266
|
+
row.status = doc.status;
|
|
267
|
+
row.name = doc.name;
|
|
268
|
+
row.changeType = doc.changeType;
|
|
269
|
+
row.owner = doc.owner;
|
|
270
|
+
if (!row.path.trim()) {
|
|
271
|
+
row.path = doc.relativePath.replace(/^docs\//, '');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function findIndexRowById(rows, id) {
|
|
275
|
+
const rowIndex = rows.findIndex((row) => row.contractChangeId === id);
|
|
276
|
+
if (rowIndex === -1) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return { row: rows[rowIndex], rowIndex };
|
|
280
|
+
}
|
|
281
|
+
function findIndexRowByAlias(rows, aliasToken) {
|
|
282
|
+
const rowIndex = rows.findIndex((row) => parseAliasTokens(row.aliases).includes(aliasToken));
|
|
283
|
+
if (rowIndex === -1) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
return { row: rows[rowIndex], rowIndex };
|
|
287
|
+
}
|
|
288
|
+
function loadContractModeContracts(context, contractChangeId) {
|
|
289
|
+
const selectedRows = context.indexRows
|
|
290
|
+
.map((row, rowIndex) => ({ row, rowIndex }))
|
|
291
|
+
.filter((entry) => (contractChangeId ? entry.row.contractChangeId === contractChangeId : true))
|
|
292
|
+
.sort((a, b) => a.row.contractChangeId.localeCompare(b.row.contractChangeId));
|
|
293
|
+
if (contractChangeId && selectedRows.length === 0) {
|
|
294
|
+
throw new Error(`Contract change '${contractChangeId}' was not found in docs/CONTRACT_CHANGES.md.`);
|
|
295
|
+
}
|
|
296
|
+
return selectedRows.map((entry) => {
|
|
297
|
+
try {
|
|
298
|
+
const doc = (0, contractChanges_1.readContractChangeDoc)(context.sourceRepoPath, entry.row);
|
|
299
|
+
return { row: entry.row, rowIndex: entry.rowIndex, doc, sourceChanged: false };
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
303
|
+
return { row: entry.row, rowIndex: entry.rowIndex, loadError: message, sourceChanged: false };
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function buildSourceDocFromServicePlan(context, input) {
|
|
308
|
+
if (!input.planPath) {
|
|
309
|
+
throw new Error('`--plan <file>` is required when --notice-type service is used.');
|
|
310
|
+
}
|
|
311
|
+
const planPath = node_path_1.default.resolve(context.cwd, input.planPath);
|
|
312
|
+
if (!node_fs_1.default.existsSync(planPath)) {
|
|
313
|
+
throw new Error(`Service plan file not found: ${planPath}`);
|
|
314
|
+
}
|
|
315
|
+
const planText = node_fs_1.default.readFileSync(planPath, 'utf8');
|
|
316
|
+
const plan = (0, serviceNoticePlan_1.parseServiceNoticePlan)(planText);
|
|
317
|
+
const serviceAlias = `service:${plan.serviceId}`;
|
|
318
|
+
let selected = input.contractChangeId
|
|
319
|
+
? findIndexRowById(context.indexRows, input.contractChangeId)
|
|
320
|
+
: findIndexRowByAlias(context.indexRows, serviceAlias);
|
|
321
|
+
if (!selected && input.contractChangeId) {
|
|
322
|
+
if (!/^CC-\d{3}$/.test(input.contractChangeId)) {
|
|
323
|
+
throw new Error(`Invalid --contract-change-id '${input.contractChangeId}'. Expected format CC-###.`);
|
|
324
|
+
}
|
|
325
|
+
const newRow = {
|
|
326
|
+
contractChangeId: input.contractChangeId,
|
|
327
|
+
name: `${plan.name} onboarding`,
|
|
328
|
+
status: 'approved',
|
|
329
|
+
changeType: SERVICE_NOTICE_CHANGE_TYPE,
|
|
330
|
+
owner: context.sourceRepoParts.owner,
|
|
331
|
+
path: contractFilePath(input.contractChangeId, plan.name),
|
|
332
|
+
aliases: serviceAlias,
|
|
333
|
+
};
|
|
334
|
+
context.indexRows.push(newRow);
|
|
335
|
+
selected = { row: newRow, rowIndex: context.indexRows.length - 1 };
|
|
336
|
+
}
|
|
337
|
+
if (!selected) {
|
|
338
|
+
const sourceContractId = (0, contractChanges_1.nextContractChangeId)(context.indexRows);
|
|
339
|
+
const newRow = {
|
|
340
|
+
contractChangeId: sourceContractId,
|
|
341
|
+
name: `${plan.name} onboarding`,
|
|
342
|
+
status: 'approved',
|
|
343
|
+
changeType: SERVICE_NOTICE_CHANGE_TYPE,
|
|
344
|
+
owner: context.sourceRepoParts.owner,
|
|
345
|
+
path: contractFilePath(sourceContractId, plan.name),
|
|
346
|
+
aliases: serviceAlias,
|
|
347
|
+
};
|
|
348
|
+
context.indexRows.push(newRow);
|
|
349
|
+
selected = { row: newRow, rowIndex: context.indexRows.length - 1 };
|
|
350
|
+
}
|
|
351
|
+
const existingRow = selected.row;
|
|
352
|
+
const existingTargetsByRepo = new Map();
|
|
353
|
+
try {
|
|
354
|
+
const existingDoc = (0, contractChanges_1.readContractChangeDoc)(context.sourceRepoPath, existingRow);
|
|
355
|
+
for (const target of existingDoc.targets) {
|
|
356
|
+
existingTargetsByRepo.set(normalizeRepoRef(target.repo), target);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// No existing source doc yet.
|
|
361
|
+
}
|
|
362
|
+
const nextTargets = plan.targets.map((target) => {
|
|
363
|
+
const preserved = existingTargetsByRepo.get(normalizeRepoRef(target.repo));
|
|
364
|
+
return {
|
|
365
|
+
repo: target.repo,
|
|
366
|
+
owner: target.owner,
|
|
367
|
+
context: target.context,
|
|
368
|
+
prUrl: preserved?.prUrl ?? '',
|
|
369
|
+
state: preserved?.state ?? 'pending',
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
existingRow.name = `${plan.name} onboarding`;
|
|
373
|
+
existingRow.changeType = SERVICE_NOTICE_CHANGE_TYPE;
|
|
374
|
+
existingRow.owner = context.sourceRepoParts.owner;
|
|
375
|
+
existingRow.path = existingRow.path || contractFilePath(existingRow.contractChangeId, plan.name);
|
|
376
|
+
existingRow.aliases = mergeAliasTokens(existingRow.aliases, [serviceAlias]);
|
|
377
|
+
existingRow.status = existingRow.status === 'draft' ? 'approved' : existingRow.status;
|
|
378
|
+
const doc = {
|
|
379
|
+
contractChangeId: existingRow.contractChangeId,
|
|
380
|
+
name: existingRow.name,
|
|
381
|
+
status: existingRow.status,
|
|
382
|
+
changeType: existingRow.changeType,
|
|
383
|
+
owner: existingRow.owner,
|
|
384
|
+
lastUpdated: today(),
|
|
385
|
+
absolutePath: node_path_1.default.join(context.sourceRepoPath, 'docs', existingRow.path),
|
|
386
|
+
relativePath: `docs/${existingRow.path}`,
|
|
387
|
+
sections: {
|
|
388
|
+
summary: plan.summary,
|
|
389
|
+
contractSurface: plan.contractSurface,
|
|
390
|
+
changeDetails: plan.changeDetails,
|
|
391
|
+
compatibilityAndMigrationGuidance: plan.migrationGuidance,
|
|
392
|
+
},
|
|
393
|
+
targets: nextTargets,
|
|
394
|
+
};
|
|
395
|
+
return {
|
|
396
|
+
row: existingRow,
|
|
397
|
+
rowIndex: selected.rowIndex,
|
|
398
|
+
doc,
|
|
399
|
+
sourceChanged: true,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function buildNoticeContracts(context, input) {
|
|
403
|
+
if (input.noticeType === 'service') {
|
|
404
|
+
return [buildSourceDocFromServicePlan(context, input)];
|
|
405
|
+
}
|
|
406
|
+
return loadContractModeContracts(context, input.contractChangeId);
|
|
407
|
+
}
|
|
408
|
+
async function upsertSourceSync(context, doc, ready) {
|
|
409
|
+
if (!context.ops) {
|
|
410
|
+
throw new Error('GitHub publish operations are not configured.');
|
|
411
|
+
}
|
|
412
|
+
const branch = createSourceSyncBranch(doc.contractChangeId);
|
|
413
|
+
await context.ops.ensureBranch(context.sourceRepoParts.owner, context.sourceRepoParts.repo, branch);
|
|
414
|
+
const sourceDocPath = doc.relativePath.startsWith('docs/') ? doc.relativePath : `docs/${doc.relativePath}`;
|
|
415
|
+
await context.ops.upsertTextFile(context.sourceRepoParts.owner, context.sourceRepoParts.repo, branch, sourceDocPath, (0, contractChanges_1.renderContractChangeDoc)(doc), `chore(sdx): sync ${doc.contractChangeId} downstream state`);
|
|
416
|
+
await context.ops.upsertTextFile(context.sourceRepoParts.owner, context.sourceRepoParts.repo, branch, 'docs/CONTRACT_CHANGES.md', (0, contractChanges_1.renderContractChangeIndex)(context.indexRows, context.indexMeta), `chore(sdx): refresh contract index for ${doc.contractChangeId}`);
|
|
417
|
+
const pr = await context.ops.upsertPullRequest({
|
|
418
|
+
owner: context.sourceRepoParts.owner,
|
|
419
|
+
repo: context.sourceRepoParts.repo,
|
|
420
|
+
branch,
|
|
421
|
+
title: `chore(sdx): sync ${doc.contractChangeId} downstream state`,
|
|
422
|
+
body: buildSourceSyncPrBody(doc),
|
|
423
|
+
draft: !ready,
|
|
424
|
+
});
|
|
425
|
+
return pr.url;
|
|
426
|
+
}
|
|
427
|
+
function findExistingTargetRow(rows, source, sourceDoc) {
|
|
428
|
+
const markers = sourceAliasTokens(source, sourceDoc.contractChangeId);
|
|
429
|
+
return rows.find((row) => {
|
|
430
|
+
const aliases = parseAliasTokens(row.aliases);
|
|
431
|
+
return markers.every((marker) => aliases.includes(marker));
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
async function publishTargetContractChange(context, sourceDoc, target, targetRepo, ready) {
|
|
435
|
+
if (!context.ops) {
|
|
436
|
+
throw new Error('GitHub publish operations are not configured.');
|
|
437
|
+
}
|
|
438
|
+
const branch = createTargetBranch(sourceDoc.contractChangeId, targetRepo.repo);
|
|
439
|
+
await context.ops.ensureBranch(targetRepo.owner, targetRepo.repo, branch);
|
|
440
|
+
const indexPath = 'docs/CONTRACT_CHANGES.md';
|
|
441
|
+
const indexBody = await context.ops.readTextFile(targetRepo.owner, targetRepo.repo, indexPath, branch);
|
|
442
|
+
if (!indexBody || !(0, contractChanges_1.hasContractChangeIndexShape)(indexBody)) {
|
|
443
|
+
throw new Error('spec-system not instantiated (or invalid): missing or invalid docs/CONTRACT_CHANGES.md');
|
|
444
|
+
}
|
|
445
|
+
const parsed = (0, contractChanges_1.parseContractChangeIndexText)(indexBody);
|
|
446
|
+
const existingTargetRow = findExistingTargetRow(parsed.rows, context.sourceRepoParts, sourceDoc);
|
|
447
|
+
const targetContractChangeId = existingTargetRow
|
|
448
|
+
? existingTargetRow.contractChangeId
|
|
449
|
+
: (0, contractChanges_1.nextContractChangeId)(parsed.rows);
|
|
450
|
+
const sourceAliases = sourceAliasTokens(context.sourceRepoParts, sourceDoc.contractChangeId);
|
|
451
|
+
const targetRow = {
|
|
452
|
+
contractChangeId: targetContractChangeId,
|
|
453
|
+
name: `Respond to ${sourceDoc.contractChangeId}: ${sourceDoc.name}`,
|
|
454
|
+
status: 'draft',
|
|
455
|
+
changeType: sourceDoc.changeType || 'api_contract_changed',
|
|
456
|
+
owner: target.owner,
|
|
457
|
+
path: existingTargetRow?.path || contractFilePath(targetContractChangeId, sourceDoc.name),
|
|
458
|
+
aliases: mergeAliasTokens(existingTargetRow?.aliases ?? '', sourceAliases),
|
|
459
|
+
};
|
|
460
|
+
const rowIndex = parsed.rows.findIndex((row) => row.contractChangeId === targetRow.contractChangeId);
|
|
461
|
+
if (rowIndex === -1) {
|
|
462
|
+
parsed.rows.push(targetRow);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
parsed.rows[rowIndex] = targetRow;
|
|
466
|
+
}
|
|
467
|
+
parsed.rows.sort((a, b) => a.contractChangeId.localeCompare(b.contractChangeId));
|
|
468
|
+
const targetDoc = buildTargetContractDoc(context.sourceRepoParts, sourceDoc, targetContractChangeId, target.owner);
|
|
469
|
+
const targetDocPath = `docs/${targetRow.path.replace(/^docs\//, '')}`;
|
|
470
|
+
await context.ops.upsertTextFile(targetRepo.owner, targetRepo.repo, branch, indexPath, (0, contractChanges_1.renderContractChangeIndex)(parsed.rows, parsed.meta), `chore(sdx): register ${targetContractChangeId} from ${sourceDoc.contractChangeId}`);
|
|
471
|
+
await context.ops.upsertTextFile(targetRepo.owner, targetRepo.repo, branch, targetDocPath, (0, contractChanges_1.renderContractChangeDoc)(targetDoc), `chore(sdx): add ${targetContractChangeId} from ${sourceDoc.contractChangeId}`);
|
|
472
|
+
const pr = await context.ops.upsertPullRequest({
|
|
473
|
+
owner: targetRepo.owner,
|
|
474
|
+
repo: targetRepo.repo,
|
|
475
|
+
branch,
|
|
476
|
+
title: `chore(contract-change): ${targetContractChangeId} for ${sourceDoc.contractChangeId}`,
|
|
477
|
+
body: buildTargetPrBody(context.sourceRepoParts, sourceDoc, target.context, targetContractChangeId),
|
|
478
|
+
draft: !ready,
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
prUrl: pr.url,
|
|
482
|
+
created: pr.created,
|
|
483
|
+
targetContractChangeId,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function buildNoticesArtifactMarkdown(result) {
|
|
487
|
+
const lines = [
|
|
488
|
+
'# Publish Notices Run',
|
|
489
|
+
'',
|
|
490
|
+
`- Generated: ${result.generatedAt}`,
|
|
491
|
+
`- Map: ${result.mapId}`,
|
|
492
|
+
`- Source Repo: ${result.sourceRepo}`,
|
|
493
|
+
`- Notice Type: ${result.noticeType}`,
|
|
494
|
+
`- Dry Run: ${result.dryRun ? 'yes' : 'no'}`,
|
|
495
|
+
`- Draft Mode: ${result.ready ? 'ready PRs' : 'draft PRs'}`,
|
|
496
|
+
...(result.planPath ? [`- Plan Path: ${result.planPath}`] : []),
|
|
497
|
+
...(result.failFastStoppedAt ? [`- Fail Fast Stop: ${result.failFastStoppedAt}`] : []),
|
|
498
|
+
'',
|
|
499
|
+
`Totals: created=${result.totals.created}, updated=${result.totals.updated}, skipped=${result.totals.skipped}, failed=${result.totals.failed}`,
|
|
500
|
+
'',
|
|
501
|
+
];
|
|
502
|
+
for (const contract of result.contracts) {
|
|
503
|
+
lines.push(`## ${contract.contractChangeId}: ${contract.name}`);
|
|
504
|
+
lines.push(`- Eligible: ${contract.eligible ? 'yes' : 'no'}`);
|
|
505
|
+
lines.push(`- Status: ${contract.sourceStatusBefore} -> ${contract.sourceStatusAfter}`);
|
|
506
|
+
lines.push(`- Source Sync PR: ${contract.sourceSyncPrUrl ?? '-'}`);
|
|
507
|
+
lines.push('');
|
|
508
|
+
lines.push('| Target | Target CC | Result | State | PR | Notes |');
|
|
509
|
+
lines.push('|---|---|---|---|---|---|');
|
|
510
|
+
if (contract.targetResults.length === 0) {
|
|
511
|
+
lines.push('| - | - | skipped | - | - | No target rows processed |');
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
for (const target of contract.targetResults) {
|
|
515
|
+
lines.push(`| ${target.targetRepoInput} | ${target.targetContractChangeId ?? '-'} | ${target.status} | ${target.stateBefore} -> ${target.stateAfter} | ${target.prUrl ?? '-'} | ${target.reason ?? '-'} |`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
lines.push('');
|
|
519
|
+
}
|
|
520
|
+
return `${lines.join('\n')}\n`;
|
|
521
|
+
}
|
|
522
|
+
function buildSyncArtifactMarkdown(result) {
|
|
523
|
+
const lines = [
|
|
524
|
+
'# Publish Sync Run',
|
|
525
|
+
'',
|
|
526
|
+
`- Generated: ${result.generatedAt}`,
|
|
527
|
+
`- Map: ${result.mapId}`,
|
|
528
|
+
`- Source Repo: ${result.sourceRepo}`,
|
|
529
|
+
`- Dry Run: ${result.dryRun ? 'yes' : 'no'}`,
|
|
530
|
+
'',
|
|
531
|
+
`Totals: updated=${result.totals.updated}, skipped=${result.totals.skipped}, failed=${result.totals.failed}`,
|
|
532
|
+
'',
|
|
533
|
+
];
|
|
534
|
+
for (const contract of result.contracts) {
|
|
535
|
+
lines.push(`## ${contract.contractChangeId}: ${contract.name}`);
|
|
536
|
+
lines.push(`- Status: ${contract.sourceStatusBefore} -> ${contract.sourceStatusAfter}`);
|
|
537
|
+
lines.push(`- Source Sync PR: ${contract.sourceSyncPrUrl ?? '-'}`);
|
|
538
|
+
lines.push('');
|
|
539
|
+
lines.push('| Target | Result | State | PR | Notes |');
|
|
540
|
+
lines.push('|---|---|---|---|---|');
|
|
541
|
+
if (contract.targetResults.length === 0) {
|
|
542
|
+
lines.push('| - | skipped | - | - | No target rows processed |');
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
for (const target of contract.targetResults) {
|
|
546
|
+
lines.push(`| ${target.repo} | ${target.status} | ${target.stateBefore} -> ${target.stateAfter} | ${target.prUrl || '-'} | ${target.reason ?? '-'} |`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
lines.push('');
|
|
550
|
+
}
|
|
551
|
+
return `${lines.join('\n')}\n`;
|
|
552
|
+
}
|
|
553
|
+
function totalsForNotice(result) {
|
|
554
|
+
const totals = { created: 0, updated: 0, skipped: 0, failed: 0 };
|
|
555
|
+
for (const contract of result) {
|
|
556
|
+
for (const target of contract.targetResults) {
|
|
557
|
+
totals[target.status] += 1;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return totals;
|
|
561
|
+
}
|
|
562
|
+
function totalsForSync(result) {
|
|
563
|
+
const totals = { updated: 0, skipped: 0, failed: 0 };
|
|
564
|
+
for (const contract of result) {
|
|
565
|
+
for (const target of contract.targetResults) {
|
|
566
|
+
totals[target.status] += 1;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return totals;
|
|
570
|
+
}
|
|
571
|
+
function lifecycleToTargetState(value) {
|
|
572
|
+
if (value === 'merged') {
|
|
573
|
+
return 'merged';
|
|
574
|
+
}
|
|
575
|
+
if (value === 'blocked') {
|
|
576
|
+
return 'blocked';
|
|
577
|
+
}
|
|
578
|
+
return 'opened';
|
|
579
|
+
}
|
|
580
|
+
function formatError(error) {
|
|
581
|
+
if (error instanceof Error) {
|
|
582
|
+
return error.message;
|
|
583
|
+
}
|
|
584
|
+
return String(error);
|
|
585
|
+
}
|
|
586
|
+
async function publishNotices(input) {
|
|
587
|
+
const noticeType = input.noticeType ?? 'contract';
|
|
588
|
+
const dryRun = Boolean(input.dryRun);
|
|
589
|
+
const ready = Boolean(input.ready);
|
|
590
|
+
const maxTargets = input.maxTargets && input.maxTargets > 0 ? input.maxTargets : undefined;
|
|
591
|
+
const context = createPublishContext(input, !dryRun);
|
|
592
|
+
const sourceSyncPrUrls = new Set();
|
|
593
|
+
let remainingTargets = maxTargets ?? Number.POSITIVE_INFINITY;
|
|
594
|
+
let stopReason;
|
|
595
|
+
const contractsToProcess = buildNoticeContracts(context, input);
|
|
596
|
+
const contracts = [];
|
|
597
|
+
for (const loaded of contractsToProcess) {
|
|
598
|
+
const fallbackName = loaded.row.name;
|
|
599
|
+
if (!loaded.doc) {
|
|
600
|
+
contracts.push({
|
|
601
|
+
contractChangeId: loaded.row.contractChangeId,
|
|
602
|
+
name: fallbackName,
|
|
603
|
+
sourceStatusBefore: loaded.row.status,
|
|
604
|
+
sourceStatusAfter: loaded.row.status,
|
|
605
|
+
eligible: false,
|
|
606
|
+
targetResults: [
|
|
607
|
+
{
|
|
608
|
+
contractChangeId: loaded.row.contractChangeId,
|
|
609
|
+
targetRepoInput: '-',
|
|
610
|
+
owner: '-',
|
|
611
|
+
stateBefore: '-',
|
|
612
|
+
stateAfter: '-',
|
|
613
|
+
status: 'failed',
|
|
614
|
+
reason: loaded.loadError ?? 'Unable to load source contract change artifact.',
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
});
|
|
618
|
+
stopReason = contracts[contracts.length - 1].targetResults[0].reason;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
const doc = loaded.doc;
|
|
622
|
+
const contractResult = {
|
|
623
|
+
contractChangeId: doc.contractChangeId,
|
|
624
|
+
name: doc.name,
|
|
625
|
+
sourceStatusBefore: doc.status,
|
|
626
|
+
sourceStatusAfter: doc.status,
|
|
627
|
+
eligible: (0, contractChanges_1.isNotifiableContractStatus)(doc.status),
|
|
628
|
+
targetResults: [],
|
|
629
|
+
};
|
|
630
|
+
if (!contractResult.eligible) {
|
|
631
|
+
contractResult.targetResults.push({
|
|
632
|
+
contractChangeId: doc.contractChangeId,
|
|
633
|
+
targetRepoInput: '-',
|
|
634
|
+
owner: '-',
|
|
635
|
+
stateBefore: '-',
|
|
636
|
+
stateAfter: '-',
|
|
637
|
+
status: 'skipped',
|
|
638
|
+
reason: `Status '${doc.status}' is not eligible. Expected approved or published.`,
|
|
639
|
+
});
|
|
640
|
+
contracts.push(contractResult);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
for (const target of doc.targets) {
|
|
644
|
+
const targetResult = {
|
|
645
|
+
contractChangeId: doc.contractChangeId,
|
|
646
|
+
targetRepoInput: target.repo,
|
|
647
|
+
owner: target.owner,
|
|
648
|
+
stateBefore: target.state,
|
|
649
|
+
stateAfter: target.state,
|
|
650
|
+
status: 'skipped',
|
|
651
|
+
};
|
|
652
|
+
if (target.state !== 'pending') {
|
|
653
|
+
targetResult.reason = `Target state is '${target.state}', only pending targets are publishable.`;
|
|
654
|
+
contractResult.targetResults.push(targetResult);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (!(0, contractChanges_1.hasRequiredTargetContext)(target)) {
|
|
658
|
+
targetResult.status = 'failed';
|
|
659
|
+
targetResult.reason = 'Target row is missing required repo/owner/context values.';
|
|
660
|
+
contractResult.targetResults.push(targetResult);
|
|
661
|
+
stopReason = targetResult.reason;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
if (remainingTargets <= 0) {
|
|
665
|
+
targetResult.reason = `Skipped due to --max-targets=${maxTargets}.`;
|
|
666
|
+
contractResult.targetResults.push(targetResult);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const resolution = resolveRepoFromScope(target.repo, target.owner, context.scopedRepos, context.allRepos);
|
|
670
|
+
if (!resolution.record) {
|
|
671
|
+
targetResult.status = 'failed';
|
|
672
|
+
targetResult.reason = resolution.error;
|
|
673
|
+
contractResult.targetResults.push(targetResult);
|
|
674
|
+
stopReason = targetResult.reason;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
const targetRepo = repoParts(resolution.record);
|
|
678
|
+
targetResult.targetRepoResolved = targetRepo.fullName;
|
|
679
|
+
if (dryRun) {
|
|
680
|
+
targetResult.reason = 'Dry run: would create/update target spec-system contract change PR.';
|
|
681
|
+
contractResult.targetResults.push(targetResult);
|
|
682
|
+
remainingTargets -= 1;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const published = await publishTargetContractChange(context, doc, target, targetRepo, ready);
|
|
687
|
+
target.prUrl = published.prUrl;
|
|
688
|
+
target.state = 'opened';
|
|
689
|
+
targetResult.stateAfter = 'opened';
|
|
690
|
+
targetResult.prUrl = published.prUrl;
|
|
691
|
+
targetResult.targetContractChangeId = published.targetContractChangeId;
|
|
692
|
+
targetResult.status = published.created ? 'created' : 'updated';
|
|
693
|
+
loaded.sourceChanged = true;
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
targetResult.status = 'failed';
|
|
697
|
+
targetResult.reason = formatError(error);
|
|
698
|
+
stopReason = targetResult.reason;
|
|
699
|
+
}
|
|
700
|
+
contractResult.targetResults.push(targetResult);
|
|
701
|
+
remainingTargets -= 1;
|
|
702
|
+
if (targetResult.status === 'failed') {
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
doc.status = (0, contractChanges_1.computeContractChangeStatus)(doc.status, doc.targets);
|
|
707
|
+
contractResult.sourceStatusAfter = doc.status;
|
|
708
|
+
updateIndexRowFromDoc(loaded.row, doc);
|
|
709
|
+
if (!dryRun && loaded.sourceChanged) {
|
|
710
|
+
try {
|
|
711
|
+
const sourceSyncPrUrl = await upsertSourceSync(context, doc, ready);
|
|
712
|
+
contractResult.sourceSyncPrUrl = sourceSyncPrUrl;
|
|
713
|
+
sourceSyncPrUrls.add(sourceSyncPrUrl);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
const syncFailure = {
|
|
717
|
+
contractChangeId: doc.contractChangeId,
|
|
718
|
+
targetRepoInput: context.sourceRepoParts.fullName,
|
|
719
|
+
owner: context.sourceRepoParts.owner,
|
|
720
|
+
stateBefore: contractResult.sourceStatusBefore,
|
|
721
|
+
stateAfter: contractResult.sourceStatusAfter,
|
|
722
|
+
status: 'failed',
|
|
723
|
+
reason: `Source sync failed: ${formatError(error)}`,
|
|
724
|
+
};
|
|
725
|
+
contractResult.targetResults.push(syncFailure);
|
|
726
|
+
stopReason = syncFailure.reason;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
contracts.push(contractResult);
|
|
730
|
+
if (stopReason) {
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const artifactPaths = ensureArtifactPaths(context.cwd, 'notices', input.mapId, input.contractChangeId);
|
|
735
|
+
const result = {
|
|
736
|
+
schemaVersion: constants_1.SCHEMA_VERSION,
|
|
737
|
+
generatedAt: nowIso(),
|
|
738
|
+
mapId: input.mapId,
|
|
739
|
+
sourceRepo: context.sourceRepoParts.fullName,
|
|
740
|
+
contractChangeId: input.contractChangeId,
|
|
741
|
+
noticeType,
|
|
742
|
+
planPath: input.planPath ? node_path_1.default.resolve(context.cwd, input.planPath) : undefined,
|
|
743
|
+
dryRun,
|
|
744
|
+
ready,
|
|
745
|
+
maxTargets,
|
|
746
|
+
failFastStoppedAt: stopReason,
|
|
747
|
+
totals: totalsForNotice(contracts),
|
|
748
|
+
contracts,
|
|
749
|
+
sourceSyncPrUrls: [...sourceSyncPrUrls].sort((a, b) => a.localeCompare(b)),
|
|
750
|
+
artifactJsonPath: artifactPaths.jsonPath,
|
|
751
|
+
artifactMarkdownPath: artifactPaths.markdownPath,
|
|
752
|
+
};
|
|
753
|
+
(0, fs_1.writeJsonFile)(result.artifactJsonPath, result);
|
|
754
|
+
(0, fs_1.writeTextFile)(result.artifactMarkdownPath, buildNoticesArtifactMarkdown(result));
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
async function publishSync(input) {
|
|
758
|
+
const dryRun = Boolean(input.dryRun);
|
|
759
|
+
const context = createPublishContext(input, !dryRun);
|
|
760
|
+
const sourceSyncPrUrls = new Set();
|
|
761
|
+
const contractsToProcess = loadContractModeContracts(context, input.contractChangeId);
|
|
762
|
+
const contracts = [];
|
|
763
|
+
for (const loaded of contractsToProcess) {
|
|
764
|
+
if (!loaded.doc) {
|
|
765
|
+
contracts.push({
|
|
766
|
+
contractChangeId: loaded.row.contractChangeId,
|
|
767
|
+
name: loaded.row.name,
|
|
768
|
+
sourceStatusBefore: loaded.row.status,
|
|
769
|
+
sourceStatusAfter: loaded.row.status,
|
|
770
|
+
targetResults: [
|
|
771
|
+
{
|
|
772
|
+
contractChangeId: loaded.row.contractChangeId,
|
|
773
|
+
repo: '-',
|
|
774
|
+
prUrl: '',
|
|
775
|
+
stateBefore: '-',
|
|
776
|
+
stateAfter: '-',
|
|
777
|
+
status: 'failed',
|
|
778
|
+
reason: loaded.loadError ?? 'Unable to load source contract change artifact.',
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
});
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const doc = loaded.doc;
|
|
785
|
+
const contractResult = {
|
|
786
|
+
contractChangeId: doc.contractChangeId,
|
|
787
|
+
name: doc.name,
|
|
788
|
+
sourceStatusBefore: doc.status,
|
|
789
|
+
sourceStatusAfter: doc.status,
|
|
790
|
+
targetResults: [],
|
|
791
|
+
};
|
|
792
|
+
for (const target of doc.targets) {
|
|
793
|
+
const targetResult = {
|
|
794
|
+
contractChangeId: doc.contractChangeId,
|
|
795
|
+
repo: target.repo,
|
|
796
|
+
prUrl: target.prUrl,
|
|
797
|
+
stateBefore: target.state,
|
|
798
|
+
stateAfter: target.state,
|
|
799
|
+
status: 'skipped',
|
|
800
|
+
};
|
|
801
|
+
if (!target.prUrl.trim()) {
|
|
802
|
+
targetResult.reason = 'No pr_url set.';
|
|
803
|
+
contractResult.targetResults.push(targetResult);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (dryRun) {
|
|
807
|
+
targetResult.reason = 'Dry run: would refresh PR lifecycle state.';
|
|
808
|
+
contractResult.targetResults.push(targetResult);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (!context.ops) {
|
|
812
|
+
targetResult.status = 'failed';
|
|
813
|
+
targetResult.reason = 'GitHub token not available; cannot refresh PR lifecycle.';
|
|
814
|
+
contractResult.targetResults.push(targetResult);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const lifecycle = await context.ops.getPullLifecycle(target.prUrl);
|
|
819
|
+
const nextState = lifecycleToTargetState(lifecycle);
|
|
820
|
+
target.state = nextState;
|
|
821
|
+
targetResult.stateAfter = nextState;
|
|
822
|
+
targetResult.status = nextState === targetResult.stateBefore ? 'skipped' : 'updated';
|
|
823
|
+
if (targetResult.status === 'skipped') {
|
|
824
|
+
targetResult.reason = 'State unchanged after lifecycle lookup.';
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
loaded.sourceChanged = true;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
targetResult.status = 'failed';
|
|
832
|
+
targetResult.reason = formatError(error);
|
|
833
|
+
}
|
|
834
|
+
contractResult.targetResults.push(targetResult);
|
|
835
|
+
}
|
|
836
|
+
doc.status = (0, contractChanges_1.computeContractChangeStatus)(doc.status, doc.targets);
|
|
837
|
+
contractResult.sourceStatusAfter = doc.status;
|
|
838
|
+
updateIndexRowFromDoc(loaded.row, doc);
|
|
839
|
+
if (!dryRun && loaded.sourceChanged) {
|
|
840
|
+
try {
|
|
841
|
+
const sourceSyncPrUrl = await upsertSourceSync(context, doc, false);
|
|
842
|
+
contractResult.sourceSyncPrUrl = sourceSyncPrUrl;
|
|
843
|
+
sourceSyncPrUrls.add(sourceSyncPrUrl);
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
contractResult.targetResults.push({
|
|
847
|
+
contractChangeId: doc.contractChangeId,
|
|
848
|
+
repo: context.sourceRepoParts.fullName,
|
|
849
|
+
prUrl: '',
|
|
850
|
+
stateBefore: contractResult.sourceStatusBefore,
|
|
851
|
+
stateAfter: contractResult.sourceStatusAfter,
|
|
852
|
+
status: 'failed',
|
|
853
|
+
reason: `Source sync failed: ${formatError(error)}`,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
contracts.push(contractResult);
|
|
858
|
+
}
|
|
859
|
+
const artifactPaths = ensureArtifactPaths(context.cwd, 'sync', input.mapId, input.contractChangeId);
|
|
860
|
+
const result = {
|
|
861
|
+
schemaVersion: constants_1.SCHEMA_VERSION,
|
|
862
|
+
generatedAt: nowIso(),
|
|
863
|
+
mapId: input.mapId,
|
|
864
|
+
sourceRepo: context.sourceRepoParts.fullName,
|
|
865
|
+
contractChangeId: input.contractChangeId,
|
|
866
|
+
dryRun,
|
|
867
|
+
totals: totalsForSync(contracts),
|
|
868
|
+
contracts,
|
|
869
|
+
sourceSyncPrUrls: [...sourceSyncPrUrls].sort((a, b) => a.localeCompare(b)),
|
|
870
|
+
artifactJsonPath: artifactPaths.jsonPath,
|
|
871
|
+
artifactMarkdownPath: artifactPaths.markdownPath,
|
|
872
|
+
};
|
|
873
|
+
(0, fs_1.writeJsonFile)(result.artifactJsonPath, result);
|
|
874
|
+
(0, fs_1.writeTextFile)(result.artifactMarkdownPath, buildSyncArtifactMarkdown(result));
|
|
875
|
+
return result;
|
|
876
|
+
}
|