Skip to content
Merged
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
23 changes: 11 additions & 12 deletions .github/workflows/csharp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ jobs:
id: version-check
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
PACKAGE_VERSION=$(sed -n 's|.*<VersionPrefix>\(.*\)</VersionPrefix>.*|\1|p' "$PROJECT_PATH")
if [ -z "$PACKAGE_VERSION" ]; then
echo "::error::Could not read <VersionPrefix> from $PROJECT_PATH"
Expand All @@ -161,7 +163,7 @@ jobs:
echo "Package: $PACKAGE_ID@$PACKAGE_VERSION"

# Flat-container URLs are lowercase-only.
if curl -fsS "https://api.nuget.org/v3-flatcontainer/$PACKAGE_ID_LOWER/$PACKAGE_VERSION/$PACKAGE_ID_LOWER.nuspec" > /dev/null 2>&1; then
if probe_registry "https://api.nuget.org/v3-flatcontainer/$PACKAGE_ID_LOWER/$PACKAGE_VERSION/$PACKAGE_ID_LOWER.nuspec"; then
echo "Version $PACKAGE_VERSION already exists on NuGet.org"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
Expand Down Expand Up @@ -207,18 +209,15 @@ jobs:
PACKAGE_VERSION: ${{ steps.version-check.outputs.version }}
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
# NuGet.org indexes asynchronously; poll rather than assume success.
for attempt in $(seq 1 20); do
if curl -fsS "https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID_LOWER}/index.json" \
| grep -q "\"${PACKAGE_VERSION}\""; then
echo "Verified ${PACKAGE_ID}@${PACKAGE_VERSION} on NuGet.org (attempt ${attempt})"
exit 0
fi
echo "Not indexed yet, retrying in 30s (attempt ${attempt}/20)"
sleep 30
done
echo "::error::${PACKAGE_ID}@${PACKAGE_VERSION} did not appear on NuGet.org within 10 minutes"
exit 1
# The flat container exposes one document per package, so the body
# has to be matched -- a 200 only proves the package exists at all.
wait_for_registry_match \
"${PACKAGE_ID}@${PACKAGE_VERSION} on NuGet.org" \
"https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID_LOWER}/index.json" \
"\"${PACKAGE_VERSION}\"" 20 30

generatePdfWithCode:
runs-on: ubuntu-latest
Expand Down
15 changes: 5 additions & 10 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,10 @@ jobs:
PACKAGE_VERSION: ${{ steps.version-check.outputs.version }}
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
# The proxy only fetches a version once someone asks for it, so this
# also warms it for the first consumer.
for attempt in $(seq 1 20); do
if curl -fsS "https://proxy.golang.org/${MODULE}/@v/v${PACKAGE_VERSION}.info" >/dev/null 2>&1; then
echo "Verified ${MODULE}@v${PACKAGE_VERSION} on proxy.golang.org (attempt ${attempt})"
exit 0
fi
echo "Not on the proxy yet, retrying in 15s (attempt ${attempt}/20)"
sleep 15
done
echo "::error::${MODULE}@v${PACKAGE_VERSION} did not appear on proxy.golang.org within 5 minutes"
exit 1
wait_for_registry \
"${MODULE}@v${PACKAGE_VERSION} on proxy.golang.org" \
"https://proxy.golang.org/${MODULE}/@v/v${PACKAGE_VERSION}.info" 20 15
21 changes: 9 additions & 12 deletions .github/workflows/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ jobs:
id: version-check
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
PACKAGE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
PACKAGE_GROUP=$(mvn help:evaluate -Dexpression=project.groupId -q -DforceStdout)
PACKAGE_ARTIFACT=$(mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout)
Expand All @@ -198,7 +200,7 @@ jobs:
echo "Package: $PACKAGE_GROUP:$PACKAGE_ARTIFACT:$PACKAGE_VERSION"

