Skip to content
Open
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
9 changes: 9 additions & 0 deletions .github/workflows/contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@ jobs:

- name: Validate contract surface
run: ./scripts/check-contract-surface.sh

- name: Validate protected release workflow
run: |
for workflow in .github/workflows/*.yml; do
./scripts/check-publish-workflow.sh "$workflow" allow-zero
done
./scripts/check-publish-workflow.sh
./scripts/check-publish-workflow.sh .github/workflows/publish-python.yml
./scripts/test-check-publish-workflow.sh
135 changes: 103 additions & 32 deletions .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,43 @@ jobs:
# interpolated into the shell script body.
CUSTOM_VERSION: ${{ github.event.inputs.custom_version }}
VERSION_TYPE: ${{ github.event.inputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
PYPROJECT="pyproject.toml"

CURRENT_VERSION=$(awk -F '"' '/^version = / { print $2; exit }' "$PYPROJECT")
if [ -z "$CURRENT_VERSION" ]; then
echo "Unable to read current version from $PYPROJECT"
MANIFEST_VERSION=$(awk -F '"' '/^version = / { print $2; exit }' "$PYPROJECT")
LATEST_TAG=""
while IFS= read -r CANDIDATE_TAG; do
RELEASE_ERROR=$(mktemp)
if gh api \
"repos/${GITHUB_REPOSITORY}/releases/tags/${CANDIDATE_TAG}" \
--silent 2>"$RELEASE_ERROR"; then
LATEST_TAG="$CANDIDATE_TAG"
rm -f "$RELEASE_ERROR"
break
fi
if grep -q 'HTTP 404' "$RELEASE_ERROR"; then
rm -f "$RELEASE_ERROR"
continue
fi
cat "$RELEASE_ERROR" >&2
rm -f "$RELEASE_ERROR"
echo "ERROR: could not verify completed release ${CANDIDATE_TAG}" >&2
exit 1
done < <(git -c versionsort.suffix=- for-each-ref \
--sort=-version:refname \
--format='%(refname:short)' \
'refs/tags/sdk-python-v[0-9]*')

if [ -z "$LATEST_TAG" ]; then
echo "ERROR: no completed Python SDK GitHub release exists" >&2
exit 1
fi
echo "Current version: $CURRENT_VERSION"

CURRENT_VERSION="${LATEST_TAG#sdk-python-v}"
echo "Manifest version: $MANIFEST_VERSION"
echo "Release baseline: $CURRENT_VERSION ($LATEST_TAG)"

bump_semver() {
local current="$1"
Expand Down Expand Up @@ -136,6 +164,18 @@ jobs:
echo "Bumped version: $VERSION_TYPE -> $NEW_VERSION"
fi

if git show-ref --verify --quiet "refs/tags/sdk-python-v${NEW_VERSION}"; then
TAG_COMMIT=$(git rev-list -n 1 "refs/tags/sdk-python-v${NEW_VERSION}")
if [ "$TAG_COMMIT" != "$GITHUB_SHA" ]; then
echo "ERROR: sdk-python-v${NEW_VERSION} exists at ${TAG_COMMIT}, not ${GITHUB_SHA}" >&2
exit 1
fi
echo "is_recovery=true" >> "$GITHUB_OUTPUT"
echo "Recovering Python SDK release reserved by this source commit"
else
echo "is_recovery=false" >> "$GITHUB_OUTPUT"
fi

# Update pyproject.toml [project] version (first match only).
awk -v v="$NEW_VERSION" '
!done && /^version = / { print "version = \"" v "\""; done=1; next }
Expand All @@ -160,33 +200,62 @@ jobs:
- name: Check distribution metadata
run: uvx twine check dist/*

- name: Publish to PyPI
if: github.ref == 'refs/heads/main' && github.event.inputs.dry_run != 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/sdk/python/dist
- name: Check PyPI version state
id: pypi-state
env:
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
RELEASE_RECOVERY: ${{ steps.bump.outputs.is_recovery }}
run: |
set -euo pipefail
STATUS=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
"https://pypi.org/pypi/relayfile-sdk/${NEW_VERSION}/json")
case "$STATUS" in
200)
if [ "$RELEASE_RECOVERY" != "true" ]; then
echo "ERROR: relayfile-sdk ${NEW_VERSION} exists without a same-commit tag reservation" >&2
exit 1
fi
echo "published=true" >> "$GITHUB_OUTPUT"
;;
404)
echo "published=false" >> "$GITHUB_OUTPUT"
;;
*)
echo "ERROR: PyPI version lookup returned HTTP ${STATUS}" >&2
exit 1
;;
esac
Comment on lines +208 to +227

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout and retries to the PyPI lookup.

The curl call has no --max-time and no retry. A stalled connection blocks the job, and a transient 429 or 5xx from PyPI hits the *) branch and aborts the release after the build and tests have already run. Add bounded timeouts and retries for transient status codes.

🛡️ Proposed fix
-          STATUS=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
-            "https://pypi.org/pypi/relayfile-sdk/${NEW_VERSION}/json")
+          STATUS=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
+            --connect-timeout 10 --max-time 30 \
+            --retry 3 --retry-delay 2 --retry-all-errors \
+            "https://pypi.org/pypi/relayfile-sdk/${NEW_VERSION}/json")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run: |
set -euo pipefail
STATUS=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
"https://pypi.org/pypi/relayfile-sdk/${NEW_VERSION}/json")
case "$STATUS" in
200)
if [ "$RELEASE_RECOVERY" != "true" ]; then
echo "ERROR: relayfile-sdk ${NEW_VERSION} exists without a same-commit tag reservation" >&2
exit 1
fi
echo "published=true" >> "$GITHUB_OUTPUT"
;;
404)
echo "published=false" >> "$GITHUB_OUTPUT"
;;
*)
echo "ERROR: PyPI version lookup returned HTTP ${STATUS}" >&2
exit 1
;;
esac
run: |
set -euo pipefail
STATUS=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
--connect-timeout 10 --max-time 30 \
--retry 3 --retry-delay 2 --retry-all-errors \
"https://pypi.org/pypi/relayfile-sdk/${NEW_VERSION}/json")
case "$STATUS" in
200)
if [ "$RELEASE_RECOVERY" != "true" ]; then
echo "ERROR: relayfile-sdk ${NEW_VERSION} exists without a same-commit tag reservation" >&2
exit 1
fi
echo "published=true" >> "$GITHUB_OUTPUT"
;;
404)
echo "published=false" >> "$GITHUB_OUTPUT"
;;
*)
echo "ERROR: PyPI version lookup returned HTTP ${STATUS}" >&2
exit 1
;;
esac
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish-python.yml around lines 208 - 227, Update the PyPI
lookup in the publish workflow’s STATUS curl command to use a bounded
connection/overall timeout and retry transient failures, including HTTP 429 and
5xx responses, with a finite retry limit and delay. Preserve the existing 200,
404, and unexpected-status handling after retries complete.


- name: Commit and tag
- name: Reserve Python SDK version tag
if: github.ref == 'refs/heads/main' && github.event.inputs.dry_run != 'true'
working-directory: ${{ github.workspace }}
env:
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
run: |
NEW_VERSION="${{ steps.bump.outputs.new_version }}"

set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"

git add packages/sdk/python/pyproject.toml packages/sdk/python/uv.lock
if ! git diff --staged --quiet; then
git commit -m "chore(sdk-python): v${NEW_VERSION}"
git push origin HEAD:main
if git show-ref --verify --quiet "refs/tags/sdk-python-v${NEW_VERSION}"; then
TAG_COMMIT=$(git rev-list -n 1 "refs/tags/sdk-python-v${NEW_VERSION}")
if [ "$TAG_COMMIT" != "$GITHUB_SHA" ]; then
echo "ERROR: sdk-python-v${NEW_VERSION} belongs to ${TAG_COMMIT}, not ${GITHUB_SHA}" >&2
exit 1
fi
echo "Python SDK tag is already reserved by this source commit"
exit 0
fi

git tag -a "sdk-python-v${NEW_VERSION}" -m "sdk-python v${NEW_VERSION}"
git push origin "sdk-python-v${NEW_VERSION}"
git push origin "refs/tags/sdk-python-v${NEW_VERSION}:refs/tags/sdk-python-v${NEW_VERSION}"

- name: Publish to PyPI
if: github.ref == 'refs/heads/main' && github.event.inputs.dry_run != 'true' && steps.pypi-state.outputs.published != 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packages/sdk/python/dist

- name: Create GitHub Release
if: github.ref == 'refs/heads/main' && github.event.inputs.dry_run != 'true'
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: sdk-python-v${{ steps.bump.outputs.new_version }}
name: sdk-python-v${{ steps.bump.outputs.new_version }}
Expand All @@ -204,15 +273,17 @@ jobs:
- name: Summary
if: always()
run: |
echo "## Python SDK Publish Summary" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Version**: \`${{ steps.bump.outputs.new_version }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "**Dry Run**: \`${{ github.event.inputs.dry_run }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "Dry run completed. Built and checked dist, but did not publish or tag." >> "$GITHUB_STEP_SUMMARY"
elif [ "${{ github.ref }}" != "refs/heads/main" ]; then
echo "Build and checks completed on a non-main ref; publish, tag, and release were skipped." >> "$GITHUB_STEP_SUMMARY"
else
echo "Published to PyPI and created tag \`sdk-python-v${{ steps.bump.outputs.new_version }}\`." >> "$GITHUB_STEP_SUMMARY"
fi
{
echo "## Python SDK Publish Summary"
echo ""
echo "**Version**: \`${{ steps.bump.outputs.new_version }}\`"
echo "**Dry Run**: \`${{ github.event.inputs.dry_run }}\`"
echo ""
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "Dry run completed. Built and checked dist, but did not publish or tag."
elif [ "${{ github.ref }}" != "refs/heads/main" ]; then
echo "Build and checks completed on a non-main ref; publish, tag, and release were skipped."
else
echo "Published to PyPI and created tag \`sdk-python-v${{ steps.bump.outputs.new_version }}\`."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Recovery reruns can skip the PyPI publish and tag creation because both already happened in an earlier attempt, but the summary still says “Published to PyPI and created tag.” Reporting the recovery/already-published state would prevent a misleading release status.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/publish-python.yml, line 287:

<comment>Recovery reruns can skip the PyPI publish and tag creation because both already happened in an earlier attempt, but the summary still says “Published to PyPI and created tag.” Reporting the recovery/already-published state would prevent a misleading release status.</comment>

<file context>
@@ -204,15 +273,17 @@ jobs:
+            elif [ "${{ github.ref }}" != "refs/heads/main" ]; then
+              echo "Build and checks completed on a non-main ref; publish, tag, and release were skipped."
+            else
+              echo "Published to PyPI and created tag \`sdk-python-v${{ steps.bump.outputs.new_version }}\`."
+            fi
+          } >> "$GITHUB_STEP_SUMMARY"
</file context>

fi
} >> "$GITHUB_STEP_SUMMARY"
Loading
Loading