From 0539a0f55233e7b662f036525b86d462eee8bbb9 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 27 May 2026 16:17:00 +0530 Subject: [PATCH 1/2] UN-3479 [FIX] release workflow: validate before tag/publish + lint cleanup Fixes the issues exposed when the v1.3/v1.4 dispatches blew up: - 10 E501 line-too-long violations in src/unstract/clone/ (caused the lint failure in release run 26506031574) - Release workflow did commit-bump + push-to-main + tag + GH release BEFORE running lint/tests/build, so a lint failure left main with a phantom version bump and an orphan release. Lint/tests/build now run against the bumped __init__.py in-place, and only on success does the workflow commit, tag, release, and PyPI-publish. - Add ruff to the PR gate (test.yml) so lint regressions block at PR time instead of release time. - Revert __version__ from 1.4.0 to 1.2.1 so the next manual trigger produces the intended v1.3.0 cleanly. Orphan v1.4.0 tag + GitHub release have been deleted out-of-band. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/main.yml | 91 ++++++++------------- .github/workflows/test.yml | 3 + src/unstract/api_deployments/__init__.py | 2 +- src/unstract/clone/exceptions.py | 2 +- src/unstract/clone/phases/adapter.py | 2 +- src/unstract/clone/phases/api_deployment.py | 4 +- src/unstract/clone/phases/connector.py | 2 +- src/unstract/clone/phases/custom_tool.py | 2 +- src/unstract/clone/phases/files.py | 2 +- src/unstract/clone/phases/pipeline.py | 2 +- src/unstract/clone/phases/tool_instance.py | 2 +- src/unstract/clone/report.py | 2 +- 12 files changed, 49 insertions(+), 67 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59f0af6..db7389e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,53 +67,67 @@ jobs: - name: Install dependencies run: uv sync --dev - # Handle workflow_dispatch (manual trigger) - - name: Bump version and create release + # Compute and stage the new version locally — defer commit/tag/release + # until lint+tests+build pass so a verification failure leaves main untouched. + - name: Compute new version if: github.event_name == 'workflow_dispatch' - id: create_release + id: version run: | - # Get current version from __init__.py using grep/sed (avoid importing) CURRENT_VERSION=$(grep -E "^__version__ = " src/unstract/api_deployments/__init__.py | sed -E 's/__version__ = "(.*)"/\1/') echo "Current version: $CURRENT_VERSION" - # Calculate new version based on input IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" MAJOR=${VERSION_PARTS[0]} MINOR=${VERSION_PARTS[1]} PATCH=${VERSION_PARTS[2]} case "${{ github.event.inputs.version_bump }}" in - "major") - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - ;; - "minor") - MINOR=$((MINOR + 1)) - PATCH=0 - ;; - "patch") - PATCH=$((PATCH + 1)) - ;; + "major") MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; + "minor") MINOR=$((MINOR + 1)); PATCH=0 ;; + "patch") PATCH=$((PATCH + 1)) ;; esac NEW_VERSION="$MAJOR.$MINOR.$PATCH" echo "New version: $NEW_VERSION" echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT - # Update version in __init__.py sed -i "s/__version__ = \"$CURRENT_VERSION\"/__version__ = \"$NEW_VERSION\"/" src/unstract/api_deployments/__init__.py - # Commit version changes + - name: Verify version update + run: | + PACKAGE_VERSION=$(grep -E "^__version__ = " src/unstract/api_deployments/__init__.py | sed -E 's/__version__ = "(.*)"/\1/') + echo "Package version: $PACKAGE_VERSION" + echo "Target version: ${{ steps.version.outputs.version }}" + if [ "$PACKAGE_VERSION" != "${{ steps.version.outputs.version }}" ]; then + echo "Version mismatch! Exiting..." + exit 1 + fi + + - name: Create test env + run: cp tests/sample.env tests/.env + + - name: Run linting + run: uv run ruff check src/ + + - name: Run tests + run: uv run pytest tests/ + + - name: Build package + run: uv build + + # Only reached if lint/tests/build all passed — safe to commit, tag, release. + - name: Commit version bump and create release + if: github.event_name == 'workflow_dispatch' + run: | + NEW_VERSION="${{ steps.version.outputs.version }}" + git add src/unstract/api_deployments/__init__.py git commit -m "chore: bump version to $NEW_VERSION [skip ci]" git push origin main - # Create git tag git tag "v$NEW_VERSION" git push origin "v$NEW_VERSION" - # Create GitHub release RELEASE_NOTES="${{ github.event.inputs.release_notes }}" if [ -z "$RELEASE_NOTES" ]; then gh release create "v$NEW_VERSION" \ @@ -132,44 +146,9 @@ jobs: env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - # Set version for subsequent steps - - name: Set version output - id: version - run: | - echo "version=${{ steps.create_release.outputs.version }}" >> $GITHUB_OUTPUT - - # Verify the version was updated correctly - - name: Verify version update - run: | - PACKAGE_VERSION=$(grep -E "^__version__ = " src/unstract/api_deployments/__init__.py | sed -E 's/__version__ = "(.*)"/\1/') - echo "Package version: $PACKAGE_VERSION" - echo "Target version: ${{ steps.version.outputs.version }}" - if [ "$PACKAGE_VERSION" != "${{ steps.version.outputs.version }}" ]; then - echo "Version mismatch! Exiting..." - exit 1 - fi - - # Create test environment - - name: Create test env - run: cp tests/sample.env tests/.env - - # Run linting - - name: Run linting - run: uv run ruff check src/ - - # Run tests - - name: Run tests - run: uv run pytest tests/ - - # Build the package - - name: Build package - run: uv build - - # Publish to PyPI using Trusted Publishers - name: Publish to PyPI run: uv publish - # Output success message - name: Success message run: | echo "Successfully published version ${{ steps.version.outputs.version }} to PyPI using uv publish with Trusted Publishers" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97dd996..304fbda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,5 +32,8 @@ jobs: - name: Create test env run: cp tests/sample.env tests/.env + - name: Lint (ruff) + run: uv run ruff check src/ + - name: Tests (pytest) run: uv run pytest tests/ -v diff --git a/src/unstract/api_deployments/__init__.py b/src/unstract/api_deployments/__init__.py index 094e8a8..bbe774f 100644 --- a/src/unstract/api_deployments/__init__.py +++ b/src/unstract/api_deployments/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.4.0" +__version__ = "1.2.1" from .client import APIDeploymentsClient as APIDeploymentsClient diff --git a/src/unstract/clone/exceptions.py b/src/unstract/clone/exceptions.py index 3933c1c..47feb68 100644 --- a/src/unstract/clone/exceptions.py +++ b/src/unstract/clone/exceptions.py @@ -18,7 +18,7 @@ def __init__( class NameConflictError(CloneError): - """Raised when ``on_name_conflict='abort'`` and the target has a like-named entity.""" + """Raised on name collision when ``on_name_conflict='abort'``.""" class DependencyMissingError(CloneError): diff --git a/src/unstract/clone/phases/adapter.py b/src/unstract/clone/phases/adapter.py index 522629f..c98bf63 100644 --- a/src/unstract/clone/phases/adapter.py +++ b/src/unstract/clone/phases/adapter.py @@ -83,7 +83,7 @@ def _clone_one( tgt = existing[0] if self.ctx.options.on_name_conflict == "abort": raise NameConflictError( - f"adapter '{name}' [{atype}] already exists in target as {tgt['id']}" + f"adapter '{name}' [{atype}] already on target as {tgt['id']}" ) with lock: result.adopted += 1 diff --git a/src/unstract/clone/phases/api_deployment.py b/src/unstract/clone/phases/api_deployment.py index df55983..0d2575f 100644 --- a/src/unstract/clone/phases/api_deployment.py +++ b/src/unstract/clone/phases/api_deployment.py @@ -76,7 +76,7 @@ def _clone_one( tgt_wf_id = self.ctx.remap.resolve("workflow", src_wf_id) if not tgt_wf_id: logger.warning( - "no workflow remap for api_deployment '%s' (src workflow %s) — skipping", + "no workflow remap for api_deployment '%s' (src wf %s) — skipping", api_name, src_wf_id, ) @@ -99,7 +99,7 @@ def _clone_one( tgt = existing[0] if self.ctx.options.on_name_conflict == "abort": raise NameConflictError( - f"api_deployment '{api_name}' already exists in target as {tgt['id']}" + f"api_deployment '{api_name}' already on target as {tgt['id']}" ) with lock: result.adopted += 1 diff --git a/src/unstract/clone/phases/connector.py b/src/unstract/clone/phases/connector.py index 88215da..2a30b93 100644 --- a/src/unstract/clone/phases/connector.py +++ b/src/unstract/clone/phases/connector.py @@ -86,7 +86,7 @@ def _clone_one( metadata = src.get("connector_metadata") or {} if not metadata: logger.info( - "skipping connector '%s' (src=%s, catalog=%s) — source returned no metadata", + "skipping connector '%s' (src=%s, catalog=%s) — no source metadata", name, src_id, src.get("connector_id"), diff --git a/src/unstract/clone/phases/custom_tool.py b/src/unstract/clone/phases/custom_tool.py index c43d64f..03eec4f 100644 --- a/src/unstract/clone/phases/custom_tool.py +++ b/src/unstract/clone/phases/custom_tool.py @@ -270,7 +270,7 @@ def _create_fresh( with lock: result.failed += 1 result.errors.append( - f"import {tool_name}: missing target adapter remap for default profile" + f"import {tool_name}: missing target adapter remap for default" ) return None diff --git a/src/unstract/clone/phases/files.py b/src/unstract/clone/phases/files.py index 22de854..a241fc1 100644 --- a/src/unstract/clone/phases/files.py +++ b/src/unstract/clone/phases/files.py @@ -384,7 +384,7 @@ def _ensure_default_doc( tgt_docs = self.ctx.target.list_prompt_documents(tgt_tool_id) except Exception as e: logger.warning( - "files: skipping default-doc set for tool=%s — list tgt docs failed: %s", + "files: skipping default-doc set for tool=%s — tgt list failed: %s", tool_name, e, ) diff --git a/src/unstract/clone/phases/pipeline.py b/src/unstract/clone/phases/pipeline.py index 9892b1c..cdad5f0 100644 --- a/src/unstract/clone/phases/pipeline.py +++ b/src/unstract/clone/phases/pipeline.py @@ -55,7 +55,7 @@ def run(self, report: CloneReport) -> PhaseResult: skipped_types = len(src_pipelines) - len(migratable) if skipped_types: logger.info( - "Found %d source pipeline(s); skipping %d of unsupported type (DEFAULT/APP)", + "Found %d source pipeline(s); skipping %d unsupported (DEFAULT/APP)", len(src_pipelines), skipped_types, ) diff --git a/src/unstract/clone/phases/tool_instance.py b/src/unstract/clone/phases/tool_instance.py index 293d206..4b80383 100644 --- a/src/unstract/clone/phases/tool_instance.py +++ b/src/unstract/clone/phases/tool_instance.py @@ -103,7 +103,7 @@ def _clone_workflow_tools( return if len(src_instances) > 1: logger.warning( - "source workflow %s has %d tool_instances (expected ≤1) — migrating first only", + "source workflow %s has %d tool_instances (>1) — migrating first only", src_wf_id, len(src_instances), ) diff --git a/src/unstract/clone/report.py b/src/unstract/clone/report.py index 4bec96a..a0f63d1 100644 --- a/src/unstract/clone/report.py +++ b/src/unstract/clone/report.py @@ -27,7 +27,7 @@ class PhaseResult: @dataclass class Endpoint: - """Just enough about an endpoint for the report header — never carries the API key.""" + """Endpoint identity for the report header — never carries the API key.""" base_url: str organization_id: str From 73bb811cffdcc5399e8ef0c1ae92aa5a388eaf34 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 27 May 2026 16:21:49 +0530 Subject: [PATCH 2/2] fix(ci): publish to PyPI before tag/release + drop redundant dispatch guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address greptile P1 + P2: - P1: uv publish now runs before commit/tag/release. PyPI is the only irreversible step, so if a later git/release call fails the artifact is the source of truth and the metadata can be retried. - P2: workflow only triggers on workflow_dispatch (see top-level `on:`), so the per-step `if: github.event_name == 'workflow_dispatch'` guards were redundant and inconsistent (only on bookends, not lint/test/build). Removed them — any future trigger addition should be a deliberate per-step decision. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/main.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index db7389e..e11adf8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,9 +68,8 @@ jobs: run: uv sync --dev # Compute and stage the new version locally — defer commit/tag/release - # until lint+tests+build pass so a verification failure leaves main untouched. + # until lint+tests+build+publish all pass, so any failure leaves main untouched. - name: Compute new version - if: github.event_name == 'workflow_dispatch' id: version run: | CURRENT_VERSION=$(grep -E "^__version__ = " src/unstract/api_deployments/__init__.py | sed -E 's/__version__ = "(.*)"/\1/') @@ -115,9 +114,13 @@ jobs: - name: Build package run: uv build - # Only reached if lint/tests/build all passed — safe to commit, tag, release. + # PyPI publish first because it is the only step that cannot be undone — + # if a later commit/tag/release step fails, the PyPI artifact is the + # source of truth and the git metadata can be retried manually. + - name: Publish to PyPI + run: uv publish + - name: Commit version bump and create release - if: github.event_name == 'workflow_dispatch' run: | NEW_VERSION="${{ steps.version.outputs.version }}" @@ -146,9 +149,6 @@ jobs: env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - - name: Publish to PyPI - run: uv publish - - name: Success message run: | echo "Successfully published version ${{ steps.version.outputs.version }} to PyPI using uv publish with Trusted Publishers"