Merge branch 'main' into ci/self-hosted-runner-signing

This commit is contained in:
Evan authored and GitHub committed 2026-09-06 16:55:18 +09:00
commit ef55ff67bb
241 files changed
+7211 -5170

No files matched your search

+18 -13
View File
@@ -4,29 +4,25 @@ on:
push:
branches: [main]
pull_request:
# A release-please pull request only rewrites these three files, and its pull_request run would
# be created in action_required and never execute: GITHUB_TOKEN may not start workflows, so
# anything it triggers waits for a human. Skipping it here keeps an unapprovable run out of the
# checks list. The release workflow dispatches this workflow on the release branch instead,
# which is the one trigger GITHUB_TOKEN is allowed to fire, so the version bump still gets a
# full build and the checks land on the same commit.
paths-ignore:
- version.txt
- CHANGELOG.md
- .release-please-manifest.json
workflow_dispatch:
merge_group:
permissions:
contents: read
concurrency:
group: ci-${{ github.event.pull_request.head.ref || github.ref_name }}
group: ci-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
uses: ./.github/workflows/quality.yml
lock:
name: Product inputs
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
# The dictionary validator is compared against the engine's copy of it, so this job needs the
# submodule's contents and not just its gitlink.
@@ -37,6 +33,10 @@ jobs:
run: python3 scripts/check-dictionary-contract.py
- name: Test rejection of changed or mismatched inputs
run: python3 -m unittest discover -s tests -p 'test_product_lock.py'
- name: Test local environment setup in a moved checkout
run: python3 -m unittest discover -s tests -p 'test_prepare_env.py'
- name: Test the automatic release CI gate
run: python3 -m unittest discover -s tests -p 'test_release_pr_ci.py'
- name: Read the reviewed lock
id: lock
run: |
@@ -46,6 +46,7 @@ jobs:
windows:
name: Windows ${{ matrix.name }}
runs-on: windows-2025
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -86,7 +87,7 @@ jobs:
shell: pwsh
run: cmake --build windows/build --config Release --parallel
- name: Test the wire contract on this architecture
run: ctest --test-dir windows/build -C Release --output-on-failure
run: ctest --no-tests=error --test-dir windows/build -C Release --output-on-failure
# Architecture-independent, so it runs once rather than on both matrix legs.
- name: Check the installer language file lands where ISCC looks
@@ -157,7 +158,7 @@ jobs:
run: cmake --build ui/build --config Release --parallel
- name: Test
shell: pwsh
run: ctest --test-dir ui/build -C Release --verbose --timeout 60
run: ctest --no-tests=error --test-dir ui/build -C Release --verbose --timeout 60
pages:
name: Settings page
@@ -213,3 +214,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Test full and light package contents without registering or installing
run: ./installer/tests/package-files.ps1
- name: Stop local packaging after failed component builds
run: ./installer/tests/build-failures.ps1
- name: Test local Server test entrypoints in a moved checkout
run: ./tests/server-test-entrypoints.ps1
+37
View File
@@ -0,0 +1,37 @@
name: CodeQL
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '11 3 * * 1'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: codeql-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: CodeQL / ${{ matrix.language }}
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [actions, c-cpp, javascript-typescript, python]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
with:
languages: ${{ matrix.language }}
# C/C++ source analysis complements, rather than replaces, native build CI.
build-mode: none
- uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
with:
category: '/language:${{ matrix.language }}'
+66 -11
View File
@@ -12,16 +12,21 @@ on:
paths:
- 'vendor/MetasequoiaImeEngine'
# Read-only by default, matching every other workflow here. The write scopes this job needs to merge
# the bump are granted on the job instead, so a second job added later starts from the same floor
# rather than inheriting write.
permissions:
contents: write
pull-requests: write
contents: read
jobs:
triage:
if: github.actor == 'dependabot[bot]'
name: Classify the engine range
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
env:
SUBMODULE: vendor/MetasequoiaImeEngine
ENGINE: metasequoiaime/MSIME-Engine
@@ -36,9 +41,11 @@ jobs:
- name: Resolve the engine range
id: range
env:
BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
base=$(git rev-parse "origin/${{ github.base_ref }}:$SUBMODULE")
base=$(git rev-parse "origin/$BASE_REF:$SUBMODULE")
head=$(git rev-parse "HEAD:$SUBMODULE")
echo "base=$base" >> "$GITHUB_OUTPUT"
echo "head=$head" >> "$GITHUB_OUTPUT"
@@ -46,17 +53,47 @@ jobs:
- name: Classify the changed files
id: classify
env:
BASE: ${{ steps.range.outputs.base }}
HEAD: ${{ steps.range.outputs.head }}
run: |
set -euo pipefail
base='${{ steps.range.outputs.base }}'
head='${{ steps.range.outputs.head }}'
base="$BASE"
head="$HEAD"
if [ "$base" = "$head" ]; then
echo "verdict=inert" >> "$GITHUB_OUTPUT"
exit 0
fi
# Fail closed on anything but a plain fast-forward. The compare API answers with an empty
# file list whenever head is already an ancestor of base, which the emptiness test below
# would otherwise read as "nothing suspicious changed" and auto-merge — silently accepting
# a downgrade that reverts engine source the frontend depends on.
status=$(gh api "repos/$ENGINE/compare/$base...$head" --jq '.status')
echo "compare status: $status"
if [ "$status" != "ahead" ]; then
echo "verdict=review" >> "$GITHUB_OUTPUT"
delimiter="EOF_$(openssl rand -hex 16)"
{
echo "reason<<$delimiter"
echo "引擎范围不是单纯的快进(compare status: $status),需要人工确认。"
echo "$delimiter"
} >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(gh api --paginate "repos/$ENGINE/compare/$base...$head" --jq '.files[].filename')
echo "改动文件:"; echo "$files" | sed 's/^/ /'
if [ -z "$files" ]; then
echo "verdict=review" >> "$GITHUB_OUTPUT"
delimiter="EOF_$(openssl rand -hex 16)"
{
echo "reason<<$delimiter"
echo "compare 没有返回任何文件,无法判定这次 bump 是否安全。"
echo "$delimiter"
} >> "$GITHUB_OUTPUT"
exit 0
fi
# Provably inert: documentation, CI wiring, dependency manifests, licence text. Everything
# else - sources, build files, and bare paths such as a nested submodule pointer - needs a
@@ -68,10 +105,13 @@ jobs:
echo "verdict=inert" >> "$GITHUB_OUTPUT"
else
echo "verdict=review" >> "$GITHUB_OUTPUT"
# A random delimiter: the file names come from another repository, so a path that
# happened to be the literal delimiter could otherwise close the block early.
delimiter="EOF_$(openssl rand -hex 16)"
{
echo 'reason<<EOF'
echo "reason<<$delimiter"
echo "$suspicious"
echo EOF
echo "$delimiter"
} >> "$GITHUB_OUTPUT"
fi
@@ -90,18 +130,33 @@ jobs:
fi
- name: Enable auto-merge
if: steps.classify.outputs.verdict == 'inert' && steps.lock.outputs.locked == 'yes'
if: steps.classify.outputs.verdict == 'inert'
env:
BASE: ${{ steps.range.outputs.base }}
HEAD: ${{ steps.range.outputs.head }} && steps.lock.outputs.locked == 'yes'
run: |
set -euo pipefail
gh pr merge --auto --squash "$PR_URL"
gh pr comment "$PR_URL" --body "引擎 \`${{ steps.range.outputs.base }}\` → \`${{ steps.range.outputs.head }}\` 的改动全部落在文档、CI 配置和依赖清单里,没有源码、构建文件或嵌套 submodule 指针。已开启自动合并,CI 通过后自行合入。"
body="$RUNNER_TEMP/engine-bump-comment.md"
printf '引擎 `%s` → `%s` 的改动全部落在文档、CI 配置和依赖清单里,没有源码、构建文件或嵌套 submodule 指针。已开启自动合并,CI 通过后自行合入。\n' "$BASE" "$HEAD" > "$body"
gh pr comment "$PR_URL" --body-file "$body"
# Every value below reaches the shell through the environment, never through ${{ }}
# interpolation. REASON is a list of file names from another repository: spliced into the
# command text, a path containing a single quote would have closed the quoting and run as a
# command, on a runner holding a contents:write token for this repository.
- name: Hold for review
if: steps.classify.outputs.verdict == 'review'
env:
BASE: ${{ steps.range.outputs.base }}
HEAD: ${{ steps.range.outputs.head }}
REASON: ${{ steps.classify.outputs.reason }}
run: |
set -euo pipefail
gh pr edit "$PR_URL" --add-label "needs-review" || true
gh pr comment "$PR_URL" --body "$(printf '引擎 `%s` → `%s` 的改动里有需要人看一眼的部分,未开启自动合并:\n\n```\n%s\n```\n\n源码、`CMakeLists.txt` 或嵌套 submodule 指针的变化都可能改变行为,而前端 CI 未必覆盖得到——`helpcode_enabled()` 与 `handle_character()` 的语义变更就是这样漏过去的。' '${{ steps.range.outputs.base }}' '${{ steps.range.outputs.head }}' '${{ steps.classify.outputs.reason }}')"
body="$RUNNER_TEMP/engine-bump-comment.md"
printf '引擎 `%s` → `%s` 的改动里有需要人看一眼的部分,未开启自动合并:\n\n```\n%s\n```\n\n源码、`CMakeLists.txt` 或嵌套 submodule 指针的变化都可能改变行为,而前端 CI 未必覆盖得到——`helpcode_enabled()` 与 `handle_character()` 的语义变更就是这样漏过去的。\n' "$BASE" "$HEAD" "$REASON" > "$body"
gh pr comment "$PR_URL" --body-file "$body"
- name: Ask for the lock to follow
if: steps.lock.outputs.locked == 'no'
+50
View File
@@ -0,0 +1,50 @@
name: Repository quality
# Called by CI so release-please's workflow_dispatch receives these checks too.
on:
workflow_call:
permissions:
contents: read
jobs:
workflows:
name: Workflow validation
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: '1.26'
cache: false
- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12
- name: Validate workflows and embedded shell scripts
env:
# Ignore style suggestions, but fail on shell correctness warnings and errors.
SHELLCHECK_OPTS: --severity=warning
run: actionlint -color
# Part of "Workflow validation" rather than a job of its own: it is a two-second grep, and this
# job is already a required check, so the guard starts protecting main immediately.
- name: Reject developer-specific paths in build inputs
run: bash scripts/ci/check-developer-paths.sh
# Formatting is platform independent, so it runs here on Ubuntu rather than costing a Windows
# runner. Cheap and early: a formatting slip fails in seconds instead of after the full compile.
- name: Check formatting
run: scripts/format.sh --check
dependencies:
name: Dependency review
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# This job must not skip. It is one of main's required status checks, and a required check that reports "skipped" blocks a merge instead of passing it. Guarding it on the pull_request event left the release pull request unmergeable by its own automation: ci.yml ignores that pull request by path and the release workflow dispatches ci.yml on the release branch instead, so this job saw workflow_dispatch and skipped, and land-release-pr.sh got "the base branch policy prohibits the merge" after everything else came back green.
#
# A pull_request event supplies both refs on its own. Every other trigger has to be told what to compare, and comparing the default branch against the checked-out commit is the same question the pull request asks.
- uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4
with:
fail-on-severity: high
base-ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.repository.default_branch }}
head-ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
+98 -19
View File
@@ -1,10 +1,12 @@
name: Release
# Building and publishing only ever happens on workflow_dispatch. The code signing certificate
# has a limited number of signatures, so a release costs something real and is not something a merge
# to main should spend on its own. The push trigger only keeps release-please's pull request current
# and green; merging that pull request creates the draft, and dispatching this workflow against the
# draft's tag is what builds, signs and publishes it.
# 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 a code signing signature on every releasable merge to main. The certificate allows 3000 signatures a month, so the quota is not what constrains this; what it spends is release hygiene -- see docs/product-release.md.
#
# 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:
@@ -27,38 +29,83 @@ jobs:
prepare:
name: Prepare release metadata
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
# actions: write lets check-release-pr.sh dispatch ci.yml on the release branch, which is the
# only trigger GITHUB_TOKEN is allowed to fire.
actions: write
# Read the PR-triggered run; merging stays scoped to GITHUB_TOKEN.
actions: read
contents: write
pull-requests: write
outputs:
# Never true on push: see the comment above the triggers. release-please still creates the
# draft release there, it just does not get built until somebody asks for it.
should_publish: ${{ github.event_name == 'workflow_dispatch' }}
tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || steps.release.outputs.tag_name }}
version: ${{ github.event_name == 'workflow_dispatch' && steps.requested_release.outputs.version || steps.release.outputs.version }}
target_sha: ${{ github.event_name == 'workflow_dispatch' && steps.requested_release.outputs.target_sha || steps.release.outputs.sha }}
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
- name: Prepare release
# 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' }}
id: release
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.GITHUB_TOKEN }}
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
skip-github-pull-request: true
- name: Test the release pull request
- 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/check-release-pr.sh
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' }}
@@ -74,6 +121,7 @@ jobs:
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:
@@ -264,6 +312,11 @@ jobs:
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
@@ -391,6 +444,11 @@ jobs:
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:
@@ -411,4 +469,25 @@ jobs:
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