mirror of
https://github.com/ankitects/anki.git
synced 2026-05-16 04:30:39 -04:00
ci: support releases from non-main branches (#4794)
Closes #4793 - Add `workflow_dispatch` trigger to CI (with macOS/Windows support) - Allow prepare-release and release workflows from any branch - Add `skip-ci-check` input for hotfix releases - Add `just release::prepare` and `just ci` recipes - Make `qt/release/build.sh` find uv in CI and local builds - Change publish-testpypi environment from testpypi to release - Add anki-release wheel build step --------- Co-authored-by: Andrew Sanchez <andrewsanchez@users.noreply.github.com> Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
@@ -146,6 +147,7 @@ jobs:
|
||||
check-macos:
|
||||
if: >-
|
||||
github.event_name == 'push'
|
||||
|| github.event_name == 'workflow_dispatch'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'check:macos')
|
||||
runs-on: macos-latest
|
||||
name: check (macos)
|
||||
@@ -209,6 +211,7 @@ jobs:
|
||||
check-windows:
|
||||
if: >-
|
||||
github.event_name == 'push'
|
||||
|| github.event_name == 'workflow_dispatch'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'check:windows')
|
||||
runs-on: windows-latest
|
||||
name: check (windows)
|
||||
|
||||
29
.github/workflows/prepare-release.yml
vendored
29
.github/workflows/prepare-release.yml
vendored
@@ -8,6 +8,11 @@ on:
|
||||
description: "Version (e.g. 26.04 for stable, 26.04b1 or 26.04rc1 for pre-release)"
|
||||
required: true
|
||||
type: string
|
||||
skip-ci-check:
|
||||
description: "Skip the CI status check (for hotfix releases from non-main branches)"
|
||||
default: false
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -31,14 +36,6 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Require dispatch from main
|
||||
if: github.ref_name != 'main'
|
||||
run: |
|
||||
echo "::error::Must be dispatched from main. Got ref $REF_NAME."
|
||||
exit 1
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Validate version
|
||||
run: |
|
||||
pip install --break-system-packages 'packaging>=24,<26'
|
||||
@@ -49,15 +46,15 @@ jobs:
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
|
||||
- name: Check CI passed on main
|
||||
- name: Check CI passed
|
||||
if: inputs.skip-ci-check != true
|
||||
run: |
|
||||
conclusion=$(gh run list \
|
||||
--workflow=ci.yml \
|
||||
-e push \
|
||||
--commit "$COMMIT_SHA" \
|
||||
--limit 1 \
|
||||
--json conclusion \
|
||||
--jq ".[0].conclusion")
|
||||
--limit 5 \
|
||||
--json conclusion,event \
|
||||
--jq '[.[] | select(.event == "push" or .event == "workflow_dispatch")][0].conclusion')
|
||||
|
||||
if [[ -z "$conclusion" ]]; then
|
||||
echo "::error::Could not determine CI status for commit $COMMIT_SHA"
|
||||
@@ -103,5 +100,7 @@ jobs:
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
|
||||
- name: Push to main
|
||||
run: git push origin HEAD:refs/heads/main
|
||||
- name: Push to branch
|
||||
run: git push origin HEAD:refs/heads/$BRANCH
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
|
||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -5,9 +5,14 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version for draft release/PyPI (must match .version on main)"
|
||||
description: "Version for draft release/PyPI (must match .version)"
|
||||
required: true
|
||||
type: string
|
||||
skip-ci-check:
|
||||
description: "Skip the CI status check (for hotfix releases from non-main branches)"
|
||||
default: false
|
||||
required: false
|
||||
type: boolean
|
||||
sign:
|
||||
description: "Sign macOS and Windows artifacts (requires release environment)"
|
||||
default: false
|
||||
@@ -107,26 +112,15 @@ jobs:
|
||||
DRAFT_RELEASE: ${{ inputs['draft-release'] }}
|
||||
PUBLISH_PYPI: ${{ inputs['publish-pypi'] }}
|
||||
|
||||
- name: Require dispatch from main
|
||||
if: ${{ steps.validate.outputs.public_release == 'true' }}
|
||||
run: |
|
||||
if [ "$REF_NAME" != "main" ]; then
|
||||
echo "::error::Releases must be dispatched from main. Got $REF_NAME."
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Check CI passed on main
|
||||
if: ${{ steps.validate.outputs.public_release == 'true' }}
|
||||
- name: Check CI passed
|
||||
if: ${{ steps.validate.outputs.public_release == 'true' && inputs.skip-ci-check != true }}
|
||||
run: |
|
||||
conclusion=$(gh run list \
|
||||
--workflow=ci.yml \
|
||||
-e push \
|
||||
--commit "$COMMIT_SHA" \
|
||||
--limit 1 \
|
||||
--json conclusion \
|
||||
--jq ".[0].conclusion")
|
||||
--limit 5 \
|
||||
--json conclusion,event \
|
||||
--jq '[.[] | select(.event == "push" or .event == "workflow_dispatch")][0].conclusion')
|
||||
|
||||
if [[ -z "$conclusion" ]]; then
|
||||
echo "::error::Could not determine CI status for commit $COMMIT_SHA"
|
||||
@@ -401,6 +395,9 @@ jobs:
|
||||
- name: Build wheels
|
||||
run: ./ninja wheels
|
||||
|
||||
- name: Build anki-release wheel
|
||||
run: cd qt/release && bash build.sh
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-linux-x86
|
||||
@@ -519,7 +516,7 @@ jobs:
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
environment: testpypi
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
Releases are managed by two GitHub Actions workflows under `.github/workflows/`:
|
||||
|
||||
1. **`prepare-release.yml`** — Run first. Validates the version, checks that CI
|
||||
passed on main, syncs translations, updates `.version`, and pushes everything
|
||||
to main in a single commit. Normal CI then runs on the resulting commit.
|
||||
passed, syncs translations, updates `.version`, and pushes everything
|
||||
to the dispatching branch in a single commit. Normal CI then runs on the
|
||||
resulting commit. Can be dispatched from any branch (not just main) for
|
||||
hotfix/security releases. The CI check can be skipped with `skip-ci-check`.
|
||||
|
||||
2. **`release.yml`** — Run after CI passes on the prepared commit. Builds
|
||||
installers and wheels for all platforms (Linux x86/ARM, macOS Intel/ARM,
|
||||
@@ -18,7 +20,7 @@ they cannot run simultaneously.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["<b>prepare-release.yml</b><br/>validate version<br/>check CI ✓<br/>check duplicate tag<br/>sync translations<br/>update .version<br/>push to main"] --> B["<b>CI (ci.yml)</b><br/>runs automatically<br/>on the new commit"]
|
||||
A["<b>prepare-release.yml</b><br/>validate version<br/>check CI ✓<br/>check duplicate tag<br/>sync translations<br/>update .version<br/>push to branch"] --> B["<b>CI (ci.yml)</b><br/>runs automatically<br/>on the new commit"]
|
||||
B --> C["<b>release.yml</b><br/>build all platforms<br/>optionally sign macOS/Windows<br/>optionally create draft GitHub release<br/>optionally publish to TestPyPI/PyPI"]
|
||||
|
||||
style A fill:#2d333b,stroke:#539bf5,color:#adbac7
|
||||
@@ -82,20 +84,24 @@ suffixes (`b1`, `rc1`, `a1`). Months must be zero-padded.
|
||||
|
||||
## Workflow inputs
|
||||
|
||||
**prepare-release:** takes a `version` string.
|
||||
**prepare-release:** takes a `version` string and an optional `skip-ci-check`
|
||||
boolean (default `false`). Set `skip-ci-check=true` for hotfix releases from
|
||||
non-main branches where CI was triggered via `workflow_dispatch` or hasn't run
|
||||
on the branch yet.
|
||||
|
||||
**release:** takes a `version` (must match `.version` on main for public
|
||||
release operations) and four boolean inputs:
|
||||
**release:** takes a `version` (must match `.version` for public release
|
||||
operations) and five boolean inputs:
|
||||
|
||||
- `sign` signs macOS and Windows artifacts.
|
||||
- `draft-release` creates the draft GitHub release.
|
||||
- `publish-testpypi` publishes wheels to TestPyPI.
|
||||
- `publish-pypi` publishes wheels to PyPI.
|
||||
- `skip-ci-check` skips the CI status check (for hotfix releases).
|
||||
|
||||
All four booleans default to `false`. Non-release runs use the `.version`
|
||||
All booleans default to `false`. Non-release runs use the `.version`
|
||||
already in the repo, so builds work without a prepare step.
|
||||
|
||||
For a normal public release, enable all four booleans: `sign=true`,
|
||||
For a normal public release, enable the first four booleans: `sign=true`,
|
||||
`draft-release=true`, `publish-testpypi=true`, and `publish-pypi=true`.
|
||||
|
||||
## Environment gates
|
||||
@@ -105,11 +111,9 @@ The release workflow uses GitHub
|
||||
as manual approval gates. Jobs that access signing credentials or publish
|
||||
artifacts require a reviewer to approve the deployment before they run:
|
||||
|
||||
- **`release`** — Required when `sign`, `draft-release`, or `publish-pypi` is
|
||||
enabled. Protects code-signing secrets, the release token, and PyPI trusted
|
||||
publishing/OIDC.
|
||||
- **`testpypi`** — Required by the TestPyPI publishing job. Allows test
|
||||
uploads to be gated separately from production releases.
|
||||
- **`release`** — Required when `sign`, `draft-release`, `publish-testpypi`, or
|
||||
`publish-pypi` is enabled. Protects code-signing secrets, the release token,
|
||||
and PyPI/TestPyPI trusted publishing/OIDC.
|
||||
|
||||
When `sign` is disabled, the macOS and Windows build jobs run without the
|
||||
`release` environment so they do not require approval and cannot access signing
|
||||
@@ -120,13 +124,14 @@ secrets.
|
||||
The `release.yml` workflow uses independent boolean inputs to control what gets
|
||||
signed and published:
|
||||
|
||||
| Input | Effect |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `sign` | Signs macOS and Windows artifacts. Requires the `release` environment. When false, those jobs upload unsigned artifacts and do not access signing secrets. |
|
||||
| `draft-release` | Creates a draft GitHub release with generated release notes and installer artifacts. Requires `sign=true`, the `release` environment, main branch, passing CI, no duplicate tag/release, and `version` matching `.version`. |
|
||||
| `publish-testpypi` | Publishes wheels to TestPyPI. Requires the `testpypi` environment. |
|
||||
| `publish-pypi` | Publishes wheels to PyPI. Requires the `release` environment, main branch, passing CI, and `version` matching `.version`. It also runs and waits for the TestPyPI publish job first. It does not require signing unless `draft-release=true`. |
|
||||
| `version` | For `draft-release` or `publish-pypi`: must match `.version` on main. For build-only, signed-only, or TestPyPI-only runs: ignored (`.version` from the branch is used automatically). |
|
||||
| Input | Effect |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `sign` | Signs macOS and Windows artifacts. Requires the `release` environment. When false, those jobs upload unsigned artifacts and do not access signing secrets. |
|
||||
| `draft-release` | Creates a draft GitHub release with generated release notes and installer artifacts. Requires `sign=true`, the `release` environment, passing CI (unless skipped), no duplicate tag/release, and `version` matching `.version`. |
|
||||
| `publish-testpypi` | Publishes wheels to TestPyPI. Requires the `release` environment. |
|
||||
| `publish-pypi` | Publishes wheels to PyPI. Requires the `release` environment, passing CI (unless skipped), and `version` matching `.version`. It also runs and waits for the TestPyPI publish job first. It does not require signing unless `draft-release=true`. |
|
||||
| `skip-ci-check` | Skips the CI status check. Useful for hotfix releases from non-main branches where CI was run via `workflow_dispatch`. |
|
||||
| `version` | For `draft-release` or `publish-pypi`: must match `.version`. For build-only, signed-only, or TestPyPI-only runs: ignored (`.version` from the branch is used automatically). |
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -144,14 +149,14 @@ flowchart TD
|
||||
unsigned --> artifacts
|
||||
|
||||
artifacts --> draft{"draft-release?"}
|
||||
draft -- Yes --> guards1["Release guards\n<i>main, CI, duplicate check,\nversion matches .version</i>"]
|
||||
draft -- Yes --> guards1["Release guards\n<i>CI (unless skipped), duplicate check,\nversion matches .version</i>"]
|
||||
guards1 --> ghrel["Create draft GitHub release\n<i>requires release env</i>"]
|
||||
|
||||
artifacts --> testpypi{"publish-testpypi\nor publish-pypi?"}
|
||||
testpypi -- Yes --> tpypi["Publish to TestPyPI\n<i>requires testpypi env</i>"]
|
||||
testpypi -- Yes --> tpypi["Publish to TestPyPI\n<i>requires release env</i>"]
|
||||
|
||||
tpypi --> pypi{"publish-pypi?"}
|
||||
pypi -- Yes --> guards2["Release guards\n<i>main, CI,\nversion matches .version</i>"]
|
||||
pypi -- Yes --> guards2["Release guards\n<i>CI (unless skipped),\nversion matches .version</i>"]
|
||||
ghrel --> pypi
|
||||
guards2 --> realpypi["Publish to PyPI\n<i>requires release env</i>"]
|
||||
|
||||
@@ -166,16 +171,8 @@ Release workflows can be dispatched via `just` using the `release` module
|
||||
defined in `release.just`. All recipes read the version from `.version`
|
||||
automatically.
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------------------------------------ | ---------------------------------------------------- |
|
||||
| `just release::build` | Build-only from HEAD (all booleans false) |
|
||||
| `just release::build <ref>` | Build-only from a specific branch |
|
||||
| `just release::sign` | Build and sign from HEAD |
|
||||
| `just release::sign <ref>` | Build and sign from a specific branch |
|
||||
| `just release::public` | Full release from main (sign, draft, testpypi, pypi) |
|
||||
| `just release::custom <ref> sign=true publish-testpypi=true` | Mix and match flags |
|
||||
|
||||
Run `just --list --list-submodules` to see all available recipes.
|
||||
Run `just --list --list-submodules` to see all available recipes and their
|
||||
arguments.
|
||||
|
||||
## Testing the release workflow from a feature branch
|
||||
|
||||
@@ -211,13 +208,27 @@ To test the signing flow from a feature branch:
|
||||
> modified on your branch, use `gh workflow run` to trigger it — the UI
|
||||
> dropdown won't show it until it's merged to main.
|
||||
|
||||
`prepare-release.yml` cannot be tested from a non-main branch — it
|
||||
unconditionally requires `main`. To validate its scripts locally, run:
|
||||
### Hotfix / security releases from non-main branches
|
||||
|
||||
```
|
||||
pip install 'packaging>=24,<26'
|
||||
python3 .github/scripts/validate_version.py <version> <current_version>
|
||||
```
|
||||
Both `prepare-release.yml` and `release.yml` can be dispatched from any branch.
|
||||
For a hotfix release (e.g. from a `25.09.3` branch):
|
||||
|
||||
1. Trigger CI on the branch:
|
||||
```
|
||||
just ci 25.09.3
|
||||
```
|
||||
2. Prepare the release (skip CI check if CI was triggered via `workflow_dispatch`):
|
||||
```
|
||||
just release::prepare 25.09.3 skip-ci-check=true
|
||||
```
|
||||
3. Run the release workflow:
|
||||
```
|
||||
just release::public 25.09.3
|
||||
```
|
||||
Or with `skip-ci-check`:
|
||||
```
|
||||
just release::custom 25.09.3 sign=true draft-release=true publish-testpypi=true publish-pypi=true skip-ci-check=true
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
|
||||
4
justfile
4
justfile
@@ -73,5 +73,9 @@ docs-serve:
|
||||
docs-rust:
|
||||
cargo doc --open
|
||||
|
||||
# Dispatch CI workflow on a given branch or tag
|
||||
ci branch:
|
||||
gh workflow run ci.yml --ref {{ branch }}
|
||||
|
||||
# Helper to get the right ninja command for the platform
|
||||
ninja := if os() == "windows" { "tools\\ninja" } else { "./ninja" }
|
||||
|
||||
@@ -10,8 +10,8 @@ test -f build.sh || {
|
||||
# Get the project root (two levels up from qt/release)
|
||||
PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
# Use extracted uv binary
|
||||
UV="$PROJ_ROOT/out/extracted/uv/uv"
|
||||
# Use uv from PATH (CI), falling back to the extracted copy (local builds)
|
||||
UV="${UV_BINARY:-$(command -v uv 2>/dev/null || echo "$PROJ_ROOT/out/extracted/uv/uv")}"
|
||||
|
||||
# Read version from .version file
|
||||
VERSION=$(cat "$PROJ_ROOT/.version" | tr -d '[:space:]')
|
||||
|
||||
15
release.just
15
release.just
@@ -2,6 +2,12 @@ set windows-shell := ["cmd.exe", "/c"]
|
||||
|
||||
version := `cat .version`
|
||||
|
||||
# Prepare a release: validate version, check CI, sync translations, update .version
|
||||
prepare ref="HEAD" skip-ci-check="false":
|
||||
gh workflow run prepare-release.yml --ref {{ ref }} \
|
||||
-f version="{{ version }}" \
|
||||
-f skip-ci-check={{ skip-ci-check }}
|
||||
|
||||
# Build all platforms, no signing or publishing
|
||||
build ref="HEAD":
|
||||
gh workflow run release.yml --ref {{ ref }} \
|
||||
@@ -21,8 +27,8 @@ sign ref="HEAD":
|
||||
-f publish-pypi=false
|
||||
|
||||
# Full public release: sign, draft, testpypi, pypi
|
||||
public:
|
||||
gh workflow run release.yml --ref main \
|
||||
public ref="main":
|
||||
gh workflow run release.yml --ref {{ ref }} \
|
||||
-f version="{{ version }}" \
|
||||
-f sign=true \
|
||||
-f draft-release=true \
|
||||
@@ -30,10 +36,11 @@ public:
|
||||
-f publish-pypi=true
|
||||
|
||||
# Custom release with explicit flags
|
||||
custom ref="HEAD" sign="false" draft-release="false" publish-testpypi="false" publish-pypi="false":
|
||||
custom ref="HEAD" sign="false" draft-release="false" publish-testpypi="false" publish-pypi="false" skip-ci-check="false":
|
||||
gh workflow run release.yml --ref {{ ref }} \
|
||||
-f version="{{ version }}" \
|
||||
-f sign={{ sign }} \
|
||||
-f draft-release={{ draft-release }} \
|
||||
-f publish-testpypi={{ publish-testpypi }} \
|
||||
-f publish-pypi={{ publish-pypi }}
|
||||
-f publish-pypi={{ publish-pypi }} \
|
||||
-f skip-ci-check={{ skip-ci-check }}
|
||||
|
||||
Reference in New Issue
Block a user