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__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