Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/pnpm-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 8 additions & 43 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -78,15 +78,15 @@ jobs:
HUSKY: "0"

- name: Prepare Stable Release
id: stable_prep
run: node scripts/release-stable.mjs
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
GH_TOKEN: ${{ secrets.PAT_TOKEN }}

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:
Expand All @@ -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
62 changes: 46 additions & 16 deletions .github/workflows/website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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 }}

Expand Down
5 changes: 2 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
38 changes: 17 additions & 21 deletions scripts/release-stable.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand All @@ -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");

Expand All @@ -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}`);

Expand All @@ -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`);
}
68 changes: 68 additions & 0 deletions scripts/sync-dev-after-stable.mjs
Original file line number Diff line number Diff line change
@@ -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");
Loading
Loading