Files
dify/cli/scripts/lib/edge-manifest.mjs
GareArc f1c32e1bc8 refactor(cli): split release scripts into pure rules and CLI shells
The release script tests could only observe behaviour through a process
exit code, because die() called process.exit from inside otherwise-pure
functions. That forced every test to spawn a subprocess, which forced
its input to come from cli/package.json, which is why editing that file
kept breaking tests — most recently #39658, and next the difyctl version
bump that cli-release.yml requires on every release.

Move the decisions into lib/release-rules.mjs and lib/edge-manifest.mjs,
which take every input as an argument and return values instead of
exiting. release-naming.mjs and release-r2-edge.mjs keep argv parsing,
manifest reading, stdout and exit codes, and nothing else.

Tests now split by what they ask. Logic is unit-tested against literal
inputs, the shells are tested for plumbing only, and release-config.test.ts
is the single place that reads the real manifest — asserting it is
internally consistent, never what it currently contains. Editing
cli/package.json no longer breaks a logic test; a malformed config still
fails. Verified by mutating each field in both directions.

Three intentional behaviour changes:

- compat-check compares the numeric A.B.C core only, ignoring prerelease
  and build suffixes, so Dify 1.16.0-rc1 now satisfies a 1.16.0 window.
  This matches the shipped runtime check in src/version/compat.ts, which
  already stripped suffixes; the release gate was the one disagreeing.
  Removes the hand-rolled prerelease ordering entirely.
- validate now rejects a missing or inverted compat window. It previously
  passed such a config while github-env emitted minDify=undefined.
- release-r2-edge reports a bad channel version under its own name rather
  than release-naming's, since it no longer borrows that script's die().

release-config.test.ts also pins the install scripts to the naming config.
They hardcode artifact names by necessity — they run standalone on a user
machine with no repo and no node — so changing tagPrefix or checksumsSuffix
now fails until all four installers are updated to match, instead of
silently publishing names no installer looks for.
2026-07-27 19:13:42 -07:00

79 lines
2.3 KiB
JavaScript

import { assetNameFor } from './release-rules.mjs'
// checksums lines are "<sha256> <assetName>"
export function parseChecksums(text) {
const map = new Map()
for (const line of text.split('\n')) {
const m = line.match(/^([0-9a-f]{64})\s+(\S+)$/i)
if (m) map.set(m[2], m[1])
}
return map
}
// Newline-delimited dir names of binaries that still exist in R2.
export function parseDirList(text) {
const set = new Set()
for (const line of text.split('\n')) {
const d = line.trim()
if (d) set.add(d)
}
return set
}
export function resolveTargets(release, version, shas) {
const targets = []
const missing = []
for (const t of release.targets) {
const asset = assetNameFor(release, version, t.id)
const sha = shas.get(asset)
if (sha) targets.push({ id: t.id, asset, sha })
else missing.push(asset)
}
return { targets, missing }
}
export function renderManifest({
binName,
channel,
version,
commit,
buildDate,
compat,
baseUrl,
targets,
}) {
const head = {
schema: 1,
name: binName,
channel,
version,
commit,
buildDate,
compat: { minDify: compat.minDify, maxDify: compat.maxDify },
baseUrl,
}
const headLines = Object.entries(head)
.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`)
.join(',\n')
const targetLines = targets
.map(
(t) =>
` ${JSON.stringify(t.id)}: { "asset": ${JSON.stringify(t.asset)}, "sha256": ${JSON.stringify(t.sha)} }`,
)
.join(',\n')
return `{\n${headLines},\n "targets": {\n${targetLines}\n }\n}\n`
}
// `current` is the parsed ledger or null (first publish). `existingDirs` is a
// Set of build dirs still present in R2, or null when the caller could not list
// them — lifecycle/TTL on the bin prefix is the only deletion mechanism, so the
// ledger must never advertise a build whose binary is gone. The new build is
// always kept (just uploaded). No count cap.
export function buildIndex({ channel, version, commit, buildDate, current, existingDirs }) {
const entry = { version, commit, buildDate, dir: version }
const kept = (current?.builds ?? []).filter((b) => b.version !== entry.version)
let builds = [entry, ...kept]
if (existingDirs) builds = builds.filter((b) => b.dir === entry.dir || existingDirs.has(b.dir))
return { schema: 1, channel, updated: buildDate, builds }
}