URL="https://repo1.maven.org/maven2/$(echo "$PACKAGE_GROUP" | tr '.' '/')/$PACKAGE_ARTIFACT/$PACKAGE_VERSION/"
if curl --head --silent --fail "$URL" > /dev/null 2>&1; then
if probe_registry "$URL"; then
echo "Version $PACKAGE_VERSION already exists on Maven Central"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
Expand Down Expand Up @@ -235,18 +237,13 @@ jobs:
PACKAGE_VERSION: ${{ steps.version-check.outputs.version }}
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
GROUP_PATH=$(echo "$PACKAGE_GROUP" | tr '.' '/')
URL="https://repo1.maven.org/maven2/${GROUP_PATH}/${PACKAGE_ARTIFACT}/${PACKAGE_VERSION}/${PACKAGE_ARTIFACT}-${PACKAGE_VERSION}.pom"
for attempt in $(seq 1 30); do
if curl --head --silent --fail "$URL" > /dev/null 2>&1; then
echo "Verified ${PACKAGE_GROUP}:${PACKAGE_ARTIFACT}:${PACKAGE_VERSION} on Maven Central (attempt ${attempt})"
exit 0
fi
echo "Not synced yet, retrying in 30s (attempt ${attempt}/30)"
sleep 30
done
echo "::error::${PACKAGE_GROUP}:${PACKAGE_ARTIFACT}:${PACKAGE_VERSION} did not appear on Maven Central within 15 minutes"
exit 1
wait_for_registry \
"${PACKAGE_GROUP}:${PACKAGE_ARTIFACT}:${PACKAGE_VERSION} on Maven Central" \
"https://repo1.maven.org/maven2/${GROUP_PATH}/${PACKAGE_ARTIFACT}/${PACKAGE_VERSION}/${PACKAGE_ARTIFACT}-${PACKAGE_VERSION}.pom" \
30 30

publishRelease:
runs-on: ubuntu-latest
Expand Down
15 changes: 5 additions & 10 deletions .github/workflows/js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,11 @@ jobs:
PACKAGE_VERSION: ${{ steps.version-check.outputs.version }}
run: |
set -euo pipefail
for attempt in $(seq 1 10); do
if curl -fsS "https://registry.npmjs.org/${PACKAGE_NAME}/${PACKAGE_VERSION}" >/dev/null 2>&1; then
echo "Verified ${PACKAGE_NAME}@${PACKAGE_VERSION} on npm (attempt ${attempt})"
exit 0
fi
echo "Not visible yet, retrying in 15s (attempt ${attempt}/10)"
sleep 15
done
echo "::error::${PACKAGE_NAME}@${PACKAGE_VERSION} did not appear on the npm registry within 2.5 minutes"
exit 1
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
wait_for_registry \
"${PACKAGE_NAME}@${PACKAGE_VERSION} on npm" \
"https://registry.npmjs.org/${PACKAGE_NAME}/${PACKAGE_VERSION}" 10 15

publishRelease:
runs-on: ubuntu-latest
Expand Down
24 changes: 11 additions & 13 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ jobs:
id: version-check
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
PACKAGE_NAME=$(php -r 'echo json_decode(file_get_contents("composer.json"))->name;')
PACKAGE_VERSION=$(php -r 'echo json_decode(file_get_contents("composer.json"))->version;')
echo "name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
Expand All @@ -159,7 +161,7 @@ jobs:
# Distinguish "package is not on Packagist at all" from "this version
# is not there yet". The old check collapsed both into should_publish
# and then the update call quietly did nothing.
if ! METADATA=$(curl -fsS "https://repo.packagist.org/p2/${PACKAGE_NAME}.json" 2>/dev/null); then
if ! METADATA=$(fetch_registry "https://repo.packagist.org/p2/${PACKAGE_NAME}.json"); then
echo "package_known=false" >> "$GITHUB_OUTPUT"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
exit 0
Expand Down Expand Up @@ -217,18 +219,14 @@ jobs:
PACKAGE_VERSION: ${{ steps.version-check.outputs.version }}
run: |
set -euo pipefail
# Packagist crawls asynchronously after the update call returns 202.
for attempt in $(seq 1 20); do
if curl -fsS "https://repo.packagist.org/p2/${PACKAGE_NAME}.json" \
| grep -q "\"version\":\"${PACKAGE_VERSION}\""; then
echo "Verified ${PACKAGE_NAME}@${PACKAGE_VERSION} on Packagist (attempt ${attempt})"
exit 0
fi
echo "Not crawled yet, retrying in 30s (attempt ${attempt}/20)"
sleep 30
done
echo "::error::${PACKAGE_NAME}@${PACKAGE_VERSION} did not appear on Packagist within 10 minutes"
exit 1
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
# Packagist crawls asynchronously after the update call returns 202,
# and serves every version from one document, so match the body.
wait_for_registry_match \
"${PACKAGE_NAME}@${PACKAGE_VERSION} on Packagist" \
"https://repo.packagist.org/p2/${PACKAGE_NAME}.json" \
"\"version\":\"${PACKAGE_VERSION}\"" 20 30

