diff --git a/.github/workflows/cloudflare-pages-deploy.yml b/.github/workflows/cloudflare-pages-deploy.yml new file mode 100644 index 0000000..2dbb112 --- /dev/null +++ b/.github/workflows/cloudflare-pages-deploy.yml @@ -0,0 +1,26 @@ +name: cloudflare-pages-deploy + +on: + schedule: + # Twice daily: 01:00 UTC (09:00 Beijing) and 13:00 UTC (21:00 Beijing) + - cron: '0 1,13 * * *' + workflow_dispatch: + +concurrency: + group: cloudflare-pages-deploy + cancel-in-progress: true + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Trigger Cloudflare Pages Deploy Hook + env: + CLOUDFLARE_PAGES_DEPLOY_HOOK_URL: ${{ secrets.CLOUDFLARE_PAGES_DEPLOY_HOOK_URL }} + run: | + if [ -z "$CLOUDFLARE_PAGES_DEPLOY_HOOK_URL" ]; then + echo "Missing secret CLOUDFLARE_PAGES_DEPLOY_HOOK_URL" >&2 + exit 1 + fi + + curl -sS -X POST "$CLOUDFLARE_PAGES_DEPLOY_HOOK_URL" -o /dev/null -w "HTTP %{http_code}\n" diff --git a/.github/workflows/rspress-ecosystem-ci-from-commit.yml b/.github/workflows/rspress-ecosystem-ci-from-commit.yml index 1c0a3aa..72e2642 100644 --- a/.github/workflows/rspress-ecosystem-ci-from-commit.yml +++ b/.github/workflows/rspress-ecosystem-ci-from-commit.yml @@ -56,11 +56,6 @@ jobs: env: ECOSYSTEM_CI_REF: ${{ inputs.commitSHA }} ECOSYSTEM_CI_TYPE: 'commit' - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - RSBUILD_NETLIFY_SITE_ID: ${{ secrets.RSBUILD_NETLIFY_SITE_ID }} - RSLIB_NETLIFY_SITE_ID: ${{ secrets.RSLIB_NETLIFY_SITE_ID }} - RSPACK_NETLIFY_SITE_ID: ${{ secrets.RSPACK_NETLIFY_SITE_ID }} - RSTEST_NETLIFY_SITE_ID: ${{ secrets.RSTEST_NETLIFY_SITE_ID }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/build-rspress @@ -96,11 +91,6 @@ jobs: env: ECOSYSTEM_CI_REF: ${{ inputs.commitSHA }} ECOSYSTEM_CI_TYPE: 'commit' - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - RSBUILD_NETLIFY_SITE_ID: ${{ secrets.RSBUILD_NETLIFY_SITE_ID }} - RSLIB_NETLIFY_SITE_ID: ${{ secrets.RSLIB_NETLIFY_SITE_ID }} - RSPACK_NETLIFY_SITE_ID: ${{ secrets.RSPACK_NETLIFY_SITE_ID }} - RSTEST_NETLIFY_SITE_ID: ${{ secrets.RSTEST_NETLIFY_SITE_ID }} steps: - uses: actions/checkout@v5 - uses: moonrepo/setup-rust@v1 diff --git a/.github/workflows/rspress-ecosystem-ci-from-pr.yml b/.github/workflows/rspress-ecosystem-ci-from-pr.yml index 9037028..ab0c7c0 100644 --- a/.github/workflows/rspress-ecosystem-ci-from-pr.yml +++ b/.github/workflows/rspress-ecosystem-ci-from-pr.yml @@ -53,12 +53,6 @@ jobs: execute-selected-suite: runs-on: ubuntu-latest if: "inputs.suite != '-'" - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - RSBUILD_NETLIFY_SITE_ID: ${{ secrets.RSBUILD_NETLIFY_SITE_ID }} - RSLIB_NETLIFY_SITE_ID: ${{ secrets.RSLIB_NETLIFY_SITE_ID }} - RSPACK_NETLIFY_SITE_ID: ${{ secrets.RSPACK_NETLIFY_SITE_ID }} - RSTEST_NETLIFY_SITE_ID: ${{ secrets.RSTEST_NETLIFY_SITE_ID }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/build-rspress @@ -91,12 +85,6 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} name: execute-all (${{ matrix.suite }}) - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - RSBUILD_NETLIFY_SITE_ID: ${{ secrets.RSBUILD_NETLIFY_SITE_ID }} - RSLIB_NETLIFY_SITE_ID: ${{ secrets.RSLIB_NETLIFY_SITE_ID }} - RSPACK_NETLIFY_SITE_ID: ${{ secrets.RSPACK_NETLIFY_SITE_ID }} - RSTEST_NETLIFY_SITE_ID: ${{ secrets.RSTEST_NETLIFY_SITE_ID }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/build-rspress diff --git a/.github/workflows/rspress-ecosystem-ci-selected.yml b/.github/workflows/rspress-ecosystem-ci-selected.yml index 1a9876b..545284c 100644 --- a/.github/workflows/rspress-ecosystem-ci-selected.yml +++ b/.github/workflows/rspress-ecosystem-ci-selected.yml @@ -59,14 +59,6 @@ jobs: execute-selected-suite: runs-on: ubuntu-latest if: "inputs.suite != '-'" - env: - ECOSYSTEM_CI_REF: ${{ inputs.ref }} - ECOSYSTEM_CI_TYPE: 'branch' - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - RSBUILD_NETLIFY_SITE_ID: ${{ secrets.RSBUILD_NETLIFY_SITE_ID }} - RSLIB_NETLIFY_SITE_ID: ${{ secrets.RSLIB_NETLIFY_SITE_ID }} - RSPACK_NETLIFY_SITE_ID: ${{ secrets.RSPACK_NETLIFY_SITE_ID }} - RSTEST_NETLIFY_SITE_ID: ${{ secrets.RSTEST_NETLIFY_SITE_ID }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/build-rspress @@ -85,14 +77,6 @@ jobs: execute-all: if: "inputs.suite == '-'" - env: - ECOSYSTEM_CI_REF: ${{ inputs.ref }} - ECOSYSTEM_CI_TYPE: 'branch' - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - RSBUILD_NETLIFY_SITE_ID: ${{ secrets.RSBUILD_NETLIFY_SITE_ID }} - RSLIB_NETLIFY_SITE_ID: ${{ secrets.RSLIB_NETLIFY_SITE_ID }} - RSPACK_NETLIFY_SITE_ID: ${{ secrets.RSPACK_NETLIFY_SITE_ID }} - RSTEST_NETLIFY_SITE_ID: ${{ secrets.RSTEST_NETLIFY_SITE_ID }} strategy: matrix: include: diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 74e7702..0000000 --- a/netlify.toml +++ /dev/null @@ -1,13 +0,0 @@ -[build] -command = "pnpm install --frozen-lockfile && pnpm --filter ecosystem-ci-website build" -publish = "website/dist" - -[build.environment] -NETLIFY_USE_PNPM = "true" - -[functions] -directory = "netlify/functions" -node_bundler = "esbuild" - -[functions."trigger-deploy"] -schedule = "@hourly" diff --git a/netlify/functions/trigger-deploy.ts b/netlify/functions/trigger-deploy.ts deleted file mode 100644 index 41bba20..0000000 --- a/netlify/functions/trigger-deploy.ts +++ /dev/null @@ -1,27 +0,0 @@ -const handler = async (_req: Request): Promise => { - const hookUrl = process.env.NETLIFY_BUILD_HOOK_URL; - - if (!hookUrl) { - return new Response( - 'Missing NETLIFY_BUILD_HOOK_URL environment variable.', - { - status: 500, - }, - ); - } - - const response = await fetch(hookUrl, { method: 'POST' }); - - if (!response.ok) { - const message = await response.text(); - return new Response(`Failed to trigger Netlify build hook: ${message}`, { - status: response.status, - }); - } - - return new Response('Triggered Netlify deploy successfully.', { - status: 200, - }); -}; - -export default handler; diff --git a/tests/rspress/rsbuild.ts b/tests/rspress/rsbuild.ts index 4b3406c..3b53810 100644 --- a/tests/rspress/rsbuild.ts +++ b/tests/rspress/rsbuild.ts @@ -1,8 +1,5 @@ import type { RunOptions } from '../../types'; import { cd, runInRepo } from '../../utils'; -import { MESSAGE, deployPreviewToNetlify } from './utils/_netlify'; - -const SITE_ID_ENV = 'RSBUILD_NETLIFY_SITE_ID'; export async function test(options: RunOptions) { await runInRepo({ @@ -12,14 +9,6 @@ export async function test(options: RunOptions) { beforeTest: async () => { cd('./website'); }, - test: [ - 'build', - async () => { - await deployPreviewToNetlify({ - message: MESSAGE, - siteIdEnvVar: SITE_ID_ENV, - }); - }, - ], + test: ['build'], }); } diff --git a/tests/rspress/rslib.ts b/tests/rspress/rslib.ts index 96176ac..86fb5cb 100644 --- a/tests/rspress/rslib.ts +++ b/tests/rspress/rslib.ts @@ -1,8 +1,5 @@ import type { RunOptions } from '../../types'; import { cd, runInRepo } from '../../utils'; -import { MESSAGE, deployPreviewToNetlify } from './utils/_netlify'; - -const SITE_ID_ENV = 'RSLIB_NETLIFY_SITE_ID'; export async function test(options: RunOptions) { await runInRepo({ @@ -12,14 +9,6 @@ export async function test(options: RunOptions) { beforeTest: async () => { cd('./website'); }, - test: [ - 'build', - async () => { - await deployPreviewToNetlify({ - message: MESSAGE, - siteIdEnvVar: SITE_ID_ENV, - }); - }, - ], + test: ['build'], }); } diff --git a/tests/rspress/rspack.ts b/tests/rspress/rspack.ts index 568fc6d..135430a 100644 --- a/tests/rspress/rspack.ts +++ b/tests/rspress/rspack.ts @@ -1,8 +1,5 @@ import type { RunOptions } from '../../types'; import { cd, runInRepo } from '../../utils'; -import { MESSAGE, deployPreviewToNetlify } from './utils/_netlify'; - -const SITE_ID_ENV = 'RSPACK_NETLIFY_SITE_ID'; export async function test(options: RunOptions) { await runInRepo({ @@ -12,14 +9,6 @@ export async function test(options: RunOptions) { beforeTest: async () => { cd('./website'); }, - test: [ - 'pnpm run build', - async () => { - await deployPreviewToNetlify({ - message: MESSAGE, - siteIdEnvVar: SITE_ID_ENV, - }); - }, - ], + test: ['pnpm run build'], }); } diff --git a/tests/rspress/rstest.ts b/tests/rspress/rstest.ts index e2e1bc3..9afe10e 100644 --- a/tests/rspress/rstest.ts +++ b/tests/rspress/rstest.ts @@ -1,8 +1,5 @@ import type { RunOptions } from '../../types'; import { cd, runInRepo } from '../../utils'; -import { MESSAGE, deployPreviewToNetlify } from './utils/_netlify'; - -const SITE_ID_ENV = 'RSTEST_NETLIFY_SITE_ID'; export async function test(options: RunOptions) { await runInRepo({ @@ -12,14 +9,6 @@ export async function test(options: RunOptions) { beforeTest: async () => { cd('./website'); }, - test: [ - 'build', - async () => { - await deployPreviewToNetlify({ - message: MESSAGE, - siteIdEnvVar: SITE_ID_ENV, - }); - }, - ], + test: ['build'], }); } diff --git a/tests/rspress/utils/_netlify.ts b/tests/rspress/utils/_netlify.ts deleted file mode 100644 index b412bd3..0000000 --- a/tests/rspress/utils/_netlify.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { $ } from '../../../utils'; - -interface DeployOptions { - alias?: string; - message: string; - outputDir?: string; - siteIdEnvVar?: string; -} - -const DEFAULT_OUTPUT_DIR = './website/doc_build'; -const DEFAULT_NETLIFY_CLI = 'netlify-cli@23.10.0'; -const DEFAULT_ALIAS = process.env.RSPRESS_NETLIFY_ALIAS ?? 'ecosystem-ci'; -export const MESSAGE = `${process.env.ECOSYSTEM_CI_TYPE}:${process.env.ECOSYSTEM_CI_REF}`; - -export async function deployPreviewToNetlify(options: DeployOptions) { - const { message, outputDir = DEFAULT_OUTPUT_DIR, siteIdEnvVar } = options; - const defaultAlias = - process.env.ECOSYSTEM_CI_TYPE === 'commit' - ? DEFAULT_ALIAS - : (process.env.ECOSYSTEM_CI_REF ?? DEFAULT_ALIAS); - const alias = options.alias ?? defaultAlias; - const authToken = process.env.NETLIFY_AUTH_TOKEN; - const siteId = siteIdEnvVar ? process.env[siteIdEnvVar] : undefined; - const missingVars: string[] = []; - if (!authToken) { - missingVars.push('NETLIFY_AUTH_TOKEN'); - } - if (!siteId) { - missingVars.push(siteIdEnvVar || 'site ID'); - } - - if (missingVars.length > 0) { - console.log( - `[rspress][netlify] Missing ${missingVars.join(', ')}, skip deploying alias ${alias}`, - ); - return; - } - - console.log(`[rspress][netlify] Deploying with alias: ${alias}`); - - const cliSpecifier = process.env.RSPRESS_NETLIFY_CLI ?? DEFAULT_NETLIFY_CLI; - const result = - await $`pnpm --package=${cliSpecifier} dlx netlify deploy --dir=${outputDir} --alias=${alias} --message=${message} --site=${siteId} --auth=${authToken} --json --no-build`; - - try { - const parsed = JSON.parse(result); - const previewUrl = - parsed?.deploy?.deploy_ssl_url ?? parsed?.deploy?.deploy_url; - if (previewUrl) { - console.log( - `[rspress][netlify] Alias ${alias} preview url: ${previewUrl}`, - ); - } - } catch (error) { - console.log( - `[rspress][netlify] Unable to parse deploy response JSON: ${(error as Error).message}`, - ); - } -} diff --git a/website/src/App.tsx b/website/src/App.tsx index d9b5f15..1921942 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -36,33 +36,6 @@ const RSTACK_REPOS = [ { label: 'Rspress', url: 'https://github.com/web-infra-dev/rspress' }, ] as const; -const RSPRESS_PREVIEW_LINKS: Array<{ - id: StackId; - label: string; - url: string; -}> = [ - { - id: 'rspack', - label: 'Rspack', - url: 'https://ecosystem-ci--rspack.netlify.app/', - }, - { - id: 'rsbuild', - label: 'Rsbuild', - url: 'https://ecosystem-ci--rsbuild.netlify.app/', - }, - { - id: 'rslib', - label: 'Rslib', - url: 'https://ecosystem-ci--rslib.netlify.app/', - }, - { - id: 'rstest', - label: 'Rstest', - url: 'https://ecosystem-ci--rstest-dev.netlify.app/', - }, -]; - export default function App() { const historySource = history as Record; const [searchParams, setSearchParams] = useSearchParams(); @@ -315,7 +288,6 @@ export default function App() { onStackChange={(value) => handleStackChange(value as StackId)} selectedSuite={selectedSuite} onSuiteChange={handleSuiteChange} - previewLinks={RSPRESS_PREVIEW_LINKS} /> @@ -348,9 +320,12 @@ interface UpdateSchedule { countdownMinutes: number | null; } +/** Schedule times in UTC hours: 01:00 and 13:00 UTC (09:00 and 21:00 Beijing) */ +const SCHEDULE_HOURS_UTC = [1, 13] as const; + function buildUpdateSchedule(referenceTime: Date): UpdateSchedule { - const lastUpdate = alignToHour(referenceTime, 'floor'); - const nextUpdate = alignToHour(referenceTime, 'ceil'); + const lastUpdate = getPrevScheduledTime(referenceTime); + const nextUpdate = getNextScheduledTime(referenceTime); const countdownMinutes = Math.max( 0, @@ -376,18 +351,38 @@ function formatTimestamp(date: Date) { return `${dateStr} ${utcStr}`; } -function alignToHour(date: Date, mode: 'floor' | 'ceil') { - const aligned = new Date(date); - aligned.setSeconds(0, 0); - aligned.setMinutes(0); +/** Build a Date for a specific UTC hour on a given day (offset in days from base). */ +function buildScheduleTime(base: Date, dayOffset: number, utcHour: number) { + const d = new Date(base); + d.setUTCHours(0, 0, 0, 0); + d.setUTCDate(d.getUTCDate() + dayOffset); + d.setUTCHours(utcHour); + return d; +} - if (mode === 'floor' && aligned.getTime() > date.getTime()) { - aligned.setHours(aligned.getHours() - 1); - } else if (mode === 'ceil' && aligned.getTime() <= date.getTime()) { - aligned.setHours(aligned.getHours() + 1); - } +/** Get candidates: yesterday's, today's, and tomorrow's scheduled times. */ +function getScheduleCandidates(base: Date) { + return [-1, 0, 1].flatMap((offset) => + SCHEDULE_HOURS_UTC.map((hour) => buildScheduleTime(base, offset, hour)), + ); +} - return aligned; +/** Returns the most recent scheduled time at or before `now`. */ +function getPrevScheduledTime(now: Date) { + const nowMs = now.getTime(); + const candidates = getScheduleCandidates(now).filter( + (d) => d.getTime() <= nowMs, + ); + return new Date(Math.max(...candidates.map((d) => d.getTime()))); +} + +/** Returns the next scheduled time strictly after `now`. */ +function getNextScheduledTime(now: Date) { + const nowMs = now.getTime(); + const candidates = getScheduleCandidates(now).filter( + (d) => d.getTime() > nowMs, + ); + return new Date(Math.min(...candidates.map((d) => d.getTime()))); } function formatCountdown(minutes: number | null) { @@ -396,6 +391,12 @@ function formatCountdown(minutes: number | null) { } const safeMinutes = Math.max(0, minutes); + const hours = Math.floor(safeMinutes / 60); + const mins = safeMinutes % 60; + + if (hours > 0) { + return mins > 0 ? `in ${hours}h ${mins}min` : `in ${hours}h`; + } return `in ${safeMinutes}min`; } @@ -491,7 +492,7 @@ function UpdateScheduleCard({

