@@ -244,23 +244,45 @@ runs:
244244 --output-dir .conda-build/out \
245245 "${channels[@]}"
246246
247+ # The two uploads are independent: neither can stop the other from being
248+ # attempted. Each records its own outcome and the job status is decided
249+ # afterwards, in "Report upload results".
250+ #
251+ # Everything that can fail cheaply -- tests, build, conda build -- has
252+ # already run by this point, so reaching here means the artifacts are good
253+ # and only the network calls remain. An anaconda.org upload is replaceable,
254+ # so letting it proceed after a PyPI failure costs nothing and saves a
255+ # manual re-upload.
247256 - name : Publish to PyPI
257+ id : pypi
248258 if : inputs.dry_run != 'true' && inputs.publish_pypi == 'true'
249259 shell : bash
250260 working-directory : ${{ inputs.project_dir }}
251261 env :
252262 PUBLISH_URL : ${{ inputs.pypi_publish_url }}
253263 run : |
254- set -euo pipefail
264+ # No -e: a failure here must not skip the anaconda.org upload.
265+ set -uo pipefail
266+
255267 # No token: uv detects the Actions OIDC environment and exchanges the
256268 # ID token for a short-lived PyPI token. Needs id-token: write on the job.
269+ rc=0
257270 if [ -n "$PUBLISH_URL" ]; then
258- uv publish --publish-url "$PUBLISH_URL"
271+ uv publish --publish-url "$PUBLISH_URL" || rc=$?
259272 else
260- uv publish
273+ uv publish || rc=$?
274+ fi
275+
276+ if [ "$rc" -eq 0 ]; then
277+ echo "outcome=success" >> "$GITHUB_OUTPUT"
278+ else
279+ echo "outcome=failed" >> "$GITHUB_OUTPUT"
280+ echo "::error::PyPI publish failed (exit $rc). The anaconda.org upload" \
281+ "still runs; the job fails at the end."
261282 fi
262283
263284 - name : Upload to anaconda.org
285+ id : conda
264286 if : inputs.dry_run != 'true' && inputs.anaconda_owner != ''
265287 shell : bash
266288 working-directory : ${{ inputs.project_dir }}
@@ -269,13 +291,19 @@ runs:
269291 OWNER : ${{ inputs.anaconda_owner }}
270292 LABEL : ${{ inputs.anaconda_label }}
271293 run : |
272- set -euo pipefail
294+ # No -e, for the same reason as above: this step reports rather than aborts.
295+ set -uo pipefail
296+
297+ fail() {
298+ echo "outcome=failed" >> "$GITHUB_OUTPUT"
299+ echo "::error::$*"
300+ exit 0
301+ }
273302
274303 if [ -z "$ANACONDA_API_KEY" ]; then
275- echo "::error::anaconda_api_key is empty. Create a token with write" \
276- "access at https://anaconda.org/settings/access and pass it as" \
277- "the anaconda_api_key input."
278- exit 1
304+ fail "anaconda_api_key is empty. Create a token with write access at" \
305+ "https://anaconda.org/settings/access and pass it as the" \
306+ "anaconda_api_key input."
279307 fi
280308
281309 packages=()
@@ -284,13 +312,40 @@ runs:
284312 done < <(find .conda-build/out -type f \( -name '*.conda' -o -name '*.tar.bz2' \) | sort)
285313
286314 if [ ${#packages[@]} -eq 0 ]; then
287- echo "::error::no conda package was produced under .conda-build/out"
288- exit 1
315+ fail "no conda package was produced under .conda-build/out"
289316 fi
290317
291318 printf 'uploading to anaconda.org/%s (label %s):\n' "$OWNER" "$LABEL"
292319 printf ' %s\n' "${packages[@]}"
293- rattler-build upload anaconda -o "$OWNER" -c "$LABEL" "${packages[@]}"
320+
321+ if rattler-build upload anaconda -o "$OWNER" -c "$LABEL" "${packages[@]}"; then
322+ echo "outcome=success" >> "$GITHUB_OUTPUT"
323+ else
324+ fail "anaconda.org upload failed."
325+ fi
326+
327+ # Both uploads swallow their own failure so the other still runs, so the
328+ # job status has to be decided here.
329+ - name : Report upload results
330+ if : inputs.dry_run != 'true'
331+ shell : bash
332+ env :
333+ VERSION : ${{ steps.meta.outputs.version }}
334+ PYPI : ${{ steps.pypi.outputs.outcome }}
335+ CONDA : ${{ steps.conda.outputs.outcome }}
336+ run : |
337+ set -uo pipefail
338+ printf 'PyPI: %s\n' "${PYPI:-skipped}"
339+ printf 'anaconda.org: %s\n' "${CONDA:-skipped}"
340+
341+ if [ "${PYPI:-}" = failed ] || [ "${CONDA:-}" = failed ]; then
342+ if [ "${PYPI:-}" = success ]; then
343+ echo "::error::PyPI published $VERSION but anaconda.org did not." \
344+ "Re-running this workflow will fail on PyPI as a duplicate:" \
345+ "upload the conda package by hand, or bump to the next version."
346+ fi
347+ exit 1
348+ fi
294349
295350 # Runs from the repository root, not project_dir, so a failure in prepare
296351 # cannot turn this always() step into a second, confusing error.
@@ -300,18 +355,21 @@ runs:
300355 env :
301356 VERSION : ${{ steps.meta.outputs.version }}
302357 DRY_RUN : ${{ inputs.dry_run }}
303- PYPI : ${{ inputs.publish_pypi }}
304358 OWNER : ${{ inputs.anaconda_owner }}
305359 PROJECT_DIR : ${{ inputs.project_dir }}
360+ PYPI_OUTCOME : ${{ steps.pypi.outputs.outcome }}
361+ CONDA_OUTCOME : ${{ steps.conda.outputs.outcome }}
306362 run : |
307363 set -uo pipefail
308364 {
309365 echo "### ${VERSION:-version unknown}"
310366 if [ "$DRY_RUN" = true ]; then
311367 echo "Dry run - built and checked, nothing uploaded."
312368 else
313- if [ "$PYPI" = true ]; then echo "- published to PyPI"; fi
314- if [ -n "$OWNER" ]; then echo "- published to anaconda.org/$OWNER"; fi
369+ echo "| Target | Result |"
370+ echo "|---|---|"
371+ echo "| PyPI | ${PYPI_OUTCOME:-skipped} |"
372+ echo "| anaconda.org/${OWNER:-—} | ${CONDA_OUTCOME:-skipped} |"
315373 fi
316374 echo
317375 echo '```'
0 commit comments