fix(skills): preserve released history across new tags (#9778)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin 2026-07-21 17:06:31 -04:00 committed by GitHub
parent f1c84d3858
commit ae12bb1292
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 6 deletions

View File

@ -423,20 +423,38 @@ async function buildArtifacts() {
// Why: released snapshots are the detection ground truth for existing installs,
// so a generation-logic change must not rewrite them silently. Only the one
// unreleased working-tree append per skill may change between runs.
function assertReleasedHistoryPreserved(committedRegistry, artifacts) {
function releasedSnapshotCountsFromMapping(releaseMapping) {
if (!releaseMapping || releaseMapping.schemaVersion !== RELEASE_MAPPING_SCHEMA_VERSION) {
return null
}
const counts = {}
for (const release of releaseMapping.releases ?? []) {
for (const [name, revision] of Object.entries(release.skills ?? {})) {
counts[name] = Math.max(counts[name] ?? 0, revision)
}
}
return counts
}
function assertReleasedHistoryPreserved(committedRegistry, artifacts, committedReleaseMapping) {
if (!committedRegistry || committedRegistry.schemaVersion !== SNAPSHOT_REGISTRY_SCHEMA_VERSION) {
return
}
const committedReleasedCounts = releasedSnapshotCountsFromMapping(committedReleaseMapping)
for (const [name, committedSnapshots] of Object.entries(committedRegistry.skills ?? {})) {
const releasedCount = artifacts.releasedSnapshotCounts[name] ?? 0
const regenerated = artifacts.snapshotRegistry.skills[name] ?? []
if (releasedCount < Math.max(0, committedSnapshots.length - 1)) {
const minimumReleasedCount =
committedReleasedCounts?.[name] ?? Math.max(0, committedSnapshots.length - 1)
if (releasedCount < minimumReleasedCount) {
throw new Error(
`Released snapshot history is incomplete for ${name}. ` +
'Fetch all release tags before regenerating skill artifacts.'
)
}
const protectedCount = Math.min(committedSnapshots.length, releasedCount)
const protectedCount = committedReleasedCounts
? (committedReleasedCounts[name] ?? 0)
: Math.min(committedSnapshots.length, releasedCount)
for (let index = 0; index < protectedCount; index += 1) {
const committed = committedSnapshots[index]
const rebuilt = regenerated[index]
@ -458,6 +476,14 @@ async function readCommittedRegistry() {
}
}
async function readCommittedReleaseMapping() {
try {
return JSON.parse(await readFile(RELEASE_MAPPING_PATH, 'utf8'))
} catch {
return null
}
}
function serialized(value) {
return `${JSON.stringify(value, null, 2)}\n`
}
@ -532,7 +558,11 @@ async function verifyArtifacts(artifacts) {
async function main() {
const artifacts = await buildArtifacts()
assertReleasedHistoryPreserved(await readCommittedRegistry(), artifacts)
assertReleasedHistoryPreserved(
await readCommittedRegistry(),
artifacts,
await readCommittedReleaseMapping()
)
await (process.argv.includes('--write') ? writeArtifacts : verifyArtifacts)(artifacts)
}

View File

@ -137,6 +137,36 @@ describe('skill bundle manifest generator', () => {
expect(() => assertReleasedHistoryPreserved(null, artifacts)).not.toThrow()
})
it('protects only revisions named by the committed release mapping', () => {
const snapshot = (releaseRevision, packageDigest) => ({ releaseRevision, packageDigest })
const committedRegistry = {
schemaVersion: 1,
skills: {
'linear-tickets': [snapshot(1, 'released'), snapshot(2, 'unreleased-tail')]
}
}
const artifacts = {
releasedSnapshotCounts: { 'linear-tickets': 2 },
snapshotRegistry: {
schemaVersion: 1,
skills: { 'linear-tickets': [snapshot(1, 'released'), snapshot(2, 'new-release')] }
}
}
expect(() =>
assertReleasedHistoryPreserved(committedRegistry, artifacts, {
schemaVersion: 1,
releases: [{ appVersion: '1.0.0', skills: { 'linear-tickets': 1 } }]
})
).not.toThrow()
expect(() =>
assertReleasedHistoryPreserved(committedRegistry, artifacts, {
schemaVersion: 1,
releases: [{ appVersion: '1.0.0', skills: { 'linear-tickets': 2 } }]
})
).toThrow('Released snapshot history changed for linear-tickets at revision 2')
})
it('tolerates only redundant trailing release-mapping rows', () => {
const serialized = (value) => `${JSON.stringify(value, null, 2)}\n`
const rows = [

View File

@ -22,7 +22,7 @@
{
"name": "linear-tickets",
"sourcePath": "skills/linear-tickets",
"releaseRevision": 6,
"releaseRevision": 7,
"packageDigest": "ff9f085631f753f059c631d874177ddd4fa847c5eca85a420dc85fb2bece6ff6",
"gitTreeSha": "e35ac3c0c583661983d3fc1352ff3aec74e67e8c",
"files": [
@ -94,7 +94,7 @@
{
"name": "orca-linear",
"sourcePath": "skills/orca-linear",
"releaseRevision": 4,
"releaseRevision": 5,
"packageDigest": "5e9622bd3883c0f53e6bd349758096deafceebd2fa260d3e90d677e64d06416d",
"gitTreeSha": "f3727995a4719fd522119eca6d1b57542cb5fe23",
"files": [

View File

@ -561,6 +561,19 @@
"orca-per-workspace-env": 2,
"orchestration": 25
}
},
{
"appVersion": "1.4.149-rc.2",
"skills": {
"computer-use": 5,
"linear-tickets": 6,
"orca-cli": 35,
"orca-emulator": 4,
"orca-emulator-android": 2,
"orca-linear": 4,
"orca-per-workspace-env": 2,
"orchestration": 25
}
}
]
}

View File

@ -1214,6 +1214,22 @@
},
{
"releaseRevision": 6,
"packageDigest": "f198d7b22e5ee1673dac403f9cca0553b124e0a90e4fdd05d2c23b7344e32d2b",
"gitTreeSha": "de9fc106bbb4e313a90ff9a9513a720909bbd176",
"files": [
{
"path": "SKILL.md",
"size": 10596,
"executable": false,
"classification": "text",
"exactSha256": "0c3077c93328b9965430cd6951f1a35889b8c0775037870ea3a8bcf303d9d2c5",
"textNormalizedSha256": "0c3077c93328b9965430cd6951f1a35889b8c0775037870ea3a8bcf303d9d2c5",
"identitySha256": "0c3077c93328b9965430cd6951f1a35889b8c0775037870ea3a8bcf303d9d2c5"
}
]
},
{
"releaseRevision": 7,
"packageDigest": "ff9f085631f753f059c631d874177ddd4fa847c5eca85a420dc85fb2bece6ff6",
"gitTreeSha": "e35ac3c0c583661983d3fc1352ff3aec74e67e8c",
"files": [
@ -1280,6 +1296,22 @@
},
{
"releaseRevision": 4,
"packageDigest": "d44d09e6ecb6a64da177083aad26a95f031cd1cf26ba059fdc888c2628aef64f",
"gitTreeSha": "c34f42030f43e5a85737996fa375bbd79cb5bea8",
"files": [
{
"path": "SKILL.md",
"size": 10320,
"executable": false,
"classification": "text",
"exactSha256": "48ded55ec3842ce65105e6db7adf9bc9ed263ece08555cec056e68e90321c3d5",
"textNormalizedSha256": "48ded55ec3842ce65105e6db7adf9bc9ed263ece08555cec056e68e90321c3d5",
"identitySha256": "48ded55ec3842ce65105e6db7adf9bc9ed263ece08555cec056e68e90321c3d5"
}
]
},
{
"releaseRevision": 5,
"packageDigest": "5e9622bd3883c0f53e6bd349758096deafceebd2fa260d3e90d677e64d06416d",
"gitTreeSha": "f3727995a4719fd522119eca6d1b57542cb5fe23",
"files": [