mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(release): [REL-04] checkpoint exact historical Pages recovery
This commit is contained in:
1 parent
d0c33f6258
commit
ddf3d6ccbc
17 files changed
+3460
-12
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { readJson, workspace, writeJson } from './package-consumer.mjs'
|
||||
import { digest, inside } from './pages/artifact.ts'
|
||||
import { replaceRehearsalSite, verifyRestoredTree } from './pages/recovery.ts'
|
||||
|
||||
assert.equal(process.versions.node, fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim(), 'Use pinned Node')
|
||||
const baseline = readJson(path.join(workspace, 'refactor/baselines/pages-artifact-validation.json'))
|
||||
const archived = baseline.remote.rollbackArchive
|
||||
assert(/^[a-f0-9]{40}$/.test(archived.commit))
|
||||
const previousArchive = inside(workspace, archived.path)
|
||||
assert.deepEqual(digest(previousArchive), { bytes: archived.bytes, sha256: archived.sha256 }, 'Frozen Pages archive changed')
|
||||
const git = args => execFileSync('git', args, { cwd: workspace, encoding: 'utf8', windowsHide: true })
|
||||
assert.equal(git(['rev-parse', '--show-object-format']).trim(), 'sha1', 'Recovery verifier expects Git SHA-1 object IDs')
|
||||
const tree = git(['ls-tree', '-r', '-l', '-z', archived.commit]).split('\0').filter(Boolean)
|
||||
const expected = {}
|
||||
for (const entry of tree) {
|
||||
const match = /^(100644|100755) blob ([a-f0-9]{40}) +(\d+)\t(.+)$/s.exec(entry)
|
||||
assert(match, 'Site snapshot contains unsupported Git entries')
|
||||
const [, , objectId, bytes, relative] = match
|
||||
inside(workspace, relative)
|
||||
assert(!Object.hasOwn(expected, relative), 'Duplicate Git path')
|
||||
expected[relative] = { objectId, bytes: Number(bytes) }
|
||||
}
|
||||
const parent = path.join(workspace, 'refactor/.cache/pages-recovery')
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
const output = fs.mkdtempSync(path.join(parent, 'run-'))
|
||||
const report = { task: 'REL-04', capturedAt: new Date().toISOString(), source: git(['rev-parse', 'HEAD']).trim(), node: process.versions.node, passed: false, archived, gitFiles: expected, scope: 'Local restoration of the complete frozen gh-pages snapshot; no deployment or remote setting changes' }
|
||||
try {
|
||||
const archive = path.join(output, 'canonical.zip')
|
||||
const archiveArgs = ['-c', 'core.autocrlf=false', '-c', 'core.eol=lf', 'archive', '--format=zip', `--output=${archive}`, archived.commit]
|
||||
git(archiveArgs)
|
||||
report.canonicalArchive = { command: ['git', ...archiveArgs], ...digest(archive) }
|
||||
const members = execFileSync('tar', ['-tf', archive], { encoding: 'utf8', windowsHide: true }).trim().split(/\r?\n/)
|
||||
assert.equal(new Set(members).size, members.length)
|
||||
for (const member of members) inside(workspace, member.replace(/\/$/, ''))
|
||||
assert.deepEqual(members.filter(member => !member.endsWith('/')).sort(), Object.keys(expected).sort(), 'Archive inventory differs from frozen Git tree')
|
||||
const original = path.join(output, 'original')
|
||||
const site = path.join(output, 'site')
|
||||
const prepared = path.join(output, 'prepared')
|
||||
fs.mkdirSync(original)
|
||||
execFileSync('tar', ['-xf', archive, '-C', original], { windowsHide: true })
|
||||
const before = verifyRestoredTree(original, expected)
|
||||
assert.equal(fs.readFileSync(path.join(original, 'CNAME'), 'utf8').trim(), 'github.artplayer.org')
|
||||
fs.cpSync(original, site, { recursive: true })
|
||||
fs.writeFileSync(path.join(site, 'compiled/artplayer.js'), 'broken candidate fixture\n')
|
||||
fs.writeFileSync(path.join(site, 'CNAME'), 'recovery-fixture.invalid\n')
|
||||
fs.unlinkSync(path.join(site, 'iframe.html'))
|
||||
fs.writeFileSync(path.join(site, 'candidate-only.txt'), 'must disappear after restoration\n')
|
||||
assert.throws(() => verifyRestoredTree(site, expected), /missing or additional files/)
|
||||
fs.cpSync(original, prepared, { recursive: true })
|
||||
const restored = replaceRehearsalSite(output, expected)
|
||||
assert.deepEqual(restored, before)
|
||||
assert.equal(fs.readFileSync(path.join(site, 'CNAME'), 'utf8').trim(), 'github.artplayer.org')
|
||||
assert.equal(fs.readFileSync(path.join(output, 'failed-site/compiled/artplayer.js'), 'utf8'), 'broken candidate fixture\n')
|
||||
assert(!fs.existsSync(path.join(site, 'candidate-only.txt')))
|
||||
report.files = restored
|
||||
report.simulatedChanges = ['modified compiled/artplayer.js', 'changed CNAME', 'deleted iframe.html', 'added candidate-only.txt']
|
||||
report.failedSnapshotRetained = true
|
||||
report.passed = true
|
||||
}
|
||||
catch (error) {
|
||||
report.error = { name: error.name, message: error.message }
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
writeJson(path.join(output, 'report.json'), report)
|
||||
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/'), passed: report.passed })
|
||||
console.log(`Pages recovery: ${path.relative(workspace, output)}; passed=${report.passed}`)
|
||||
}
|
||||
@@ -41,3 +41,27 @@ Failed staging builds keep their report once the output directory exists. Earlie
|
||||
preflight failures stay in the CI log. Reports are uploaded even on failure; staged
|
||||
files are uploaded for deployment only after preparation and browser checks pass.
|
||||
See `refactor/pages-deployment.md` for remote state, activation and recovery.
|
||||
|
||||
## Complete historical snapshot recovery
|
||||
|
||||
`yarn test:rollback:pages` validates the CI-02 saved ZIP identity and fixed
|
||||
gh-pages Git objects, creates a new ZIP with command-local `core.autocrlf=false`
|
||||
and `core.eol=lf`, and verifies all extracted paths, sizes and Git blob IDs.
|
||||
The initial ZIP used Windows line conversion; its 403 changed text files remain
|
||||
recorded as a failed exact-Git-byte recovery, not silently normalized.
|
||||
|
||||
`recovery.ts` rejects stale or extra files before replacement. The command
|
||||
damages only an isolated copy, restores it from a fully validated prepared
|
||||
directory, and keeps the failed copy. All outputs stay in
|
||||
`refactor/.cache/pages-recovery/run-*`; there is no recursive cleanup or remote
|
||||
mutation. Both the saved CI-02 ZIP and its Git commit must be available before
|
||||
this explicit rehearsal can run. Preserve the canonical ZIP with its report for
|
||||
future recovery; publishing a new candidate requires a fresh pre-upgrade snapshot.
|
||||
|
||||
`yarn test:rollback:pages:browser` accepts the latest successful recovery only,
|
||||
rechecks every restored Git blob, then serves original HTML without dev reload
|
||||
injection. It verifies 12 route bodies and runs six UMD/ESM playback cases in
|
||||
Chromium/Firefox/WebKit using restored scripts and media. Aborted transfers are
|
||||
retained separately only for the exact sample media path and native aborted
|
||||
request condition. This is local snapshot playback evidence, not a complete
|
||||
editor, iframe, Thumbnail extraction, external SDK or remote deployment check.
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import { inside, inventory } from './artifact.ts'
|
||||
|
||||
export interface GitFile { objectId: string, bytes: number }
|
||||
|
||||
export function verifyRestoredTree(root: string, expected: Record<string, GitFile>) {
|
||||
const files = inventory(root)
|
||||
assert.deepEqual(Object.keys(files).sort(), Object.keys(expected).sort(), 'Restored site has missing or additional files')
|
||||
for (const [relative, identity] of Object.entries(expected)) {
|
||||
const bytes = fs.readFileSync(inside(root, relative))
|
||||
assert.equal(bytes.length, identity.bytes, `Restored file size differs: ${relative}`)
|
||||
const objectId = createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex')
|
||||
assert.equal(objectId, identity.objectId, `Restored Git blob differs: ${relative}`)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
export function replaceRehearsalSite(run: string, expected: Record<string, GitFile>) {
|
||||
const root = fs.realpathSync(run)
|
||||
const live = inside(root, 'site')
|
||||
const prepared = inside(root, 'prepared')
|
||||
const failed = inside(root, 'failed-site')
|
||||
assert(!fs.existsSync(failed), 'Previous failed snapshot must not be overwritten')
|
||||
for (const directory of [live, prepared]) {
|
||||
assert.equal(fs.realpathSync(directory), directory, 'Rehearsal directories must not be links')
|
||||
assert(fs.lstatSync(directory).isDirectory())
|
||||
}
|
||||
verifyRestoredTree(prepared, expected)
|
||||
fs.renameSync(live, failed)
|
||||
try {
|
||||
fs.renameSync(prepared, live)
|
||||
}
|
||||
catch (error) {
|
||||
fs.renameSync(failed, live)
|
||||
throw error
|
||||
}
|
||||
return verifyRestoredTree(live, expected)
|
||||
}
|
||||
Reference in new issue
Block a user