Back to Vnote

VNote Version Release

.kilo/skills/release-version/SKILL.md

4.4.212.3 KB
Original Source

VNote Version Release

End-to-end checklist for releasing a new VNote version. Replace X.Y.Z with the target version (e.g. 4.3.0) throughout.

0. Preconditions

  • You are on master (releases are published from master), working tree clean except for the release changes.
  • Decide X.Y.Z. Confirm the previous tag with git tag | Sort-Object -Descending.
  • Submodules are already pinned to their intended commits (see root AGENTS.md § Submodule Push Discipline — push submodules BEFORE the parent).

1. Bump the version (use the script — do NOT hand-edit)

pwsh
python scripts/update_version.py X.Y.Z

scripts/update_version.py is the single source of truth. It updates:

  • CMakeLists.txtproject(... VERSION X.Y.Z ...)
  • .github/workflows/ci-win.yml, ci-linux.yml, ci-macos.yml, ci-linux-tsan.ymlVNOTE_VER: X.Y.Z
  • src/data/core/Info.plist — short (X.Y) and full (X.Y.Z, X.Y.Z.1) strings
  • src/core/configmgr2.cppConfigMgr2::c_version{X, Y, Z}
  • src/data/core/fun.vnote.app.VNote.metainfo.xml — prepends a dated <release> entry

Note: ci-linux-tsan.yml IS in the script's file list (scripts/update_version.py:21) and the [Release] VNote 4.3.0 commit a282ef07 bumped it too, so keep the script's change. Its VNOTE_VER is only a build sanity value and does not publish anything — it just tracks the release version.

Verify: git diff --stat should show exactly the files listed above.

2. Update translations (extract, then fill, both locales)

The two maintained catalogs are src/data/core/translations/vnote_zh_CN.ts (Simplified Chinese) and vnote_ja.ts (Japanese).

2a. Extract new/changed strings with lupdate

lupdate ships with Qt, and the installed Qt version changes over time — do NOT hardcode a path (C:\Qt\6.9.3\... was already gone by 4.3.1). Resolve it first:

