add update functionality

This commit is contained in:
Joey Kimsey
2026-09-12 21:45:37 -04:00
parent 1ee2774241
commit 188702e516
7 changed files with 63 additions and 24 deletions

View File

@ -10,6 +10,6 @@ if (!args.includes('--upload')) {
for (const file of release.files) console.log(` ${file.name} (${file.data.length} bytes)`)
console.log('No files uploaded. Add --upload with CAPSULE_UPLOAD_URL and CAPSULE_UPLOAD_TOKEN to publish.')
} else {
// This adapter targets an authenticated HTTPS PUT/WebDAV endpoint, not any TTP site API.
// Publish through the TTP Capsule plugin's authenticated chunk upload endpoint.
await uploadRelease(release, { uploadUrl: process.env.CAPSULE_UPLOAD_URL, token: process.env.CAPSULE_UPLOAD_TOKEN })
}

View File

@ -38,6 +38,10 @@ export async function readReleaseArtifacts(directory) {
/** Mark a successful signed build with hashes of its exact publishable files. */
export async function writeReleaseManifest(directory, feedUrl) {
// JSON is valid YAML. This gives the standalone PHP host a strict, dependency-free parser.
const metadataPath = join(directory, 'latest.yml')
const metadata = parse(await readFile(metadataPath, 'utf8'))
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`)
const release = await readReleaseArtifacts(directory)
const manifest = {
version: release.version,

View File

@ -1,5 +1,8 @@
import { digest } from './releaseArtifacts.mjs'
import { validateFeedUrl } from '../build/config.mjs'
import { randomBytes } from 'node:crypto'
export const UPLOAD_CHUNK_SIZE = 512 * 1024
/** Publish immutable assets first and the feed pointer last, verifying public bytes. */
export async function uploadRelease(release, { uploadUrl, token, fetchImpl = fetch, log = console.log }) {
@ -7,21 +10,33 @@ export async function uploadRelease(release, { uploadUrl, token, fetchImpl = fet
if (!token || /[\r\n]/.test(token)) throw new Error('Set CAPSULE_UPLOAD_TOKEN in the release environment.')
for (const file of release.files) {
const metadata = file.name === 'latest.yml'
const response = await fetchImpl(new URL(file.name, uploadUrl), {
method: 'PUT',
redirect: 'error',
signal: AbortSignal.timeout(10 * 60 * 1000),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': metadata ? 'application/yaml' : 'application/octet-stream',
'Cache-Control': metadata ? 'no-cache' : 'public, max-age=31536000, immutable',
...(!metadata ? { 'If-None-Match': '*' } : {})
},
body: file.data
})
// Existing immutable files are safe to reuse only after verifying their public bytes.
if (!response.ok && !(response.status === 412 && !metadata)) {
throw new Error(`Upload failed for ${file.name}: HTTP ${response.status}. Publication stopped.`)
const uploadId = randomBytes(16).toString('hex')
const chunked = !metadata && file.data.length > UPLOAD_CHUNK_SIZE
const step = chunked ? UPLOAD_CHUNK_SIZE : file.data.length
if (!step) throw new Error(`Cannot upload empty artifact ${file.name}`)
for (let offset = 0; offset < file.data.length; offset += step) {
const body = file.data.subarray(offset, offset + step)
const response = await fetchImpl(new URL(file.name, uploadUrl), {
method: 'PUT',
redirect: 'error',
signal: AbortSignal.timeout(10 * 60 * 1000),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': metadata ? 'application/yaml' : 'application/octet-stream',
'Cache-Control': metadata ? 'no-cache' : 'public, max-age=31536000, immutable',
...(!metadata ? { 'If-None-Match': '*' } : {}),
...(chunked ? {
'Content-Range': `bytes ${offset}-${offset + body.length - 1}/${file.data.length}`,
'X-Capsule-Upload-Id': uploadId
} : {})
},
body
})
// Existing immutable files are safe to reuse only after verifying their public bytes.
if (!response.ok && !(response.status === 412 && !metadata)) {
throw new Error(`Upload failed for ${file.name}: HTTP ${response.status}. Publication stopped.`)
}
if (response.status === 412) break
}
const publicResponse = await fetchImpl(new URL(file.name, release.feedUrl), {
redirect: 'error', signal: AbortSignal.timeout(10 * 60 * 1000), cache: 'no-store'