From cc9e435ca86b6d1f4331833bfdc18ad5953fbc95 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 22:23:12 +0000 Subject: [PATCH 1/2] ci: pin uv so release lockfile refreshes keep the committed lock format The latest uv rewrites uv.lock with a newer revision, which the release metadata check rejects as a change beyond the version substitution. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .github/workflows/publish.yml | 2 ++ .github/workflows/release-please.yml | 2 ++ .github/workflows/verify.yml | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7de9a50..6684308 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -135,6 +135,8 @@ jobs: with: toolchain: 1.95.0 - uses: astral-sh/setup-uv@v6 + with: + version: "0.12.9" # The sqlclean package is pure Python, so one wheel serves every platform. - name: Build Python package sdist and wheel run: uv build --sdist --wheel --out-dir dist diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 6d6d6dc..64bce78 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -128,6 +128,8 @@ jobs: - uses: astral-sh/setup-uv@v6 if: steps.release.outputs.prs_created == 'true' + with: + version: "0.12.9" - uses: dtolnay/rust-toolchain@master if: steps.release.outputs.prs_created == 'true' diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 6b79a57..1e760fc 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -42,6 +42,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 + with: + version: "0.12.9" - run: python3 -m scripts.check_docs_skill - run: uvx ruff@0.17.3 format --check . - run: uvx ruff@0.17.3 check . @@ -59,6 +61,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - uses: astral-sh/setup-uv@v6 with: + version: "0.12.9" enable-cache: true - run: uv python install 3.12 - run: uv sync --locked --all-groups @@ -78,6 +81,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - uses: astral-sh/setup-uv@v6 with: + version: "0.12.9" enable-cache: true - run: uv python install 3.12 - run: uv sync --locked --all-groups @@ -108,6 +112,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - uses: astral-sh/setup-uv@v6 with: + version: "0.12.9" enable-cache: true - run: uv python install 3.12 - run: uv sync --locked --all-groups @@ -128,6 +133,8 @@ jobs: toolchain: 1.95.0 - uses: Swatinem/rust-cache@v2 - uses: astral-sh/setup-uv@v6 + with: + version: "0.12.9" - run: uv python install 3.12 - run: uv sync --locked --all-groups - run: uv run sqlclean --version @@ -144,6 +151,8 @@ jobs: toolchain: 1.95.0 - uses: Swatinem/rust-cache@v2 - uses: astral-sh/setup-uv@v6 + with: + version: "0.12.9" - run: uv python install 3.12 - run: uv build --sdist --out-dir dist - run: uvx maturin sdist --out ../../dist From fc82267cf4e47f1126bc83b6ea0a510e2347b4da Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 22:31:08 +0000 Subject: [PATCH 2/2] ci: rehearse releases in Verify and name missing release credentials simulate_release.sh applies Release Please's version edits on a scratch worktree, refreshes the lockfiles like the release workflow and runs the release metadata check. required-version keeps local uv on the pinned line. Release Please now fails first with the missing variable or secret named. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .github/RELEASING.md | 34 ++++++++++++++++++++++++- .github/scripts/simulate_release.sh | 37 ++++++++++++++++++++++++++++ .github/workflows/release-please.yml | 13 ++++++++++ .github/workflows/verify.yml | 19 +++++++++++++- Makefile | 5 +++- pyproject.toml | 3 +++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100755 .github/scripts/simulate_release.sh diff --git a/.github/RELEASING.md b/.github/RELEASING.md index 57ab811..0baaa41 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -26,6 +26,38 @@ Documentation edits usually do release, on purpose: `make skills` copies the doc installed skill stays in step with the docs. A homepage or styling change touches only `website/` and does not. +## One-time setup + +A repository administrator does these once, in a browser: + +1. **Release GitHub App.** Install the organization's delivery GitHub App on this repository, + with contents, pull requests, issues and actions write access. In Settings > Secrets and + variables > Actions, add the variable `RELEASE_APP_ID` (the App ID) and the secret + `RELEASE_APP_PRIVATE_KEY` (the full contents of a private key generated under the app's + Credentials). Release Please opens release pull requests and dispatches `publish.yml` with + this app's token, because events from the default `GITHUB_TOKEN` do not start workflows. + Without either setting, the Release Please run fails at its first step and names the missing + one. +2. **PyPI trusted publishers.** For both `sqlclean` and `sqlclean-cli`, add a GitHub publisher + with owner `sqlstack-io`, repository `sqlclean`, workflow `publish.yml` and environment + `pypi`. GitHub creates the `pypi` environment the first time `publish.yml` uses it. +3. **Cloudflare Pages.** The `sqlclean` Pages project builds `website/` from this repository. + Add `sqlclean.io` and `www.sqlclean.io` under its Custom domains. + +Automation tokens, such as a coding agent's GitHub App token or `GITHUB_TOKEN`, usually cannot +read or write Actions variables, secrets or app installations, and a private key can only be +downloaded when it is generated. These steps need a person. + +## Tooling versions + +`uv.lock` is only reproducible with one uv release line: a newer uv rewrites the file's format, +and the release metadata check then rejects the release pull request. Workflows pin uv 0.12.9 +and `[tool.uv] required-version` in `pyproject.toml` keeps local uv on 0.12, so update both +together. `make release-rehearsal` (the Release rehearsal job in Verify) applies Release Please's +version edits on a scratch worktree, refreshes the lockfiles the same way the release workflow +does, and runs the release metadata check, so a mismatch fails a pull request rather than a +release. + ## Verify `verify.yml` classifies a pull request by the paths it changes and runs only the suites it needs. @@ -35,6 +67,6 @@ The `Verify` job is the one check to require: it passes when every selected suit |--------|--------| | Only `website/`, `benchmarks/`, root `*.md`, `.github/demos/`, images, `website.yml` | docs skill check and ruff | | Also the bundled skill under `crates/sqlclean-cli/skill/` | plus a Linux build and the `sqlclean skills` tests | -| Anything else, including workflows, Rust, Python, tests and lockfiles | everything: `make verify`, minimum Rust, Windows, three-platform package smoke tests and the source build | +| Anything else, including workflows, Rust, Python, tests and lockfiles | everything: `make verify`, minimum Rust, Windows, three-platform package smoke tests, the source build and the release rehearsal | A manual or release dispatch has no pull request diff, so it runs everything. diff --git a/.github/scripts/simulate_release.sh b/.github/scripts/simulate_release.sh new file mode 100755 index 0000000..fb9587c --- /dev/null +++ b/.github/scripts/simulate_release.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Rehearse a release on a scratch worktree: apply Release Please's version edits, refresh the +# lockfiles the way release-please.yml does, then run the strict release metadata check. +# This catches lockfile drift, such as a different uv rewriting uv.lock, before a real release. +set -euo pipefail + +repo="$(git rev-parse --show-toplevel)" +current="$(jq -r '."."' "${repo}/.release-please-manifest.json")" +IFS=. read -r major minor _ <<< "${current}" +next="${major}.$((minor + 1)).0" +scratch="$(mktemp -d)" +trap 'git -C "${repo}" worktree remove --force "${scratch}" >/dev/null 2>&1 || true' EXIT + +git -C "${repo}" worktree add --detach --quiet "${scratch}" HEAD +cd "${scratch}" +# Release Please rewrites the version on every line marked x-release-please-version. +for file in pyproject.toml Cargo.toml packages/sqlclean-cli/pyproject.toml; do + sed -i "/x-release-please-version/s/${current//./\\.}/${next}/" "${file}" +done +# The Python release strategy also rewrites [project] version, which carries no marker. +sed -i "0,/^version = \"${current//./\\.}\"/s//version = \"${next}\"/" pyproject.toml +jq --arg next "${next}" '."." = $next' .release-please-manifest.json > manifest.tmp +mv manifest.tmp .release-please-manifest.json +# Release Please adds the new section under the title and keeps the earlier history. +{ + printf '# Changelog\n\n## [%s](https://example.com/compare/v%s...v%s) (2000-01-01)\n\n' \ + "${next}" "${current}" "${next}" + tail -n +3 CHANGELOG.md +} > changelog.tmp +mv changelog.tmp CHANGELOG.md +# The same refresh as release-please.yml; cargo metadata rewrites Cargo.lock without compiling. +uv lock --quiet +cargo metadata --format-version 1 > /dev/null +git -c commit.gpgsign=false -c user.name=release-rehearsal -c user.email=release-rehearsal@example.com \ + commit --quiet --all --message "chore(main): release ${next}" +python3 "${repo}/.github/scripts/release_metadata_check.py" --repo "${scratch}" --base HEAD~1 --head HEAD +echo "Release rehearsal ${current} -> ${next} passed with $(uv --version)" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 64bce78..22047e3 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -20,6 +20,19 @@ jobs: release-please: runs-on: ubuntu-latest steps: + - name: Check release credentials are configured + env: + HAS_APP_ID: ${{ vars.RELEASE_APP_ID != '' }} + HAS_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY != '' }} + run: | + missing=() + [[ "${HAS_APP_ID}" == true ]] || missing+=("the Actions variable RELEASE_APP_ID") + [[ "${HAS_PRIVATE_KEY}" == true ]] || missing+=("the Actions secret RELEASE_APP_PRIVATE_KEY") + for item in "${missing[@]}"; do + echo "::error title=Release credentials missing::Add ${item} in Settings > Secrets and variables > Actions. See .github/RELEASING.md, One-time setup." + done + [[ ${#missing[@]} -eq 0 ]] + - name: Create delivery token id: delivery-token uses: actions/create-github-app-token@v2 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 1e760fc..0b8998b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -166,11 +166,27 @@ jobs: "${GITHUB_WORKSPACE}/.source-venv/bin/sqlclean" skills --target agents test -s .agents/skills/sqlclean/references/docs/CONTENTS.md + release-rehearsal: + name: Release rehearsal + needs: [changes] + if: needs.changes.outputs.full == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + version: "0.12.9" + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + - run: bash .github/scripts/simulate_release.sh + verify: # The single check to require: it passes when every suite the change selected passed. name: Verify if: always() - needs: [changes, light, skills, checks, minimum-rust, windows, native-package-smoke, source-build] + needs: + [changes, light, skills, checks, minimum-rust, windows, native-package-smoke, source-build, release-rehearsal] runs-on: ubuntu-latest steps: - env: @@ -181,6 +197,7 @@ jobs: SUITES: >- ${{ needs.skills.result }} ${{ needs.minimum-rust.result }} ${{ needs.windows.result }} ${{ needs.native-package-smoke.result }} ${{ needs.source-build.result }} + ${{ needs.release-rehearsal.result }} run: | if [[ "${CHANGES}" != success || "${LIGHT}" != success ]]; then echo "Change classification or the light checks did not pass" >&2 diff --git a/Makefile b/Makefile index a848e61..334cef9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ SHELL := /bin/bash -.PHONY: check check-ci check-docs-skill check-rust develop-native skills test test-e2e test-integration test-rust test-unit verify +.PHONY: check check-ci check-docs-skill check-rust develop-native release-rehearsal skills test test-e2e test-integration test-rust test-unit verify check: uv run ruff format . @@ -20,6 +20,9 @@ check-ci: check-docs-skill: uv run python -m scripts.check_docs_skill +release-rehearsal: + bash .github/scripts/simulate_release.sh + skills: uv run python -m scripts.generate_docs_skill diff --git a/pyproject.toml b/pyproject.toml index 60ae1a5..511c94e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,9 @@ dev = [ ] [tool.uv] +# Keep in step with the uv pinned in .github/workflows: other uv versions rewrite uv.lock +# differently, which the release metadata check rejects. +required-version = ">=0.12.9,<0.13" cache-keys = [ { file = "pyproject.toml" }, ]