Next refresh

- 1h cron + 12h cron

diff --git a/website/src/assets/netlify-symbol.svg b/website/src/assets/netlify-symbol.svg deleted file mode 100644 index 07da8c4..0000000 --- a/website/src/assets/netlify-symbol.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/src/components/timeline.tsx b/website/src/components/timeline.tsx index 78c474d..b004c1b 100644 --- a/website/src/components/timeline.tsx +++ b/website/src/components/timeline.tsx @@ -1,6 +1,5 @@ import { useMemo, useState } from 'react'; -import netlifyLogomark from '@/assets/netlify-symbol.svg'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -63,7 +62,6 @@ interface TimelineProps { }>; selectedStack?: string; onStackChange?: (stackId: string) => void; - previewLinks?: Array<{ id: string; label: string; url: string }>; } export function Timeline({ @@ -73,7 +71,6 @@ export function Timeline({ stacks, selectedStack: externalSelectedStack, onStackChange, - previewLinks, }: TimelineProps) { const [internalSelectedSuite, setInternalSelectedSuite] = useState('all'); @@ -203,38 +200,6 @@ export function Timeline({ - {externalSelectedStack === 'rspress' && previewLinks?.length ? ( -

- - rspress ecosystem ci preview - -
- {previewLinks.map((preview) => ( - - - {preview.label} - - ↗ - - - ))} -
-
- ) : null} - {filteredEntries.length > 0 ? (