diff --git a/.github/actions/pnpm-install/action.yml b/.github/actions/pnpm-install/action.yml index 9a3eb3894..ca3bf2111 100644 --- a/.github/actions/pnpm-install/action.yml +++ b/.github/actions/pnpm-install/action.yml @@ -39,7 +39,7 @@ runs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 with: version: 10 run_install: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30150a958..1dda9110f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 with: version: 10 run_install: false @@ -47,21 +47,21 @@ jobs: stable: name: Stable Release - if: github.ref == 'refs/heads/prod' && github.repository_owner == 'SkyCryptWebsite' && github.actor != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, 'changeset-release/prod') + if: github.ref == 'refs/heads/prod' && github.repository_owner == 'SkyCryptWebsite' && github.actor != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]') runs-on: ubuntu-latest permissions: contents: write - pull-requests: write steps: - name: Git checkout uses: actions/checkout@v6 with: fetch-depth: 0 + token: ${{ secrets.PAT_TOKEN }} - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 with: version: 10 run_install: false @@ -78,7 +78,6 @@ jobs: HUSKY: "0" - name: Prepare Stable Release - id: stable_prep run: node scripts/release-stable.mjs env: GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} @@ -86,7 +85,8 @@ jobs: sync_dev_after_stable: name: Sync Prod Back To Dev - if: github.ref == 'refs/heads/prod' && github.repository_owner == 'SkyCryptWebsite' && github.actor != 'github-actions[bot]' && contains(github.event.head_commit.message, 'changeset-release/prod') + needs: stable + if: github.ref == 'refs/heads/prod' && github.repository_owner == 'SkyCryptWebsite' && github.actor != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]') runs-on: ubuntu-latest permissions: @@ -97,42 +97,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + token: ${{ secrets.PAT_TOKEN }} - name: Sync prod into dev - run: | - git fetch origin prod dev - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -B dev-sync origin/dev - git merge --no-ff -m "chore: sync prod into dev [skip ci]" origin/prod || true - - if git diff --quiet origin/dev HEAD; then - echo "No prod changes to sync back into dev" - exit 0 - fi - - git push origin HEAD:dev - - - name: Create Stable GitHub Release - if: steps.stable_prep.outputs.release_ready == 'true' - run: | - VERSION=$(node -p "require('./package.json').version") - - if echo "$VERSION" | grep -q "beta"; then - echo "Current version ($VERSION) is still a beta, skipping" - exit 0 - fi - - TAG="v${VERSION}" - - if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then - echo "Tag $TAG already exists, skipping" - exit 0 - fi - - git tag "$TAG" - git push origin "$TAG" - gh release create "$TAG" --title "$TAG" --generate-notes --latest - env: - GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} - GH_TOKEN: ${{ secrets.PAT_TOKEN }} + run: node scripts/sync-dev-after-stable.mjs diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 34af1c169..954c04e48 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -2,26 +2,61 @@ name: Website on: push: - paths: ["src/**", "Dockerfile", ".github/workflows/website.yml"] + paths: ["src/**", "Dockerfile", "package.json", ".changeset/**", ".github/workflows/website.yml"] workflow_dispatch: env: CONTAINER_REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} - SERVER_API_URL: cupcake.shiiyu.moe SHORT_SHA: 0000000 jobs: + prepare: + name: Determine Deployment + runs-on: ubuntu-latest + if: | + (github.ref == 'refs/heads/prod' || github.ref == 'refs/heads/dev') + && github.repository_owner == 'SkyCryptWebsite' + && github.event_name != 'pull_request' + outputs: + should_deploy: ${{ steps.decision.outputs.should_deploy }} + server_api_url: ${{ steps.decision.outputs.server_api_url }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Decide whether to deploy + id: decision + run: | + VERSION=$(node -p "require('./package.json').version") + SERVER_API_URL=cupcake.shiiyu.moe + SHOULD_DEPLOY=false + + if [ "$GITHUB_REF" = "refs/heads/dev" ]; then + SHOULD_DEPLOY=true + else + SERVER_API_URL=sky.shiiyu.moe + + if [[ "$VERSION" != *beta* ]]; then + SHOULD_DEPLOY=true + fi + fi + + echo "Detected version: $VERSION" + echo "should_deploy=$SHOULD_DEPLOY" >> "$GITHUB_OUTPUT" + echo "server_api_url=$SERVER_API_URL" >> "$GITHUB_OUTPUT" + checks: + needs: prepare + if: needs.prepare.outputs.should_deploy == 'true' uses: ./.github/workflows/checks.yml analyze: name: Analyze runs-on: ubuntu-latest + needs: prepare if: | - (github.ref == 'refs/heads/prod' || github.ref == 'refs/heads/dev') - && github.repository_owner == 'SkyCryptWebsite' - && github.event_name != 'pull_request' + needs.prepare.outputs.should_deploy == 'true' permissions: security-events: write @@ -39,20 +74,19 @@ jobs: uses: github/codeql-action/analyze@v4 build: - if: | - (github.ref == 'refs/heads/prod' || github.ref == 'refs/heads/dev') - && github.repository_owner == 'SkyCryptWebsite' - && github.event_name != 'pull_request' + needs: prepare + if: needs.prepare.outputs.should_deploy == 'true' uses: ./.github/workflows/build.yml with: - server-api-url: ${{ github.ref == 'refs/heads/prod' && 'sky.shiiyu.moe' || 'cupcake.shiiyu.moe' }} + server-api-url: ${{ needs.prepare.outputs.server_api_url }} secrets: server-api-token: ${{ secrets.SERVER_API_TOKEN }} package: name: Package into Container runs-on: ubuntu-latest - needs: [checks, analyze, build] + needs: [prepare, checks, analyze, build] + if: needs.prepare.outputs.should_deploy == 'true' permissions: packages: write @@ -68,10 +102,6 @@ jobs: - name: Assign short SHA run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - name: Re-assign server API URL if prod - if: github.ref == 'refs/heads/prod' - run: echo "SERVER_API_URL=sky.shiiyu.moe" >> $GITHUB_ENV - - name: Registry login uses: docker/login-action@v4 with: @@ -93,7 +123,7 @@ jobs: push: true build-args: | PUBLIC_COMMIT_HASH=${{ env.SHORT_SHA }} - PUBLIC_SERVER_API_URL=https://${{ env.SERVER_API_URL }}/api/ + PUBLIC_SERVER_API_URL=https://${{ needs.prepare.outputs.server_api_url }}/api/ tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a06317f26..226b25098 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,10 +94,9 @@ Release behavior details: - Changesets are authored in feature PRs and merged into `dev`. - The release workflow commits version/changelog updates directly on the release branch (`dev` or `prod`) using `github-actions[bot]`. -- Stable release versioning is proposed via `changeset-release/prod` and merged into `prod` through a PR. -- Do **not** manually maintain `changeset-release/*` branches. +- Merging `dev` into `prod` promotes the current beta line to a stable release directly on `prod`; no release PR is created. - Avoid routine manual `prod` → `dev` sync merges unless you are intentionally bringing over a stable-only hotfix. -- The stable release workflow uses `PAT_TOKEN` for GitHub CLI release PR/release operations (required when repository setting **"Allow GitHub Actions to create and approve pull requests"** is disabled). +- The stable release workflow uses `PAT_TOKEN` for direct pushes and GitHub release operations on `prod`. The version bump is determined automatically by changesets: diff --git a/scripts/release-stable.mjs b/scripts/release-stable.mjs index 12df5eaad..5d858360d 100644 --- a/scripts/release-stable.mjs +++ b/scripts/release-stable.mjs @@ -3,7 +3,6 @@ import fs from "node:fs"; import path from "node:path"; const changesetDir = path.join(process.cwd(), ".changeset"); -const outputFile = process.env.GITHUB_OUTPUT; function run(command) { execSync(command, { stdio: "inherit" }); @@ -21,18 +20,18 @@ function output(command) { return execSync(command, { encoding: "utf8" }).trim(); } -function setOutput(name, value) { - if (!outputFile) { - return; +function outputOptional(command) { + try { + return execSync(command, { encoding: "utf8" }).trim(); + } catch { + return ""; } - - fs.appendFileSync(outputFile, `${name}=${value}\n`); } run("git fetch origin prod dev"); run('git config user.name "github-actions[bot]"'); run('git config user.email "github-actions[bot]@users.noreply.github.com"'); -run("git checkout -B changeset-release/prod origin/prod"); +run("git checkout -B prod origin/prod"); const preStatePath = path.join(changesetDir, "pre.json"); @@ -53,30 +52,22 @@ run("pnpm changeset version"); try { execSync("git diff --quiet"); console.info("No stable version changes detected"); - const version = output("node -p \"require('./package.json').version\""); - setOutput("release_ready", version.includes("beta") ? "false" : "true"); - process.exit(0); } catch { // There are changes to commit. run("git add -A"); - run('git commit -m "chore: version packages [skip ci]"'); - run("git push origin HEAD:changeset-release/prod --force"); + run('git commit -m "chore: version packages (stable) [skip ci]"'); + run("git push origin HEAD:prod"); + run("git fetch origin prod"); + run("git checkout -B prod origin/prod"); } const version = output("node -p \"require('./package.json').version\""); if (version.includes("beta")) { console.info(`Current version (${version}) is still a beta, skipping stable release`); - setOutput("release_ready", "false"); process.exit(0); } -const existingPr = output("gh pr list --head changeset-release/prod --base prod --json number --jq '.[0].number'"); - -if (!existingPr) { - run('gh pr create --title "Version Packages (Stable)" --body "" --base prod --head changeset-release/prod'); -} - const tag = `v${version}`; const remoteTag = output(`git ls-remote --tags origin ${tag}`); @@ -85,7 +76,12 @@ if (remoteTag) { } else { run(`git tag ${tag}`); run(`git push origin ${tag}`); - run(`gh release create ${tag} --title ${tag} --generate-notes`); } -setOutput("release_ready", "false"); +const existingRelease = outputOptional(`gh release view ${tag} --json tagName --jq '.tagName'`); + +if (existingRelease) { + console.info(`Release ${tag} already exists, skipping`); +} else { + run(`gh release create ${tag} --title ${tag} --generate-notes`); +} diff --git a/scripts/sync-dev-after-stable.mjs b/scripts/sync-dev-after-stable.mjs new file mode 100644 index 000000000..871f34055 --- /dev/null +++ b/scripts/sync-dev-after-stable.mjs @@ -0,0 +1,68 @@ +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +const changesetDir = path.join(process.cwd(), ".changeset"); + +function run(command) { + execSync(command, { stdio: "inherit" }); +} + +function output(command) { + return execSync(command, { encoding: "utf8" }).trim(); +} + +function outputOptional(command) { + try { + return execSync(command, { encoding: "utf8" }).trim(); + } catch { + return ""; + } +} + +run("git fetch origin prod dev"); +run('git config user.name "github-actions[bot]"'); +run('git config user.email "github-actions[bot]@users.noreply.github.com"'); +run("git checkout -B dev-sync origin/dev"); + +try { + run('git merge --no-ff -m "chore: sync prod into dev [skip ci]" origin/prod'); +} catch { + const conflicts = outputOptional("git diff --name-only --diff-filter=U"); + + if (conflicts) { + throw new Error(`Syncing prod into dev has merge conflicts:\n${conflicts}`); + } +} + +const preStatePath = path.join(changesetDir, "pre.json"); +let isInBetaPreMode = false; + +if (fs.existsSync(preStatePath)) { + try { + const preState = JSON.parse(fs.readFileSync(preStatePath, "utf8")); + isInBetaPreMode = preState?.mode === "pre" && preState?.tag === "beta"; + } catch { + isInBetaPreMode = false; + } +} + +if (!isInBetaPreMode) { + run("pnpm changeset pre enter beta"); +} + +try { + execSync("git diff --quiet"); +} catch { + run("git add -A"); + run('git commit -m "chore: re-enter beta pre mode [skip ci]"'); +} + +const commitsAhead = Number(output("git rev-list --count origin/dev..HEAD")); + +if (commitsAhead === 0) { + console.info("No prod changes to sync back into dev"); + process.exit(0); +} + +run("git push origin HEAD:dev"); diff --git a/src/lib/layouts/stats/Main.svelte b/src/lib/layouts/stats/Main.svelte index 0cc807ed0..c5fa215f6 100644 --- a/src/lib/layouts/stats/Main.svelte +++ b/src/lib/layouts/stats/Main.svelte @@ -14,6 +14,7 @@ import Sections from "$lib/sections/Sections.svelte"; import type { ModelsStatsOutput } from "$lib/shared/api/orval-generated"; import { cn, flyAndScale } from "$lib/shared/utils"; + import { isAprilFoolsActive } from "$src/lib/shared/april-fools"; import Image from "@lucide/svelte/icons/image"; import { Avatar, Dialog } from "bits-ui"; import { Pane } from "paneforge"; @@ -193,7 +194,7 @@ --> -