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
21 changes: 21 additions & 0 deletions .github/actions/setup-build/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Set up preview build tools
description: Use Node 24 and a checksum-verified XcodeGen binary.
runs:
using: composite
steps:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
package-manager-cache: false
- name: Install XcodeGen 2.46.0
shell: bash
run: |
tool_directory="$RUNNER_TEMP/xcodegen"
mkdir -p "$tool_directory"
curl --fail --location --retry 5 --connect-timeout 20 \
https://github.com/yonaskolb/XcodeGen/releases/download/2.46.0/xcodegen.zip \
-o "$tool_directory/xcodegen.zip"
echo '4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806 xcodegen.zip' \
| (cd "$tool_directory" && shasum -a 256 -c -)
ditto -x -k "$tool_directory/xcodegen.zip" "$tool_directory"
echo "$tool_directory/xcodegen/bin" >> "$GITHUB_PATH"
42 changes: 42 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Validate Preview Build

on:
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: preview-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
validate:
runs-on: macos-26
timeout-minutes: 60
env:
DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer
PYTHONDONTWRITEBYTECODE: '1'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-build
- name: Test release recovery and version allocation
run: python3 -m unittest discover -s Scripts/tests -v
- name: Check shell syntax
run: bash -n Scripts/build_preview.sh
- name: Test the browser extension
run: npm --prefix BrowserExtension test
- name: Test the native app
run: |
xcodegen generate
xcodebuild -project SmartShot.xcodeproj -scheme SmartShot \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath "$RUNNER_TEMP/TestDerivedData" test \
CODE_SIGNING_ALLOWED=NO REGISTER_WITH_LAUNCH_SERVICES=NO
- name: Build and verify packages without publishing
run: |
read -r version build < <(python3 Scripts/release_artifacts.py version .)
bash Scripts/build_preview.sh "$version" "$build" "$RUNNER_TEMP/preview-check"
88 changes: 88 additions & 0 deletions .github/workflows/preview-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Publish Preview

on:
pull_request_target:
branches: [main]
types: [closed]
workflow_dispatch:
inputs:
pull_request:
description: Number of the merged PR to release or retry
required: true
type: string

permissions:
contents: read

# Preserve pending merges; GitHub's default queue keeps only one pending run.
concurrency:
group: smartshot-preview-release
queue: max

env:
PYTHONDONTWRITEBYTECODE: '1'

jobs:
publish:
if: >-
(github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
runs-on: macos-26
timeout-minutes: 60
permissions:
contents: write
pull-requests: read
env:
DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pull_request }}
steps:
# This is the trusted base revision. Never check out an unmerged PR head.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set temporary paths
run: |
echo "SOURCE_DIRECTORY=$RUNNER_TEMP/release-source" >> "$GITHUB_ENV"
echo "PLAN_FILE=$RUNNER_TEMP/release-plan.json" >> "$GITHUB_ENV"
echo "ASSET_DIRECTORY=$RUNNER_TEMP/release-assets" >> "$GITHUB_ENV"
- name: Resolve and validate the merged PR
id: merged
env:
GH_TOKEN: ${{ github.token }}
run: python3 Scripts/preview_release.py resolve --repository "$GITHUB_REPOSITORY" --pr "$PR_NUMBER"
- name: Check out the exact merged source
env:
MERGE_COMMIT: ${{ steps.merged.outputs.commit }}
run: git worktree add --detach "$SOURCE_DIRECTORY" "$MERGE_COMMIT"
- uses: ./.github/actions/setup-build
- name: Test release recovery and version allocation
run: python3 -m unittest discover -s Scripts/tests -v
- name: Test the browser extension
run: npm --prefix "$SOURCE_DIRECTORY/BrowserExtension" test
- name: Test the native app
run: |
(cd "$SOURCE_DIRECTORY" && xcodegen generate)
xcodebuild -project "$SOURCE_DIRECTORY/SmartShot.xcodeproj" -scheme SmartShot \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath "$RUNNER_TEMP/TestDerivedData" test \
CODE_SIGNING_ALLOWED=NO REGISTER_WITH_LAUNCH_SERVICES=NO
- name: Reserve or reuse a preview version
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
python3 Scripts/preview_release.py reserve --repository "$GITHUB_REPOSITORY" \
--pr "$PR_NUMBER" --source "$SOURCE_DIRECTORY" --plan "$PLAN_FILE"
- name: Build and verify universal packages
if: steps.release.outputs.needs_build == 'true'
env:
RELEASE_VERSION: ${{ steps.release.outputs.version }}
RELEASE_BUILD: ${{ steps.release.outputs.build }}
run: bash Scripts/build_preview.sh "$RELEASE_VERSION" "$RELEASE_BUILD" "$ASSET_DIRECTORY" "$SOURCE_DIRECTORY"
- name: Upload all assets and publish the preview
env:
GH_TOKEN: ${{ github.token }}
run: |
python3 Scripts/preview_release.py publish --repository "$GITHUB_REPOSITORY" \
--plan "$PLAN_FILE" --assets "$ASSET_DIRECTORY"
45 changes: 45 additions & 0 deletions Docs/PREVIEW_RELEASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Automatic preview releases

