From 764536bd340ed5c62c0ce04cb9fb7a4b7f272559 Mon Sep 17 00:00:00 2001 From: Robin | Liquidium Date: Mon, 7 Sep 2026 14:28:38 +0700 Subject: [PATCH 1/2] chore: release v0.3.0 and automate Cloudflare deployment --- .github/workflows/changelog.yml | 24 ++++++++++++++++++-- .github/workflows/main-build.yml | 11 +++++++++ .github/workflows/release-from-changelog.yml | 22 ++++++++---------- CHANGELOG.md | 7 +++++- docs/cloudflare-migration.md | 8 ++++++- package.json | 2 +- src/routes/changelog.tsx | 11 +++------ 7 files changed, 59 insertions(+), 26 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 3869f19..d2ede45 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -14,12 +14,32 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 - - name: Require an Unreleased changelog entry + persist-credentials: false + - name: Require a changelog entry or a versioned release env: BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -euo pipefail + VERSION=$(node -p "require('./package.json').version") + BASE_VERSION=$(git show "${BASE_SHA}:package.json" | node -pe "JSON.parse(require('fs').readFileSync(0, 'utf8')).version") + if [ "$VERSION" != "$BASE_VERSION" ]; then + node -e ' + const [next, base] = process.argv.slice(1).map(v => v.split(".").map(Number)); + const difference = next.map((part, i) => part - base[i]).find(part => part !== 0); + if (!(difference > 0)) process.exit(1); + ' "$VERSION" "$BASE_VERSION" + FIRST_RELEASE=$(grep -m1 -E '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$' CHANGELOG.md) + if [[ "$FIRST_RELEASE" != "## [$VERSION] - "* ]]; then + echo "The latest dated release must match package.json." >&2 + exit 1 + fi + if git show "${BASE_SHA}:CHANGELOG.md" | grep -Fq "## [$VERSION]"; then + echo "The release version already exists in the base changelog." >&2 + exit 1 + fi + exit 0 + fi + extract_unreleased() { awk ' /^## \[Unreleased\]$/ { in_unreleased = 1; next } diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index 9b1723b..d579077 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -3,12 +3,18 @@ name: Main Build on: push: branches: [ "main" ] + workflow_dispatch: permissions: contents: read +concurrency: + group: production + cancel-in-progress: false + jobs: build: + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Checkout @@ -23,3 +29,8 @@ jobs: run: bun ci - name: Validate application and Worker build run: bun run ai-check + - name: Deploy validated Worker + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + run: bunx --no-install wrangler deploy diff --git a/.github/workflows/release-from-changelog.yml b/.github/workflows/release-from-changelog.yml index 888200d..ebe8a06 100644 --- a/.github/workflows/release-from-changelog.yml +++ b/.github/workflows/release-from-changelog.yml @@ -1,35 +1,30 @@ name: Release from Changelog on: - push: - branches: [ main ] - paths: - - 'CHANGELOG.md' - workflow_dispatch: + workflow_run: + workflows: [Main Build] + types: [completed] + branches: [main] permissions: contents: write jobs: changelog_release: + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + persist-credentials: false - name: Extract latest version and notes id: extract run: | set -euo pipefail - # Ensure the top section is a version, not Unreleased - TOP_LINE=$(grep -m1 -E '^## \[' CHANGELOG.md || true) - if echo "$TOP_LINE" | grep -qE '^## \[Unreleased\]'; then - echo "Top CHANGELOG section is [Unreleased]; skipping release." >&2 - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi VERSION_LINE=$(grep -m1 -E '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md || true) if [ -z "${VERSION_LINE}" ]; then echo "No version section found; skipping." @@ -76,6 +71,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.extract.outputs.version }} + DEPLOYED_SHA: ${{ github.event.workflow_run.head_sha }} run: | set -euo pipefail TAG="v${VERSION}" @@ -84,5 +80,5 @@ jobs: if gh release view "$TAG" >/dev/null 2>&1; then gh release edit "$TAG" --title "$TITLE" --latest -F RELEASE_NOTES.md else - gh release create "$TAG" --title "$TITLE" --latest -F RELEASE_NOTES.md --target "${GITHUB_SHA}" + gh release create "$TAG" --title "$TITLE" --latest -F RELEASE_NOTES.md --target "${DEPLOYED_SHA}" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index febe4a0..df260b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,15 @@ ## [Unreleased] +## [0.3.0] - 2026-09-07 + ### Fixed +- Aligned release checks with dated releases and kept unreleased notes out of the public changelog. - Hardened PR CI by restricting token permissions and disabling persisted checkout credentials before running repository scripts. - Prevented config-only commits from failing when lint-staged passed unsupported Markdown and YAML files to Biome. ### Added +- Automatic Cloudflare Worker deployment after successful main-branch validation, followed by GitHub release publication. - Automatic PR reviews in the opencode workflow (runs on PR open/update, reviews with Kimi k3). - Add opencode GitHub workflow for automated PR reviews and `/oc` commands (Kimi k3). - Centralized QueryClient defaults and query key factories. @@ -273,7 +277,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release: swap interface, price chart, core API routes and foundational UI. -[Unreleased]: https://github.com/ropl-btc/RunesSwap.app/compare/v0.2.8...HEAD +[Unreleased]: https://github.com/robin-liquidium/RunesSwap.app/compare/v0.3.0...HEAD +[0.3.0]: https://github.com/robin-liquidium/RunesSwap.app/compare/v0.2.8...v0.3.0 [0.2.8]: https://github.com/ropl-btc/RunesSwap.app/compare/v0.2.7...v0.2.8 [0.2.7]: https://github.com/ropl-btc/RunesSwap.app/compare/v0.2.6...v0.2.7 [0.2.4]: https://github.com/ropl-btc/RunesSwap.app/compare/v0.2.3...v0.2.4 diff --git a/docs/cloudflare-migration.md b/docs/cloudflare-migration.md index 2115503..e9cfbff 100644 --- a/docs/cloudflare-migration.md +++ b/docs/cloudflare-migration.md @@ -17,7 +17,7 @@ Hostinger is the domain registrar. Nameservers were changed to Cloudflare on 202 The zone initially mirrored Vercel's apex/wildcard aliases and CAA records to preserve service during DNS activation. No MX or TXT records were present at migration inventory time. -Both `runesswap.app` and `www.runesswap.app` are attached directly to the `runesswap` Worker and recorded in `wrangler.jsonc`. The apex Vercel CNAME was replaced; the wildcard record and CAA records remain. Future deployments use `bun run deploy` and retain both custom domains. Verify TLS, redirects, pages, static assets, and APIs after deployments. +Both `runesswap.app` and `www.runesswap.app` are attached directly to the `runesswap` Worker and recorded in `wrangler.jsonc`. The apex Vercel CNAME was replaced; the wildcard record and CAA records remain. Pushes to `main` run `bun run ai-check` and then deploy the validated build with Wrangler in the Main Build workflow. Production runs are serialized. GitHub releases are published only after that workflow succeeds, using its deployed commit. Manual deployments use `bun run deploy`; both paths retain the custom domains. Verify TLS, redirects, pages, static assets, and APIs after deployments. ## Rollback @@ -36,3 +36,9 @@ A registrar-level rollback can restore Vercel's original nameservers while the V ## Verification boundaries Automated checks can exercise validation, quotes, market data, routing, wallet menus, and mocked signing flows. Actual wallet signatures, swaps, borrowing, and repayment require Robin's final verification. Do not delete Vercel until that verification is complete. + +## Releases + +Feature PRs add notes under `Unreleased`. A release PR moves shipped notes into a dated version section and updates `package.json` to match. Keep an empty `Unreleased` section for subsequent work. The public changelog shows only dated releases. The changelog gate accepts either a new unreleased bullet or a new version section matching a changed package version. + +The Main Build workflow uses the repository secret `CLOUDFLARE_API_TOKEN` and variable `CLOUDFLARE_ACCOUNT_ID`. Runtime service secrets stay on the Worker. Do not add them to GitHub or the build environment. diff --git a/package.json b/package.json index f81d63c..1e29c28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "runesswap.app", - "version": "0.2.8", + "version": "0.3.0", "private": true, "scripts": { "test": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest --runInBand", diff --git a/src/routes/changelog.tsx b/src/routes/changelog.tsx index 63e65aa..4ecedad 100644 --- a/src/routes/changelog.tsx +++ b/src/routes/changelog.tsx @@ -7,11 +7,8 @@ import fileContents from '../../CHANGELOG.md?raw'; function getCleanChangelog(raw: string): string { const lines = raw.split(/\r?\n/); - // Find first version heading (e.g., "## [0.2.1] - 2025-08-23" or any H2) - let startIdx = lines.findIndex((l) => l.trim().startsWith('## ')); - if (startIdx === -1) startIdx = 0; - - // Slice from first H2 to end + const startIdx = lines.findIndex((line) => /^## \[\d+\.\d+\.\d+\]/.test(line)); + if (startIdx === -1) return 'No releases are currently available.'; const sliced = lines.slice(startIdx); // Filter out reference-style link definitions at the bottom like: [0.2.1]: https://... @@ -21,9 +18,7 @@ function getCleanChangelog(raw: string): string { } function ChangelogPage() { - const content = fileContents - ? getCleanChangelog(fileContents) - : 'No changelog is currently available.'; + const content = getCleanChangelog(fileContents); return (
From af3eb873bf9c003bb3825115f5c447d58ae5ec70 Mon Sep 17 00:00:00 2001 From: Robin | Liquidium Date: Mon, 7 Sep 2026 14:51:30 +0700 Subject: [PATCH 2/2] ci: use native Cloudflare Git builds for production --- .github/workflows/changelog.yml | 9 +++++++ .github/workflows/main-build.yml | 11 --------- .github/workflows/release-from-changelog.yml | 26 +++++++++++--------- CHANGELOG.md | 2 +- docs/cloudflare-migration.md | 6 ++--- src/routes/changelog.tsx | 4 ++- 6 files changed, 31 insertions(+), 27 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index d2ede45..430341b 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -37,6 +37,15 @@ jobs: echo "The release version already exists in the base changelog." >&2 exit 1 fi + if ! awk -v heading="$FIRST_RELEASE" ' + $0 == heading { in_release = 1; next } + in_release && /^## / { exit } + in_release && /^- [[:space:]]*[^[:space:]]/ { found = 1 } + END { exit !found } + ' CHANGELOG.md; then + echo "The release section must contain a nonempty changelog bullet." >&2 + exit 1 + fi exit 0 fi diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index d579077..9b1723b 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -3,18 +3,12 @@ name: Main Build on: push: branches: [ "main" ] - workflow_dispatch: permissions: contents: read -concurrency: - group: production - cancel-in-progress: false - jobs: build: - if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Checkout @@ -29,8 +23,3 @@ jobs: run: bun ci - name: Validate application and Worker build run: bun run ai-check - - name: Deploy validated Worker - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} - run: bunx --no-install wrangler deploy diff --git a/.github/workflows/release-from-changelog.yml b/.github/workflows/release-from-changelog.yml index ebe8a06..c27bbfc 100644 --- a/.github/workflows/release-from-changelog.yml +++ b/.github/workflows/release-from-changelog.yml @@ -1,31 +1,35 @@ name: Release from Changelog on: - workflow_run: - workflows: [Main Build] + check_run: types: [completed] - branches: [main] permissions: contents: write jobs: changelog_release: - if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository + if: >- + github.event.check_run.conclusion == 'success' && + github.event.check_run.app.slug == 'cloudflare-workers-and-pages' && + github.event.check_run.name == 'Workers Builds: runesswap' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - ref: ${{ github.event.workflow_run.head_sha }} + ref: ${{ github.event.check_run.head_sha }} persist-credentials: false + - name: Verify deployed commit belongs to main + run: git merge-base --is-ancestor HEAD origin/main + - name: Extract latest version and notes id: extract run: | set -euo pipefail - VERSION_LINE=$(grep -m1 -E '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md || true) + VERSION_LINE=$(grep -m1 -E '^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$' CHANGELOG.md || true) if [ -z "${VERSION_LINE}" ]; then echo "No version section found; skipping." echo "skip=true" >> "$GITHUB_OUTPUT" @@ -40,13 +44,13 @@ jobs: exit 0 fi - awk -v v="$VERSION" ' - $0 ~ "^## \\[" v "\\]" {found=1; next} - found && $0 ~ "^## \\[" { exit } + awk -v heading="$VERSION_LINE" ' + $0 == heading {found=1; next} + found && /^## / { exit } found { print } ' CHANGELOG.md > RELEASE_NOTES.md - if [ ! -s RELEASE_NOTES.md ]; then + if ! grep -qE '^- [[:space:]]*[^[:space:]]' RELEASE_NOTES.md; then echo "No notes captured for $VERSION; skipping." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 @@ -71,7 +75,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.extract.outputs.version }} - DEPLOYED_SHA: ${{ github.event.workflow_run.head_sha }} + DEPLOYED_SHA: ${{ github.event.check_run.head_sha }} run: | set -euo pipefail TAG="v${VERSION}" diff --git a/CHANGELOG.md b/CHANGELOG.md index df260b2..cf3f904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - Prevented config-only commits from failing when lint-staged passed unsupported Markdown and YAML files to Biome. ### Added -- Automatic Cloudflare Worker deployment after successful main-branch validation, followed by GitHub release publication. +- Automatic deployments from `main` through native Cloudflare Workers Builds, with GitHub releases published after Cloudflare reports a successful deployment. - Automatic PR reviews in the opencode workflow (runs on PR open/update, reviews with Kimi k3). - Add opencode GitHub workflow for automated PR reviews and `/oc` commands (Kimi k3). - Centralized QueryClient defaults and query key factories. diff --git a/docs/cloudflare-migration.md b/docs/cloudflare-migration.md index e9cfbff..abb1d42 100644 --- a/docs/cloudflare-migration.md +++ b/docs/cloudflare-migration.md @@ -17,7 +17,7 @@ Hostinger is the domain registrar. Nameservers were changed to Cloudflare on 202 The zone initially mirrored Vercel's apex/wildcard aliases and CAA records to preserve service during DNS activation. No MX or TXT records were present at migration inventory time. -Both `runesswap.app` and `www.runesswap.app` are attached directly to the `runesswap` Worker and recorded in `wrangler.jsonc`. The apex Vercel CNAME was replaced; the wildcard record and CAA records remain. Pushes to `main` run `bun run ai-check` and then deploy the validated build with Wrangler in the Main Build workflow. Production runs are serialized. GitHub releases are published only after that workflow succeeds, using its deployed commit. Manual deployments use `bun run deploy`; both paths retain the custom domains. Verify TLS, redirects, pages, static assets, and APIs after deployments. +Both `runesswap.app` and `www.runesswap.app` are attached directly to the `runesswap` Worker and recorded in `wrangler.jsonc`. The apex Vercel CNAME was replaced; the wildcard record and CAA records remain. Native Cloudflare Workers Builds connects the repository's `main` branch to production deployments. Its build command is `bun run ai-check`, its deploy command is `bunx --no-install wrangler deploy`, and its build variables are `BUN_VERSION=1.4.2` and `NODE_VERSION=22`. The Main Build GitHub workflow validates the application separately. GitHub releases are published only after Cloudflare's successful GitHub check, using the deployed commit. Manual deployments use `bun run deploy`; both paths retain the custom domains. Verify TLS, redirects, pages, static assets, and APIs after deployments. ## Rollback @@ -39,6 +39,6 @@ Automated checks can exercise validation, quotes, market data, routing, wallet m ## Releases -Feature PRs add notes under `Unreleased`. A release PR moves shipped notes into a dated version section and updates `package.json` to match. Keep an empty `Unreleased` section for subsequent work. The public changelog shows only dated releases. The changelog gate accepts either a new unreleased bullet or a new version section matching a changed package version. +Feature PRs add notes under `Unreleased`. A release PR moves shipped notes into a dated version section and updates `package.json` to match. Keep an empty `Unreleased` section for subsequent work. The public changelog shows only dated releases. The changelog gate accepts either a new unreleased bullet or a new dated version section containing a nonempty bullet and matching an increased package version. -The Main Build workflow uses the repository secret `CLOUDFLARE_API_TOKEN` and variable `CLOUDFLARE_ACCOUNT_ID`. Runtime service secrets stay on the Worker. Do not add them to GitHub or the build environment. +Cloudflare manages deployment authentication through its native Git integration; no Cloudflare GitHub secret or account variable is required. Runtime service secrets stay on the Worker. Do not add them to GitHub or the build environment. diff --git a/src/routes/changelog.tsx b/src/routes/changelog.tsx index 4ecedad..a476caf 100644 --- a/src/routes/changelog.tsx +++ b/src/routes/changelog.tsx @@ -7,7 +7,9 @@ import fileContents from '../../CHANGELOG.md?raw'; function getCleanChangelog(raw: string): string { const lines = raw.split(/\r?\n/); - const startIdx = lines.findIndex((line) => /^## \[\d+\.\d+\.\d+\]/.test(line)); + const startIdx = lines.findIndex((line) => + /^## \[\d+\.\d+\.\d+\] - \d{4}-\d{2}-\d{2}$/.test(line), + ); if (startIdx === -1) return 'No releases are currently available.'; const sliced = lines.slice(startIdx);