diff --git a/.github/workflows/generate-autocomplete.yml b/.github/workflows/generate-autocomplete.yml index 6db6fa2..53273e2 100644 --- a/.github/workflows/generate-autocomplete.yml +++ b/.github/workflows/generate-autocomplete.yml @@ -1,44 +1,74 @@ name: Generate Autocomplete Artifact +# artifacts/autocomplete-types.ts is generated from the SDK and stamped with the +# version in pyproject.toml. It is committed to the repo so consumers can fetch it +# at a tagged ref, which means it must be correct *at the commit a tag points at* - +# a fixup pushed afterwards can never reach an already-cut tag. +# +# So `verify` blocks any PR whose committed artifact is stale (including a version +# bump that forgot to regenerate). `generate` stays on as a backstop for direct +# pushes to main; once verify is enforced it should find nothing to do. + on: push: branches: [main] - release: - types: [published] + pull_request: + branches: [main, dev] workflow_dispatch: jobs: + # Fails the PR if the committed artifact does not match the current SDK, forcing + # the regenerated file into the same commit rather than a follow-up push. + verify: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Verify committed artifact is up to date + run: | + python scripts/generate-autocomplete-artifact.py \ + --output-path ./artifacts/autocomplete-types.ts --check + generate: + if: github.event_name != 'pull_request' runs-on: ubuntu-latest - + permissions: contents: write - + steps: - name: Checkout repository uses: actions/checkout@v4 with: token: ${{ secrets.GH_TOKEN }} fetch-depth: 0 - + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - + - name: Upgrade pip run: python -m pip install --upgrade pip - + - name: Generate autocomplete artifacts run: | python scripts/generate-autocomplete-artifact.py --output-path ./artifacts/autocomplete-types.ts - + - name: Check for changes id: changes run: | git diff --exit-code artifacts/ || echo "changed=true" >> $GITHUB_OUTPUT continue-on-error: true - + - name: Commit artifact to repo if: steps.changes.outputs.changed == 'true' run: | @@ -47,4 +77,3 @@ jobs: git add artifacts/autocomplete-types.ts git commit -m "🤖 Auto-generate autocomplete types [skip ci]" || exit 0 git push - diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 12c82d9..47400e4 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -45,6 +45,14 @@ jobs: with: python-version: "3.14" + # The tagged commit is what consumers pin to, and tags are immutable - a + # stale artifact here ships a version stamp that disagrees with the release + # and can never be corrected in place. Fail before anything is published. + - name: Verify committed autocomplete artifact matches this release + run: | + python scripts/generate-autocomplete-artifact.py \ + --output-path ./artifacts/autocomplete-types.ts --check + - name: Build release distributions run: | uv build diff --git a/artifacts/autocomplete-types.ts b/artifacts/autocomplete-types.ts index cff45ef..cf72e8d 100644 --- a/artifacts/autocomplete-types.ts +++ b/artifacts/autocomplete-types.ts @@ -1,6 +1,6 @@ // This file is auto-generated by the datamaker-py SDK // DO NOT EDIT MANUALLY -// Generated from version 0.8.0 +// Generated from version 0.8.1 // Monaco Editor CompletionItemKind enum values // Reference: https://microsoft.github.io/monaco-editor/api/enums/monaco.languages.CompletionItemKind.html @@ -49,7 +49,7 @@ export interface DataMakerFieldType { documentation?: string; } -export const SDK_VERSION = "0.8.0"; +export const SDK_VERSION = "0.8.1"; export const METHOD_SUGGESTIONS: DataMakerMethod[] = [ { diff --git a/pyproject.toml b/pyproject.toml index ecb1d14..76b692b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "datamaker-py" -version = "0.8.0" +version = "0.8.1" description = "The official Python library for the Automators DataMaker API." readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/generate-autocomplete-artifact.py b/scripts/generate-autocomplete-artifact.py index 5522331..61d0c6b 100644 --- a/scripts/generate-autocomplete-artifact.py +++ b/scripts/generate-autocomplete-artifact.py @@ -8,6 +8,7 @@ import ast import sys +from difflib import unified_diff from pathlib import Path from typing import Dict, List, Any import importlib.util @@ -307,9 +308,9 @@ def _escape_quotes(self, text: str) -> str: return text.replace('"', '\\"').replace('\n', ' ').strip() -def get_version() -> str: +def get_version(repo_root: Path = None) -> str: """Get the SDK version from pyproject.toml.""" - pyproject = Path("pyproject.toml") + pyproject = (repo_root / "pyproject.toml") if repo_root else Path("pyproject.toml") if pyproject.exists(): with open(pyproject, "r", encoding="utf-8") as f: for line in f: @@ -330,15 +331,27 @@ def main(): default="./artifacts/autocomplete-types.ts", help="Output path for the generated TypeScript file", ) + parser.add_argument( + "--check", + action="store_true", + help=( + "Verify the committed artifact matches what this script generates, " + "without writing. Exits 1 if it is stale." + ), + ) args = parser.parse_args() # Find the repository root repo_root = Path(__file__).parent.parent # Get version - version = get_version() + version = get_version(repo_root) print(f"📦 SDK Version: {version}") + if version == "unknown": + print("❌ Could not read version from pyproject.toml") + return 1 + # Analyze SDK analyzer = SDKAnalyzer(repo_root) analyzer.analyze() @@ -348,8 +361,35 @@ def main(): generator = TypeScriptGenerator(analyzer, version) ts_code = generator.generate() - # Write output output_path = Path(args.output_path) + + # Verify-only: the committed artifact must already match what we just built. + # Guards the release path, where a stale artifact would be tagged with a + # version stamp that disagrees with pyproject.toml. + if args.check: + if not output_path.exists(): + print(f"❌ Missing artifact: {output_path}") + return 1 + committed = output_path.read_text(encoding="utf-8") + if committed != ts_code: + print(f"❌ Stale artifact: {output_path}") + print(" It does not match the current SDK. Regenerate and commit:") + print( + " python scripts/generate-autocomplete-artifact.py " + f"--output-path {output_path}" + ) + diff = unified_diff( + committed.splitlines(keepends=True), + ts_code.splitlines(keepends=True), + fromfile=f"{output_path} (committed)", + tofile=f"{output_path} (regenerated)", + ) + sys.stdout.writelines(diff) + return 1 + print(f"✅ Artifact is up to date: {output_path}") + return 0 + + # Write output output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: @@ -358,8 +398,9 @@ def main(): print(f"✅ Generated: {output_path}") print(f" Methods: {len(analyzer.methods)}") print(f" Field types: {len(analyzer.field_types)}") + return 0 if __name__ == "__main__": - main() + sys.exit(main())