mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* fix(release): create tag before release and set make_latest post-publish The Create Release workflow failed for every stable maintenance release while pre-releases succeeded. Two independent bugs were behind that. createRelease was minting the tag from target_commitish, and that path returns "Resource not accessible by integration" (403) to the Actions token, or 404 to a user token, for certain commits (cli/cli#9773). The stable-line tips tripped it; the dev/rc commits happened not to. Create the tag up front with git.createRef and drop target_commitish so the release attaches to the existing tag instead of minting one. A 422 from createRef (tag already exists) is tolerated so re-runs are idempotent. make_latest is silently ignored during the draft-to-published transition (cli/cli#8201), so a backport that published would seize the repo "latest" badge from a newer line. Publish first, then set make_latest in a separate call, and only for non-prereleases. Each operation here is already runtime-proven: git.createRef under the workflow token by prior release-branch jobs, the no-target createRelease and non-prerelease publish and separate make_latest PATCH by a manual 1.89.5 cut. * fix(release): pin tag_name on publish so the draft binding can't reset Pre-creating the tag means the draft is edited while a tag ref already exists, and a draft PATCH that omits tag_name can reset it to the untagged placeholder. Send tag_name explicitly on both updateRelease calls so publish always attaches to the intended tag. * fix(release): fail loudly when the tag exists at a different commit The createRef 422 swallow kept re-runs idempotent but also masked a tag that already exists at the wrong SHA, which would publish the release against the wrong commit silently. On 422, compare the existing tag ref to the intended commit and error on a mismatch, keeping idempotency only for a genuine same-SHA re-run.
186 lines
7.2 KiB
YAML
186 lines
7.2 KiB
YAML
name: Create Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Release tag (e.g. 1.84.0, 1.84.0rc1, 1.84.0.dev42, 1.84.0-dev.2, 1.84.0.post1; legacy v1.83.10-stable still accepted)"
|
|
required: true
|
|
type: string
|
|
commit_hash:
|
|
description: "Full 40-char commit SHA to target"
|
|
required: true
|
|
type: string
|
|
|
|
permissions: {}
|
|
|
|
jobs:
|
|
release:
|
|
name: Create Release
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- name: Validate inputs
|
|
env:
|
|
TAG: ${{ inputs.tag }}
|
|
COMMIT_HASH: ${{ inputs.commit_hash }}
|
|
run: |
|
|
if ! echo "${COMMIT_HASH}" | grep -qE '^[0-9a-f]{40}$'; then
|
|
echo "::error::commit_hash must be a full 40-character commit SHA"
|
|
exit 1
|
|
fi
|
|
if ! echo "${TAG}" | grep -qE '^v?[0-9]+\.[0-9]+\.[0-9]+'; then
|
|
echo "::error::tag must start with X.Y.Z (optional leading v), e.g. 1.84.0, 1.84.0rc1, 1.84.0.dev42, or v1.83.10-stable"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Create release
|
|
env:
|
|
TAG: ${{ inputs.tag }}
|
|
COMMIT_HASH: ${{ inputs.commit_hash }}
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
|
with:
|
|
script: |
|
|
const tag = process.env.TAG;
|
|
const commitHash = process.env.COMMIT_HASH;
|
|
|
|
// Mark RC / dev / nightly / alpha / beta tags as GitHub pre-releases.
|
|
// Accept both PEP 440 (`.dev`) and SemVer (`-dev`) separators so tags
|
|
// like `1.84.0.dev2` and `1.84.0-dev.2` are both detected.
|
|
// PEP 440 post-releases (e.g. `1.84.0.post1`) and legacy `-stable[.patch.N]`
|
|
// are stable maintenance releases, not pre-releases.
|
|
const isPrerelease = /(?:rc|nightly|alpha|beta|[-.]dev)/i.test(tag);
|
|
|
|
// A stable release should only claim the repo "latest" badge when its
|
|
// version is >= the current latest. Otherwise a backport (e.g. 1.84.6)
|
|
// would steal "latest" from a newer line (e.g. 1.88.1).
|
|
const versionKey = (rawTag) => {
|
|
const m = String(rawTag).match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
if (!m) return null;
|
|
const maintenance = String(rawTag).match(/(?:\.post|\.patch\.)(\d+)/i);
|
|
return [Number(m[1]), Number(m[2]), Number(m[3]), maintenance ? Number(maintenance[1]) : 0];
|
|
};
|
|
const isAtLeast = (a, b) => {
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) return a[i] > b[i];
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const cosignSection = [
|
|
`## Verify Docker Image Signature`,
|
|
``,
|
|
`All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit \`0112e53\`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).`,
|
|
``,
|
|
`**Verify using the pinned commit hash (recommended):**`,
|
|
``,
|
|
`A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:`,
|
|
``,
|
|
'```bash',
|
|
`cosign verify \\`,
|
|
` --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \\`,
|
|
` ghcr.io/berriai/litellm:${tag}`,
|
|
'```',
|
|
``,
|
|
`**Verify using the release tag (convenience):**`,
|
|
``,
|
|
`Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:`,
|
|
``,
|
|
'```bash',
|
|
`cosign verify \\`,
|
|
` --key https://raw.githubusercontent.com/BerriAI/litellm/${tag}/cosign.pub \\`,
|
|
` ghcr.io/berriai/litellm:${tag}`,
|
|
'```',
|
|
``,
|
|
`Expected output:`,
|
|
``,
|
|
'```',
|
|
`The following checks were performed on each of these signatures:`,
|
|
` - The cosign claims were validated`,
|
|
` - The signatures were verified against the specified public key`,
|
|
'```',
|
|
``,
|
|
`---`,
|
|
``,
|
|
].join('\n');
|
|
|
|
try {
|
|
let makeLatest = "false";
|
|
const newVersion = versionKey(tag);
|
|
if (!isPrerelease && newVersion) {
|
|
let latestVersion = null;
|
|
try {
|
|
const latest = await github.rest.repos.getLatestRelease({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
});
|
|
latestVersion = versionKey(latest.data.tag_name);
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
makeLatest = (!latestVersion || isAtLeast(newVersion, latestVersion)) ? "true" : "false";
|
|
}
|
|
|
|
try {
|
|
await github.rest.git.createRef({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
ref: `refs/tags/${tag}`,
|
|
sha: commitHash,
|
|
});
|
|
} catch (error) {
|
|
if (error.status !== 422) throw error;
|
|
const existing = await github.rest.git.getRef({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
ref: `tags/${tag}`,
|
|
});
|
|
if (existing.data.object.sha !== commitHash) {
|
|
throw new Error(`Tag ${tag} already exists at ${existing.data.object.sha}, expected ${commitHash}`);
|
|
}
|
|
}
|
|
|
|
const response = await github.rest.repos.createRelease({
|
|
draft: true,
|
|
generate_release_notes: true,
|
|
name: tag,
|
|
owner: context.repo.owner,
|
|
prerelease: isPrerelease,
|
|
repo: context.repo.repo,
|
|
tag_name: tag,
|
|
});
|
|
|
|
const updatedBody = cosignSection + (response.data.body ?? '');
|
|
await github.rest.repos.updateRelease({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
release_id: response.data.id,
|
|
tag_name: tag,
|
|
body: updatedBody,
|
|
draft: false,
|
|
});
|
|
|
|
if (!isPrerelease) {
|
|
await github.rest.repos.updateRelease({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
release_id: response.data.id,
|
|
tag_name: tag,
|
|
make_latest: makeLatest,
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
core.setFailed(error.message);
|
|
}
|
|
|
|
create-branch:
|
|
name: Create Release Branch
|
|
needs: release
|
|
permissions:
|
|
contents: write
|
|
uses: ./.github/workflows/create-release-branch.yml
|
|
with:
|
|
tag: ${{ inputs.tag }}
|
|
commit_hash: ${{ inputs.commit_hash }}
|