Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51a4b0d9ca | ||
|
|
c0e8ab4bad | ||
|
|
6ceea8df18 | ||
|
|
aca9d203b0 | ||
|
|
8fa6a18c81 | ||
|
|
944da34c45 |
@@ -0,0 +1,55 @@
|
|||||||
|
name: Publish GitHub Pages output
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
- cron: '*/5 * * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: publish-github-pages
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out source
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Use Node.js 24
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Check publication time
|
||||||
|
id: publication
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
node scripts/verify-pages-public-data.mjs
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
if [ "$status" -eq 0 ]; then
|
||||||
|
echo "ready=true" >> "$GITHUB_OUTPUT"
|
||||||
|
elif [ "$status" -eq 78 ]; then
|
||||||
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Public data is not due yet; skipping this scheduled deployment."
|
||||||
|
else
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build public site
|
||||||
|
if: steps.publication.outputs.ready == 'true'
|
||||||
|
run: npm run build:pages
|
||||||
|
|
||||||
|
- name: Publish generated files
|
||||||
|
if: steps.publication.outputs.ready == 'true'
|
||||||
|
env:
|
||||||
|
GITHUB_PAGES_TOKEN: ${{ secrets.GITHUB_PAGES_TOKEN }}
|
||||||
|
run: npm run publish:pages
|
||||||
@@ -2,6 +2,7 @@ node_modules
|
|||||||
.svelte-kit
|
.svelte-kit
|
||||||
build
|
build
|
||||||
offline-dist
|
offline-dist
|
||||||
|
pages-dist
|
||||||
release
|
release
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# GitHub Pages 배포
|
||||||
|
|
||||||
|
원본 저장소와 전체 게임 데이터는 개인 Git 서버에만 보관합니다. 공개 GitHub 저장소 `DD4SN/DD4SN.github.io`에는 공개 데이터 검증을 통과한 `pages-dist` 산출물과 Pages 배포 워크플로만 전송합니다.
|
||||||
|
|
||||||
|
## 공개 데이터 게이트
|
||||||
|
|
||||||
|
`pages/publication-policy.json`은 Pages에 공개해도 되는 HolodoriDB 마스터 데이터 버전과 최초 공개 가능 시각을 고정합니다.
|
||||||
|
|
||||||
|
- 카드, 보드, 메모리 데이터 버전이 승인 버전과 다르면 빌드를 중단합니다.
|
||||||
|
- 공개 시각이 지나지 않았거나 공개 시각이 없는 악곡 채보가 있으면 빌드를 중단합니다.
|
||||||
|
- 시작 시각이 지나지 않았거나 시작 시각이 없는 이벤트가 있으면 빌드를 중단합니다.
|
||||||
|
- Pages 빌드는 서버 시간 API를 호출하지 않고 브라우저 시각을 사용합니다.
|
||||||
|
|
||||||
|
새 마스터 데이터를 수집한 뒤에는 포함된 카드, 보드와 메모리 정보가 모두 공개 가능한지 검토한 후에만 정책의 `approvedMasterDataVersion`과 `approvedNotBefore`를 갱신합니다. 한 마스터 버전에 서로 다른 공개 시각의 데이터가 함께 있으면 가장 늦은 공개 시각까지 전체 Pages 빌드가 차단됩니다.
|
||||||
|
|
||||||
|
검증과 정적 빌드:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run check:pages-data
|
||||||
|
npm run build:pages
|
||||||
|
```
|
||||||
|
|
||||||
|
테스트에서만 `PAGES_PUBLICATION_TIME`에 ISO-8601 시각을 지정할 수 있습니다. 실제 배포에서는 설정하지 않아 신뢰할 수 있는 CI 실행 시각을 사용합니다.
|
||||||
|
|
||||||
|
## 배포
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run deploy:pages
|
||||||
|
```
|
||||||
|
|
||||||
|
배포 스크립트는 다음 순서로 동작합니다.
|
||||||
|
|
||||||
|
1. 공개 데이터 검증과 `pages-dist` 빌드
|
||||||
|
2. 임시 디렉터리에 `DD4SN/DD4SN.github.io` 저장소 복제
|
||||||
|
3. 저장소 최상위에 `index.html`, `assets/`, `icons/` 등 정적 산출물 복사
|
||||||
|
4. `.github/workflows/deploy.yml` 갱신
|
||||||
|
5. 배포 저장소의 `main` 브랜치에 산출물 커밋과 푸시
|
||||||
|
|
||||||
|
최초 배포 전에 저장소의 Pages 소스를 GitHub Actions 방식으로 한 번 활성화해야 합니다.
|
||||||
|
|
||||||
|
개인 Git 서버의 예약 작업에서 실행할 때는 GitHub 저장소에만 쓰기 가능한 최소 권한 자격증명을 사용합니다. 원본 저장소를 GitHub 원격으로 추가하거나 원본 Git 이력을 Pages 저장소로 푸시하지 않습니다.
|
||||||
|
|
||||||
|
## Gitea 예약 배포
|
||||||
|
|
||||||
|
`.gitea/workflows/deploy-pages.yml`은 `main` 갱신, 수동 실행, 5분 간격 예약 실행에서 공개 가능 여부를 확인합니다. 공개 시각 전이면 정상적으로 배포를 건너뛰고, 승인 버전 불일치나 날짜 누락 같은 정책 오류는 실패로 처리합니다. 검증을 통과하면 정적 사이트를 만든 뒤 생성 파일만 GitHub Pages 저장소 최상위에 커밋합니다.
|
||||||
|
|
||||||
|
이 워크플로는 더 이상 사용하지 않는 서버용 온라인 빌드를 만들지 않습니다. 오프라인 빌드도 로컬 회귀 테스트용으로만 유지하며 GitHub Pages 배포에서는 `build:pages`만 실행합니다.
|
||||||
|
|
||||||
|
사전 준비:
|
||||||
|
|
||||||
|
1. Gitea에 `ubuntu-latest` 레이블을 처리할 수 있는 Actions runner를 연결합니다.
|
||||||
|
2. GitHub에서 `DD4SN/DD4SN.github.io` 저장소의 Contents 읽기/쓰기만 허용하는 fine-grained PAT를 발급합니다.
|
||||||
|
3. 개인 Gitea 저장소의 Actions secret `GITHUB_PAGES_TOKEN`에 PAT를 등록합니다.
|
||||||
|
|
||||||
|
토큰은 원본 저장소 파일, 로그, 생성 사이트에 기록하지 않습니다. GitHub Pages 저장소에 변경이 없으면 새 커밋을 만들지 않습니다.
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
"dev": "vite dev --host 0.0.0.0 --port 5173",
|
"dev": "vite dev --host 0.0.0.0 --port 5173",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:offline": "vite build --config vite.offline.config.ts && node scripts/finalize-offline-build.mjs",
|
"build:offline": "vite build --config vite.offline.config.ts && node scripts/finalize-offline-build.mjs",
|
||||||
|
"check:pages-data": "node scripts/verify-pages-public-data.mjs",
|
||||||
|
"build:pages": "npm run check:pages-data && vite build --config vite.pages.config.ts && node scripts/finalize-offline-build.mjs pages-dist",
|
||||||
|
"deploy:pages": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-pages.ps1",
|
||||||
|
"publish:pages": "node scripts/publish-pages.mjs",
|
||||||
"package:offline": "npm run build:offline && powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-offline.ps1",
|
"package:offline": "npm run build:offline && powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-offline.ps1",
|
||||||
"preview": "vite preview --host 0.0.0.0",
|
"preview": "vite preview --host 0.0.0.0",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Deploy GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Configure Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
- name: Upload site
|
||||||
|
uses: actions/upload-pages-artifact@v4
|
||||||
|
with:
|
||||||
|
path: .
|
||||||
|
- name: Deploy
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"repository": "https://github.com/DD4SN/DD4SN.github.io.git",
|
||||||
|
"approvedMasterDataVersion": "fbca8c670e074558b24708bf163fff446a614f709afa9d1784395a15444121b0",
|
||||||
|
"approvedNotBefore": "2026-08-29T15:00:00.000Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export type ReleaseClockSource = 'system';
|
||||||
|
|
||||||
|
export type ReleaseClock = {
|
||||||
|
source: ReleaseClockSource;
|
||||||
|
now: () => number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReleaseClockOptions = {
|
||||||
|
wallNow?: () => number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function systemClock(wallNow: () => number): ReleaseClock {
|
||||||
|
return { source: 'system', now: wallNow };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInitialReleaseClock(
|
||||||
|
_serverNow: unknown,
|
||||||
|
options: ReleaseClockOptions = {}
|
||||||
|
): ReleaseClock {
|
||||||
|
return systemClock(options.wallNow ?? Date.now);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function synchronizeReleaseClock(options: ReleaseClockOptions = {}): Promise<ReleaseClock> {
|
||||||
|
return systemClock(options.wallNow ?? Date.now);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
param(
|
||||||
|
[string]$Repository = 'https://github.com/DD4SN/DD4SN.github.io.git'
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$siteSource = Join-Path $projectRoot 'pages-dist'
|
||||||
|
$workflowSource = Join-Path $projectRoot 'pages/deploy.yml'
|
||||||
|
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("holodori-pages-{0}" -f [guid]::NewGuid().ToString('N'))
|
||||||
|
$checkout = Join-Path $temporaryRoot 'repository'
|
||||||
|
|
||||||
|
try {
|
||||||
|
& npm --prefix $projectRoot run build:pages
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Pages build failed.' }
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
|
||||||
|
& git clone $Repository $checkout
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Pages repository clone failed.' }
|
||||||
|
|
||||||
|
$resolvedCheckout = (Resolve-Path -LiteralPath $checkout).Path
|
||||||
|
$resolvedTemporaryRoot = (Resolve-Path -LiteralPath $temporaryRoot).Path
|
||||||
|
if (-not $resolvedCheckout.StartsWith($resolvedTemporaryRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw 'Refusing to update a checkout outside the temporary deployment directory.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldSiteTarget = Join-Path $resolvedCheckout 'site'
|
||||||
|
if (Test-Path -LiteralPath $oldSiteTarget) {
|
||||||
|
Remove-Item -LiteralPath $oldSiteTarget -Recurse -Force
|
||||||
|
}
|
||||||
|
foreach ($generatedPath in @('assets', 'icons', 'index.html', 'README.txt', 'VERSION.txt', '.nojekyll')) {
|
||||||
|
$target = Join-Path $resolvedCheckout $generatedPath
|
||||||
|
if (Test-Path -LiteralPath $target) {
|
||||||
|
Remove-Item -LiteralPath $target -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Copy-Item -Path (Join-Path $siteSource '*') -Destination $resolvedCheckout -Recurse -Force
|
||||||
|
New-Item -ItemType File -Path (Join-Path $resolvedCheckout '.nojekyll') -Force | Out-Null
|
||||||
|
|
||||||
|
$workflowTarget = Join-Path $resolvedCheckout '.github/workflows'
|
||||||
|
New-Item -ItemType Directory -Path $workflowTarget -Force | Out-Null
|
||||||
|
Copy-Item -LiteralPath $workflowSource -Destination (Join-Path $workflowTarget 'deploy.yml') -Force
|
||||||
|
|
||||||
|
Push-Location $resolvedCheckout
|
||||||
|
try {
|
||||||
|
& git add --all -- .
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to stage Pages files.' }
|
||||||
|
& git diff --cached --quiet
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Output 'GitHub Pages output is already up to date.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$package = Get-Content -LiteralPath (Join-Path $projectRoot 'package.json') -Raw | ConvertFrom-Json
|
||||||
|
& git commit -m ("deploy: HolodoriCalc v{0}" -f $package.version)
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Pages commit failed.' }
|
||||||
|
& git push origin HEAD:main
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Pages push failed.' }
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (Test-Path -LiteralPath $temporaryRoot) {
|
||||||
|
$resolvedTemporaryRoot = (Resolve-Path -LiteralPath $temporaryRoot).Path
|
||||||
|
$systemTemporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd('\')
|
||||||
|
if ($resolvedTemporaryRoot.StartsWith($systemTemporaryRoot, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||||
|
(Split-Path -Leaf $resolvedTemporaryRoot).StartsWith('holodori-pages-')) {
|
||||||
|
Remove-Item -LiteralPath $resolvedTemporaryRoot -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readFile, rm, writeFile } from 'node:fs/promises';
|
import { readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
const output = path.resolve('offline-dist');
|
const output = path.resolve(process.argv[2] ?? 'offline-dist');
|
||||||
const htmlPath = path.join(output, 'index.html');
|
const htmlPath = path.join(output, 'index.html');
|
||||||
const scriptPath = path.join(output, 'assets', 'app.js');
|
const scriptPath = path.join(output, 'assets', 'app.js');
|
||||||
const stylePath = path.join(output, 'assets', 'app.css');
|
const stylePath = path.join(output, 'assets', 'app.css');
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { cp, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const siteSource = path.join(projectRoot, 'pages-dist');
|
||||||
|
const workflowSource = path.join(projectRoot, 'pages', 'deploy.yml');
|
||||||
|
const repository = process.env.GITHUB_PAGES_REPOSITORY ?? 'https://github.com/DD4SN/DD4SN.github.io.git';
|
||||||
|
|
||||||
|
if (!process.env.GITHUB_PAGES_TOKEN) {
|
||||||
|
throw new Error('GITHUB_PAGES_TOKEN is required to publish Pages output.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGit(args, options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn('git', args, {
|
||||||
|
cwd: options.cwd ?? projectRoot,
|
||||||
|
env: options.env ?? process.env,
|
||||||
|
stdio: options.quiet ? 'ignore' : 'inherit',
|
||||||
|
shell: false
|
||||||
|
});
|
||||||
|
child.on('error', reject);
|
||||||
|
child.on('exit', (code) => resolve(code ?? 1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requireGit(args, options) {
|
||||||
|
const code = await runGit(args, options);
|
||||||
|
if (code !== 0) throw new Error(`git ${args[0]} failed with exit code ${code}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), 'holodori-pages-'));
|
||||||
|
const checkout = path.join(temporaryRoot, 'repository');
|
||||||
|
const askPass = path.join(temporaryRoot, process.platform === 'win32' ? 'askpass.cmd' : 'askpass.sh');
|
||||||
|
const askPassContents = process.platform === 'win32'
|
||||||
|
? '@echo off\necho %1 | findstr /I "Username" >nul\nif %errorlevel%==0 (echo x-access-token) else (echo %GITHUB_PAGES_TOKEN%)\n'
|
||||||
|
: '#!/bin/sh\ncase "$1" in\n *Username*) printf "%s\\n" "x-access-token" ;;\n *) printf "%s\\n" "$GITHUB_PAGES_TOKEN" ;;\nesac\n';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile(askPass, askPassContents, { mode: 0o700 });
|
||||||
|
const gitEnvironment = {
|
||||||
|
...process.env,
|
||||||
|
GIT_ASKPASS: askPass,
|
||||||
|
GIT_TERMINAL_PROMPT: '0'
|
||||||
|
};
|
||||||
|
|
||||||
|
await requireGit(['clone', repository, checkout], { env: gitEnvironment });
|
||||||
|
|
||||||
|
const generatedPaths = ['site', 'assets', 'icons', 'index.html', 'README.txt', 'VERSION.txt', '.nojekyll'];
|
||||||
|
await Promise.all(generatedPaths.map((generatedPath) => rm(path.join(checkout, generatedPath), { recursive: true, force: true })));
|
||||||
|
await cp(siteSource, checkout, { recursive: true });
|
||||||
|
await writeFile(path.join(checkout, '.nojekyll'), '');
|
||||||
|
|
||||||
|
const workflowTarget = path.join(checkout, '.github', 'workflows');
|
||||||
|
await mkdir(workflowTarget, { recursive: true });
|
||||||
|
await cp(workflowSource, path.join(workflowTarget, 'deploy.yml'));
|
||||||
|
|
||||||
|
await requireGit(['config', 'user.name', 'HolodoriCalc deployment'], { cwd: checkout, env: gitEnvironment });
|
||||||
|
await requireGit(['config', 'user.email', 'actions@users.noreply.github.com'], { cwd: checkout, env: gitEnvironment });
|
||||||
|
await requireGit(['add', '--all', '--', '.'], { cwd: checkout, env: gitEnvironment });
|
||||||
|
|
||||||
|
const diffCode = await runGit(['diff', '--cached', '--quiet'], { cwd: checkout, env: gitEnvironment, quiet: true });
|
||||||
|
if (diffCode === 0) {
|
||||||
|
process.stdout.write('GitHub Pages output is already up to date.\n');
|
||||||
|
} else if (diffCode === 1) {
|
||||||
|
const packageJson = JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8'));
|
||||||
|
await requireGit(['commit', '-m', `deploy: HolodoriCalc v${packageJson.version}`], { cwd: checkout, env: gitEnvironment });
|
||||||
|
await requireGit(['push', 'origin', 'HEAD:main'], { cwd: checkout, env: gitEnvironment });
|
||||||
|
} else {
|
||||||
|
throw new Error(`git diff failed with exit code ${diffCode}.`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const resolvedTemporaryRoot = path.resolve(temporaryRoot);
|
||||||
|
const resolvedSystemTemp = path.resolve(os.tmpdir());
|
||||||
|
if (resolvedTemporaryRoot.startsWith(`${resolvedSystemTemp}${path.sep}`) && path.basename(resolvedTemporaryRoot).startsWith('holodori-pages-')) {
|
||||||
|
await rm(resolvedTemporaryRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
|
async function readJson(relativePath) {
|
||||||
|
return JSON.parse(await readFile(path.join(projectRoot, relativePath), 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicationTime() {
|
||||||
|
const configured = process.env.PAGES_PUBLICATION_TIME;
|
||||||
|
const value = configured ? Date.parse(configured) : Date.now();
|
||||||
|
if (!Number.isFinite(value)) throw new Error('PAGES_PUBLICATION_TIME must be a valid ISO-8601 date.');
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = publicationTime();
|
||||||
|
const policy = await readJson('pages/publication-policy.json');
|
||||||
|
const cards = await readJson('src/lib/data/cards-summary.json');
|
||||||
|
const boards = await readJson('src/lib/data/boards-summary.json');
|
||||||
|
const memory = await readJson('src/lib/data/memory-summary.json');
|
||||||
|
const charts = await readJson('src/lib/data/exact-chart-catalog.json');
|
||||||
|
const { events } = await import(pathToFileURL(path.join(projectRoot, 'src/lib/data/events.ts')).href);
|
||||||
|
|
||||||
|
const approvedVersion = policy.approvedMasterDataVersion;
|
||||||
|
const versions = new Map([
|
||||||
|
['cards-summary.json', cards.masterDataVersion],
|
||||||
|
['boards-summary.json', boards.masterDataVersion],
|
||||||
|
['memory-summary.json', memory.masterDataVersion]
|
||||||
|
]);
|
||||||
|
const mismatchedVersions = [...versions].filter(([, version]) => version !== approvedVersion);
|
||||||
|
if (mismatchedVersions.length > 0) {
|
||||||
|
throw new Error(`Unapproved master data: ${mismatchedVersions.map(([file, version]) => `${file}=${version}`).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const approvedNotBefore = Date.parse(policy.approvedNotBefore);
|
||||||
|
if (!Number.isFinite(approvedNotBefore)) {
|
||||||
|
throw new Error('approvedNotBefore must be a valid ISO-8601 date.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const undatedCharts = charts.charts.filter((chart) => !Number.isFinite(chart.releaseTime));
|
||||||
|
if (undatedCharts.length > 0) {
|
||||||
|
throw new Error(`Pages build contains ${undatedCharts.length} undated charts: ${undatedCharts.map((chart) => `${chart.songId}/${chart.difficulty}`).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const undatedEvents = events.filter((event) => !Number.isFinite(Date.parse(event.startsAt)));
|
||||||
|
if (undatedEvents.length > 0) {
|
||||||
|
throw new Error(`Pages build contains undated events: ${undatedEvents.map((event) => event.id).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notReadyReasons = [];
|
||||||
|
if (now < approvedNotBefore) {
|
||||||
|
notReadyReasons.push(`approved master data cannot be published before ${policy.approvedNotBefore}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreleasedCharts = charts.charts.filter((chart) => chart.releaseTime > now);
|
||||||
|
if (unreleasedCharts.length > 0) {
|
||||||
|
notReadyReasons.push(`${unreleasedCharts.length} charts are not released yet: ${unreleasedCharts.map((chart) => `${chart.songId}/${chart.difficulty}`).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreleasedEvents = events.filter((event) => Date.parse(event.startsAt) > now);
|
||||||
|
if (unreleasedEvents.length > 0) {
|
||||||
|
notReadyReasons.push(`events are not released yet: ${unreleasedEvents.map((event) => event.id).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notReadyReasons.length > 0) {
|
||||||
|
process.stderr.write(`Pages public-data gate is not ready at ${new Date(now).toISOString()}: ${notReadyReasons.join('; ')}.\n`);
|
||||||
|
process.exit(78);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(`Pages public-data gate passed at ${new Date(now).toISOString()}.\n`);
|
||||||
|
process.stdout.write(`Approved master data: ${approvedVersion}; cards=${cards.cards.length}; charts=${charts.charts.length}; events=${events.length}.\n`);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: path.join(root, 'offline'),
|
||||||
|
base: './',
|
||||||
|
publicDir: path.join(root, 'static'),
|
||||||
|
plugins: [svelte()],
|
||||||
|
resolve: {
|
||||||
|
alias: [
|
||||||
|
{ find: '$app/stores', replacement: path.join(root, 'offline/page-store.ts') },
|
||||||
|
{ find: '$app/environment', replacement: path.join(root, 'offline/app-environment.ts') },
|
||||||
|
{ find: '$lib/data/chart-loader.ts', replacement: path.join(root, 'offline/chart-loader.ts') },
|
||||||
|
{ find: '$lib/data/release-clock.ts', replacement: path.join(root, 'pages/release-clock.ts') },
|
||||||
|
{ find: '$lib', replacement: path.join(root, 'src/lib') }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
worker: { format: 'iife' },
|
||||||
|
build: {
|
||||||
|
outDir: path.join(root, 'pages-dist'),
|
||||||
|
emptyOutDir: true,
|
||||||
|
cssCodeSplit: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
format: 'iife',
|
||||||
|
entryFileNames: 'assets/app.js',
|
||||||
|
assetFileNames: (assetInfo) => assetInfo.name?.endsWith('.css') ? 'assets/app.css' : 'assets/[name][extname]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chunkSizeWarningLimit: 20000
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user