Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ concurrency:

jobs:
release-protocol:
name: Windows signing protocol and release manifest
name: Signing protocols and release manifest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: python3 scripts/windows-release.test.py
- run: python3 scripts/macos-release.test.py
- run: node --test scripts/render-version-json.test.mjs

rust:
Expand Down
53 changes: 46 additions & 7 deletions .github/workflows/publish-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ concurrency:
cancel-in-progress: false

jobs:
verify:
verify-windows:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: windows-latest
timeout-minutes: 40
timeout-minutes: 70
outputs:
version: ${{ steps.collect.outputs.version }}
tag: ${{ steps.collect.outputs.tag }}
artifact_id: ${{ steps.upload.outputs.artifact-id }}
steps:
# The privileged publishing code always comes from the default branch.
- uses: actions/checkout@v6
Expand All @@ -42,7 +43,7 @@ jobs:
SOURCE_RUN_ID: ${{ github.event.workflow_run.id || inputs.run_id }}
SOURCE_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
run: |
$arguments = @('scripts/windows-release.py', 'collect', '--run-id', $env:SOURCE_RUN_ID, '--timeout', '1800')
$arguments = @('scripts/windows-release.py', 'collect', '--run-id', $env:SOURCE_RUN_ID, '--timeout', '3600', '--linux-only')
if ($env:SOURCE_ATTEMPT) { $arguments += @('--attempt', $env:SOURCE_ATTEMPT) }
python @arguments
if ($LASTEXITCODE -ne 0) { throw 'Release provenance validation failed' }
Expand All @@ -60,17 +61,51 @@ jobs:
run: Compress-Archive -Path signed/bsk.exe -DestinationPath "dist/bsk-v$env:VERSION-x86_64-pc-windows-msvc.zip"

