diff --git a/scripts/ci-eduwork-windows-release.ps1 b/scripts/ci-eduwork-windows-release.ps1 index 49114907..e66df8ee 100644 --- a/scripts/ci-eduwork-windows-release.ps1 +++ b/scripts/ci-eduwork-windows-release.ps1 @@ -166,8 +166,11 @@ $name $Version — Windows x64 Electron 开发版 $receipt | ConvertTo-Json -Depth 20 | Set-Content (Join-Path $publish 'release-receipt.json') -Encoding utf8NoBOM if (-not $Development) { Copy-Item -LiteralPath $notesPath -Destination (Join-Path $publish 'RELEASE-NOTES.md') - & node (Join-Path $CoreRoot 'scripts/github-update-manifest.mjs') (Join-Path $publish 'release-receipt.json') "ecnu/$name" - if ($LASTEXITCODE -ne 0) { throw 'GitHub update manifest generation failed' } + # Other institutions publish their own update feeds from the release receipt. + if ($name -in @('EduWork','EduWork-ECNU')) { + & node (Join-Path $CoreRoot 'scripts/github-update-manifest.mjs') (Join-Path $publish 'release-receipt.json') "ecnu/$name" + if ($LASTEXITCODE -ne 0) { throw 'GitHub update manifest generation failed' } + } } } catch { $receipt.error = $_.Exception.Message diff --git a/scripts/ci-pinned-dsh-desktop.ps1 b/scripts/ci-pinned-dsh-desktop.ps1 index 3ab9c41a..4c608993 100644 --- a/scripts/ci-pinned-dsh-desktop.ps1 +++ b/scripts/ci-pinned-dsh-desktop.ps1 @@ -135,7 +135,9 @@ try { $result.asset=@{name=[IO.Path]::GetFileName($archive);bytes=(Get-Item $archive).Length;sha256=(Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant()} $result.passed=$true $result | ConvertTo-Json -Depth 16 | Set-Content (Join-Path $publish "$platform-$(if ($Stable) {'release'} else {'alpha'})-receipt.json") -Encoding utf8NoBOM - if ($Stable -and $IsWindows) { + # The GitHub update manifest only exists for the ECNU editions; institutional + # editions publish their own update source from the receipt. + if ($Stable -and $IsWindows -and $name -in @('EduWork','EduWork-ECNU')) { & node (Join-Path $CoreRoot 'scripts/github-update-manifest.mjs') (Join-Path $publish 'windows-release-receipt.json') "ECNU/$name" } } catch { $result.error=$_.Exception.Message;Write-Host $_.ScriptStackTrace;throw } diff --git a/scripts/lib/windows-stages.mjs b/scripts/lib/windows-stages.mjs index 888e8e38..46b96251 100644 --- a/scripts/lib/windows-stages.mjs +++ b/scripts/lib/windows-stages.mjs @@ -383,7 +383,8 @@ async function publishWindows({ workspace, coreRoot, receiptBase }) { delete receipt.checks.asset delete receipt.checks.portableExtractorAssets await writeJSON(join(publish, 'release-receipt.json'), receipt) - if (!development && !desktopVersion(workspace.parameters.version).prerelease) { + // Other institutions publish their own update feeds from the release receipt. + if (!development && !desktopVersion(workspace.parameters.version).prerelease && ['EduWork', 'EduWork-ECNU'].includes(name)) { await copyFileTo(join(editionRoot, releaseNotesFile), join(publish, 'RELEASE-NOTES.md')) await runNode(join(coreRoot, 'scripts/github-update-manifest.mjs'), [join(publish, 'release-receipt.json'), `ecnu/${name}`]) .catch(() => { throw new Error('GitHub update manifest generation failed') }) diff --git a/source-receipt.json b/source-receipt.json index e83afe86..c360c353 100644 --- a/source-receipt.json +++ b/source-receipt.json @@ -5,7 +5,7 @@ "version": "0.4.1", "sourceState": "local-development-snapshot", "createdAt": "2026-10-10T12:18:00.131Z", - "fileSetSHA256": "acc953eec3ccb84d8011f95e5ec90fa31bf4e0b1f1f1a4a09b69fab45cf29f67", + "fileSetSHA256": "25cb95e6524ac89129f7d9bc4bb25f9effe353a8842eafb5cf033fffadd9bd2b", "files": [ { "path": ".gitattributes", @@ -5169,11 +5169,11 @@ }, { "path": "scripts/ci-eduwork-windows-release.ps1", - "sha256": "5577252103532f5e4d73a8e8bbba77792bb9d8d9764f82c42da089bfe1f4528f" + "sha256": "010bcc6aa378adc9f93eff86c7545d4b4af39764fb65d93b39ded2fd8d10aa9e" }, { "path": "scripts/ci-pinned-dsh-desktop.ps1", - "sha256": "aace3cee93fa1e65a78c67f16b7842cbc46f122754617386650927f3cad5bc1d" + "sha256": "0952ffe55c29b531280de8f61ba1a7fc1469dc27320fff6fd4f325f4fa1faccc" }, { "path": "scripts/close-release-test-desktop.mjs", @@ -5305,7 +5305,7 @@ }, { "path": "scripts/lib/windows-stages.mjs", - "sha256": "d54afc7c9cb82e4df0ac86eb19571288f6017115a9b04b842eb28b47739278f8" + "sha256": "ca10a3e3f5cdd0f4509abf5c633c02abded381adcbb903e29abf4f0076e7de97" }, { "path": "scripts/lib/zip.mjs", @@ -5721,7 +5721,7 @@ }, { "path": "tests/eduwork-windows-release.test.mjs", - "sha256": "83cf53a36a4fa58a6d5592153fca42e0ac78af910c5af5934fb482a19235d771" + "sha256": "babfc9dd028ba7faf0fbd960b68bc7db7a333fa920e13d37f5f7dfef0d268f30" }, { "path": "tests/fixtures/audit-stack-probe.mjs", diff --git a/tests/eduwork-windows-release.test.mjs b/tests/eduwork-windows-release.test.mjs index 8c792b44..e5c5ed84 100644 --- a/tests/eduwork-windows-release.test.mjs +++ b/tests/eduwork-windows-release.test.mjs @@ -1,16 +1,61 @@ import test from 'node:test' import assert from 'node:assert/strict' import { validateReceipt,releasePublication,validateExtractorReceipt,validatedReleaseFiles } from '../scripts/publish-windows-release.mjs' -import {mkdtemp,writeFile,rm} from 'node:fs/promises' +import {mkdtemp,mkdir,writeFile,readFile,readdir,rm} from 'node:fs/promises' import {tmpdir} from 'node:os' -import {join} from 'node:path' +import {basename,join,resolve,sep} from 'node:path' import {createHash} from 'node:crypto' import {githubUpdateManifest,githubUpdateManifestBytes} from '../scripts/github-update-manifest.mjs' +import {windowsStages} from '../scripts/lib/windows-stages.mjs' +import {fileURLToPath} from 'node:url' const context={repository:'ECNU/EduWork',version:'0.3.0',commit:'a'.repeat(40)} const receipt={schemaVersion:1,kind:'eduwork-windows-release',validationProfile:'ci-build-launch-and-extract-v2',passed:true,version:'0.3.0',edition:'EduWork',shell:'electron',platform:'windows-x64',coreCommit:'d'.repeat(40),editionCommit:context.commit,checks:Object.fromEntries(['sourceAndDependencies','desktopLaunch','archiveManifest','nativeRuntimes','portableExtractor'].map(key=>[key,'passed'])),asset:{name:'EduWork-0.3.0-windows-x64-electron.zip',bytes:1024,sha256:'b'.repeat(64)}} function extractorFiles(edition,version) {return {asset:{name:`${edition}-${version}-windows-x64-setup.zip`,bytes:2048,sha256:'e'.repeat(64)},receipt:{name:`${edition}-${version}-windows-x64-setup.zip.json`,bytes:512,sha256:'f'.repeat(64)}}} receipt.portableExtractor=extractorFiles('EduWork',receipt.version) receipt.releaseNotes={approved:true,sha256:'c'.repeat(64)} + +test('Windows publish stage keeps institution artifacts without inventing an ECNU update repository',async t=>{ + const coreRoot=fileURLToPath(new URL('..',import.meta.url)) + for(const recipe of ['npm','pinned-source']) for(const [name,distribution] of [['EduWork','eduwork'],['EduWork-ECNU','eduwork-chatecnu'],['ExampleCampus','example-campus']]) { + for(const [version,development] of [['0.4.3',false],['0.4.3-alpha.1',false],['0.4.3-dev.20261010.1',true]]) await t.test(`${recipe}: ${name} ${version}`,async t=>{ + const root=await mkdtemp(join(tmpdir(),'eduwork-publish-stage-')) + t.after(async()=>{ + const target=resolve(root) + assert.ok(target.startsWith(resolve(tmpdir())+sep) && basename(target).startsWith('eduwork-publish-stage-')) + await rm(target,{recursive:true,force:true}) + }) + const file=async(path,content)=>{await mkdir(join(root,path,'..'),{recursive:true});await writeFile(join(root,path),content)} + const json=async(path,value)=>file(path,JSON.stringify(value)) + const asset={name:`${name}-${version}-windows-x64-electron.zip`,bytes:32,sha256:'a'.repeat(64)} + const extractor=extractorFiles(name,version) + const archive=Buffer.from('Synthetic publication input, not an application') + await file(`desktop/${asset.name}`,archive) + await json('desktop/package.json',{output:join(root,'desktop',asset.name)}) + await json(recipe==='pinned-source'?'product/assembly.json':'web/assembly/assembly.json',{distribution,dshVersion:'0.2.0-rc.2',dshCommit:'b'.repeat(40)}) + await json('inputs/inputs.json',{}) + await json('gui/acceptance-checks.json',{asset,portableExtractorAssets:extractor}) + for(const filename of [extractor.asset.name,`${extractor.asset.name}.sha256`,extractor.receipt.name]) await file(`gui/extractor-publish/${filename}`,'Synthetic extractor evidence') + await file('RELEASE-NOTES.md','Synthetic approved release notes') + const workspace={parameters:{name,version,development,releaseNotesFile:'RELEASE-NOTES.md',editionRoot:root,recipe},resolvePath:path=>join(root,path)} + const receiptBase={schemaVersion:1,kind:development?'eduwork-windows-development':'eduwork-windows-release',edition:name,version,shell:'electron',platform:'windows-x64'} + await windowsStages({coreRoot,receiptBase,recipe}).find(stage=>stage.name==='publish').run(workspace) + const files=await readdir(join(root,'publish')) + const expectedManifest=version==='0.4.3' && name!=='ExampleCampus' + assert.equal(files.includes('update-windows-amd64.json'),expectedManifest) + assert.equal(files.includes('RELEASE-NOTES.md'),!development) + assert.equal(files.includes(extractor.asset.name),!development) + assert.deepEqual(await readFile(join(root,'publish',asset.name)),archive) + const published=JSON.parse(await readFile(join(root,'publish/release-receipt.json'),'utf8')) + assert.equal(published.distribution,distribution) + assert.deepEqual(published.asset,asset) + if(expectedManifest){ + const manifest=JSON.parse(await readFile(join(root,'publish/update-windows-amd64.json'),'utf8')) + assert.equal(manifest.artifacts[0].url,`https://github.com/ecnu/${name}/releases/download/v${version}/${asset.name}`) + assert.equal(manifest.artifacts[0].sha256,asset.sha256) + } + }) + } +}) test('release publisher accepts the complete matching Windows receipt',()=>assert.equal(validateReceipt(receipt,context),'EduWork')) test('release publisher rejects missing or failed packaged native runtime checks',()=>{