Files
msime-windows/.github/workflows/release.yml
T
Evan 4fc16952b3 wip(windows): 补齐隐私与许可交付,修好外部人的构建入口
- 新增 PRIVACY.md:逐项写明每个联网端点、发送内容、默认开关与关闭方式,明确云候选默认开启、
  token 在 config.toml 明文存储、无任何遥测
- 设置页「用户协议」「隐私政策」两个无 handler 的死按钮接上;前者改名为「开源许可协议」指向
  LICENSE,开源产品的用户条款就是许可证本身,不再承诺一份不存在的 EULA
- 安装包随包装入 GPL 正文并在安装向导显示(GPLv3 §4/§6),补上打包测试断言
- release.yml 增加 build provenance attestation,并在发布后向 MSIME-Web 发 repository_dispatch,
  让 msime.app/update.json 不再等两小时一次的 cron
- 所有 CMakePresets 的 vcpkg 路径改用 $env{VCPKG_ROOT},server 的 Boost 回退不再指向个人目录
- ui/README.md 指向本机绝对路径的 markdown 死链改成相对链接
- 新增 check-developer-paths.sh 并接入 required 的 quality 检查,防止路径回潮
- docs/installation.md 不再写死版本号
2026-09-06 12:05:06 +09:00

494 lines
20 KiB
YAML

