Skip to content

Commit 6f07c65

Browse files
paulteehanclaude
andcommitted
ci(publish): restore Soda PyPI upload, fix --skip-existing on devpi
Reverses the "drop Soda PyPI" half of this branch — soda-core clean releases are still needed on pypi.cloud.soda.io. The original failure was not that the registry is wrong, but that #2758 passed --skip-existing to every leg, and pypi.cloud.soda.io (devpi) rejects that flag: UnsupportedConfiguration: 'https://pypi.cloud.soda.io' does not have support for the following features: --skip-existing Fix per-registry instead of removing the leg: - public PyPI -> keeps `twine upload --skip-existing` (supported) - pypi.cloud.soda.io -> upload plain; capture output and treat an "already exists" / 409 rejection as success, so re-runs and backfills stay idempotent Keeps this branch's single-approval gate (one approval email per run, not one per matrix leg). Restores the AWS secrets fetch the Soda leg needs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c97eac4 commit 6f07c65

1 file changed

Lines changed: 55 additions & 5 deletions

File tree

.github/workflows/publish.yaml

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: Publish to PyPI
33

4-
# Publishes an ALREADY-TAGGED release to public PyPI.
4+
# Publishes an ALREADY-TAGGED release to Soda PyPI and public PyPI.
55
#
66
# Why this exists, separate from release.yaml:
77
# release.yaml ("one-button release") resolves a version, tags it, builds,
@@ -28,8 +28,13 @@ name: Publish to PyPI
2828
# (e.g. v4.14.0, or the 4.8.0-4.13.0 backlog); a tag
2929
# pushed before this workflow existed won't re-fire.
3030
#
31-
# twine runs with --skip-existing, so re-runs and backfills are safe and the
32-
# automatic tag path does not fail when public PyPI already has the version.
31+
# Idempotency (re-runs / backfills must not fail on an already-published
32+
# version) is handled per-registry, because the two indexes differ:
33+
# public PyPI -> supports `twine upload --skip-existing`.
34+
# pypi.cloud.soda.io -> devpi index; does NOT support --skip-existing
35+
# (`UnsupportedConfiguration: ... --skip-existing`).
36+
# So we upload plain and treat an "already exists"
37+
# rejection as success.
3338
#
3439
# Approval: only the `approve` job references the production-release
3540
# environment, so reviewers get ONE approval request per run rather than one
@@ -122,6 +127,17 @@ jobs:
122127
- name: Set up UV
123128
uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
124129

130+
- name: Get external secrets
131+
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 # v2.0.10
132+
env:
133+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_BUILD_ACCESS_KEY_ID }}
134+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_BUILD_SECRET_ACCESS_KEY }}
135+
AWS_REGION: ${{ secrets.AWS_BUILD_DEFAULT_REGION }}
136+
with:
137+
secret-ids: |
138+
PYPI_USERNAME,/soda/github/common/SODA_PYPI_CLOUD_WRITE_USERNAME
139+
PYPI_PASSWORD,/soda/github/common/SODA_PYPI_CLOUD_WRITE_PASSWORD
140+
125141
- name: Build ${{ matrix.module }}
126142
run: |
127143
uv venv .venv
@@ -152,6 +168,39 @@ jobs:
152168
echo "Staggering upload by ${DELAY}s..."
153169
sleep "$DELAY"
154170
171+
- name: Publish to Soda PyPI
172+
id: soda_pypi
173+
if: ${{ !inputs.dry_run }}
174+
env:
175+
TWINE_REPOSITORY_URL: ${{ vars.CLOUD_PYPI_REPOSITORY }}
176+
TWINE_USERNAME: ${{ env.PYPI_USERNAME }}
177+
TWINE_PASSWORD: ${{ env.PYPI_PASSWORD }}
178+
run: |
179+
source .venv/bin/activate
180+
# pypi.cloud.soda.io (devpi) does NOT support --skip-existing, so we
181+
# upload plain and treat an "already exists" rejection as success to
182+
# keep re-runs / backfills idempotent.
183+
RESULT="failure"
184+
for attempt in 1 2 3; do
185+
OUT="$(twine upload "${{ matrix.module }}"/dist/* 2>&1)" && {
186+
echo "$OUT"; RESULT="success"
187+
echo "Soda PyPI upload succeeded on attempt $attempt"; break
188+
}
189+
echo "$OUT"
190+
if echo "$OUT" | grep -qiE "already (exists|been uploaded)|this filename has already been used|409 (conflict|client error)"; then
191+
RESULT="success"
192+
echo "Soda PyPI already has this version — treating as success."; break
193+
fi
194+
if [ "$attempt" -lt 3 ]; then
195+
DELAY=$((attempt * 15))
196+
echo "::warning::Soda PyPI upload failed (attempt $attempt/3), retrying in ${DELAY}s..."
197+
sleep "$DELAY"
198+
else
199+
echo "::error::Soda PyPI upload failed after 3 attempts"
200+
fi
201+
done
202+
echo "result=$RESULT" >> "$GITHUB_OUTPUT"
203+
155204
- name: Publish to public PyPI
156205
id: public_pypi
157206
if: ${{ !inputs.dry_run }}
@@ -179,8 +228,9 @@ jobs:
179228
if: ${{ always() && !inputs.dry_run }}
180229
run: |
181230
echo "### ${{ matrix.module }} @ ${{ needs.resolve.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
231+
echo "- Soda PyPI: ${{ steps.soda_pypi.outputs.result }}" >> "$GITHUB_STEP_SUMMARY"
182232
echo "- Public PyPI: ${{ steps.public_pypi.outputs.result }}" >> "$GITHUB_STEP_SUMMARY"
183-
if [ "${{ steps.public_pypi.outputs.result }}" != "success" ]; then
184-
echo "::error::${{ matrix.module }} did not publish to public PyPI — see logs."
233+
if [ "${{ steps.soda_pypi.outputs.result }}" != "success" ] || [ "${{ steps.public_pypi.outputs.result }}" != "success" ]; then
234+
echo "::error::${{ matrix.module }} did not publish cleanly to both registries — see logs."
185235
exit 1
186236
fi

0 commit comments

Comments
 (0)