Skip to content

Commit 5688ea3

Browse files
committed
fix(release-tracks): if using abort policy and found conflicts, throw with all of them
1 parent ccdf50f commit 5688ea3

File tree

3 files changed

+96
-22
lines changed

3 files changed

+96
-22
lines changed

app/lib/release-tracks/conflict-resolution.js

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ const { ReleaseConflictError } = require('../../exceptions');
2323
* @param {Array<Object>} incomingEntries - Entries being promoted into the tier
2424
* @param {string} policy - One of: 'always_overwrite' | 'always_reject' | 'prefer_latest' | 'abort'
2525
* @returns {{ merged: Array<Object>, rejected: Array<Object> }}
26-
* @throws {ReleaseConflictError} If policy is 'abort' and a conflict is detected
26+
* @throws {ReleaseConflictError} If policy is 'abort' and any conflicts are detected
2727
*/
2828
exports.applyConflictPolicy = function applyConflictPolicy(existingTier, incomingEntries, policy) {
2929
const merged = [...existingTier];
3030
const rejected = [];
31+
const conflicts = []; // Collect all conflicts for 'abort' policy
3132

3233
for (const incoming of incomingEntries) {
3334
const conflictIdx = merged.findIndex((e) => e.object_ref === incoming.object_ref);
@@ -61,23 +62,29 @@ exports.applyConflictPolicy = function applyConflictPolicy(existingTier, incomin
6162
}
6263

6364
case 'abort':
64-
throw new ReleaseConflictError(
65-
`Conflict on ${incoming.object_ref}: abort policy prevents promotion`,
66-
{
67-
conflicts: [
68-
{
69-
object_ref: incoming.object_ref,
70-
incumbent_version: incumbent.object_modified,
71-
incoming_version: incoming.object_modified,
72-
},
73-
],
74-
},
75-
);
65+
// Collect all conflicts instead of throwing immediately
66+
conflicts.push({
67+
object_ref: incoming.object_ref,
68+
incumbent_version: incumbent.object_modified,
69+
incoming_version: incoming.object_modified,
70+
});
71+
break;
7672

7773
default:
7874
throw new Error(`Unknown conflict resolution policy: ${policy}`);
7975
}
8076
}
8177

78+
// If we're using 'abort' policy and found any conflicts, throw with all of them
79+
if (policy === 'abort' && conflicts.length > 0) {
80+
const conflictCount = conflicts.length;
81+
const message =
82+
conflictCount === 1
83+
? `Conflict on ${conflicts[0].object_ref}: abort policy prevents promotion`
84+
: `Cannot complete release: ${conflictCount} conflict(s) detected`;
85+
86+
throw new ReleaseConflictError(message, { conflicts });
87+
}
88+
8289
return { merged, rejected };
8390
};

app/services/release-tracks/versioning-service.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ exports.previewBump = async function previewBump(trackId, _format) {
235235
staged_count: staged.length,
236236
members_count: existingMembers.length,
237237
candidates_count: (snapshot.candidates || []).length,
238-
conflict_error: err.message,
238+
conflicts: err.conflicts || [], // Include full conflicts array
239239
};
240240
}
241241
}

docs/COLLECTIONS_V2/05_RELEASE_WORKFLOW.md

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,14 @@ Keep whichever version has the newer `modified` timestamp.
342342

343343
##### 4. `abort` (Tagging/Release Operations Only)
344344

