From 68339379668c2e0d7bd01743ca14c405cce3d702 Mon Sep 17 00:00:00 2001 From: Gwinyai Date: Thu, 16 Jul 2026 12:40:58 +0200 Subject: [PATCH] fix: keep autocomplete artifact version stamp correct at tag time artifacts/autocomplete-types.ts is committed so consumers can fetch it at a tagged ref, but it was only ever corrected by a follow-up push to main. The v0.8.0 tag was cut at the version-bump commit and the regenerated artifact landed after it, so that tag permanently carries the 0.8.0 method set stamped 0.7.0. Tags are immutable, so the fixup can never reach it. Add --check to the generator (regenerate in memory, diff against the committed file, exit 1 with the diff if stale) and wire it in two places: - generate-autocomplete.yml gains a verify job on PRs, so a bump that forgets to regenerate fails review instead of being repaired after tagging. - python-publish.yml verifies before building, so a mislabeled artifact can never be published. It runs in release-build, which pypi-publish needs. Also drop the release:published trigger from the generate job. It regenerated from a detached tag checkout and pushed to the branch, which could never move the tag it was meant to fix. Bump to 0.8.1 to publish a correctly stamped tag; the method set is unchanged from 0.8.0 (89 methods), only the version stamp differs. Fixes #24 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/generate-autocomplete.yml | 49 ++++++++++++++++---- .github/workflows/python-publish.yml | 8 ++++ artifacts/autocomplete-types.ts | 4 +- pyproject.toml | 2 +- scripts/generate-autocomplete-artifact.py | 51 +++++++++++++++++++-- 5 files changed, 96 insertions(+), 18 deletions(-) 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())