publishRelease:
runs-on: ubuntu-latest
Expand Down
30 changes: 18 additions & 12 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ jobs:
id: version-check
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
read -r PACKAGE_NAME PACKAGE_VERSION <<<"$(python -c "
import tomllib
with open('pyproject.toml','rb') as f:
Expand All @@ -170,7 +172,7 @@ jobs:
# The JSON API is an exact lookup. The previous `pip index versions |
# grep` matched substrings, so 0.1.0 looked published once 0.1.0.1
# existed.
if curl -fsS "https://pypi.org/pypi/${PACKAGE_NAME}/${PACKAGE_VERSION}/json" >/dev/null 2>&1; then
if probe_registry "https://pypi.org/pypi/${PACKAGE_NAME}/${PACKAGE_VERSION}/json"; then
echo "Version $PACKAGE_VERSION already exists on PyPI"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
Expand All @@ -179,12 +181,21 @@ jobs:
fi
# Trusted publishing when configured; an empty `password` makes the
# action fall back to OIDC, and PYPI_TOKEN is only the bootstrap.
#
# The action defaults `attestations` to true, but attestations require
# trusted publishing, so passing a password made every release log
# "the attestations input is ignored" as a warning. The two settings are
# now driven by the same opt-in variable and can no longer contradict
# each other. Register the publisher at
# https://pypi.org/manage/project/links-notation/settings/publishing/
# and set the `PYPI_TRUSTED_PUBLISHING` repository variable to `true`.
- name: Publish to PyPI
if: steps.version-check.outputs.should_publish == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: python/dist
password: ${{ secrets.PYPI_TOKEN }}
password: ${{ vars.PYPI_TRUSTED_PUBLISHING == 'true' && '' || secrets.PYPI_TOKEN }}
attestations: ${{ vars.PYPI_TRUSTED_PUBLISHING == 'true' }}
- name: Record publish outcome
id: publish
run: |
Expand All @@ -201,16 +212,11 @@ jobs:
PACKAGE_VERSION: ${{ steps.version-check.outputs.version }}
run: |
set -euo pipefail
for attempt in $(seq 1 10); do
if curl -fsS "https://pypi.org/pypi/${PACKAGE_NAME}/${PACKAGE_VERSION}/json" >/dev/null 2>&1; then
echo "Verified ${PACKAGE_NAME}@${PACKAGE_VERSION} on PyPI (attempt ${attempt})"
exit 0
fi
echo "Not visible yet, retrying in 15s (attempt ${attempt}/10)"
sleep 15
done
echo "::error::${PACKAGE_NAME}@${PACKAGE_VERSION} did not appear on PyPI within 2.5 minutes"
exit 1
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
wait_for_registry \
"${PACKAGE_NAME}@${PACKAGE_VERSION} on PyPI" \
"https://pypi.org/pypi/${PACKAGE_NAME}/${PACKAGE_VERSION}/json" 10 15

publishRelease:
runs-on: ubuntu-latest
Expand Down
44 changes: 43 additions & 1 deletion .github/workflows/release-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ jobs:

