mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 15:56:59 +08:00
339 lines
10 KiB
JavaScript
339 lines
10 KiB
JavaScript
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
const {
|
|
sha256,
|
|
} = require('../cli/src/internal/maintenance/platformBundle.cjs');
|
|
const platforms = [
|
|
'linux-x64-musl',
|
|
'linux-armv6-musl',
|
|
'linux-armv7-musl',
|
|
'linux-arm64-musl',
|
|
'linux-ppc64-musl',
|
|
'linux-s390x-musl',
|
|
'linux-ia32-musl',
|
|
'linux-x64-glibc',
|
|
'linux-arm64-glibc',
|
|
'linux-ppc64-glibc',
|
|
'linux-s390x-glibc',
|
|
];
|
|
function releaseIndex(directory, commit, repository, options = {}) {
|
|
const { version, releaseTag = `upgrade-${commit}`, assetBuildId } = options;
|
|
if (
|
|
!/^[a-f0-9]{40}$/.test(commit) ||
|
|
!/^[\w.-]+\/[\w.-]+$/.test(repository) ||
|
|
!/^[\w.+-]+$/.test(releaseTag) ||
|
|
(assetBuildId && !/^\d+-\d+$/.test(assetBuildId))
|
|
)
|
|
throw new Error('Invalid release source');
|
|
const packages = {};
|
|
for (const target of platforms) {
|
|
const filename = `qinglong-${commit}-${target}.tar.gz`;
|
|
const record = JSON.parse(
|
|
fs.readFileSync(path.join(directory, filename + '.json')),
|
|
);
|
|
const bytes = fs.readFileSync(path.join(directory, filename));
|
|
if (
|
|
record.format !== 2 ||
|
|
record.sourceCommit !== commit ||
|
|
(version !== undefined && record.version !== version) ||
|
|
record.target !== target ||
|
|
record.filename !== filename ||
|
|
record.size !== bytes.length ||
|
|
record.sha256 !== sha256(bytes)
|
|
)
|
|
throw new Error(`Unverified platform bundle: ${target}`);
|
|
packages[target] = {
|
|
size: record.size,
|
|
sha256: record.sha256,
|
|
url: `https://github.com/${repository}/releases/download/${encodeURIComponent(
|
|
releaseTag,
|
|
)}/${
|
|
assetBuildId
|
|
? filename.replace(/\.tar\.gz$/, `-${assetBuildId}.tar.gz`)
|
|
: filename
|
|
}`,
|
|
};
|
|
}
|
|
return { format: 2, sourceCommit: commit, packages };
|
|
}
|
|
function stageReleaseAssets(directory, commit, buildId, channel) {
|
|
const staged = fs.mkdtempSync(path.join(directory, `.upgrade-${channel}-`));
|
|
const assets = [];
|
|
try {
|
|
for (const target of platforms) {
|
|
const filename = `qinglong-${commit}-${target}.tar.gz`;
|
|
const archiveName = filename.replace(/\.tar\.gz$/, `-${buildId}.tar.gz`);
|
|
const archive = path.join(staged, archiveName);
|
|
fs.linkSync(path.join(directory, filename), archive);
|
|
const record = JSON.parse(
|
|
fs.readFileSync(path.join(directory, filename + '.json')),
|
|
);
|
|
fs.writeFileSync(
|
|
archive + '.json',
|
|
JSON.stringify({
|
|
...record,
|
|
filename: archiveName,
|
|
}) + '\n',
|
|
);
|
|
assets.push(archive, archive + '.json');
|
|
}
|
|
const installer = path.join(staged, `install-bundle-${buildId}.cjs`);
|
|
fs.linkSync(path.join(directory, 'install-bundle.cjs'), installer);
|
|
assets.push(installer);
|
|
return { staged, assets };
|
|
} catch (error) {
|
|
fs.rmSync(staged, { recursive: true, force: true });
|
|
throw error;
|
|
}
|
|
}
|
|
function stageDevelopmentAssets(directory, commit, buildId) {
|
|
return stageReleaseAssets(directory, commit, buildId, 'develop');
|
|
}
|
|
function publishBundles({
|
|
directory,
|
|
commit,
|
|
repository,
|
|
branch,
|
|
version,
|
|
buildId,
|
|
gh = (...args) =>
|
|
execFileSync('gh', args, {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
}),
|
|
}) {
|
|
if (!['master', 'develop'].includes(branch))
|
|
throw new Error('Only master or develop images publish bundles');
|
|
if (!/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(version || ''))
|
|
throw new Error('Invalid bundle version');
|
|
if (!/^\d+-\d+$/.test(buildId || ''))
|
|
throw new Error('Invalid bundle build ID');
|
|
// Rebuilding a version gets new download URLs, including when its commit is
|
|
// unchanged. Only the version/channel pointer is overwritten, never bytes
|
|
// that an in-flight download has already selected by hash.
|
|
const development = branch === 'develop';
|
|
const tag = development ? 'upgrade-develop' : `upgrade-v${version}`;
|
|
const index = {
|
|
...releaseIndex(directory, commit, repository, {
|
|
version,
|
|
releaseTag: tag,
|
|
assetBuildId: buildId,
|
|
}),
|
|
version,
|
|
buildId,
|
|
channel: branch,
|
|
};
|
|
const installer = fs.readFileSync(path.join(directory, 'install-bundle.cjs'));
|
|
index.installer = {
|
|
size: installer.length,
|
|
sha256: sha256(installer),
|
|
url: `https://github.com/${repository}/releases/download/${encodeURIComponent(
|
|
tag,
|
|
)}/install-bundle-${buildId}.cjs`,
|
|
};
|
|
fs.writeFileSync(
|
|
path.join(directory, 'latest.json'),
|
|
JSON.stringify(index) + '\n',
|
|
);
|
|
const lookup = (ref) => {
|
|
try {
|
|
return JSON.parse(
|
|
gh(
|
|
'api',
|
|
`repos/${repository}/releases/tags/${encodeURIComponent(ref)}`,
|
|
),
|
|
);
|
|
} catch (error) {
|
|
if (/\bHTTP 404\b/.test(String(error.stderr))) return null;
|
|
throw error;
|
|
}
|
|
};
|
|
const notes =
|
|
`${
|
|
development ? 'Development' : 'Verified'
|
|
} platform bundles for v${version}, source ${commit}, build ${buildId}. Existing Node runtime is retained.\n\n` +
|
|
Object.entries(index.packages)
|
|
.map(([target, entry]) => `- [${target}](${entry.url})`)
|
|
.join('\n') +
|
|
`\n- [Standalone offline installer](${index.installer.url})`;
|
|
const ensureRelease = (ref, title) => {
|
|
if (!lookup(ref))
|
|
gh(
|
|
'release',
|
|
'create',
|
|
ref,
|
|
'--repo',
|
|
repository,
|
|
'--target',
|
|
commit,
|
|
'--prerelease',
|
|
'--latest=false',
|
|
'--title',
|
|
title,
|
|
'--notes',
|
|
notes,
|
|
);
|
|
};
|
|
const currentSource = () =>
|
|
gh(
|
|
'api',
|
|
`repos/${repository}/git/ref/heads/${branch}`,
|
|
'--jq',
|
|
'.object.sha',
|
|
).trim() === commit;
|
|
if (!currentSource()) {
|
|
console.log('Newer source exists; leaving version and channel unchanged');
|
|
return false;
|
|
}
|
|
const staged = development
|
|
? stageDevelopmentAssets(directory, commit, buildId)
|
|
: stageReleaseAssets(directory, commit, buildId, `v${version}`);
|
|
try {
|
|
ensureRelease(
|
|
tag,
|
|
development ? 'Upgrade channel develop' : `Platform upgrade v${version}`,
|
|
);
|
|
const assets = staged.assets;
|
|
const published = lookup(tag);
|
|
const missing = [];
|
|
const matches = (asset, file) => {
|
|
const bytes = fs.readFileSync(file);
|
|
return (
|
|
asset?.size === bytes.length &&
|
|
asset.digest === `sha256:${sha256(bytes)}`
|
|
);
|
|
};
|
|
for (const file of assets) {
|
|
const previous = published.assets.find(
|
|
(a) => a.name === path.basename(file),
|
|
);
|
|
if (!previous) missing.push(file);
|
|
else if (!matches(previous, file))
|
|
throw new Error(
|
|
`Immutable release asset differs: ${path.basename(file)}`,
|
|
);
|
|
}
|
|
if (missing.length)
|
|
gh('release', 'upload', tag, ...missing, '--repo', repository);
|
|
const uploaded = lookup(tag);
|
|
if (
|
|
!assets.every((file) =>
|
|
matches(
|
|
uploaded.assets.find((a) => a.name === path.basename(file)),
|
|
file,
|
|
),
|
|
)
|
|
)
|
|
throw new Error('Published platform bundle verification failed');
|
|
|
|
// Each channel serializes its image builds and pointer writes as one release.
|
|
// A rerun of an earlier workflow is allowed to replace the same
|
|
// version/commit; an obsolete source commit must never advance either alias.
|
|
const aliases = development ? ['upgrade-develop'] : [tag, 'upgrade-master'];
|
|
for (const ref of aliases) {
|
|
if (!currentSource()) {
|
|
console.log(
|
|
'Newer source exists; leaving version and channel unchanged',
|
|
);
|
|
return false;
|
|
}
|
|
ensureRelease(
|
|
ref,
|
|
ref === `upgrade-${branch}`
|
|
? `Upgrade channel ${branch}`
|
|
: `Platform upgrade v${version}`,
|
|
);
|
|
gh(
|
|
'release',
|
|
'upload',
|
|
ref,
|
|
path.join(directory, 'latest.json'),
|
|
'--repo',
|
|
repository,
|
|
'--clobber',
|
|
);
|
|
gh('release', 'edit', ref, '--repo', repository, '--notes', notes);
|
|
if (
|
|
!matches(
|
|
lookup(ref)?.assets.find((a) => a.name === 'latest.json'),
|
|
path.join(directory, 'latest.json'),
|
|
)
|
|
)
|
|
throw new Error('Published channel index verification failed');
|
|
}
|
|
// Only prune development releases after the complete new batch and its
|
|
// channel pointer are verified. Production versions and aliases are retained.
|
|
if (development && currentSource()) {
|
|
const currentAssets = new Set(assets.map((file) => path.basename(file)));
|
|
for (const asset of lookup(tag).assets) {
|
|
if (
|
|
!currentAssets.has(asset.name) &&
|
|
(/^qinglong-[a-f0-9]{40}-linux-[\w-]+-\d+-\d+\.tar\.gz(?:\.json)?$/.test(
|
|
asset.name,
|
|
) ||
|
|
/^install-bundle-\d+-\d+\.cjs$/.test(asset.name))
|
|
) {
|
|
if (!currentSource()) break;
|
|
gh(
|
|
'api',
|
|
'-X',
|
|
'DELETE',
|
|
`repos/${repository}/releases/assets/${asset.id}`,
|
|
);
|
|
}
|
|
}
|
|
const pages = JSON.parse(
|
|
gh(
|
|
'api',
|
|
`repos/${repository}/releases?per_page=100`,
|
|
'--paginate',
|
|
'--slurp',
|
|
),
|
|
);
|
|
for (const release of pages.flat()) {
|
|
if (
|
|
release.tag_name !== tag &&
|
|
/^upgrade-build-develop-(?:v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?-)?\d+-\d+$/.test(
|
|
release.tag_name,
|
|
) &&
|
|
release.prerelease === true &&
|
|
release.draft === false
|
|
) {
|
|
if (!currentSource()) break;
|
|
gh(
|
|
'release',
|
|
'delete',
|
|
release.tag_name,
|
|
'--repo',
|
|
repository,
|
|
'--yes',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
} finally {
|
|
fs.rmSync(staged.staged, { recursive: true, force: true });
|
|
}
|
|
}
|
|
module.exports = { releaseIndex, platforms, publishBundles };
|
|
if (require.main === module) {
|
|
const [directory, commit, repository, branch] = process.argv.slice(2);
|
|
const version = require('../package.json').version;
|
|
if (
|
|
require('js-yaml').load(
|
|
fs.readFileSync(path.join(__dirname, '../version.yaml'), 'utf8'),
|
|
).version !== version
|
|
)
|
|
throw new Error('Package and image release versions differ');
|
|
publishBundles({
|
|
directory,
|
|
commit,
|
|
repository,
|
|
branch,
|
|
version,
|
|
buildId: `${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}`,
|
|
});
|
|
}
|