- name: Upload verified release files
id: upload
uses: actions/upload-artifact@v7
with:
name: verified-cli-${{ github.run_id }}-${{ github.run_attempt }}
name: verified-windows-${{ github.run_id }}-${{ github.run_attempt }}
path: |
dist/*
release-state.json
if-no-files-found: error
retention-days: 7

verify-macos:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: macos-latest
timeout-minutes: 80
outputs:
artifact_id: ${{ steps.upload.outputs.artifact-id }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false

- name: Verify Mac provenance, Developer ID signatures and Apple notarization
env:
GH_TOKEN: ${{ github.token }}
SOURCE_RUN_ID: ${{ github.event.workflow_run.id || inputs.run_id }}
SOURCE_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
run: |
args=(collect --run-id "$SOURCE_RUN_ID" --timeout 4200)
if [[ -n "$SOURCE_ATTEMPT" ]]; then args+=(--attempt "$SOURCE_ATTEMPT"); fi
python3 scripts/macos-release.py "${args[@]}"

- name: Upload independently verified Mac release files
id: upload
uses: actions/upload-artifact@v7
with:
name: verified-macos-${{ github.run_id }}-${{ github.run_attempt }}
path: |
dist/*
macos-release-state.json
if-no-files-found: error
retention-days: 7

publish:
needs: verify
needs: [verify-windows, verify-macos]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand All @@ -79,13 +114,17 @@ jobs:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false

# Partial reruns retain successful jobs and their outputs from earlier attempts.
- uses: actions/download-artifact@v8
with:
name: verified-cli-${{ github.run_id }}-${{ github.run_attempt }}
artifact-ids: ${{ needs.verify-windows.outputs.artifact_id }},${{ needs.verify-macos.outputs.artifact_id }}
github-token: ${{ github.token }}
run-id: ${{ github.run_id }}
merge-multiple: true

- name: Generate checksums from the final signed archives
env:
VERSION: ${{ needs.verify.outputs.version }}
VERSION: ${{ needs.verify-windows.outputs.version }}
run: |
node scripts/render-version-json.mjs \
--version "$VERSION" --repo "$GITHUB_REPOSITORY" \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
shell: bash
run: cargo build --release --locked -p bsk --target "${{ matrix.target }}"

- name: Package archive (Unix)
- name: Package Unix build input (Mac signing happens internally)
if: matrix.archive == 'tar.gz'
shell: bash
run: |
Expand Down
119 changes: 119 additions & 0 deletions ci/macos-signing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Import into the internal BK-CI project, replace placeholders, then add the
# GitHub Create Branch Or Tag trigger in the UI LAST (Tencent/BrowserSkill, cli-v*).
# Keep Apple credentials and the signing keychain exclusively on the Mac node.
version: v3.0
name: BrowserSkill Mac signing
'on':
manual: enabled
variables:
release_tag:
value: ''
allow-modify-at-startup: true
props:
type: vuex-input
description: Existing CLI tag for manual recovery or verification
required: false
source_run_id:
value: ''
allow-modify-at-startup: true
props:
type: vuex-input
description: Exact successful GitHub Release CLI run ID
required: false
verification_only:
value: 'false'
allow-modify-at-startup: true
props:
type: vuex-input
description: Set true to sign and notarize without modifying any GitHub release
required: true
stages:
- name: Sign and notarize Mac executables
jobs:
sign_macos:
name: Mac Developer ID signing and Apple notarization
mutex:
label: BrowserSkill_macos_signing
runs-on:
self-hosted: true
node-name: MAC_SIGNING_NODE
agent-selector: [macos]
timeout-minutes: 180
steps:
- name: Download pinned helpers and validate GitHub artifacts
uses: linuxScript@1.*
timeout-minutes: 65
env:
BSK_MANUAL_TAG: '${{variables.release_tag}}'
BSK_SOURCE_RUN_ID: '${{variables.source_run_id}}'
BSK_VERIFY_ONLY: '${{variables.verification_only}}'
with:
script: |
#!/usr/bin/env bash
set -euo pipefail
set +x
export GH_TOKEN='${{settings.GITHUB_RELEASE_CREDENTIAL.access_token}}'
cd '${{ci.workspace}}'
SCRIPT_COMMIT=REVIEWED_COMMIT_SHA
for script in windows-release.py macos-release.py; do
curl --fail --silent --show-error --location --retry 3 --max-time 180 \
"https://raw.githubusercontent.com/Tencent/BrowserSkill/$SCRIPT_COMMIT/scripts/$script" --output "$script"
done
export BSK_RELEASE_TAG="${BK_CI_REPO_GITHUB_WEBHOOK_CREATE_REF_NAME:-${BSK_MANUAL_TAG:-}}"
if [[ -n "${BK_CI_REPO_GITHUB_WEBHOOK_CREATE_REF_TYPE:-}" && "$BK_CI_REPO_GITHUB_WEBHOOK_CREATE_REF_TYPE" != tag ]]; then
echo 'Only tag creation events may request signing'; exit 1
fi
args=(prepare --timeout 3600)
if [[ -n "${BSK_SOURCE_RUN_ID:-}" ]]; then args+=(--run-id "$BSK_SOURCE_RUN_ID"); fi
case "$BSK_VERIFY_ONLY" in
true) args+=(--verification-only) ;;
false) ;;
*) echo 'verification_only must be true or false'; exit 1 ;;
esac
python3 macos-release.py "${args[@]}"
- name: Sign both architectures and submit to Apple notarization
uses: linuxScript@1.*
timeout-minutes: 70
with:
script: |
#!/usr/bin/env bash
set -euo pipefail
set +x
export APPLE_ID='${{settings.APPLE_ACCOUNT_CREDENTIAL.username}}'
export APPLE_PASSWORD='${{settings.APPLE_ACCOUNT_CREDENTIAL.password}}'
export APPLE_TEAM_ID='${{settings.APPLE_TEAM_CREDENTIAL.password}}'
cd '${{ci.workspace}}'
helper="$PWD/macos-release.py"
workspace="$PWD"
cd "$(cat macos-workdir.txt)"
python3 "$helper" sign --timeout 3600
mkdir -p "$workspace/evidence"
cp signed-macos.zip macos-signing.json signing-state.json notary-submission.json notary-log.json "$workspace/evidence/"
- name: Return verified result to the existing GitHub draft
uses: linuxScript@1.*
timeout-minutes: 35
env:
BSK_VERIFY_ONLY: '${{variables.verification_only}}'
with:
script: |
#!/usr/bin/env bash
set -euo pipefail
set +x
if [[ "$BSK_VERIFY_ONLY" == true ]]; then
echo 'Verification-only run: no GitHub release is modified.'
exit 0
fi
export GH_TOKEN='${{settings.GITHUB_RELEASE_CREDENTIAL.access_token}}'
cd '${{ci.workspace}}'
helper="$PWD/macos-release.py"
cd "$(cat macos-workdir.txt)"
python3 "$helper" return-signed --timeout 1800
- name: Archive signed binaries and notarization evidence
uses: UploadArtifactory@8.*
timeout-minutes: 5
with:
filePath: evidence/*
concurrency:
queue-timeout-minutes: 30
syntax-dialect: INHERIT
cancel-policy: RESTRICTED
109 changes: 109 additions & 0 deletions docs/macos-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# macOS CLI signing and notarization

The CLI release uses the same trust boundary as Windows: GitHub builds the
executables, BK-CI signs them internally, and GitHub independently verifies the
returned files before publishing one complete release. Apple credentials and the
Developer ID private key never enter GitHub Actions.

## Release flow

1. Creating a `cli-v<version>` tag triggers `Release CLI` and the two internal
signing pipelines through the existing GitHub repository integration. No new
BK-CI API secret is required in GitHub.
2. The Mac pipeline waits for the successful tag build. It checks repository,
workflow, tag commit reachable from the default branch, run ID, run attempt,
artifact identity and archive SHA256 before reading the two Mac executables.
3. The internal Mac signs both thin executables with the approved Tencent
Developer ID Application identity (team `88L2Q4487U`), secure timestamp and
Hardened Runtime. No extra entitlements are added.
4. A ZIP containing both architectures is submitted using `notarytool`. Only
`Accepted` is successful. Each executable's CodeDirectory hash must appear in
Apple's ticket log. Retrying the same build attempt resumes its saved notary
submission and does not re-sign or resubmit the files. Once the handoff is
complete, retries revalidate and reuse the exact ZIP and manifest; refreshed
log timestamps cannot change an already uploaded bundle's identity.
5. Windows creates the GitHub draft. Mac waits for that draft and its Windows
completion marker, then uploads its signed bundle and completion manifest.
Both handoffs include the source run and attempt; neither publishes the draft.
6. `Publish CLI` runs Windows and Mac verification separately. The Mac runner
verifies the Developer ID certificate requirement, team, timestamp, runtime,
embedded signature, online notarization and original program bytes. Only
signature metadata, zero padding and required Mach-O header fields may change.
7. Publishing waits for both jobs and downloads their output artifact IDs,
including a successful job retained from an earlier publishing attempt.
It checks that all platforms refer to the same
build and draft, packages the signed bytes into the existing archive names,
generates `version.json` from the final archives, removes staging assets and
makes the release public. An unsigned Mac result cannot pass this gate.

The existing `bsk-v<version>-{aarch64,x86_64}-apple-darwin.tar.gz` download format
is unchanged. ZIP is used only for notarization and the internal handoff.
Standalone Mach-O files and ZIPs cannot have tickets stapled to them. Online
notarization checks are therefore required; an offline stapled installer would
need a separate PKG or DMG distribution design.

## Internal pipeline setup

Import `ci/macos-signing.yml` into the same BK-CI project as the Windows pipeline.
Replace its placeholders with:

- The existing Mac signing node, running under the account with access to the
Developer ID certificate and private key in its keychain.
- The existing GitHub release credential (`AccessToken`, Actions read and
Contents write for this repository).
- The approved Apple account credential (`username` and app-specific `password`)
and Apple team credential (`password` field). These are project references,
never literal values in YAML or source control.
- A fixed reviewed Git commit containing **both** release helper scripts.

Then add the GitHub **Create Branch Or Tag** trigger for `Tencent/BrowserSkill`,
filter `cli-v*`, and publish the configuration. Add the trigger after YAML import:
this BK-CI deployment can drop CREATE events on UI-to-YAML conversion. The helper
also rejects branch events and validates the actual release tag.

The job uses a mutex group to keep concurrent builds from sharing a signing
workspace. Select the actual node in the UI; a display label including an IP
address or status suffix is not its YAML node alias.

The Mac node needs Python 3, curl, codesign, Xcode command-line tools with
notarytool, and network access to GitHub and Apple's notarization services. It
only signs downloaded build inputs; it does not execute code from the tag or
check out that tag on a machine with signing credentials.

## Verification without publishing

Manually supply an existing CLI tag and successful `Release CLI` run ID, and set
`verification_only=true`. This mode still verifies provenance, signs both
architectures, submits to Apple, verifies the result and archives evidence.
It cannot upload to GitHub or change the existing release. It can reuse retained
Mac build artifacts from an already published version.

Inspect `macos-signing.json`, `notary-log.json`, `notary-submission.json` and
`signed-macos.zip` in the BK build artifacts. The log must show `Accepted`, and the
bundle must contain both architectures. Do not infer validity only from a green
plugin status. Local ad-hoc tests exercise content comparison, not Developer ID
trust or notarization.

## Recovery

- Pending notarization: rerun the same BK build with the same source run and
attempt. Its workspace keeps the signed files and submission ID. Do not clean
that workspace while Apple is processing it.
- Failed/invalid notarization: inspect Apple's log and fix the cause. Do not
publish. If submission succeeded but the connection failed before its ID was
saved, reconcile it with `notarytool history` on the internal node first.
- Source `Release CLI` build rerun: rerun **all** build jobs. Attempt IDs
deliberately prevent mixing stale source artifacts. Start signing for the new
source attempt.
- Windows draft unavailable: recover Windows signing first, then retry the Mac
return step. Only the Windows pipeline owns draft creation.
- GitHub verification timeout: recover internal signing, then use **Re-run failed
jobs** on `Publish CLI`, or manually dispatch it with the successful source run
ID. Partial publishing retries use each verification job's artifact ID, so a
successful platform need not run again. If its verified artifact has expired
(retained for seven days), rerun all `Publish CLI` jobs to verify both platforms
again. Published releases are never silently replaced.

Apple references: [notarization overview](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution),
[custom workflows](https://developer.apple.com/documentation/security/customizing-the-notarization-workflow),
and [resolving notarization issues](https://developer.apple.com/documentation/security/resolving-common-notarization-issues).
Loading