Files
小莫 dd018f7985 fix(server): 修复关于页显示旧版本 (#1084)
* fix(server): 从产品版本文件显示关于页版本

* fix(server): 使用发布版本更新关于页
2026-10-19 13:27:46 +08:00

826 lines
43 KiB
YAML

name: Release
run-name: ${{ inputs.test_signing && 'SignPath test signing' || '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 spends code signing signatures on every releasable merge to main -- one per payload EXE/DLL plus one for the installer, so roughly fifteen per release rather than one. The certificate allows 3000 signatures a month, so the quota is still not what constrains this; what it spends is release hygiene -- see docs/product-release.md.
#
# workflow_dispatch either repairs an existing draft or tests SignPath with the selected commit.
# Test signing uses the same build and packaging steps, but never creates or publishes a release.
on:
push:
branches:
- main
workflow_dispatch:
inputs:
tag:
description: Draft release tag to build, sign and publish, for example v2026.9.1
required: false
type: string
test_signing:
description: Test SignPath on the selected ref without creating or publishing a release (leave tag empty)
required: false
type: boolean
default: false
permissions:
contents: read
concurrency:
# A push refreshes release-please metadata while workflow_dispatch builds a
# selected draft. They must not block each other on the same branch lock.
group: release-${{ inputs.test_signing && 'test-signing' || github.event_name }}-${{ 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: ${{ inputs.test_signing || github.event_name == 'workflow_dispatch' || steps.target.outputs.release_created == 'true' }}
tag_name: ${{ inputs.test_signing && format('test-{0}', github.run_id) || (github.event_name == 'workflow_dispatch' && inputs.tag || steps.target.outputs.tag_name) }}
signing_provider: ${{ inputs.test_signing && 'signpath' || vars.WINDOWS_SIGNING_PROVIDER }}
signing_policy: ${{ inputs.test_signing && 'test-signing' || vars.SIGNPATH_SIGNING_POLICY_SLUG }}
test_certificate_sha256: ${{ steps.test_signing.outputs.certificate_sha256 }}
version: ${{ inputs.test_signing && steps.test_signing.outputs.version || (github.event_name == 'workflow_dispatch' && steps.requested_release.outputs.version || steps.target.outputs.version) }}
target_sha: ${{ inputs.test_signing && github.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
- name: Validate the test signing policy and download its public certificate
if: ${{ inputs.test_signing }}
id: test_signing
env:
SIGNPATH_TOKEN: ${{ secrets.SIGNPATH_TOKEN }}
SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }}
run: python3 scripts/ci/prepare-test-signing.py
- name: Upload the test signing public certificate
if: ${{ inputs.test_signing }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signpath-test-certificate
path: ${{ runner.temp }}/signpath-test.cer
if-no-files-found: error
# 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:
# release-please reads and writes the repository's default branch unless told otherwise, and that is now develop. The release branch is main: without this the version pull request, the changelog and the tag all land on develop, and the branch this workflow waits for (release-please--branches--main--*) is never created.
target-branch: main
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
skip-github-pull-request: true
- name: Refresh the release pull request
# When recovery found a release to publish, do not create the next
# release PR in the same run. That PR belongs to the next main push.
if: ${{ github.event_name == 'push' && steps.release_pr.outputs.release_created != 'true' }}
id: release_update
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
with:
target-branch: main
# 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:
target-branch: main
# 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' && !inputs.test_signing }}
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-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' }}
# SignPath's open-source plan accepts a signing request only when every job leading up to it ran on a GitHub-hosted runner, so under SignPath the three Windows jobs move off the release machine. The certificate path keeps the self-hosted runner, where the certificate lives.
runs-on: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 'windows-2025' || fromJSON('["self-hosted","Windows","X64","msime-windows"]') }}
env:
# Each self-hosted runner has a different default vcpkg installation.
# Keep every matrix job aligned with the registry selected below even
# when GitHub schedules x64 and Win32 on different runner instances.
# A GitHub-hosted runner has one, at C:\vcpkg on windows-2025 (VCPKG_INSTALLATION_ROOT); the verification step below checks the image still puts it there.
VCPKG_ROOT: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 'C:\vcpkg' || matrix.vcpkg_root }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- name: x64
architecture: x64
triplet: x64-windows-static
build_dir: windows/build64-release
vcpkg_root: E:\msime-runner\vcpkg-1
- name: Win32
architecture: Win32
triplet: x86-windows-static
build_dir: windows/build32-release
vcpkg_root: E:\msime-runner\vcpkg-2
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
# Some runner instances leave _work owned by SYSTEM while the job itself
# runs as `runner`. checkout still succeeds, so the first plain git call
# is what exits 128 on dubious ownership, minutes into the job.
#
# The self-hosted jobs are separate runner instances on one machine, so
# they share C:\Users\runner\.gitconfig and reach this step in the same
# second. Whoever loses that race gets "could not lock config file". Read
# the list back before each attempt so a peer's write counts as success.
- name: Trust the runner workspace
shell: pwsh
run: |
$workspace = $env:GITHUB_WORKSPACE -replace '\\', '/'
foreach ($attempt in 1..10) {
$trusted = git config --global --get-all safe.directory
if ($trusted -contains $workspace) { exit 0 }
git config --global --add safe.directory $workspace
if ($LASTEXITCODE -eq 0) { exit 0 }
Start-Sleep -Milliseconds (200 * $attempt)
}
throw "Could not add $workspace to the global safe.directory list"
- name: Stamp the version resource
shell: pwsh
env:
TARGET_VERSION: ${{ needs.prepare.outputs.version }}
run: |
python scripts/apply_version.py --version $env:TARGET_VERSION
python scripts/apply_version.py --version $env:TARGET_VERSION --check
# Before anything installs into or caches from VCPKG_ROOT: a mismatch found after the cache save would already have stored the wrong root's archives under the -complete key.
- name: Verify the selected vcpkg root
shell: pwsh
env:
EXPECTED_VCPKG_ROOT: ${{ needs.prepare.outputs.signing_provider != 'signpath' && matrix.vcpkg_root || '' }}
run: |
# On a hosted runner the image's own vcpkg is the expected one.
$expected = if ($env:EXPECTED_VCPKG_ROOT) { $env:EXPECTED_VCPKG_ROOT } else { $env:VCPKG_INSTALLATION_ROOT }
if ($env:VCPKG_ROOT -ne $expected) {
throw "VCPKG_ROOT is '$env:VCPKG_ROOT'; expected '$expected' for ${{ matrix.name }} on runner '$env:RUNNER_NAME'"
}
# A hosted runner starts with no vcpkg binaries, so it restores the archives ci.yml's TSF job saves under the same scope. A run on main can read the caches develop's CI wrote, since develop is the default branch.
- name: Restore dependency binaries
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' }}
id: vcpkg-cache
uses: ./.github/actions/vcpkg-cache
with:
scope: tip-${{ matrix.triplet }}
- name: Install dependencies
shell: pwsh
working-directory: windows
run: |
& "$env:VCPKG_ROOT\\vcpkg.exe" install --triplet ${{ matrix.triplet }}
- name: Save dependency binaries before compiling
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' && steps.vcpkg-cache.outputs.matched-key != format('{0}-complete', steps.vcpkg-cache.outputs.key) }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.vcpkg-cache.outputs.path }}
key: ${{ steps.vcpkg-cache.outputs.key }}-complete
# 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_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 TSF binary and symbols 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 TSF binary and symbols
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: tsf-${{ matrix.name }}
# Upload the output directory so download-artifact restores its contents directly into
# the layout Prepare-PackageFiles.ps1 expects.
path: ${{ matrix.build_dir }}/Release
if-no-files-found: error
server:
name: Build Server
needs: [prepare, dependencies]
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 'windows-2025' || fromJSON('["self-hosted","Windows","X64","msime-windows"]') }}
env:
# windows-2025 already sets this to its own vcpkg; the self-hosted runner's default is replaced with the instance reserved for this job.
VCPKG_INSTALLATION_ROOT: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 'C:\vcpkg' || 'E:\\msime-runner\\vcpkg-3' }}
# Only the GitHub-hosted path compiles through sccache (see Configure); the variable is inert where sccache is not the compiler launcher.
SCCACHE_GHA_ENABLED: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 'true' || '' }}
SCCACHE_IGNORE_SERVER_IO_ERROR: ${{ needs.prepare.outputs.signing_provider == 'signpath' && '1' || '' }}
# A 4-core hosted runner is slower than the release machine; sccache hits from ci.yml's Server job on develop carry most of it.
timeout-minutes: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 120 || 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
# Some runner instances leave _work owned by SYSTEM while the job itself
# runs as `runner`. checkout still succeeds, so the first plain git call
# is what exits 128 on dubious ownership, minutes into the job.
#
# The self-hosted jobs are separate runner instances on one machine, so
# they share C:\Users\runner\.gitconfig and reach this step in the same
# second. Whoever loses that race gets "could not lock config file". Read
# the list back before each attempt so a peer's write counts as success.
- name: Trust the runner workspace
shell: pwsh
run: |
$workspace = $env:GITHUB_WORKSPACE -replace '\\', '/'
foreach ($attempt in 1..10) {
$trusted = git config --global --get-all safe.directory
if ($trusted -contains $workspace) { exit 0 }
git config --global --add safe.directory $workspace
if ($LASTEXITCODE -eq 0) { exit 0 }
Start-Sleep -Milliseconds (200 * $attempt)
}
throw "Could not add $workspace to the global safe.directory list"
# The job env replaces the image's own VCPKG_INSTALLATION_ROOT, so check C:\vcpkg against the vcpkg the image puts on PATH before install-boost.ps1, the cache key and the toolchain all read it.
- name: Verify the hosted vcpkg root
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' }}
shell: pwsh
run: |
$onPath = Get-Command vcpkg.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
$imageRoot = if ($onPath) { Split-Path -Parent $onPath.Source } else { '' }
if (-not (Test-Path -LiteralPath (Join-Path $env:VCPKG_INSTALLATION_ROOT 'vcpkg.exe') -PathType Leaf) -or
$imageRoot -ne $env:VCPKG_INSTALLATION_ROOT) {
throw "VCPKG_INSTALLATION_ROOT is '$env:VCPKG_INSTALLATION_ROOT' but the image's vcpkg is at '$imageRoot'"
}
# The GitHub-hosted steps below are ci.yml's Server job: the same vcpkg cache scope, MSVC environment and sccache, so a release run on main restores what develop's CI saved instead of compiling Boost and the whole server cold on four cores.
- name: Restore dependency binaries
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' }}
id: vcpkg-cache
uses: ./.github/actions/vcpkg-cache
with:
scope: server-x64-windows-and-static-md
- name: Set up MSVC environment
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' }}
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
- name: Set up sccache
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' }}
id: sccache
continue-on-error: true
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
- name: Install Boost
shell: pwsh
run: ./scripts/ci/install-boost.ps1
- name: Save Boost binaries before configuring
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' && steps.vcpkg-cache.outputs.matched-key != format('{0}-complete', steps.vcpkg-cache.outputs.key) && steps.vcpkg-cache.outputs.matched-key != format('{0}-boost', steps.vcpkg-cache.outputs.key) }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.vcpkg-cache.outputs.path }}
key: ${{ steps.vcpkg-cache.outputs.key }}-boost
# The package job stages these outputs into the installer's expected directory.
- name: Configure
if: ${{ needs.prepare.outputs.signing_provider != 'signpath' }}
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
-DMSIME_PRODUCT_VERSION_OVERRIDE=${{ needs.prepare.outputs.version }}
# Ninja Multi-Config because sccache plugs in through CMAKE_<LANG>_COMPILER_LAUNCHER, which the Visual Studio generator ignores; it keeps the --config Release / bin/Release layout the later steps read.
- name: Configure hosted build
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' }}
shell: pwsh
run: |
$cmakeArgs = @(
'-S', 'server', '-B', 'server/build-release', '-G', 'Ninja Multi-Config',
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake",
'-DVCPKG_TARGET_TRIPLET=x64-windows',
'-DMSIME_PRODUCT_VERSION_OVERRIDE=${{ needs.prepare.outputs.version }}'
)
$cmakeArgs += @(./scripts/ci/cmake-sccache-args.ps1 -SetupOutcome '${{ steps.sccache.outcome }}')
cmake @cmakeArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Save dependency binaries before compiling
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' && steps.vcpkg-cache.outputs.matched-key != format('{0}-complete', steps.vcpkg-cache.outputs.key) }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.vcpkg-cache.outputs.path }}
key: ${{ steps.vcpkg-cache.outputs.key }}-complete
- name: Build
shell: pwsh
run: cmake --build server/build-release --config Release --parallel
- name: Report sccache hit rate
if: ${{ !cancelled() && needs.prepare.outputs.signing_provider == 'signpath' && steps.sccache.outcome == 'success' }}
continue-on-error: true
shell: pwsh
run: sccache --show-stats
- 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
# A uiAccess=true binary cannot be launched from the non-interactive
# service session used by this runner. Test the normal build first, then
# embed the manifest before upload and signing.
- name: Embed the Server uiAccess manifest
shell: pwsh
run: ./scripts/ci/embed-server-manifest.ps1 -BuildDir server/build-release/bin/Release
- name: Verify the embedded Server uiAccess manifest
shell: pwsh
run: ./scripts/ci/check-server-binaries.ps1 -BuildDir server/build-release/bin/Release
- name: Upload the Server build
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: server
path: server/build-release/bin/Release
if-no-files-found: error
package:
name: Package and publish installer
needs: [prepare, dependencies, tsf, server]
# 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 the other jobs' conditions that a later edit could quietly undo.
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
runs-on: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 'windows-2025' || fromJSON('["self-hosted","Windows","X64","msime-windows"]') }}
# A SignPath signing request waits for an approver, twice per release, so that path gets room for both approvals on top of the build.
timeout-minutes: ${{ needs.prepare.outputs.signing_provider == 'signpath' && 150 || 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
# signpath/github-action-submit-signing-request reads this job's details and downloads the artifact it submits with GITHUB_TOKEN; without this the request fails before SignPath sees it.
actions: read
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 engine/helpcode, which is a directory of this repository,
# so a plain checkout of the release commit already carries them.
- name: Check out the release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.target_sha }}
persist-credentials: false
# Some runner instances leave _work owned by SYSTEM while the job itself
# runs as `runner`. checkout still succeeds, so the first plain git call
# is what exits 128 on dubious ownership, minutes into the job.
#
# The self-hosted jobs are separate runner instances on one machine, so
# they share C:\Users\runner\.gitconfig and reach this step in the same
# second. Whoever loses that race gets "could not lock config file". Read
# the list back before each attempt so a peer's write counts as success.
- name: Trust the runner workspace
shell: pwsh
run: |
$workspace = $env:GITHUB_WORKSPACE -replace '\\', '/'
foreach ($attempt in 1..10) {
$trusted = git config --global --get-all safe.directory
if ($trusted -contains $workspace) { exit 0 }
git config --global --add safe.directory $workspace
if ($LASTEXITCODE -eq 0) { exit 0 }
Start-Sleep -Milliseconds (200 * $attempt)
}
throw "Could not add $workspace to the global safe.directory list"
- name: Verify the release commit is on main
if: ${{ !inputs.test_signing }}
shell: bash
run: bash scripts/ci/verify-commit-on-main.sh
- name: Download the test signing public certificate
if: ${{ inputs.test_signing }}
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: signpath-test-certificate
path: ${{ runner.temp }}/signpath-test-certificate
- name: Trust the pinned test certificate on this ephemeral runner
if: ${{ inputs.test_signing }}
id: test_certificate
shell: pwsh
env:
EXPECTED_SHA256: ${{ needs.prepare.outputs.test_certificate_sha256 }}
run: >
./scripts/ci/trust-test-signing-certificate.ps1
-Path "$env:RUNNER_TEMP/signpath-test-certificate/signpath-test.cer"
-ExpectedSha256 $env:EXPECTED_SHA256
# The hosted image's preinstalled ISCC compiles the installer only after the payload signing request has been approved. Prove now, before any signature is spent, that it resolves the Chinese language file and is new enough (6.6+) for msime_setup.iss.
- name: Check the hosted Inno Setup can compile the installer
if: ${{ needs.prepare.outputs.signing_provider == 'signpath' }}
shell: pwsh
run: ./scripts/ci/check-inno-language.ps1
- name: Download the x64 TSF binary and symbols
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: tsf-x64
path: windows/build64-release/Release
- name: Download the 32-bit TSF binary and symbols
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: tsf-Win32
path: windows/build32-release/Release
- name: Download the Server build
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: server
path: server/build-release/bin/Release
- name: Download the dictionaries
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash scripts/ci/download-dictionaries.sh
# The word-lattice language model is the one product input that is neither built from this
# repository's sources nor published in the dictionary release: it is converted here from the
# ARPA corpus pinned in language-model/lock.json. Both the download and the converted result
# are checked against that lock, so this step is reproducible rather than a fetch of whatever
# the upstream file server serves today. It configures engine/ngram alone, so it needs no
# vcpkg and no SQLite on this runner.
- name: Build the word-lattice language model
shell: pwsh
run: ./scripts/build-language-model.ps1
# The neural sentence models are downloaded from the chinese-ime-lm release pinned in
# neural-model/lock.json and checked against the SHA256 recorded there. Prepare-PackageFiles.ps1
# asserts they exist, so this has to run before the payload is collected.
- name: Download the neural sentence models
shell: pwsh
run: ./scripts/fetch-neural-model.ps1
- name: Decide the signing mode
id: signing
shell: pwsh
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
CERTIFICATE_THUMBPRINT: ${{ vars.WINDOWS_SIGNING_CERTIFICATE_THUMBPRINT }}
SIGNING_PROVIDER: ${{ needs.prepare.outputs.signing_provider }}
SIGNPATH_TOKEN: ${{ secrets.SIGNPATH_TOKEN }}
SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }}
SIGNPATH_SIGNING_POLICY_SLUG: ${{ needs.prepare.outputs.signing_policy }}
run: ./scripts/ci/detect-release-signing.ps1
# -IncludeSymbols is what makes the published installer carry the PDBs matching its binaries.
# Staging symbols is opt-in because they are ~140 MB against ~20 MB of binaries and Inno Setup
# compresses them with solid LZMA2, which is why the local test scripts leave them out.
- 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 -NoticesDirectory .
-HelpCodeDirectory engine/helpcode -IncludeSymbols
- 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
# Every payload EXE/DLL, not just the uiAccess server. The server is the only one that
# technically requires a signature (uiAccess=true is ignored without one), but Microsoft
# Defender blocks the unsigned helper processes -- settings page, emoji panel, watchdog --
# and the user sees an input method that installs and then cannot open its own panels.
- name: Sign the payload binaries
if: ${{ steps.signing.outputs.signing_provider == 'certificate' }}
shell: pwsh
working-directory: installer
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
CERTIFICATE_THUMBPRINT: ${{ vars.WINDOWS_SIGNING_CERTIFICATE_THUMBPRINT }}
TIMESTAMP_URL: ${{ vars.WINDOWS_SIGNING_TIMESTAMP_URL || 'http://timestamp.digicert.com' }}
run: ../scripts/ci/sign-binaries.ps1 -Recurse -Path 'server_exe', 'tsf_dll'
# The SignPath path signs the same files minus the third-party DLLs, which SignPath Foundation does not allow a project to sign. See scripts/ci/signpath-files.ps1 and installer/signpath/.
- name: Stage the payload binaries for SignPath
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
shell: pwsh
working-directory: installer
run: ../scripts/ci/signpath-files.ps1 -Mode Stage -Set Payload -Directory "$env:RUNNER_TEMP/signpath-payload-unsigned"
- name: Upload the unsigned payload for SignPath
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
id: signpath-payload
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signpath-payload-unsigned
path: ${{ runner.temp }}/signpath-payload-unsigned
if-no-files-found: error
- name: Sign the payload binaries with SignPath
id: signed_payload
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
with:
api-token: ${{ secrets.SIGNPATH_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ needs.prepare.outputs.signing_policy }}
artifact-configuration-slug: payload
github-artifact-id: ${{ steps.signpath-payload.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: ${{ runner.temp }}/signpath-payload-signed
- name: Put the signed payload binaries back
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
shell: pwsh
working-directory: installer
env:
EXPECTED_SIGNER: ${{ steps.test_certificate.outputs.thumbprint }}
run: ../scripts/ci/signpath-files.ps1 -ExpectedSignerThumbprint "$env:EXPECTED_SIGNER" -AllowUntrustedSigner:${{ inputs.test_signing }} -Mode Restore -Set Payload -Directory "$env:RUNNER_TEMP/signpath-payload-signed"
- 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_provider == 'certificate' }}
shell: pwsh
working-directory: installer
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
CERTIFICATE_THUMBPRINT: ${{ vars.WINDOWS_SIGNING_CERTIFICATE_THUMBPRINT }}
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: Stage the installer for SignPath
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
shell: pwsh
working-directory: installer
env:
TARGET_VERSION: ${{ needs.prepare.outputs.version }}
run: ../scripts/ci/signpath-files.ps1 -Mode Stage -Set Installer -Version $env:TARGET_VERSION -Directory "$env:RUNNER_TEMP/signpath-installer-unsigned"
- name: Upload the unsigned installer for SignPath
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
id: signpath-installer
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signpath-installer-unsigned
path: ${{ runner.temp }}/signpath-installer-unsigned
if-no-files-found: error
- name: Sign the installer with SignPath
id: signed_installer
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
with:
api-token: ${{ secrets.SIGNPATH_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ needs.prepare.outputs.signing_policy }}
artifact-configuration-slug: installer
github-artifact-id: ${{ steps.signpath-installer.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: ${{ runner.temp }}/signpath-installer-signed
- name: Put the signed installer back
if: ${{ steps.signing.outputs.signing_provider == 'signpath' }}
shell: pwsh
working-directory: installer
env:
TARGET_VERSION: ${{ needs.prepare.outputs.version }}
EXPECTED_SIGNER: ${{ steps.test_certificate.outputs.thumbprint }}
run: ../scripts/ci/signpath-files.ps1 -ExpectedSignerThumbprint "$env:EXPECTED_SIGNER" -AllowUntrustedSigner:${{ inputs.test_signing }} -Mode Restore -Set Installer -Version $env:TARGET_VERSION -Directory "$env:RUNNER_TEMP/signpath-installer-signed"
- name: Record the signing request links for review
if: ${{ always() && steps.signing.outputs.signing_provider == 'signpath' }}
shell: pwsh
env:
PAYLOAD_REQUEST: ${{ steps.signed_payload.outputs.signing-request-web-url }}
INSTALLER_REQUEST: ${{ steps.signed_installer.outputs.signing-request-web-url }}
TEST_SIGNING: ${{ inputs.test_signing }}
run: |
$mode = if ($env:TEST_SIGNING -eq 'true') { 'Test signing (not for distribution)' } else { 'Release signing' }
@(
"## SignPath: $mode"
"GitHub Actions run: $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID"
"Payload request: $env:PAYLOAD_REQUEST"
"Installer request: $env:INSTALLER_REQUEST"
) >> $env:GITHUB_STEP_SUMMARY
- name: Name the asset and record its checksum
id: asset
shell: pwsh
working-directory: installer
env:
TARGET_VERSION: ${{ needs.prepare.outputs.version }}
ASSET_SUFFIX: ${{ inputs.test_signing && '-test-signed' || steps.signing.outputs.asset_suffix }}
run: ../scripts/ci/name-installer-asset.ps1
- name: Upload the installer as a workflow artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ inputs.test_signing && 'installer-test-signed' || '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
if: ${{ !inputs.test_signing }}
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: ${{ steps.asset.outputs.asset_path }}
- name: Revalidate the draft release target
if: ${{ !inputs.test_signing }}
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
if: ${{ !inputs.test_signing }}
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 }}
TARGET_SHA: ${{ needs.prepare.outputs.target_sha }}
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.
#
# No event condition: the enclosing job already only runs when a release is being published, and gating this on workflow_dispatch limited the notification to the repair path -- the one branch of this workflow that almost never runs. Every release from the normal push path published in silence, MSIME-Web received no dispatch at all, and the site waited for a cron that GitHub delivers roughly seven times a day rather than the forty-eight the schedule asks for. 0.8.0 reached msime.app a day late that way.
- name: Refresh the download manifest on msime.app
if: ${{ !inputs.test_signing }}
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