345+
[](./05_RELEASE_WORKFLOW.md#4-abort-taggingrelease-operations-only)
345346
**Only available for `staged_to_members` during tagging/release operations.**
346347

347348
If a conflict occurs during a tagging/release operation (`POST /api/release-tracks/:id/bump`), reject and abort the entire release. The snapshot will NOT be tagged, and no immutable snapshot will be created.
348349

349-
**Example:**
350+
**The error response will include ALL conflicting objects**, not just the first one encountered. This allows editors to see the full scope of conflicts that must be resolved before the release can proceed.
351+
352+
**Example with single conflict:**
350353
```javascript
351354
// Current state:
352355
// - members: attack-pattern--T1234, modified: 2024-01-15
@@ -360,28 +363,62 @@ POST /api/release-tracks/release-track--123/bump
360363
// ERROR Response:
361364
{
362365
"error": "ReleaseConflictError",
363-
"message": "Cannot complete release due to promotion conflict",
366+
"message": "Cannot complete release: 1 conflict(s) detected",
364367
"conflicts": [
365368
{
366369
"object_ref": "attack-pattern--T1234",
367370
"incumbent_version": "2024-01-15T10:00:00Z",
368-
"incoming_version": "2024-02-20T10:00:00Z",
369-
"tier": "members"
371+
"incoming_version": "2024-02-20T10:00:00Z"
370372
}
371-
],
372-
"resolution": "Resolve conflicts manually before releasing, or change promotion_conflicts.staged_to_members policy"
373+
]
374+
}
375+
```
376+
377+
**Example with multiple conflicts:**
378+
```javascript
379+
// Current state:
380+
// - members: attack-pattern--T1234, modified: 2024-01-15
381+
// - members: attack-pattern--T5678, modified: 2024-01-16
382+
// - staged: attack-pattern--T1234, modified: 2024-02-20
383+
// - staged: attack-pattern--T5678, modified: 2024-02-21
384+
// - staged: attack-pattern--T9999, modified: 2024-02-22 (no conflict)
385+
386+
// Tagging request:
387+
POST /api/release-tracks/release-track--123/bump
388+
{ "type": "minor" }
389+
390+
// Result with abort - shows ALL conflicts:
391+
// ERROR Response:
392+
{
393+
"error": "ReleaseConflictError",
394+
"message": "Cannot complete release: 2 conflict(s) detected",
395+
"conflicts": [
396+
{
397+
"object_ref": "attack-pattern--T1234",
398+
"incumbent_version": "2024-01-15T10:00:00Z",
399+
"incoming_version": "2024-02-20T10:00:00Z"
400+
},
401+
{
402+
"object_ref": "attack-pattern--T5678",
403+
"incumbent_version": "2024-01-16T10:00:00Z",
404+
"incoming_version": "2024-02-21T10:00:00Z"
405+
}
406+
]
373407
}
374408

375409
// State unchanged:
376410
// - Snapshot NOT tagged
377411
// - No new version history entry
378412
// - Objects remain in current tiers
413+
// - Editor must resolve both conflicts before retrying
379414
```
380415

381416
**Use case:** "Never accidentally overwrite released content during a release; require explicit conflict resolution"
382417

383418
**Why abort is important:** Once a snapshot is tagged and released, it becomes immutable. The `abort` policy ensures that releases don't inadvertently overwrite existing released content, providing an additional safety guardrail for critical release operations.
384419

420+
**Why report all conflicts:** When multiple conflicts exist, reporting all of them in a single error response allows editors to address all issues at once, rather than discovering them one at a time through repeated release attempts. This significantly improves the workflow efficiency when dealing with complex release scenarios.
421+
385422
#### Configuring Conflict Resolution Policies
386423

387424
**Update release track configuration:**
@@ -459,12 +496,13 @@ GET /api/release-tracks/:id?include=all
459496

460497
### 6. Preview Release
461498

462-
Compute a release preview, which outputs a verbose diff of what will change in the next release.
499+
Compute a release preview, which outputs a verbose diff of what will change in the next release. **This endpoint will detect and report all conflicts** that would prevent the release from proceeding, allowing editors to resolve issues before attempting to tag.
500+
463501
```
464502
GET /api/release-tracks/:id/bump/preview
465503
```
466504

467-
**Response:**
505+
**Response (success - no conflicts):**
468506
```json
469507
{
470508
"current_version": "1.1",
@@ -507,6 +545,35 @@ GET /api/release-tracks/:id/bump/preview
507545
}
508546
```
509547

548+
**Response (with conflicts detected):**
549+
```json
550+
{
551+
"track_id": "release-track--123",
552+
"snapshot_modified": "2024-01-15T16:20:00.000Z",
553+
"is_already_tagged": false,
554+
"current_version": null,
555+
"next_version_minor": "1.2",
556+
"next_version_major": "2.0",
557+
"staged_count": 3,
558+
"members_count": 2,
559+
"candidates_count": 1,
560+
"conflicts": [
561+
{
562+
"object_ref": "attack-pattern--T1234",
563+
"incumbent_version": "2024-01-15T10:00:00Z",
564+
"incoming_version": "2024-02-20T10:00:00Z"
565+
},
566+
{
567+
"object_ref": "attack-pattern--T5678",
568+
"incumbent_version": "2024-01-16T10:00:00Z",
569+
"incoming_version": "2024-02-21T10:00:00Z"
570+
}
571+
]
572+
}
573+
```
574+
575+
**Note:** When the `staged_to_members` conflict policy is set to `abort` and conflicts are detected, the preview will include a `conflicts` array listing **all** conflicting objects, not just the first one encountered.
576+
510577
### 7. Bump with Staging
511578

512579
```

0 commit comments

Comments
 (0)