name: Release
# Releasing is automatic. A push to main that carries a fix or a feat ends with a published installer, without anybody merging or dispatching anything: this workflow refreshes release-please's pull request, tests it, merges it, turns the merge into a draft release, then builds, signs and publishes that draft.
#
# The whole cycle runs inside the one run that the original push started. land-release-pr.sh merges with GITHUB_TOKEN, whose pushes do not start workflow runs, so the merge cannot produce a second Release run queued behind this one on the concurrency group.
#
# This costs a code signing signature on every releasable merge to main, which is exactly what the previous manual design existed to avoid. That is a deliberate trade: see docs/product-release.md for what it buys and what it spends.
#
# workflow_dispatch stays, and is now a repair path rather than the normal one. It builds an existing draft, which is what a run that failed after creating the draft leaves behind.
on:
push:
branches:
- main
workflow_dispatch:
inputs:
tag:
description: Draft release tag to build, sign and publish, for example v2026.9.1
required: true
type: string
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
prepare:
name: Prepare release metadata
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
# Read the PR-triggered run; merging stays scoped to GITHUB_TOKEN.
actions: read
contents: write
pull-requests: write
outputs:
should_publish: ${{ github.event_name == 'workflow_dispatch' || steps.target.outputs.release_created == 'true' }}
tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || steps.target.outputs.tag_name }}
version: ${{ github.event_name == 'workflow_dispatch' && steps.requested_release.outputs.version || steps.target.outputs.version }}
target_sha: ${{ github.event_name == 'workflow_dispatch' && steps.requested_release.outputs.target_sha || steps.target.outputs.target_sha }}
steps:
- name: Check out release automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Separate release metadata from PR updates to preserve release-please ordering.
# Recover an already-merged release before calculating the next PR, matching release-please's
# normal releases-then-pull-requests ordering.
- name: Require the release metadata credential
if: ${{ github.event_name == 'push' }}
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
run: |
if [[ -z "$RELEASE_TOKEN" ]]; then
echo '::error::RELEASE_PLEASE_TOKEN must allow contents and workflows writes to create release tags.'
exit 1
fi
- name: Recover releases from previously merged pull requests
if: ${{ github.event_name == 'push' }}
id: release_pr
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
skip-github-pull-request: true
- name: Refresh the release pull request
if: ${{ github.event_name == 'push' }}
id: release_update
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
with:
# A trusted credential starts normal PR checks without manual workflow approval.
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
skip-github-release: true
- name: Test and merge the release pull request
if: ${{ github.event_name == 'push' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: bash scripts/ci/land-release-pr.sh
- name: Create the release for the merged pull request
if: ${{ github.event_name == 'push' }}
id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
with:
# Creating a tag at a historical workflow-changing commit needs workflows:write.
# Only GITHUB_TOKEN merges to main, preventing a second Release run.
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
skip-github-pull-request: true
- name: Resolve the release this run publishes
if: ${{ github.event_name == 'push' }}
id: target
env:
FIRST_CREATED: ${{ steps.release_pr.outputs.release_created }}
FIRST_TAG: ${{ steps.release_pr.outputs.tag_name }}
FIRST_VERSION: ${{ steps.release_pr.outputs.version }}
FIRST_SHA: ${{ steps.release_pr.outputs.sha }}
SECOND_CREATED: ${{ steps.release.outputs.release_created }}
SECOND_TAG: ${{ steps.release.outputs.tag_name }}
SECOND_VERSION: ${{ steps.release.outputs.version }}
SECOND_SHA: ${{ steps.release.outputs.sha }}
run: bash scripts/ci/resolve-auto-release.sh
- name: Validate requested draft release
if: ${{ github.event_name == 'workflow_dispatch' }}
id: requested_release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ inputs.tag }}
run: bash scripts/ci/validate-draft-release.sh
dependencies:
name: Load the release product lock
needs: prepare
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 60
outputs:
dictionary_tag: ${{ steps.lock.outputs.dictionary_tag }}
steps:
- name: Read dependencies from the release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.target_sha }}
persist-credentials: false
# verify-published is a release gate and is deliberately absent from ci.yml: a pull request
# legitimately locks a branch commit while a change lands in the engine and here at once, and
# enforcing reachability there would deadlock the landing it protects.
- name: Validate and export the product lock
id: lock
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 scripts/product_lock.py verify-contracts .
python3 scripts/product_lock.py verify-published
python3 scripts/product_lock.py outputs --github-output "$GITHUB_OUTPUT"
tsf:
name: Build TSF ${{ matrix.name }}
needs: [prepare, dependencies]
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: windows-2025
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- name: x64
architecture: x64
triplet: x64-windows-static
build_dir: windows/build64-release
- name: Win32
architecture: Win32
triplet: x86-windows-static
build_dir: windows/build32-release
steps:
- name: Check out release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.target_sha }}
submodules: recursive
persist-credentials: false
- name: Stamp the version resource
shell: pwsh
run: |
python scripts/apply_version.py
python scripts/apply_version.py --check
- name: Install dependencies
shell: pwsh
working-directory: windows
run: vcpkg install --triplet ${{ matrix.triplet }}
# CMakePresets.json points at a developer machine's vcpkg, so the presets cannot be used
# here. This mirrors what .github/workflows/ci.yml already does, into the build directory
# layout that the installer's Prepare-PackageFiles.ps1 expects.
- name: Configure
shell: pwsh
run: >
cmake -S windows -B ${{ matrix.build_dir }} -A ${{ matrix.architecture }}
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake"
-DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }}
- name: Build
shell: pwsh
run: cmake --build ${{ matrix.build_dir }} --config Release --parallel
- name: Check the DLL the installer expects
shell: pwsh
run: ./scripts/ci/check-tsf-dll.ps1 -BuildDir '${{ matrix.build_dir }}'
- name: Test the wire contract on this architecture
run: ctest --test-dir ${{ matrix.build_dir }} -C Release --output-on-failure
- name: Upload the DLL
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tsf-${{ matrix.name }}
path: ${{ matrix.build_dir }}/Release/MetasequoiaImeTsf.dll
if-no-files-found: error
server:
name: Build Server
needs: [prepare, dependencies]
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: windows-2025
timeout-minutes: 90
steps:
- name: Check out release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.target_sha }}
submodules: recursive
persist-credentials: false
- name: Install Boost
shell: pwsh
run: ./scripts/ci/install-boost.ps1
# The package job stages these outputs into the installer's expected directory.
- name: Configure
shell: pwsh
run: >
cmake -S server -B server/build-release -A x64
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake"
-DVCPKG_TARGET_TRIPLET=x64-windows
- name: Build
shell: pwsh
run: cmake --build server/build-release --config Release --parallel
- name: Check the binaries the installer requires
shell: pwsh
run: ./scripts/ci/check-server-binaries.ps1 -BuildDir server/build-release/bin/Release
- name: Test Server with the release's exact dictionaries
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./scripts/ci/test-server.ps1 -ServerRoot server -StagingRoot "$env:RUNNER_TEMP/product-data" -BuildDir build-release
- name: Upload the Server build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: server
path: server/build-release/bin/Release
if-no-files-found: error
ui:
name: Build settings page
needs: [prepare, dependencies]
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Check out release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.target_sha }}
persist-credentials: false
submodules: true
- name: Verify the shipped pages carry the engine's contract
run: python3 ui-html/scripts/sync-contracts.py --check
- name: Set up pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 10
- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: pnpm
cache-dependency-path: ui-html/webview2/settings/ime-settings/pnpm-lock.yaml
- name: Install and build the settings page
working-directory: ui-html/webview2/settings/ime-settings
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Upload the web assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: webview2
path: |
ui-html/webview2/candwnd
ui-html/webview2/ftb
ui-html/webview2/menu
ui-html/webview2/shared
ui-html/webview2/settings/ime-settings/dist
if-no-files-found: error
package:
name: Package and publish installer
needs: [prepare, dependencies, tsf, server, ui]
# Stated explicitly even though skipped needs would already skip this job. This is the only job
# that spends a signature, so the condition that stops it belongs here rather than being a
# consequence of four other jobs' conditions that a later edit could quietly undo.
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: windows-2025
timeout-minutes: 60
permissions:
contents: write
# Build provenance. Unlike Authenticode this costs nothing and needs no certificate, and the
# Sigstore transparency log it writes to is outside this organisation's control, so it is the
# one integrity signal a repository admin cannot forge by swapping a release asset.
id-token: write
attestations: write
env:
DICTIONARY_TAG: ${{ needs.dependencies.outputs.dictionary_tag }}
# download-dictionaries.sh stages MetasequoiaImeDict/out under this root, and
# Prepare-PackageFiles.ps1 reads its dictionary directory from the same place. Before the
# consolidation this was a synthetic tree holding five sibling checkouts under their
# historical names; now the dictionary release is the only source that still arrives from
# outside, so it lands beside the components in the one checkout.
STAGING_ROOT: .
steps:
# The helpcodes are collected out of the engine submodule now, so this job needs its contents
# and not just the gitlink.
- name: Check out the release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.target_sha }}
submodules: true
persist-credentials: false
- name: Verify the release commit is on main
shell: bash
run: bash scripts/ci/verify-commit-on-main.sh
- name: Download the TSF DLLs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: tsf-x64
path: windows/build64-release/Release
- name: Download the 32-bit TSF DLL
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: tsf-Win32
path: windows/build32-release/Release
- name: Download the Server build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: server
path: server/build-release/bin/Release
- name: Download the web assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: webview2
path: ui-html/webview2
- name: Download the dictionaries
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash scripts/ci/download-dictionaries.sh
- name: Decide the signing mode
id: signing
shell: pwsh
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
run: ./scripts/ci/detect-release-signing.ps1
- name: Collect the installer payload
shell: pwsh
working-directory: installer
env:
TARGET_VERSION: ${{ needs.prepare.outputs.version }}
run: >
pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion $env:TARGET_VERSION
-RepoRoot (Resolve-Path '..').Path
-TsfDirectory windows -ServerDirectory server -UiHtmlDirectory ui-html -NoticesDirectory .
-HelpCodeDirectory vendor/MetasequoiaImeEngine/helpcode
- name: Embed the exact product dependency manifest
env:
WINDOWS_COMMIT: ${{ needs.prepare.outputs.target_sha }}
run: >
python scripts/product_lock.py manifest
--windows-commit "$env:WINDOWS_COMMIT"
--output installer/app_data/product-manifest.json
- name: Sign the packaged binaries
if: ${{ steps.signing.outputs.signing_enabled == 'true' }}
shell: pwsh
working-directory: installer
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
TIMESTAMP_URL: ${{ vars.WINDOWS_SIGNING_TIMESTAMP_URL || 'http://timestamp.digicert.com' }}
run: ../scripts/ci/sign-binaries.ps1 -Path 'server_exe', 'tsf_dll' -Recurse
- name: Install the Chinese language file for Inno Setup
shell: pwsh
run: ./scripts/ci/install-inno-language.ps1
- name: Compile the installer
shell: pwsh
working-directory: installer
run: pwsh -File ./Compile-Installer.ps1
- name: Sign the installer
if: ${{ steps.signing.outputs.signing_enabled == 'true' }}
shell: pwsh
working-directory: installer
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
TIMESTAMP_URL: ${{ vars.WINDOWS_SIGNING_TIMESTAMP_URL || 'http://timestamp.digicert.com' }}
TARGET_VERSION: ${{ needs.prepare.outputs.version }}
run: ../scripts/ci/sign-binaries.ps1 -Path "Output/MetasequoiaIME_Setup_v$env:TARGET_VERSION.exe"
- name: Name the asset and record its checksum
id: asset
shell: pwsh
working-directory: installer
env:
TARGET_VERSION: ${{ needs.prepare.outputs.version }}
ASSET_SUFFIX: ${{ steps.signing.outputs.asset_suffix }}
run: ../scripts/ci/name-installer-asset.ps1
- name: Upload the installer as a workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: installer
path: |
${{ steps.asset.outputs.asset_path }}
installer/app_data/product-manifest.json
if-no-files-found: error
- name: Attest the installer's build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: ${{ steps.asset.outputs.asset_path }}
- name: Revalidate the draft release target
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}
run: bash scripts/ci/revalidate-draft-release.sh
- name: Upload and publish the release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
ASSET_PATH: ${{ steps.asset.outputs.asset_path }}
ASSET_NAME: ${{ steps.asset.outputs.asset_name }}
ASSET_SHA256: ${{ steps.asset.outputs.asset_sha256 }}
SIGNING_ENABLED: ${{ steps.signing.outputs.signing_enabled }}
PRODUCT_MANIFEST: installer/app_data/product-manifest.json
RELEASE_TRIGGER: ${{ github.event_name }}
run: bash scripts/ci/publish-release.sh
# msime.app/update.json is what the settings page's "检查更新" reads, and MSIME-Web only
# regenerated it from a */30 cron. Scheduled events on public repositories are delayed heavily
# -- measured runs sat roughly two hours apart -- so the site advertised an old version for
# hours after every release. MSIME-Web already declares a repository_dispatch trigger for
# exactly this and nothing was ever sending it.
#
# This runs after publishing on purpose. If the dispatch fails the release is already out and
# correct; what the failure means is that the website is now stale and someone has to act, so
# it is reported as a failure rather than swallowed.
- name: Refresh the download manifest on msime.app
shell: bash
env:
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
run: |
if [[ -z "$GH_TOKEN" ]]; then
echo '::error::RELEASE_PLEASE_TOKEN is required to notify MSIME-Web; msime.app/update.json will stay on the previous version until its cron catches up.'
exit 1
fi
gh api --method POST repos/metasequoiaime/MSIME-Web/dispatches -f event_type=update-manifest