mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
chore(release): deploy web release to Vercel
This commit is contained in:
@@ -340,6 +340,14 @@ jobs:
|
|||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||||
run: node scripts/release-body > release-body.txt
|
run: node scripts/release-body > release-body.txt
|
||||||
|
- name: Check Vercel deployment token
|
||||||
|
env:
|
||||||
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ -z "${VERCEL_TOKEN:-}" ]; then
|
||||||
|
echo "::error::VERCEL_TOKEN is required to deploy the release HTML archive to Vercel Production."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -347,3 +355,27 @@ jobs:
|
|||||||
bodyFile: "release-body.txt"
|
bodyFile: "release-body.txt"
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Deploy web release to Vercel Production
|
||||||
|
env:
|
||||||
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
VERCEL_ORG_ID: team_drHWHjDIQypSvbjZ1F468uLm
|
||||||
|
VERCEL_PROJECT_ID: prj_oeEtip9l1zUvJF00QdhMXpXn2CH7
|
||||||
|
run: |
|
||||||
|
if [ -z "${VERCEL_TOKEN:-}" ]; then
|
||||||
|
echo "::error::VERCEL_TOKEN is required to deploy the release HTML archive to Vercel Production."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
RELEASE_ASSET="release-assets/5chan-${VERSION}-html.zip"
|
||||||
|
STAGED_DIR=$(node scripts/prepare-vercel-release-deploy.mjs --tag "$GITHUB_REF_NAME" --asset "$RELEASE_ASSET" --out-dir vercel-release-deploy)
|
||||||
|
corepack yarn dlx vercel@50.23.2 deploy "$STAGED_DIR" \
|
||||||
|
--prebuilt \
|
||||||
|
--prod \
|
||||||
|
--yes \
|
||||||
|
--force \
|
||||||
|
--archive=tgz \
|
||||||
|
--token "$VERCEL_TOKEN" \
|
||||||
|
--meta source=github-release \
|
||||||
|
--meta releaseTag="$GITHUB_REF_NAME" \
|
||||||
|
--meta releaseAsset="5chan-${VERSION}-html.zip"
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDir, '..');
|
||||||
|
|
||||||
|
const parseArgs = (argv) => {
|
||||||
|
const args = {};
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const item = argv[index];
|
||||||
|
if (!item.startsWith('--')) {
|
||||||
|
throw new Error(`Unexpected argument: ${item}`);
|
||||||
|
}
|
||||||
|
const key = item.slice(2);
|
||||||
|
const value = argv[index + 1];
|
||||||
|
if (!value || value.startsWith('--')) {
|
||||||
|
throw new Error(`Missing value for --${key}`);
|
||||||
|
}
|
||||||
|
args[key] = value;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvePath = (value) => path.resolve(repoRoot, value);
|
||||||
|
|
||||||
|
const readProjectJson = () => {
|
||||||
|
const projectJsonPath = path.join(repoRoot, '.vercel', 'project.json');
|
||||||
|
if (!fs.existsSync(projectJsonPath)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(fs.readFileSync(projectJsonPath, 'utf8'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyStaticFiles = (sourceDir, targetDir) => {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
||||||
|
if (entry.name === '.vercel' || entry.name === 'vercel.json') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fs.cpSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name), { recursive: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const headersToMap = (headers) =>
|
||||||
|
Object.fromEntries(headers.map((header) => [header.key, header.value]));
|
||||||
|
|
||||||
|
const routeSourceForHeader = (source) => {
|
||||||
|
if (source === '/') {
|
||||||
|
return '^/$';
|
||||||
|
}
|
||||||
|
if (/^\/[A-Za-z0-9_.-]+$/.test(source)) {
|
||||||
|
return `^${source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`;
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAppDir = (unpackDir, version) => {
|
||||||
|
const expectedDir = path.join(unpackDir, `5chan-${version}-html`);
|
||||||
|
if (fs.existsSync(path.join(expectedDir, 'index.html'))) {
|
||||||
|
return expectedDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join(unpackDir, 'index.html'))) {
|
||||||
|
return unpackDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = fs
|
||||||
|
.readdirSync(unpackDir, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => path.join(unpackDir, entry.name))
|
||||||
|
.filter((candidate) => fs.existsSync(path.join(candidate, 'index.html')));
|
||||||
|
|
||||||
|
if (candidates.length !== 1) {
|
||||||
|
throw new Error(`Could not find exactly one release app directory in ${unpackDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeBuildOutputConfig = (outputDir) => {
|
||||||
|
const repoConfig = JSON.parse(fs.readFileSync(path.join(repoRoot, 'vercel.json'), 'utf8'));
|
||||||
|
const routes = [];
|
||||||
|
|
||||||
|
for (const headerRule of repoConfig.headers || []) {
|
||||||
|
routes.push({
|
||||||
|
src: routeSourceForHeader(headerRule.source),
|
||||||
|
headers: headersToMap(headerRule.headers),
|
||||||
|
continue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.push({ handle: 'filesystem' });
|
||||||
|
|
||||||
|
for (const rewriteRule of repoConfig.rewrites || []) {
|
||||||
|
routes.push({
|
||||||
|
src: rewriteRule.source,
|
||||||
|
dest: rewriteRule.destination,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildOutputConfig = {
|
||||||
|
version: 3,
|
||||||
|
routes,
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(outputDir, 'config.json'), `${JSON.stringify(buildOutputConfig, null, 2)}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeProjectLink = (deployDir) => {
|
||||||
|
const project = readProjectJson();
|
||||||
|
const orgId = process.env.VERCEL_ORG_ID || project.orgId;
|
||||||
|
const projectId = process.env.VERCEL_PROJECT_ID || project.projectId;
|
||||||
|
const projectName = process.env.VERCEL_PROJECT_NAME || project.projectName || '5chan';
|
||||||
|
|
||||||
|
if (!orgId || !projectId) {
|
||||||
|
throw new Error('VERCEL_ORG_ID and VERCEL_PROJECT_ID are required to link the staged deploy.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const vercelDir = path.join(deployDir, '.vercel');
|
||||||
|
fs.mkdirSync(vercelDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(vercelDir, 'project.json'),
|
||||||
|
`${JSON.stringify({ orgId, projectId, projectName }, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateRelease = (appDir, version) => {
|
||||||
|
for (const file of ['index.html', 'version.json', '5chan-release-manifest.json', '5chan-release-manifest.sig.json']) {
|
||||||
|
if (!fs.existsSync(path.join(appDir, file))) {
|
||||||
|
throw new Error(`Release HTML archive is missing ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionJson = JSON.parse(fs.readFileSync(path.join(appDir, 'version.json'), 'utf8'));
|
||||||
|
if (versionJson.version !== version) {
|
||||||
|
throw new Error(`version.json has ${versionJson.version}, expected ${version}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stagePrebuiltDeploy = (appDir, outDir) => {
|
||||||
|
const deployDir = path.join(outDir, 'prebuilt');
|
||||||
|
const outputDir = path.join(deployDir, '.vercel', 'output');
|
||||||
|
const staticDir = path.join(outputDir, 'static');
|
||||||
|
|
||||||
|
copyStaticFiles(appDir, staticDir);
|
||||||
|
writeBuildOutputConfig(outputDir);
|
||||||
|
writeProjectLink(deployDir);
|
||||||
|
|
||||||
|
return deployDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
const tag = args.tag || process.env.GITHUB_REF_NAME;
|
||||||
|
if (!tag?.startsWith('v')) {
|
||||||
|
throw new Error('Pass --tag vX.Y.Z or set GITHUB_REF_NAME to a release tag.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = tag.slice(1);
|
||||||
|
const assetPath = resolvePath(args.asset || `release-assets/5chan-${version}-html.zip`);
|
||||||
|
if (!fs.existsSync(assetPath)) {
|
||||||
|
throw new Error(`Release HTML archive not found: ${assetPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outDir = args['out-dir'] ? resolvePath(args['out-dir']) : fs.mkdtempSync(path.join(os.tmpdir(), '5chan-vercel-release-'));
|
||||||
|
fs.rmSync(outDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
|
||||||
|
const unpackDir = path.join(outDir, 'unpacked');
|
||||||
|
fs.mkdirSync(unpackDir, { recursive: true });
|
||||||
|
execFileSync('unzip', ['-q', assetPath, '-d', unpackDir], { stdio: 'inherit' });
|
||||||
|
|
||||||
|
const appDir = findAppDir(unpackDir, version);
|
||||||
|
validateRelease(appDir, version);
|
||||||
|
const deployDir = stagePrebuiltDeploy(appDir, outDir);
|
||||||
|
|
||||||
|
console.error(`Prepared ${tag} release deployment from ${path.relative(repoRoot, assetPath)}`);
|
||||||
|
console.error(`Staged Vercel prebuilt directory: ${deployDir}`);
|
||||||
|
process.stdout.write(`${deployDir}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user