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
17 changes: 11 additions & 6 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,17 @@ jobs:
exit 1
}

# Verify is the required status, so it only succeeds after every trusted check passes,
# including building the distributions that Publish reuses once the release is tagged.
# Verify's test suites skip release pull requests, whose product code already passed
# them on main; release performance runs on main after each merge and does not gate.
# Each dispatched run carries this attempt's request id in its run name and is matched
# on it exactly.
# Verify is the required status. On Release Please branches this commit status is its
# only source, because the Verify workflow reports its job under another name there, so
# the pull request merges only after every trusted check passes on this head, which
# already includes any lockfile refresh. The checks include building the distributions
# that Publish reuses once the release is tagged. Verify's test suites skip release pull
# requests, whose product code already passed them on main; release performance runs on
# main after each merge and does not gate. Each dispatched run carries this attempt's
# request id in its run name and is matched on it exactly.
if ! python3 -m scripts.check_native_layering; then
fail_verify "Release versions or Cargo.lock workspace versions disagree"
fi
workflows=(pull-request-metadata.yml version-guard.yml release-distributions.yml)
contexts=("Pull Request Metadata" "Block automatic 1.0 releases" "Release distributions")
for context in "${contexts[@]}"; do
Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ jobs:

# Release Please pull requests change only release metadata on top of product code that
# already passed every suite before it reached main, so the suites are skipped. Their
# required Verify status is posted by the trusted release checks in release-please.yml
# after the metadata, version and distribution checks pass.
# required Verify status is posted only by the trusted release checks in
# release-please.yml after the metadata, version and distribution checks pass; the
# verify job below reports under another name on Release Please branches.
if [[ "${EVENT_NAME}" == "pull_request" \
&& "${HEAD_REPOSITORY}" == "${GITHUB_REPOSITORY}" \
&& "${HEAD_REF}" == release-please--branches--* ]] && release_metadata_only; then
Expand Down Expand Up @@ -861,7 +862,17 @@ jobs:
sdist: false

