diff --git a/.github/workflows/macos-private-build.yml b/.github/workflows/macos-private-build.yml index 97d37e55..11b557c7 100644 --- a/.github/workflows/macos-private-build.yml +++ b/.github/workflows/macos-private-build.yml @@ -2,6 +2,10 @@ name: macOS Private Build on: workflow_call: + outputs: + source_runtime_coordinates: + description: Verified producer coordinates for the source runtime promotion + value: ${{ jobs.build-macos-arm64.outputs.source_runtime_coordinates }} inputs: version: description: Package version; omitted callers derive it from a v* tag @@ -34,6 +38,8 @@ jobs: runs-on: macos-14 environment: macos-private-pki-production timeout-minutes: 120 + outputs: + source_runtime_coordinates: ${{ steps.source-runtime-coordinates.outputs.json }} env: UV_MANAGED_PYTHON: "true" WCE_MACOS_SIGNING_MODE: self-signed @@ -120,6 +126,29 @@ jobs: --component macos-integrity \ --output-root "$RUNNER_TEMP" + - name: Expose source runtime producer coordinates + id: source-runtime-coordinates + shell: bash + run: | + python3 - <<'PY' >> "$GITHUB_OUTPUT" + import json + import os + + fields = { + "native_core_run_id": "WCE_NATIVE_CORE_ARTIFACT_RUN_ID", + "native_core_source_revision": "WCE_NATIVE_CORE_SOURCE_REVISION", + "native_core_build_id": "WCE_NATIVE_CORE_BUILD_ID", + "xkey_run_id": "WCE_MACOS_XKEY_ARTIFACT_RUN_ID", + "xkey_source_revision": "WCE_MACOS_XKEY_SOURCE_REVISION", + "xkey_build_id": "WCE_MACOS_XKEY_BUILD_ID", + "integrity_run_id": "WCE_INTEGRITY_ARTIFACT_RUN_ID", + "integrity_source_revision": "WCE_INTEGRITY_SOURCE_REVISION", + "integrity_build_id": "WCE_INTEGRITY_BUILD_ID", + "integrity_binary_sha256": "WCE_INTEGRITY_BINARY_SHA256", + } + print("json=" + json.dumps({key: os.environ[value] for key, value in fields.items()}, separators=(",", ":"))) + PY + - name: Verify protected pins shell: bash env: diff --git a/.github/workflows/macos-source-runtime-promotion.yml b/.github/workflows/macos-source-runtime-promotion.yml index 725d5471..3d4378e3 100644 --- a/.github/workflows/macos-source-runtime-promotion.yml +++ b/.github/workflows/macos-source-runtime-promotion.yml @@ -1,4 +1,5 @@ name: macOS public source runtime promotion +run-name: macOS source runtime ${{ inputs.release_tag }} on: workflow_dispatch: @@ -31,6 +32,22 @@ on: description: Exact XKey build ID required: true type: string + integrity_run_id: + description: Exact private Producer run containing production export integrity + required: true + type: string + integrity_source_revision: + description: Exact export-integrity Producer revision + required: true + type: string + integrity_build_id: + description: Exact export-integrity build ID + required: true + type: string + integrity_binary_sha256: + description: SHA-256 of the exact export-integrity binary + required: true + type: string integrity_ui_source_revision: description: WCDA revision pinned by the existing integrity artifact required: true @@ -62,10 +79,10 @@ jobs: WCE_NATIVE_CORE_PRIVATE_ROOT_SHA256: ${{ vars.WCE_NATIVE_CORE_PRIVATE_ROOT_SHA256 }} WCE_MACOS_XKEY_ARTIFACT_REPOSITORY: ${{ vars.WCE_MACOS_XKEY_ARTIFACT_REPOSITORY }} WCE_INTEGRITY_ARTIFACT_REPOSITORY: ${{ vars.WCE_INTEGRITY_ARTIFACT_REPOSITORY }} - WCE_INTEGRITY_ARTIFACT_RUN_ID: ${{ vars.WCE_INTEGRITY_ARTIFACT_RUN_ID }} - WCE_INTEGRITY_SOURCE_REVISION: ${{ vars.WCE_INTEGRITY_SOURCE_REVISION }} - WCE_INTEGRITY_BUILD_ID: ${{ vars.WCE_INTEGRITY_BUILD_ID }} - WCE_INTEGRITY_BINARY_SHA256: ${{ vars.WCE_INTEGRITY_BINARY_SHA256 }} + WCE_INTEGRITY_ARTIFACT_RUN_ID: ${{ inputs.integrity_run_id }} + WCE_INTEGRITY_SOURCE_REVISION: ${{ inputs.integrity_source_revision }} + WCE_INTEGRITY_BUILD_ID: ${{ inputs.integrity_build_id }} + WCE_INTEGRITY_BINARY_SHA256: ${{ inputs.integrity_binary_sha256 }} WCE_INTEGRITY_UI_SOURCE_REPOSITORY: ${{ github.repository }} WCE_INTEGRITY_UI_SOURCE_REVISION: ${{ inputs.integrity_ui_source_revision }} @@ -120,34 +137,72 @@ jobs: env: GH_TOKEN: ${{ secrets.WCE_MACOS_PRODUCER_READ_TOKEN }} NATIVE_RUN_ID: ${{ inputs.native_core_run_id }} + NATIVE_REVISION: ${{ inputs.native_core_source_revision }} + NATIVE_BUILD_ID: ${{ inputs.native_core_build_id }} XKEY_RUN_ID: ${{ inputs.xkey_run_id }} + XKEY_REVISION: ${{ inputs.xkey_source_revision }} + XKEY_BUILD_ID: ${{ inputs.xkey_build_id }} + INTEGRITY_RUN_ID: ${{ inputs.integrity_run_id }} + INTEGRITY_REVISION: ${{ inputs.integrity_source_revision }} + INTEGRITY_BUILD_ID: ${{ inputs.integrity_build_id }} run: | set -euo pipefail test -n "$GH_TOKEN" - download_artifact() { + download_release() { local repository="$1" local run_id="$2" - local artifact_name="$3" - local destination="$4" - local downloaded=0 - for attempt in 1 2 3; do - rm -rf "$destination" - mkdir -p "$destination" - if gh run download "$run_id" --repo "$repository" \ - --name "$artifact_name" --dir "$destination"; then - downloaded=1 - break - fi - sleep "$((attempt * 10))" - done - test "$downloaded" = 1 + local revision="$3" + local build_id="$4" + local workflow="$5" + local release_tag="$6" + local asset_name="$7" + local destination="$8" + local executable="$9" + local run_meta + local head_sha head_branch event status conclusion path display_title + local digest archive actual + run_meta="$(gh api "repos/$repository/actions/runs/$run_id" \ + --jq '[.head_sha,.head_branch,.event,.status,.conclusion,.path,.display_title] | @tsv')" + IFS=$'\t' read -r head_sha head_branch event status conclusion path display_title <<< "$run_meta" + test "$head_sha" = "$revision" + test "$head_branch" = main + test "$event" = workflow_dispatch + test "$status" = completed + test "$conclusion" = success + test "$path" = "$workflow" + test "$display_title" = "WCDB $build_id" + test "$(gh api "repos/$repository/releases/tags/$release_tag" --jq '.target_commitish')" = "$revision" + digest="$(gh api "repos/$repository/releases/tags/$release_tag" \ + --jq "[.assets[] | select(.name == \"$asset_name\") | .digest] | if length == 1 then .[0] else \"\" end")" + [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] + archive="$RUNNER_TEMP/$asset_name" + rm -f "$archive" + rm -rf "$destination" + gh release download "$release_tag" --repo "$repository" \ + --pattern "$asset_name" --dir "$RUNNER_TEMP" + test -s "$archive" + actual="$(shasum -a 256 "$archive" | awk '{print tolower($1)}')" + test "sha256:$actual" = "$digest" + mkdir -p "$destination" + /usr/bin/unzip -q "$archive" -d "$destination" + test -f "$destination/$executable" || test -z "$executable" + if [[ -n "$executable" ]]; then chmod 0755 "$destination/$executable"; fi } - download_artifact "$WCE_NATIVE_CORE_ARTIFACT_REPOSITORY" "$NATIVE_RUN_ID" \ - wechatdb-native-macos-arm64-source-public "$RUNNER_TEMP/source-native-core" - download_artifact "$WCE_MACOS_XKEY_ARTIFACT_REPOSITORY" "$XKEY_RUN_ID" \ - wda-xkey-macos-universal-source-public "$RUNNER_TEMP/source-xkey" - download_artifact "$WCE_INTEGRITY_ARTIFACT_REPOSITORY" "$WCE_INTEGRITY_ARTIFACT_RUN_ID" \ - wce-integrity-macos-arm64-production "$RUNNER_TEMP/source-integrity" + download_release "$WCE_NATIVE_CORE_ARTIFACT_REPOSITORY" "$NATIVE_RUN_ID" "$NATIVE_REVISION" "$NATIVE_BUILD_ID" \ + .github/workflows/macos-native-production.yml "macos-native-$NATIVE_BUILD_ID" \ + "wechatdb-native-macos-arm64-source-public-$NATIVE_BUILD_ID.zip" "$RUNNER_TEMP/source-native-core" wechatdb_broker + download_release "$WCE_MACOS_XKEY_ARTIFACT_REPOSITORY" "$XKEY_RUN_ID" "$XKEY_REVISION" "$XKEY_BUILD_ID" \ + .github/workflows/macos-key-capture-production.yml "macos-xkey-$XKEY_BUILD_ID" \ + "wda-xkey-macos-universal-source-public-$XKEY_BUILD_ID.zip" "$RUNNER_TEMP/source-xkey" wda_xkey_helper + download_release "$WCE_INTEGRITY_ARTIFACT_REPOSITORY" "$INTEGRITY_RUN_ID" "$INTEGRITY_REVISION" "$INTEGRITY_BUILD_ID" \ + .github/workflows/macos-integrity-production.yml "macos-integrity-$INTEGRITY_BUILD_ID" \ + "wce-integrity-macos-arm64-production-$INTEGRITY_BUILD_ID.zip" "$RUNNER_TEMP/source-integrity" "" + test "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["buildId"])' \ + "$RUNNER_TEMP/source-native-core/wechatdb_native_build.json")" = "$NATIVE_BUILD_ID" + test "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["build"]["id"])' \ + "$RUNNER_TEMP/source-xkey/wda_xkey_build.json")" = "$XKEY_BUILD_ID" + test "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["buildId"])' \ + "$RUNNER_TEMP/source-integrity/wce_integrity_build.json")" = "$INTEGRITY_BUILD_ID" echo "WCE_SOURCE_PUBLIC_NATIVE_CORE_DIR=$RUNNER_TEMP/source-native-core" >> "$GITHUB_ENV" echo "WCE_SOURCE_PUBLIC_XKEY_DIR=$RUNNER_TEMP/source-xkey" >> "$GITHUB_ENV" echo "WCE_INTEGRITY_ARTIFACT_DIR=$RUNNER_TEMP/source-integrity" >> "$GITHUB_ENV" @@ -237,13 +292,49 @@ jobs: actual="$(shasum -a 256 "$public_asset" | awk '{print tolower($1)}')" test "$actual" = "$expected" - - name: Upload promotion evidence - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: macos-source-runtime-promotion-${{ inputs.release_tag }} - path: | - ${{ runner.temp }}/macos-source-runtime-promotion/promotion-output.json - ${{ runner.temp }}/macos-source-runtime-promotion/SHA256SUMS-macos-source-runtime.txt - ${{ runner.temp }}/macos-source-runtime-promotion/wechatdataanalysis-macos-source-runtime-arm64-v1.manifest.json - if-no-files-found: error - retention-days: 90 + - name: Update macOS source-runtime pin on main + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + current="$(gh api \ + --header "Accept: application/vnd.github+json" \ + "repos/${GITHUB_REPOSITORY}/contents/desktop/resources/native-core-source-macos.json?ref=main")" + request="$( + WCE_SOURCE_RUNTIME_CURRENT_RESPONSE="$current" \ + WCE_SOURCE_RUNTIME_PROMOTION_JSON="$WCE_SOURCE_RUNTIME_OUTPUT_DIR/promotion-output.json" \ + node <<'NODE' + const fs = require("node:fs"); + + const currentResponse = JSON.parse(process.env.WCE_SOURCE_RUNTIME_CURRENT_RESPONSE); + const currentPin = JSON.parse( + Buffer.from(currentResponse.content.replace(/\s/g, ""), "base64").toString("utf8") + ); + const promotion = JSON.parse( + fs.readFileSync(process.env.WCE_SOURCE_RUNTIME_PROMOTION_JSON, "utf8") + ); + if (currentPin.expiresAtUnix >= promotion.expiresAtUnix) process.exit(0); + for (const field of [ + "releaseTag", + "assetSha256", + "runtimeManifestSha256", + "expiresAtUnix", + ]) currentPin[field] = promotion[field]; + process.stdout.write(JSON.stringify({ + message: "ci: refresh macOS source runtime pin", + content: Buffer.from(`${JSON.stringify(currentPin, null, 2)}\n`).toString("base64"), + sha: currentResponse.sha, + branch: "main", + })); + NODE + )" + if [[ -z "$request" ]]; then + echo "main already has a source-runtime pin with an equal or newer expiry" + exit 0 + fi + gh api \ + --method PUT \ + --header "Accept: application/vnd.github+json" \ + "repos/${GITHUB_REPOSITORY}/contents/desktop/resources/native-core-source-macos.json" \ + --input - <<< "$request" > /dev/null diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49324605..2dd4ce95 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -748,6 +748,65 @@ jobs: fail_on_unmatched_files: true files: release-assets/* + refresh-macos-source-runtime: + needs: + - build-macos-arm64 + - publish-release + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + - name: Promote and pin this release's macOS source runtime + env: + GH_TOKEN: ${{ github.token }} + PRODUCER_COORDINATES: ${{ needs.build-macos-arm64.outputs.source_runtime_coordinates }} + SOURCE_RUNTIME_TAG: macos-source-runtime-${{ github.run_id }}-${{ github.run_attempt }} + run: | + python3 - <<'PY' + import json + import os + import subprocess + import time + + repository = os.environ["GITHUB_REPOSITORY"] + release_tag = os.environ["SOURCE_RUNTIME_TAG"] + coordinates = json.loads(os.environ["PRODUCER_COORDINATES"]) + fields = ( + "native_core_run_id", + "native_core_source_revision", + "native_core_build_id", + "xkey_run_id", + "xkey_source_revision", + "xkey_build_id", + "integrity_run_id", + "integrity_source_revision", + "integrity_build_id", + "integrity_binary_sha256", + ) + command = [ + "gh", "workflow", "run", "macos-source-runtime-promotion.yml", + "--repo", repository, "--ref", "main", + "-f", f"release_tag={release_tag}", + "-f", f"integrity_ui_source_revision={os.environ['GITHUB_SHA']}", + ] + for field in fields: + command.extend(("-f", f"{field}={coordinates[field]}")) + subprocess.run(command, check=True) + + while True: + result = subprocess.run( + ["gh", "api", f"repos/{repository}/actions/workflows/macos-source-runtime-promotion.yml/runs?event=workflow_dispatch&per_page=30"], + check=True, capture_output=True, text=True, + ) + runs = json.loads(result.stdout)["workflow_runs"] + match = next((run for run in runs if run["display_title"] == f"macOS source runtime {release_tag}" and run["head_branch"] == "main"), None) + if match: + break + time.sleep(5) + subprocess.run(["gh", "run", "watch", str(match["id"]), "--repo", repository, "--exit-status"], check=True) + PY + # ========================== QQ 群通知 ========================== # 等 Release 发布完成后,发送 QQ 群通知。 # 消息内容取最后一次 commit 正文(用户约定在此写本次更新说明)。