pwsh
$lupdate = (Get-Command lupdate -ErrorAction SilentlyContinue).Source
if (-not $lupdate) {
  $lupdate = (Get-ChildItem C:\Qt -Recurse -Filter lupdate.exe -ErrorAction SilentlyContinue |
    Where-Object FullName -like '*msvc*' | Sort-Object FullName -Descending |
    Select-Object -First 1).FullName
}
$lupdate   # e.g. C:\Qt\6.10.3\msvc2022_64\bin\lupdate.exe
pwsh
& $lupdate -no-obsolete -locations relative src `
  -ts src/data/core/translations/vnote_zh_CN.ts src/data/core/translations/vnote_ja.ts
  • -no-obsolete drops entries no longer in the source (keeps the catalog lean).
  • Harmless pdf.js JS parse errors are expected — lupdate still finishes.
  • The summary reports "N new" strings; those become type="unfinished".

2b. Fill in the unfinished translations

Every type="unfinished" entry must be translated for BOTH locales. Count them:

pwsh
(Select-String -Path "src/data/core/translations/vnote_zh_CN.ts" -Pattern 'type="unfinished"').Count
(Select-String -Path "src/data/core/translations/vnote_ja.ts" -Pattern 'type="unfinished"').Count

Extract the source strings needing translation, then for each <message> whose <translation type="unfinished"> is empty, provide the localized text and drop the type="unfinished" attribute (<translation>...</translation>).

Practical approach: script it. Build a source -> translation map per locale and rewrite each unfinished <message> block, escaping &/</> in the output and preserving %1/%2 placeholders, &-accelerators (e.g. &View -> 查看(&V) / 表示(&V)), and literal newlines. Re-run the count above; both must reach 0.

Two traps when scripting the rewrite:

  • Anchor the <source> capture. A pattern like <source>(.*?)</source> with (?s) lets the group swallow whole </message> blocks to reach a later unfinished <translation>, silently deleting every message in between (a whole <context> disappeared this way during 4.3.1). Forbid the closing tag inside the group and stop the gap before the next message: <source>((?:(?!</source>).)*)</source>((?:(?!</message>|<source>).)*?)<translation type="unfinished">
  • Numerus entries are <translation type="unfinished"><numerusform></numerusform></translation>. Fill the <numerusform> rather than replacing the element body, so the plural structure and indentation survive.

Then VERIFY against the pre-edit file — an over-matching regex leaves the counts looking fine while having eaten unrelated entries:

pwsh
foreach ($l in 'zh_CN','ja') {
  git show "HEAD:src/data/core/translations/vnote_$l.ts" > "$env:TEMP\old_$l.ts"
  $o = [xml](Get-Content -Raw "$env:TEMP\old_$l.ts")
  $n = [xml](Get-Content -Raw "src/data/core/translations/vnote_$l.ts")
  # NOTE: PowerShell's XML adapter returns a bare string for <translation> once
  # the type attribute is gone, so .InnerText is $null — normalize first.
  function Tr($m) { $t = $m.translation; if ($null -eq $t) { '' } elseif ($t -is [string]) { $t } else { $t.InnerText } }
  $old = @{}; foreach ($c in $o.TS.context) { foreach ($m in $c.message) { $old["$($c.name)|$($m.source)"] = (Tr $m) } }
  $changed = 0; $bad = 0
  foreach ($c in $n.TS.context) { foreach ($m in $c.message) {
    $k = "$($c.name)|$($m.source)"; $tr = (Tr $m)
    if ($old.ContainsKey($k)) { if ($old[$k] -ne $tr) { $changed++; "CHANGED $k" }; continue }
    $ps = @([regex]::Matches($m.source, '%\d|%n') | ForEach-Object { $_.Value } | Sort-Object -Unique)
    $pt = @([regex]::Matches($tr,        '%\d|%n') | ForEach-Object { $_.Value } | Sort-Object -Unique)
    if (($ps -join ',') -ne ($pt -join ',')) { $bad++; "PLACEHOLDER $k" }
  } }
  "$l changed_existing=$changed placeholder_mismatch=$bad"
}

changed_existing MUST be 0 (only lupdate's own obsolete removals may drop keys — cross-check them against git log / the source tree) and placeholder_mismatch MUST be 0. Finally, compile both catalogs; each must report 0 unfinished:

pwsh
foreach ($l in 'zh_CN','ja') {
  & ($lupdate -replace 'lupdate\.exe$','lrelease.exe') "src/data/core/translations/vnote_$l.ts" -qm "$env:TEMP\$l.qm"
}
Remove-Item "$env:TEMP\zh_CN.qm","$env:TEMP\ja.qm"

The .qm binaries are generated at build time by the lrelease CMake target (see src/CMakeLists.txt), so you do NOT commit .qm files.

3. Write the changelog

Prepend a new ## vX.Y.Z section at the TOP of changes.md (right under the # Changes header, above the previous version).

  • Summarize git log <prev-tag>..HEAD --oneline grouped by theme (Editor, Export, Tasks, Fixes, Security, Translations, …), matching the style of existing entries.
  • Lead with a one-line summary sentence "… on top of VNote <prev>:".
  • If an ## Unreleased section exists, FOLD it into the new ## vX.Y.Z section (it is not a separate release) rather than leaving both.
  • Always end with a Translations bullet noting zh_CN + ja were updated.

4. Review

Per repo AGENTS.md rule 17, delegate to the review subagent (Task tool) for a read-only second opinion on the release diff before finalizing.

5. Commit and trigger the release

CI publishes a (draft) GitHub release from master ONLY when the head commit message starts with [Release] (see the Release job in each ci-*.yml; condition: github.ref == 'refs/heads/master' && startsWith(head_commit.message, '[Release]')). It creates tag vX.Y.Z and uploads the platform artifacts.

  • Commit message MUST start with [Release], e.g. [Release] VNote X.Y.Z.
  • Follow repo AGENTS.md rule 13 for author/commit date (night-time), and only commit when the user explicitly asks.
  • If submodule pointers moved, push submodules first, then the parent (rule + root AGENTS.md § Submodule Push Discipline).

6. Once CI is green, assemble the draft release

The [Release] commit makes CI create a draft GitHub release for tag vX.Y.Z. Wait until ALL platform jobs are green, then make sure the draft carries the four release artifacts before publishing. The ncipollo/release-action step in each ci-*.yml uploads its own platform's asset directly, but confirm all four are present (and if any job's upload was skipped/failed, download that job's build artifact and attach it manually).

The four artifacts (with X.Y.Z substituted):

PlatformArtifact fileProduced by
LinuxVNote-X.Y.Z-linux-x64.AppImageci-linux.yml
macOSVNote-X.Y.Z-mac-<arch>.dmg (universal)ci-macos.yml
Win64 (Qt 6)VNote-X.Y.Z-win64.zipci-win.yml (suffix "")
Windows 7 (Qt 5.15)VNote-X.Y.Z-win64-windows7.zipci-win.yml (suffix -windows7)

Watch the runs and confirm the draft, using gh:

pwsh
# Watch the release-triggering runs on master until they finish.
gh run list --branch master --limit 8
gh run watch <run-id>

# Inspect the draft release and its currently-attached assets.
gh release view vX.Y.Z

If an asset is missing, download it from the corresponding workflow run and upload it to the draft:

pwsh
# Download the build artifact(s) from a finished run into ./_artifacts.
gh run download <run-id> -D _artifacts

# Attach a missing asset to the draft release (repeat per file).
gh release upload vX.Y.Z "_artifacts\<path>\VNote-X.Y.Z-...zip" --clobber

Set the release description from changes.md

The draft's body MUST be the ## vX.Y.Z section of changes.md (the same changelog written in step 3) — nothing more, nothing less. CI seeds a generic body, so overwrite it. Extract exactly that one section (from its ## vX.Y.Z heading up to, but not including, the next ## heading) and set it as the notes:

pwsh
# Extract the ## vX.Y.Z section into a temp notes file...
$ver = "X.Y.Z"
$md  = Get-Content changes.md -Raw
$sec = [regex]::Match($md, "(?ms)^## v$([regex]::Escape($ver))\b.*?(?=^## |\z)").Value.TrimEnd()
Set-Content -Path notes.md -Value $sec -NoNewline -Encoding utf8

# ...and apply it as the draft's description.
gh release edit vX.Y.Z --notes-file notes.md

Drop the leading ## vX.Y.Z line if you prefer the version to appear only as the release title; keep the bullet body either way. Verify with gh release view vX.Y.Z.

Publish

Only when the draft vX.Y.Z release shows all 4 artifacts (linux / macos / win64 / windows7) AND its body matches the changes.md section do you publish it:

pwsh
gh release edit vX.Y.Z --draft=false

Publishing fires the Gitee Mirror workflow (.github/workflows/gitee-mirror.yml), which creates the matching release on gitee.com/vnotex/vnote and prunes all but the two most recent ones. Confirm it started and check its result:

pwsh
gh run list --workflow "Gitee Mirror" --limit 3
curl.exe -s "https://gitee.com/api/v5/repos/vnotex/vnote/releases" |
  ConvertFrom-Json | Select-Object tag_name, name

Expect at most two Gitee releases, the newest with tag_name = vX.Y.Z. If no run appeared, re-drive it manually with gh workflow run "Gitee Mirror" -f tag=vX.Y.Z.

The workflow does not upload binaries — Gitee's attachment endpoint runs at well under 50 KB/s from a GitHub-hosted runner. Download the 4 artifacts from the GitHub release and attach them to the Gitee release by hand:

pwsh
gh release download vX.Y.Z -D gitee-assets

Then upload them at https://gitee.com/vnotex/vnote/releases → edit the release. Ordering does not matter: the workflow calls no attachment endpoint, and the release it just mirrored is always one of the two it keeps, so re-running it can never strip the files you uploaded. (Pruning does delete older releases along with whatever was attached to them.)

Quick reference

StepCommand / File
Version bumppython scripts/update_version.py X.Y.Z
Extract stringslupdate -no-obsolete -locations relative src -ts <zh_CN.ts> <ja.ts>
Fill translationsedit vnote_zh_CN.ts, vnote_ja.ts until 0 unfinished
Changelogprepend ## vX.Y.Z to changes.md
Release triggercommit on master with message starting [Release]
Set descriptiongh release edit vX.Y.Z --notes-file notes.md (the ## vX.Y.Z section of changes.md)
Assemble releasewait for green CI, ensure draft vX.Y.Z has all 4 artifacts + changelog body, then gh release edit vX.Y.Z --draft=false

Release artifacts (must all be present before publishing)

  1. VNote-X.Y.Z-linux-x64.AppImage (linux)
  2. VNote-X.Y.Z-mac-<arch>.dmg (macos, universal)
  3. VNote-X.Y.Z-win64.zip (win64, Qt 6)
  4. VNote-X.Y.Z-win64-windows7.zip (windows7, Qt 5.15)