diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73b97574b2..b56ac19763 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,13 +17,13 @@ The ty repository only includes code relevant to distributing the ty project. Clone the repository: -```bash +```shell git clone https://github.com/astral-sh/ty.git ``` Then, ensure the submodule is initialized: -```bash +```shell git submodule update --init --recursive ``` @@ -44,7 +44,7 @@ pre-commit install The Python package can be built with any Python build frontend (Maturin is used as a backend), e.g.: -```bash +```shell uv build ``` @@ -52,19 +52,19 @@ uv build To update the Ruff submodule to the latest commit: -```bash +```shell git -C ruff pull origin main ``` Or, to update the Ruff submodule to a specific commit: -```bash +```shell git -C ruff checkout ``` To commit the changes: -```bash +```shell commit=$(git -C ruff rev-parse --short HEAD) git switch -c "sync/ruff-${commit}" git add ruff @@ -73,14 +73,14 @@ git commit -m "Update ruff submodule to https://github.com/astral-sh/ruff/commit To restore the Ruff submodule to a clean-state, reset, then update the submodule: -```bash +```shell git -C ruff reset --hard git submodule update ``` To restore the Ruff submodule to the commit from `main`: -```bash +```shell git -C ruff reset --hard $(git ls-tree main -- ruff | awk '{print $3}') git add ruff ``` @@ -89,7 +89,9 @@ git add ruff Releases can only be performed by Astral team members. -Preparation for the release is automated. First, run: +Preparation for the release is automated. + +1. Run `./scripts/release.sh` ```shell ./scripts/release.sh @@ -101,17 +103,20 @@ The release script will: - Generate changelog entries based on pull requests here, and in Ruff - Bump the versions in the `pyproject.toml` and `dist-workspace.toml` -After running the script, editorialize the `CHANGELOG.md` file to ensure entries are consistently -styled. - -Then, open a pull request, e.g., `Bump version to ...`. - -Binary builds will automatically be tested for the release. - -After merging the pull request, run the -[release workflow](https://github.com/astral-sh/ty/actions/workflows/release.yml) with the version -tag. **Do not include a leading `v`**. The release will automatically be created on GitHub after -everything else publishes. +1. Editorialize the `CHANGELOG.md` file to ensure entries are consistently styled. +1. Create a pull request with the changelog and version changes, e.g., `Bump version to ...`. + Binary builds will automatically be tested for the release. +1. Merge the PR +1. Run the [release workflow](https://github.com/astral-sh/ty/actions/workflows/release.yml) with the version + tag. **Do not include a leading `v`**. The release will automatically be created on GitHub after + everything else publishes. +1. Run `uv run --no-project ./scripts/update_schemastore.py` to prepare a PR to update the `ty.json` schema in the schemastore repository. + Follow the link in the script's output to submit the PR. The script is a no-op if there are no schema changes. +1. If necessary, update and release [`ty-vscode`](https://github.com/astral-sh/ty-vscode). + Follow the instructions in the `ty-vscode` repository. Updating the extension is required when: + - for minor releases to bump the bundled ty version + - for patch releases after fixing an important bug in `ty lsp` to bump the bundled ty version + - when releasing new `ty lsp` features that require changes in `ty-vscode` When running the release workflow for pre-release versions, use the Cargo version format (not PEP 440), e.g. `0.0.0-alpha.5` (not `0.0.0a5`). For stable releases, these formats are identical. diff --git a/scripts/update_schemastore.py b/scripts/update_schemastore.py new file mode 100755 index 0000000000..d4b0cc6190 --- /dev/null +++ b/scripts/update_schemastore.py @@ -0,0 +1,185 @@ +"""Update ty.json in schemastore. + +This script will clone astral-sh/schemastore, update the schema and push the changes +to a new branch tagged with the ty git hash. You should see a URL to create the PR +to schemastore in the CLI. +""" + +from __future__ import annotations + +import enum +import json +from pathlib import Path +from subprocess import check_call, check_output +from tempfile import TemporaryDirectory +from typing import NamedTuple, assert_never + +TY_REPO = "https://github.com/astral-sh/ty" +TY_JSON = Path("schemas/json/ty.json") + + +class SchemastoreRepos(NamedTuple): + fork: str + upstream: str + + +class GitProtocol(enum.Enum): + SSH = "ssh" + HTTPS = "https" + + def schemastore_repos(self) -> SchemastoreRepos: + match self: + case GitProtocol.SSH: + return SchemastoreRepos( + fork="git@github.com:astral-sh/schemastore.git", + upstream="git@github.com:SchemaStore/schemastore.git", + ) + case GitProtocol.HTTPS: + return SchemastoreRepos( + fork="https://github.com/astral-sh/schemastore.git", + upstream="https://github.com/SchemaStore/schemastore.git", + ) + case _: + assert_never(self) + + +def update_schemastore( + schemastore_path: Path, schemastore_repos: SchemastoreRepos, root: Path +) -> None: + if not schemastore_path.is_dir(): + check_call( + ["git", "clone", schemastore_repos.fork, schemastore_path, "--depth=1"] + ) + check_call( + [ + "git", + "remote", + "add", + "upstream", + schemastore_repos.upstream, + ], + cwd=schemastore_path, + ) + # Create a new branch tagged with the current ty commit up to date with the latest + # upstream schemastore + check_call(["git", "fetch", "upstream"], cwd=schemastore_path) + current_sha = check_output(["git", "rev-parse", "HEAD"], text=True).strip() + branch = f"update-ty-{current_sha}" + check_call( + ["git", "switch", "-c", branch], + cwd=schemastore_path, + ) + check_call( + ["git", "reset", "--hard", "upstream/master"], + cwd=schemastore_path, + ) + + # Run npm install + src = schemastore_path.joinpath("src") + check_call(["npm", "install"], cwd=schemastore_path) + + # Update the schema and format appropriately + schema = json.loads(root.joinpath("ruff/ty.schema.json").read_text()) + schema["$id"] = "https://json.schemastore.org/ty.json" + src.joinpath(TY_JSON).write_text( + json.dumps(dict(schema.items()), indent=2, ensure_ascii=False), + ) + check_call( + [ + "../node_modules/prettier/bin/prettier.cjs", + "--plugin", + "prettier-plugin-sort-json", + "--write", + TY_JSON, + ], + cwd=src, + ) + + # Check if the schema has changed + # https://stackoverflow.com/a/9393642/3549270 + if check_output(["git", "status", "-s"], cwd=schemastore_path).strip(): + # Schema has changed, commit and push + commit_url = f"{TY_REPO}/commit/{current_sha}" + commit_body = f"This updates ty's JSON schema to [{current_sha}]({commit_url})" + # https://stackoverflow.com/a/22909204/3549270 + check_call( + [ + "git", + "commit", + "-a", + "-m", + "Update ty's JSON schema", + "-m", + commit_body, + ], + cwd=schemastore_path, + ) + # This should show the link to create a PR + check_call( + ["git", "push", "--set-upstream", "origin", branch, "--force"], + cwd=schemastore_path, + ) + else: + print("No changes") + + +def determine_git_protocol(argv: list[str] | None = None) -> GitProtocol: + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--proto", + choices=[proto.value for proto in GitProtocol], + default="https", + help="Protocol to use for git authentication", + ) + args = parser.parse_args(argv) + return GitProtocol(args.proto) + + +def main() -> None: + root = Path( + check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip(), + ) + + expected_ruff_revision = check_output( + ["git", "ls-tree", "main", "--format", "%(objectname)", "ruff"] + ).strip() + actual_ruff_revision = check_output( + ["git", "-C", "ruff", "rev-parse", "HEAD"] + ).strip() + + if expected_ruff_revision != actual_ruff_revision: + print( + f"The ruff submodule is at {expected_ruff_revision} but main expects {actual_ruff_revision}" + ) + match input( + "How do you want to proceed (u=reset submodule, n=abort, y=continue)? " + ): + case "u": + check_call( + ["git", "-C", "ruff", "reset", "--hard", expected_ruff_revision] + ) + case "n": + return + case "y": + ... + case command: + print(f"Invalid input '{command}', abort") + return + + schemastore_repos = determine_git_protocol().schemastore_repos() + schemastore_existing = root.joinpath("schemastore") + if schemastore_existing.is_dir(): + update_schemastore(schemastore_existing, schemastore_repos, root) + else: + with TemporaryDirectory() as temp_dir: + update_schemastore( + Path(temp_dir).joinpath("schemastore"), schemastore_repos, root + ) + + +if __name__ == "__main__": + main()