verify:
name: Verify
# The required Verify of a Release Please pull request is the commit status that the trusted
# release checks in release-please.yml post on the branch's current head, after the lockfile
# refresh and every dispatched check. This job reports under another name there, so its quick
# metadata-only pass can never satisfy Verify first; a skipped job would count as passing, so
# the job always runs and only its name changes. Until those checks post Verify on the head,
# the pull request waits for an expected check, and every new push to the branch waits again.
name: >-
${{ (github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.event.pull_request.head.ref, 'release-please--branches--'))
&& 'Release pull request suites' || 'Verify' }}
if: always()
needs:
[
Expand Down
34 changes: 17 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SHELL := /bin/bash

.PHONY: verify verify-quick verify-pg coverage cli-preview rust-check check-native-layering check-python-boundary \
.PHONY: verify verify-quick verify-pg coverage cli-preview rust-check check-native-layering check-python-boundary check-release-gate \
check-e2e-shards test-e2e-duckdb test-e2e-duckdb-build-core \
test-e2e-duckdb-build-incremental \
test-e2e-duckdb-cli-data test-e2e-duckdb-cli test-e2e-duckdb-inspection \
Expand Down Expand Up @@ -623,7 +623,7 @@ verify-quick:
exit $$status


check-ci: check-e2e-shards check-native-layering check-python-boundary rust-lint
check-ci: check-e2e-shards check-native-layering check-python-boundary check-release-gate rust-lint
uv run ruff format --check .
uv run ruff check .
uv run ty check src tests
Expand All @@ -639,6 +639,10 @@ check-python-boundary:
uv run python -m scripts.check_python_boundary


check-release-gate:
uv run python -m scripts.check_release_gate


check-pr-metadata:
@test -n "$(PR_TITLE)" || { echo "PR_TITLE is required" >&2; exit 2; }
@test -n "$(PR_BODY_FILE)" || { echo "PR_BODY_FILE is required" >&2; exit 2; }
Expand Down
3 changes: 2 additions & 1 deletion crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ in `[workspace.metadata.native-layers]` in the workspace `Cargo.toml`, and
- a crate below `sqlbuild-analysis` reaches polyglot.

The same check fails when the workspace version, the `pyproject.toml` version and
`.release-please-manifest.json` disagree, because the version is part of the build identity.
`.release-please-manifest.json` disagree, or when a workspace crate's `Cargo.lock` entry carries a
version other than its manifest's, because the version is part of the build identity.

`fensu check` applies the Rust structure rules inside each crate: other crates use items through
`main/` entry points, `models.rs`, `types.rs` and `constants.rs`, not through `_helpers/`.
Expand Down
14 changes: 14 additions & 0 deletions scripts/check_release_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Direct wrapper for the release pull request Verify gate check."""

from __future__ import annotations

from scripts.release_gate.main.check import run_release_gate_check


def main(argv: list[str] | None = None) -> int:
"""Check the release pull request Verify gate."""
return run_release_gate_check(argv)


if __name__ == "__main__":
raise SystemExit(main())
72 changes: 65 additions & 7 deletions scripts/native_layering/_helpers/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,28 @@
from typing import Any

from scripts.native_layering.constants import (
CARGO_LOCKFILE,
CARGO_MANIFEST,
CLI_DISTRIBUTION_PIN_PREFIX,
LOCK_SOURCE_KEY,
PYPROJECT_LOCK_SOURCE,
PYPROJECT_LOCKFILE,
PYPROJECT_MANIFEST,
RELEASE_MANIFEST,
RELEASE_MANIFEST_PACKAGE,
)


def get_native_version_errors(root: Path) -> tuple[str, ...]:
"""Return an error when the Cargo workspace, Python package and release versions differ."""
"""Return errors when the Cargo, Python, lockfile and release versions disagree."""
with (root / CARGO_MANIFEST).open("rb") as handle:
cargo: dict[str, Any] = tomllib.load(handle)
with (root / PYPROJECT_MANIFEST).open("rb") as handle:
pyproject: dict[str, Any] = tomllib.load(handle)
release: dict[str, Any] = json.loads((root / RELEASE_MANIFEST).read_text(encoding="utf-8"))
workspace_version: Any = cargo.get("workspace", {}).get("package", {}).get("version")
versions: dict[str, Any] = {
f"{CARGO_MANIFEST} [workspace.package] version": cargo.get("workspace", {})
.get("package", {})
.get("version"),
f"{CARGO_MANIFEST} [workspace.package] version": workspace_version,
f"{PYPROJECT_MANIFEST} [project] version": pyproject.get("project", {}).get("version"),
f"{RELEASE_MANIFEST} version": release.get(RELEASE_MANIFEST_PACKAGE),
f"{PYPROJECT_MANIFEST} sqlbuild-cli dependency": next(
Expand All @@ -38,7 +41,62 @@ def get_native_version_errors(root: Path) -> tuple[str, ...]:
None,
),
}
if len(set(versions.values())) == 1:
errors: list[str] = []
if len(set(versions.values())) != 1:
listed: str = ", ".join(f"{source} is {version}" for source, version in versions.items())
errors.append(f"Release versions differ: {listed}.")
errors.extend(
_cargo_lockfile_errors(root=root, cargo=cargo, workspace_version=workspace_version)
)
errors.extend(_python_lockfile_errors(root=root, pyproject=pyproject))
return tuple(errors)


def _python_lockfile_errors(*, root: Path, pyproject: dict[str, Any]) -> tuple[str, ...]:
"""Report a uv.lock entry for the project that carries a version other than pyproject's."""
project: dict[str, Any] = pyproject.get("project", {})
with (root / PYPROJECT_LOCKFILE).open("rb") as handle:
lock: dict[str, Any] = tomllib.load(handle)
locked: list[Any] = [
entry.get("version")
for entry in lock.get("package", ())
if entry.get("name") == project.get("name")
and entry.get(LOCK_SOURCE_KEY) == PYPROJECT_LOCK_SOURCE
]
if locked == [project.get("version")]:
return ()
listed: str = ", ".join(map(str, locked)) or "no version"
return (
f"{PYPROJECT_LOCKFILE} locks {project.get('name')} at {listed}, "
f"not {PYPROJECT_MANIFEST} {project.get('version')}. Run `uv lock`.",
)


def _cargo_lockfile_errors(
*, root: Path, cargo: dict[str, Any], workspace_version: Any
) -> tuple[str, ...]:
"""Report workspace crates whose Cargo.lock entry differs from their manifest version."""
expected: dict[str, Any] = {}
for pattern in cargo.get("workspace", {}).get("members", ()):
for directory in sorted(root.glob(pattern)):
with (directory / CARGO_MANIFEST).open("rb") as handle:
package: dict[str, Any] = tomllib.load(handle).get("package", {})
version: Any = package.get("version")
if isinstance(version, dict) and version.get("workspace") is True:
version = workspace_version
expected[package.get("name")] = version
with (root / CARGO_LOCKFILE).open("rb") as handle:
lock: dict[str, Any] = tomllib.load(handle)
stale: list[str] = sorted(
f"{entry['name']} is {entry.get('version')}, not {expected[entry['name']]}"
for entry in lock.get("package", ())
if LOCK_SOURCE_KEY not in entry
and entry.get("name") in expected
and entry.get("version") != expected[entry["name"]]
)
if not stale:
return ()
listed: str = ", ".join(f"{source} is {version}" for source, version in versions.items())
return (f"Release versions differ: {listed}.",)
return (
f"{CARGO_LOCKFILE} workspace crate versions are stale: {'; '.join(stale)}. "
"Run `cargo update --workspace`.",
)
4 changes: 4 additions & 0 deletions scripts/native_layering/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
POLYGLOT_PACKAGE: str = "polyglot-sql"
DEPENDENCY_TABLES: tuple[str, ...] = ("dependencies", "build-dependencies", "dev-dependencies")
CARGO_MANIFEST: str = "Cargo.toml"
CARGO_LOCKFILE: str = "Cargo.lock"
PYPROJECT_MANIFEST: str = "pyproject.toml"
PYPROJECT_LOCKFILE: str = "uv.lock"
LOCK_SOURCE_KEY: str = "source"
PYPROJECT_LOCK_SOURCE: dict[str, str] = {"editable": "."}
RELEASE_MANIFEST: str = ".release-please-manifest.json"
RELEASE_MANIFEST_PACKAGE: str = "."
CLI_DISTRIBUTION_PIN_PREFIX: str = "sqlbuild-cli=="
1 change: 1 addition & 0 deletions scripts/release_gate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Release pull request Verify gate checks."""
1 change: 1 addition & 0 deletions scripts/release_gate/_helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Release gate helpers."""
Loading
Loading