Skip to content

fix(ci): keep uv.lock in step with the release version bump (#699) #57

fix(ci): keep uv.lock in step with the release version bump (#699)

fix(ci): keep uv.lock in step with the release version bump (#699) #57

Workflow file for this run

name: Publish Release
on:
push:
branches: [main]
workflow_dispatch: {}
permissions:
contents: write
issues: write
pull-requests: write
concurrency:
group: publish-release-${{ github.ref }}
cancel-in-progress: false
env:
PYTHON_VERSION: "3.11"
UV_VERSION: "0.12.3"
AUTO_VERSION: "11.3.6"
RELEASE_BOT_NAME: "applied-ai-releases[bot]"
RELEASE_BOT_EMAIL: "applied-ai-releases[bot]@users.noreply.github.com"
jobs:
gate:
name: Gate on merged PR label
runs-on: ubuntu-latest
# Prevent infinite loops from the bot's "chore(release)" commit.
if: github.actor != 'applied-ai-releases[bot]'
outputs:
should_release: ${{ steps.find_pr.outputs.should_release }}
pr_number: ${{ steps.find_pr.outputs.pr_number }}
steps:
- name: Find merged PR for this commit and check labels
id: find_pr
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const sha = context.sha;
const maxAttempts = 6;
let pulls;
// For workflow_dispatch there may not be an associated PR;
// allow manual runs by setting should_release=true only if you want that behavior.
// Here we keep it strict: only release if we can find a PR with auto:release.
// GitHub can briefly lag in associating the merge commit on main back to its PR,
// so retry for a short window before concluding there is no releasable PR.
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner, repo, commit_sha: sha
});
if (pulls.data.length) {
core.notice(
`Found ${pulls.data.length} PR association(s) for commit ${sha} on attempt ${attempt}/${maxAttempts}.`
);
break;
}
if (attempt < maxAttempts) {
core.notice(
`No PR associated with commit ${sha} on attempt ${attempt}/${maxAttempts}. Retrying in 10 seconds...`
);
await new Promise((resolve) => setTimeout(resolve, 10000));
}
}
if (!pulls.data.length) {
core.setOutput("should_release", "false");
core.setOutput("pr_number", "");
core.notice(
`No PR associated with commit ${sha} after ${maxAttempts} attempts. Not releasing.`
);
return;
}
// Pick the merged PR targeting main (most common).
const pr = pulls.data.find(p => p.merged_at && p.base?.ref === "main") ?? pulls.data[0];
const labels = (pr.labels || []).map(l => l.name);
core.setOutput("pr_number", String(pr.number));
const should = labels.includes("auto:release");
core.setOutput("should_release", should ? "true" : "false");
core.notice(`PR #${pr.number} labels: ${labels.join(", ")}`);
core.notice(`should_release=${should}`);
canary-build:
runs-on: ubuntu-latest
needs: gate
if: needs.gate.outputs.should_release == 'true'
steps:
- name: Check out repository
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- name: Install Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v10.0.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
python-version: ${{ env.PYTHON_VERSION }}
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Install dependencies
run: uv sync --frozen
- name: Check that package can be built
run: uv build
release:
runs-on: ubuntu-latest
needs: [gate, canary-build]
if: needs.gate.outputs.should_release == 'true'
outputs:
version: ${{ steps.latest_release.outputs.version }}
steps:
- name: Generate GitHub App token
id: app_token
uses: actions/create-github-app-token@v2
with:
app-id: 2959093
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Checkout code
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
fetch-tags: true
token: ${{ steps.app_token.outputs.token }}
- name: Download and install auto
run: |
curl -L https://github.com/intuit/auto/releases/download/v${{ env.AUTO_VERSION }}/auto-linux.gz -o auto-linux.gz
gunzip auto-linux.gz
chmod +x auto-linux
sudo mv auto-linux /usr/local/bin/auto
auto --version
- name: Sanity check
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
auto shipit --name "${RELEASE_BOT_NAME}" --email "${RELEASE_BOT_EMAIL}" --dry-run -v
- name: Capture previous tag
id: previous_tag
run: |
set -euo pipefail
echo "tag=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
- name: Resolve release version
id: latest_release
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
RAW_VERSION="$(auto shipit --name "${RELEASE_BOT_NAME}" --email "${RELEASE_BOT_EMAIL}" --dry-run --quiet | tail -n1 | tr -d '\r')"
VERSION="${RAW_VERSION#v}"
if [ -z "$VERSION" ]; then
echo "Could not resolve release version"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved version: $VERSION"
# Only the bump needs uv. Kept after the `auto` calls, which resolve the
# release version today without the env setup-python/setup-uv export.
- name: Install Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v10.0.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
python-version: ${{ env.PYTHON_VERSION }}
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Apply release version to pyproject.toml and uv.lock
run: |
set -euo pipefail
VERSION="${{ steps.latest_release.outputs.version }}"
sed -i "s/^version = \".*\"$/version = \"${VERSION}\"/" pyproject.toml
# uv.lock carries the project's own version. Edit that one field rather
# than running `uv lock`, which re-serializes the whole file.
sed -i "/^name = \"redisvl\"$/{n;s/^version = \".*\"$/version = \"${VERSION}\"/;}" uv.lock
grep '^version = ' pyproject.toml
grep -A1 '^name = "redisvl"$' uv.lock
- name: Verify the version bump
run: |
set -euo pipefail
VERSION="${{ steps.latest_release.outputs.version }}"
fail() { echo "::error::$1"; exit 1; }
# Catch a sed that matched the wrong line, before anything is pushed or
# published. No change is fine: a re-run of an already-bumped commit.
for f in pyproject.toml uv.lock; do
stat="$(git diff --numstat -- "$f" | cut -f1,2)"
if [ -n "$stat" ] && [ "$stat" != "$(printf '1\t1')" ]; then
fail "$f: expected a single-line version change, got $(git diff --shortstat -- "$f")"
fi
other="$(git diff -U0 -- "$f" | grep -E '^[-+][^-+]' | grep -vE '^[-+]version = "' || true)"
if [ -n "$other" ]; then
fail "$f: changed something other than a version line: $other"
fi
done
# In uv.lock it has to be the project's own entry, not a dependency's pin.
grep -qx "version = \"${VERSION}\"" pyproject.toml \
|| fail "pyproject.toml does not carry ${VERSION}"
grep -A1 '^name = "redisvl"$' uv.lock | grep -qx "version = \"${VERSION}\"" \
|| fail "uv.lock: the redisvl entry does not carry ${VERSION}"
uv lock --check
- name: Commit and push version bump
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if git diff --quiet -- pyproject.toml uv.lock; then
echo "No version change to commit."
else
git config user.name "${RELEASE_BOT_NAME}"
git config user.email "${RELEASE_BOT_EMAIL}"
git add pyproject.toml uv.lock
# Include [skip ci] to avoid running the workflow again on this bot commit.
git commit -m "chore(release): set version to ${{ steps.latest_release.outputs.version }} [skip ci]"
git push origin HEAD:main
fi
- name: Capture release commit SHA
id: release_commit
run: |
set -euo pipefail
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Create labels
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: auto create-labels
- name: Create and push tag
run: |
set -euo pipefail
TAG="v${{ steps.latest_release.outputs.version }}"
TARGET_SHA="${{ steps.release_commit.outputs.sha }}"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
EXISTING_SHA="$(git rev-list -n1 "$TAG")"
if [ "$EXISTING_SHA" = "$TARGET_SHA" ]; then
echo "Tag $TAG already exists at $TARGET_SHA. Skipping creation."
exit 0
fi
echo "Tag $TAG already exists at $EXISTING_SHA but expected $TARGET_SHA."
exit 1
fi
git tag "$TAG" "$TARGET_SHA"
git push origin "$TAG"
- name: Create GitHub release with auto notes
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
auto release \
--from "${{ steps.previous_tag.outputs.tag }}" \
--to "${{ steps.release_commit.outputs.sha }}" \
--use-version "v${{ steps.latest_release.outputs.version }}"
build-and-publish:
runs-on: ubuntu-latest
needs: [gate, release]
if: needs.gate.outputs.should_release == 'true'
permissions:
contents: read
id-token: write
steps:
- name: Check out repository
uses: actions/checkout@v6
with:
ref: v${{ needs.release.outputs.version }}
fetch-depth: 0
- name: Install Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v10.0.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
python-version: ${{ env.PYTHON_VERSION }}
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Install dependencies
run: uv sync --frozen
- name: Build package
run: uv build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1