Every PR merged into `main` triggers **Publish Preview**. Closing an unmerged PR or pushing directly to `main` does not publish a release. **Validate Preview Build** runs the tests and packaging on PRs with read-only permissions, without creating tags or releases.

The release workflow uses `pull_request_target: closed` so merged contributions from forks can also publish. It runs only after merge, checks the PR's repository, base branch and merge commit against `main`, and builds that exact commit in a separate worktree. It never builds an unmerged PR head with a write token. Manual runs are accepted only from `main`.

## Pipeline

1. Run release-script tests, browser extension tests, and native Swift tests on `macos-26` with Xcode 26.6, Node 24 and XcodeGen 2.46.0.
2. Reserve a version in a draft release. Starting after `v0.2.3`, the next version is `v0.2.4`; subsequent releases increase the patch number. Existing tags and draft releases are included when allocating versions. A higher `MARKETING_VERSION` in `project.yml` starts a new version line. Build numbers increase from the source value or the highest automated release's build number.
3. Copy sources to a temporary directory and set the same version in `project.yml`, the browser manifest and its package metadata. Generate the Xcode project, then build the app, Safari extension and both command-line helpers for `arm64` and `x86_64`.
4. Verify the architecture of all four executables, nested signatures, app/extension versions, hardened runtime, required entitlements, and the absence of `get-task-allow`. Create and verify the DMG, app ZIP, browser extension ZIP and `SHA256SUMS.txt`.
5. Create the version tag at the PR's merge commit, upload all four assets to the draft, check their sizes and available server digests, then publish it as a prerelease. Notes are generated from the previous published preview.

The workflow uses the repository's `GITHUB_TOKEN` with `contents: write` and `pull-requests: read`; no personal access token or signing certificate is required. GitHub Actions must be enabled and repository/organization policies must permit these permissions and release tag creation. No source version commit is pushed to `main`: the release tag and its hidden metadata record the source commit, final version and build number. Versions in the checkout remain defaults for local development.

Releases remain ad-hoc signed previews, without Developer ID signing or notarization. They do not use the Safari development build script or its debugging entitlements. Publishing a release does not install it on any developer's Mac.

## Queue and recovery

