fix: sort alpha releases chronologically

This commit is contained in:
lucaronin
2026-04-21 09:10:16 +02:00
parent 8cefbe949a
commit 7be5519f61
5 changed files with 88 additions and 7 deletions

View File

@@ -42,6 +42,21 @@ jobs:
output = subprocess.check_output(command, text=True).strip()
return [line for line in output.splitlines() if line]
alpha_pattern = re.compile(r"^alpha-v(\d{4}\.\d{1,2}\.\d{1,2})-alpha\.(\d+)$")
def parse_alpha_tag(tag: str) -> tuple[str, int] | None:
match = alpha_pattern.fullmatch(tag)
if not match:
return None
calendar_version, sequence = match.groups()
return calendar_version, int(sequence)
def alpha_version(calendar_version: str, sequence: int) -> str:
return f"{calendar_version}-alpha.{sequence}"
def alpha_tag(calendar_version: str, sequence: int) -> str:
return f"alpha-v{calendar_version}-alpha.{sequence:04d}"
existing_tags = [
tag for tag in lines(["git", "tag", "--points-at", "HEAD"])
if tag.startswith("alpha-v")
@@ -49,7 +64,8 @@ jobs:
if existing_tags:
tag = existing_tags[0]
version = tag.removeprefix("alpha-v")
parsed = parse_alpha_tag(tag)
version = alpha_version(*parsed) if parsed is not None else tag.removeprefix("alpha-v")
else:
today = datetime.now(timezone.utc).date()
stable_date = None
@@ -71,8 +87,8 @@ jobs:
calendar_version = f"{alpha_date.year}.{alpha_date.month}.{alpha_date.day}"
sequence = len(lines(["git", "tag", "--list", f"alpha-v{calendar_version}-alpha.*"])) + 1
version = f"{calendar_version}-alpha.{sequence}"
tag = f"alpha-v{version}"
version = alpha_version(calendar_version, sequence)
tag = alpha_tag(calendar_version, sequence)
display_match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)", version)
display_version = (
@@ -275,7 +291,27 @@ jobs:
- name: Generate release notes
run: |
PREV_TAG=$(git tag --list 'alpha-v*' --sort=-version:refname | head -n 1 || echo "")
PREV_TAG=$(python3 <<'PY'
import re
import subprocess
current_tag = '${{ needs.version.outputs.tag }}'
pattern = re.compile(r'^alpha-v(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)$')
output = subprocess.check_output(['git', 'tag', '--list', 'alpha-v*'], text=True).strip()
tags = [line for line in output.splitlines() if line and line != current_tag]
parsed_tags = []
for tag in tags:
match = pattern.fullmatch(tag)
if not match:
continue
year, month, day, sequence = map(int, match.groups())
parsed_tags.append(((year, month, day, sequence), tag))
print(max(parsed_tags)[1] if parsed_tags else '')
PY
)
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else

View File

@@ -669,6 +669,6 @@ Managed by `useSettings` hook and `SettingsPanel` component. `default_ai_agent`
- **`download_and_install_app_update`** — Channel-aware download/install with streamed progress events.
### CI/CD
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. Publishes `alpha/latest.json` and refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed.
- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` and refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed.
- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`.
- **Beta cohorts** are handled in PostHog targeting only. There is no beta updater feed.

View File

@@ -782,6 +782,7 @@ Every push to `main` triggers `.github/workflows/release.yml`:
```
push to main
→ version job: compute calendar alpha version YYYY.M.D-alpha.N
and a GitHub-sorted tag alpha-vYYYY.M.D-alpha.NNNN
→ use today's UTC date unless the latest stable-vYYYY.M.D tag already uses today
→ if stable already uses today, advance alpha to the next calendar day so semver still increases
→ build job:
@@ -789,7 +790,7 @@ push to main
→ upload signed .app.tar.gz + .sig updater artifacts
→ release job:
→ generate alpha-latest.json
→ publish GitHub prerelease alpha-v<version> named Tolaria Alpha YYYY.M.D.N
→ publish GitHub prerelease alpha-vYYYY.M.D-alpha.NNNN named Tolaria Alpha YYYY.M.D.N
→ pages job:
→ build static HTML release history page
→ publish alpha/latest.json
@@ -819,7 +820,7 @@ push stable-vYYYY.M.D tag
### Versioning
- Stable promotions use git tags in the form `stable-vYYYY.M.D` and stamp the technical version `YYYY.M.D`.
- Alpha builds stamp the technical version `YYYY.M.D-alpha.N` and display it as `Alpha YYYY.M.D.N`.
- Alpha builds stamp the technical version `YYYY.M.D-alpha.N` and display it as `Alpha YYYY.M.D.N`. The GitHub release tag zero-pads the sequence as `alpha-vYYYY.M.D-alpha.NNNN` so GitHub release ordering remains chronological.
- If the latest stable tag already uses today's date, alpha advances to the next calendar day before assigning `-alpha.N` so Alpha remains semver-newer than Stable across channel switches.
- The workflows stamp the computed version into `tauri.conf.json` and `Cargo.toml` at build time.
- This keeps display strings clean while preserving semver monotonicity when a user switches between Stable and Alpha.

View File

@@ -60,6 +60,35 @@ describe('buildReleaseHistoryPage', () => {
expect(html).toContain('<p>First paragraph<br>with a line break.</p><p>Second paragraph</p>')
})
it('sorts releases within each channel by published date descending even when the payload order is wrong', () => {
const html = buildReleaseHistoryPage([
{
body: 'Older alpha release',
name: 'Tolaria Alpha 2026.4.20.9',
prerelease: true,
published_at: '2026-04-20T09:44:02Z',
tag_name: 'alpha-v2026.4.20-alpha.9',
},
{
body: 'Newest alpha release',
name: 'Tolaria Alpha 2026.4.20.12',
prerelease: true,
published_at: '2026-04-20T16:53:41Z',
tag_name: 'alpha-v2026.4.20-alpha.12',
},
{
body: 'Middle alpha release',
name: 'Tolaria Alpha 2026.4.20.10',
prerelease: true,
published_at: '2026-04-20T10:32:01Z',
tag_name: 'alpha-v2026.4.20-alpha.10',
},
])
expect(html.indexOf('Tolaria Alpha 2026.4.20.12')).toBeLessThan(html.indexOf('Tolaria Alpha 2026.4.20.10'))
expect(html.indexOf('Tolaria Alpha 2026.4.20.10')).toBeLessThan(html.indexOf('Tolaria Alpha 2026.4.20.9'))
})
it('filters draft releases and shows an empty state for channels without published builds', () => {
const html = buildReleaseHistoryPage([
{

View File

@@ -27,6 +27,7 @@ type ReleaseEntry = {
githubUrl: string | null
notesHtml: string
publishedLabel: string
publishedTimestamp: number
tagName: string
title: string
}
@@ -419,6 +420,15 @@ function formatPublishedLabel(value: unknown): string {
})
}
function parsePublishedTimestamp(value: unknown): number {
const text = normalizeText(value)
if (text === null) return Number.NEGATIVE_INFINITY
const publishedAt = new Date(text)
const timestamp = publishedAt.getTime()
return Number.isNaN(timestamp) ? Number.NEGATIVE_INFINITY : timestamp
}
function isDownloadableAsset(name: string): boolean {
return name.endsWith('.dmg') || name.endsWith('.app.tar.gz') || name.endsWith('.zip')
}
@@ -471,6 +481,7 @@ function normalizeReleaseEntry(release: GitHubReleasePayload): [ReleaseChannel,
githubUrl: normalizeUrl(release.html_url),
notesHtml: resolveReleaseNotesHtml(release.body_html, release.body),
publishedLabel: formatPublishedLabel(release.published_at),
publishedTimestamp: parsePublishedTimestamp(release.published_at),
tagName,
title,
}]
@@ -490,6 +501,10 @@ function collectReleaseSections(payload: unknown): ReleaseSections {
sections[channel].push(release)
}
for (const channel of ['stable', 'alpha'] as const) {
sections[channel].sort((left, right) => right.publishedTimestamp - left.publishedTimestamp)
}
return sections
}