audit:
runs-on: ubuntu-latest
timeout-minutes: 10
# The wait below can take up to 20 minutes on its own.
timeout-minutes: 35
permissions:
contents: read
# Required so `gh run list` can see this commit's other workflow runs.
actions: read
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -61,9 +66,46 @@ jobs:
uses: actions/setup-node@v7
with:
node-version: '22'
# On a push the audit is triggered by the very commit whose publish
# workflows are still running, so without this wait it compares the
# just-bumped version against a registry that cannot possibly have it
# yet. Run 33168552493 finished 8 seconds after the push and reported all
# seven languages as drifted while rust was still publishing, six minutes
# from done. Every one of those warnings was noise.
- name: Wait for this commit's publish workflows
if: ${{ github.event_name == 'push' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CI_VERBOSE: ${{ inputs.verbose && 'true' || vars.CI_VERBOSE || 'false' }}
run: |
set -euo pipefail
for attempt in $(seq 1 60); do
# This workflow is itself a run for this commit, so it has to be
# excluded or the wait can never finish.
PENDING=$(gh run list --commit "$GITHUB_SHA" --limit 100 \
--json name,status \
--jq '[.[] | select(.name != "release-audit")
| select(.status != "completed")] | length')
if [ "$PENDING" -eq 0 ]; then
echo "All workflows for $GITHUB_SHA have completed (attempt ${attempt})"
exit 0
fi
if [ "${CI_VERBOSE:-false}" = "true" ]; then
gh run list --commit "$GITHUB_SHA" --limit 100 \
--json name,status,conclusion \
--jq '.[] | " \(.name): \(.status) \(.conclusion // "")"'
fi
echo "${PENDING} workflow(s) still running (attempt ${attempt}/60); waiting 20s"
sleep 20
done
echo "::warning::Workflows for $GITHUB_SHA were still running after 20 minutes; the audit below may report releases that are still in flight"
# Drift is reported as annotations rather than enforced as a failure: a
# version bump legitimately lands before the release that publishes it.
- name: Audit declared versions against the registries
env:
CI_VERBOSE: ${{ inputs.verbose && 'true' || vars.CI_VERBOSE || 'false' }}
# On a pull request no publish job runs at all, so a bumped version
# being ahead of the registry is the expected state rather than
# drift. Reporting it as a warning there would flag every release PR.
AUDIT_EXPECT_PUBLISHED: ${{ github.event_name != 'pull_request' }}
run: node scripts/release-audit.mjs
53 changes: 33 additions & 20 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,17 @@ jobs:
- name: Build
run: cargo build --release
# Exchanges the workflow's OIDC identity for a short-lived crates.io
# token. Returns an empty token when trusted publishing is not
# configured for the crate, which the publish step reports explicitly.
# token. Trusted publishing has to be registered on crates.io before it
# can be used; until that is done the action fails with
# "No Trusted Publishing config found" and, because it writes an
# `::error::` annotation, paints a red cross on a run that then publishes
# perfectly well through CARGO_TOKEN. Gating on an opt-in repository
# variable keeps that false positive out of the run summary. Set the
# `CRATES_IO_TRUSTED_PUBLISHING` variable to `true` after configuring
# https://crates.io/crates/links-notation/settings to switch over.
- name: Authenticate to crates.io
id: cratesio-auth
if: ${{ vars.CRATES_IO_TRUSTED_PUBLISHING == 'true' }}
continue-on-error: true
uses: rust-lang/crates-io-auth-action@v1
- name: Publish to crates.io
Expand All @@ -165,6 +172,8 @@ jobs:
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
run: |
set -euo pipefail
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"

read_field() { grep "^$2 = " "$1" | head -1 | sed "s/$2 = \"\(.*\)\"/\1/"; }
MACRO_VERSION=$(read_field links-notation-macro/Cargo.toml version)
Expand All @@ -188,6 +197,15 @@ jobs:

publish_crate() {
local crate="$1" version="$2" log="$3"
# Every other language workflow checks the registry before pushing.
# Without it `cargo publish` prints a red "already exists on
# crates.io index" error on any re-run of an already released
# version, which reads as a failure in a job that is working
# exactly as intended.
if crate_version_published "$crate" "$version"; then
echo "${crate}@${version} is already on crates.io"
return 2
fi
if cargo publish -p "$crate" 2>&1 | tee "$log"; then
echo "Published ${crate}@${version}"
return 0
Expand All @@ -209,14 +227,11 @@ jobs:
if [ $MACRO_STATUS -eq 1 ]; then exit 1; fi

if [ $MACRO_STATUS -eq 0 ]; then
for attempt in $(seq 1 20); do
if curl -fsS "https://crates.io/api/v1/crates/links-notation-macro/${MACRO_VERSION}" >/dev/null 2>&1; then
echo "links-notation-macro@${MACRO_VERSION} is visible in the index"
break
fi
echo "Waiting for the index to catch up (attempt ${attempt}/20)"
sleep 15
done
# The sparse index, not the JSON API, is what `cargo publish`
# resolves the dependency against, so that is what has to catch up.
if ! wait_for_crate_version links-notation-macro "$MACRO_VERSION" 20 15; then
echo "::warning::Continuing anyway; cargo will fail explicitly if the dependency is still unresolvable"
fi
fi

set +e
Expand All @@ -235,16 +250,14 @@ jobs:
PACKAGE_VERSION: ${{ steps.publish.outputs.version }}
run: |
set -euo pipefail
for attempt in $(seq 1 20); do
if curl -fsS "https://crates.io/api/v1/crates/${PACKAGE_NAME}/${PACKAGE_VERSION}" >/dev/null 2>&1; then
echo "Verified ${PACKAGE_NAME}@${PACKAGE_VERSION} on crates.io (attempt ${attempt})"
exit 0
fi
echo "Not visible yet, retrying in 15s (attempt ${attempt}/20)"
sleep 15
done
echo "::error::${PACKAGE_NAME}@${PACKAGE_VERSION} did not appear on crates.io within 5 minutes"
exit 1
# shellcheck source=scripts/ci/registry-probe.sh
. "$GITHUB_WORKSPACE/scripts/ci/registry-probe.sh"
# Check the index first: it is the artifact consumers actually
# resolve against, and unlike the JSON API it is not rate limited.
wait_for_crate_version "$PACKAGE_NAME" "$PACKAGE_VERSION" 20 15
wait_for_registry \
"${PACKAGE_NAME}@${PACKAGE_VERSION} on crates.io" \
"https://crates.io/api/v1/crates/${PACKAGE_NAME}/${PACKAGE_VERSION}" 20 15

publishRelease:
runs-on: ubuntu-latest
Expand Down
Loading
Loading