The shared concurrency group uses [GitHub's `queue: max`](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency), which allows up to 100 pending runs without replacing earlier pending merges. Each run allocates its version while holding that group. Versions follow reservation order; GitHub does not guarantee merge-event ordering.

- A test failure creates no draft or tag. A build failure leaves a draft reservation, and an interrupted upload keeps the release private. Fix the failure, then rerun the workflow.
- A rerun finds the reservation by PR number, keeps its version/build and replaces incomplete draft assets. A complete published release is left intact. A tag pointing to another commit stops the run instead of being moved.
- In **Actions → Publish Preview → Run workflow**, select `main` and enter the merged PR number to retry a missed run. This also bootstraps the PR that first introduces the workflow if its merge event does not start a run. Manual runs use the current workflow and release scripts but build the specified PR's exact merged source.
- Use **Re-run all jobs** for a transient failure. To use a repaired release script after a later PR, use the manual entry point. Do not remove the `smartshot-release` metadata comment from a draft or published release; it is the retry record. Do not manually publish incomplete drafts or move release tags.

Download previews from the [Releases page](https://github.com/infinityf4p/SmartShot/releases). GitHub's `/releases/latest` endpoint excludes prereleases.

## Local validation

With full Xcode, Node.js, Python 3.9+ and XcodeGen 2.46.0 installed:

```sh
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s Scripts/tests -v
npm --prefix BrowserExtension test
xcodegen generate
xcodebuild -project SmartShot.xcodeproj -scheme SmartShot \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath /tmp/SmartShotTests test \
CODE_SIGNING_ALLOWED=NO REGISTER_WITH_LAUNCH_SERVICES=NO
bash Scripts/build_preview.sh 0.2.4 12 /tmp/SmartShotPreviewPackages
```

The packaging output directory must be empty. Packaging uses disposable sources and DerivedData, cleans them on exit, and leaves only the four distributable files. Remove the test DerivedData and output directory after validation. No app is launched or installed. The Python tests use a simulated GitHub API to exercise version allocation, retry recovery, checksum failures and tag conflicts without publishing anything.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SmartShot combines smart block selection, region and long screenshots, an image

[English](README.md) | [简体中文](README.zh-CN.md)

macOS 14+ · Swift 6 · Preview version 0.2.3
macOS 14+ · Swift 6 · Preview releases

## Features

Expand All @@ -25,12 +25,12 @@ macOS 14+ · Swift 6 · Preview version 0.2.3

## Download

[SmartShot 0.2.3 Preview](https://github.com/infinityf4p/SmartShot/releases/tag/v0.2.3) supports Apple Silicon and Intel Macs in one universal application.
[Download the newest SmartShot Preview](https://github.com/infinityf4p/SmartShot/releases). Each release supports Apple Silicon and Intel Macs in one universal application. Choose its assets below:

- [macOS DMG](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SmartShot-0.2.3-universal.dmg)
- [macOS ZIP](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SmartShot-0.2.3-universal.zip)
- [Browser extension ZIP](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SmartShot-Web-Selector-0.2.3.zip)
- [SHA-256 checksums](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SHA256SUMS.txt)
- macOS DMG: `SmartShot-<version>-universal.dmg`
- macOS ZIP: `SmartShot-<version>-universal.zip`
- Browser extension ZIP: `SmartShot-Web-Selector-<version>.zip`
- SHA-256 checksums: `SHA256SUMS.txt`

This preview is ad-hoc signed, without a Developer ID certificate or Apple notarization. macOS may block a downloaded copy until you explicitly approve it in Finder or Privacy & Security. The packaged app has no `get-task-allow` debugging entitlement. Safari development enablement and the pending acceptance cases below still apply.

Expand Down Expand Up @@ -128,5 +128,6 @@ The destination above targets Apple Silicon. Use the appropriate macOS architect
- [Test plan and evidence](Docs/TEST_PLAN.md)
- [Browser extension](BrowserExtension/README.md)
- [Safari development builds](Docs/SAFARI_DEVELOPMENT_BUILD.md)
- [Automatic preview releases](Docs/PREVIEW_RELEASES.md)

Maintained by [infinityf4p](https://github.com/infinityf4p).
13 changes: 7 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SmartShot 将智能选区、区域截图、长截图、图片编辑、本地 OCR

[English](README.md) | [简体中文](README.zh-CN.md)

macOS 14+ · Swift 6 · 预览版本 0.2.3
macOS 14+ · Swift 6 · 预览版

## 功能

Expand All @@ -25,12 +25,12 @@ macOS 14+ · Swift 6 · 预览版本 0.2.3

## 下载

[SmartShot 0.2.3 预览版](https://github.com/infinityf4p/SmartShot/releases/tag/v0.2.3) 提供同时包含 Apple Silicon 和 Intel 架构的通用应用。
[下载最新 SmartShot 预览版](https://github.com/infinityf4p/SmartShot/releases)。每版均提供同时包含 Apple Silicon 和 Intel 架构的通用应用,请在对应版本的附件中选择:

- [macOS DMG](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SmartShot-0.2.3-universal.dmg)
- [macOS ZIP](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SmartShot-0.2.3-universal.zip)
- [浏览器扩展 ZIP](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SmartShot-Web-Selector-0.2.3.zip)
- [SHA-256 校验文件](https://github.com/infinityf4p/SmartShot/releases/download/v0.2.3/SHA256SUMS.txt)
- macOS DMG:`SmartShot-<version>-universal.dmg`
- macOS ZIP:`SmartShot-<version>-universal.zip`
- 浏览器扩展 ZIP:`SmartShot-Web-Selector-<version>.zip`
- SHA-256 校验文件:`SHA256SUMS.txt`

该预览版使用 ad-hoc 签名,没有 Developer ID 证书或 Apple 公证。macOS 可能会阻止下载的应用,需要你在 Finder 或“隐私与安全性”中明确允许打开。安装包不含 `get-task-allow` 调试权限;Safari 开发启用条件及下方待验收范围仍然适用。

Expand Down Expand Up @@ -128,5 +128,6 @@ npm --prefix BrowserExtension test
- [测试计划与记录](Docs/TEST_PLAN.md)
- [浏览器扩展](BrowserExtension/README.md)
- [Safari 开发构建](Docs/SAFARI_DEVELOPMENT_BUILD.md)
- [自动发布预览版](Docs/PREVIEW_RELEASES.md)

维护者:[infinityf4p](https://github.com/infinityf4p)。
61 changes: 61 additions & 0 deletions Scripts/build_preview.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/bash
set -euo pipefail

# Builds in a disposable source copy; only the four packages survive in OUTPUT.
if [[ $# -lt 3 || $# -gt 4 ]]; then
echo "Usage: $0 VERSION BUILD OUTPUT [SOURCE]" >&2
exit 1
fi
script_directory="$(cd -- "$(dirname -- "$0")" && pwd -P)"
version="$1"
build="$2"
mkdir -p "$3"
output="$(cd -- "$3" && pwd -P)"
source_directory="${4:-$script_directory/..}"
if [[ -n "$(ls -A "$output")" ]]; then
echo "Output directory must be empty: $output" >&2
exit 1
fi
scratch="$(mktemp -d "${TMPDIR:-/private/tmp}/SmartShotPreview.XXXXXX")"
app="$scratch/DerivedData/Build/Products/Release/SmartShot.app"
cleanup() {
if [[ -d "$app" ]]; then
/usr/bin/pluginkit -r "$app/Contents/PlugIns/SmartShot Safari Extension.appex" >/dev/null 2>&1 || true
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
-u "$app" >/dev/null 2>&1 || true
fi
rm -rf -- "$scratch"
}
trap cleanup EXIT

python3 "$script_directory/release_artifacts.py" stage "$source_directory" \
--destination "$scratch/source" --version "$version" --build "$build"
(
cd "$scratch/source"
xcodegen generate
)
xcodebuild -project "$scratch/source/SmartShot.xcodeproj" -scheme SmartShot \
-configuration Release -destination 'generic/platform=macOS' \
-derivedDataPath "$scratch/DerivedData" build \
CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY=- DEVELOPMENT_TEAM= \
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO CODE_SIGNING_ALLOWED=YES \
CODE_SIGNING_REQUIRED=YES AD_HOC_CODE_SIGNING_ALLOWED=YES \
ENABLE_HARDENED_RUNTIME=YES REGISTER_WITH_LAUNCH_SERVICES=NO \
ONLY_ACTIVE_ARCH=NO 'ARCHS=arm64 x86_64'
python3 "$script_directory/release_artifacts.py" verify "$app" --version "$version" --build "$build"

ditto -c -k --sequesterRsrc --keepParent "$app" "$output/SmartShot-$version-universal.zip"
mkdir "$scratch/dmg"
ditto "$app" "$scratch/dmg/SmartShot.app"
ln -s /Applications "$scratch/dmg/Applications"
hdiutil create -volname "SmartShot $version" -srcfolder "$scratch/dmg" \
-format UDZO "$output/SmartShot-$version-universal.dmg"
hdiutil verify "$output/SmartShot-$version-universal.dmg"

mkdir "$scratch/extension"
for item in manifest.json background.js content.js shared icons README.md; do
ditto "$scratch/source/BrowserExtension/$item" "$scratch/extension/$item"
done
ditto -c -k "$scratch/extension" "$output/SmartShot-Web-Selector-$version.zip"
python3 "$script_directory/release_artifacts.py" checksums "$output" --version "$version"
echo "Verified preview packages: $output"
Loading