diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 3869f19..430341b 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -14,12 +14,41 @@ 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 + 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 + extract_unreleased() { awk ' /^## \[Unreleased\]$/ { in_unreleased = 1; next } diff --git a/.github/workflows/release-from-changelog.yml b/.github/workflows/release-from-changelog.yml index 888200d..c27bbfc 100644 --- a/.github/workflows/release-from-changelog.yml +++ b/.github/workflows/release-from-changelog.yml @@ -1,36 +1,35 @@ name: Release from Changelog on: - push: - branches: [ main ] - paths: - - 'CHANGELOG.md' - workflow_dispatch: + check_run: + types: [completed] permissions: contents: write jobs: changelog_release: + 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.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 - # 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) + 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" @@ -45,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 @@ -76,6 +75,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.extract.outputs.version }} + DEPLOYED_SHA: ${{ github.event.check_run.head_sha }} run: | set -euo pipefail TAG="v${VERSION}" @@ -84,5 +84,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..cf3f904 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 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. @@ -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..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. 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. 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 @@ -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 dated version section containing a nonempty bullet and matching an increased package version. + +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/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..a476caf 100644 --- a/src/routes/changelog.tsx +++ b/src/routes/changelog.tsx @@ -7,11 +7,10 @@ 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+\] - \d{4}-\d{2}-\d{2}$/.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 +20,7 @@ function getCleanChangelog(raw: string): string { } function ChangelogPage() { - const content = fileContents - ? getCleanChangelog(fileContents) - : 'No changelog is currently available.'; + const content = getCleanChangelog(fileContents); return (