Compare commits

...
10 Commits
Author SHA1 Message Date
felixfoertsch 43c0c88e18 build releases on their native hosts 2026-08-12 11:02:56 +02:00
felixfoertsch 30c2b4fd17 make gatekeeper assessment optional
custom release / build-custom-release (push) Successful in 1m36s
2026-05-24 20:24:17 +02:00
felixfoertsch 623d697e66 avoid daemon default keychain write
custom release / build-custom-release (push) Failing after 9s
2026-05-24 20:04:35 +02:00
felixfoertsch 0dba7d9d9e set codesign default keychain search list
custom release / build-custom-release (push) Failing after 11s
2026-05-24 20:03:30 +02:00
felixfoertsch ffbbd99a79 set codesign dynamic keychain domain
custom release / build-custom-release (push) Failing after 11s
2026-05-24 19:59:31 +02:00
felixfoertsch 6ec6ce1d4e place signing keychain in runner library
custom release / build-custom-release (push) Failing after 10s
2026-05-24 19:56:47 +02:00
felixfoertsch fca9102314 resolve codesign identity from default keychain
custom release / build-custom-release (push) Failing after 9s
2026-05-24 19:55:09 +02:00
felixfoertsch b8ea748973 order codesign signing arguments
custom release / build-custom-release (push) Failing after 10s
2026-05-24 19:52:13 +02:00
felixfoertsch 4b71e4f324 sign releases with developer id common name
custom release / build-custom-release (push) Failing after 10s
2026-05-24 19:50:43 +02:00
felixfoertsch fcaef0869f allow codesign access to imported p12 key
custom release / build-custom-release (push) Successful in 10s
2026-05-24 19:40:13 +02:00
5 changed files with 217 additions and 25 deletions
+36 -8
View File
@@ -7,7 +7,7 @@ permissions:
on:
push:
branches:
- felix/release-automation
- main
paths:
- ".gitea/workflows/custom-release.yml"
- "patches/**"
@@ -18,7 +18,7 @@ on:
description: "Optional upstream Syncthing tag, for example v2.1.0"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.6"
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
@@ -55,9 +55,16 @@ jobs:
- name: Import Developer ID certificate
run: |
set -euo pipefail
keychain_path="$RUNNER_TEMP/syncthing-release-signing.keychain-db"
keychain_dir="$HOME/Library/Keychains"
mkdir -p "$keychain_dir"
keychain_path="$keychain_dir/syncthing-release-signing-${GITHUB_RUN_ID:-$$}.keychain-db"
keychain_password="$(openssl rand -hex 24)"
certificate_path="$RUNNER_TEMP/developer-id-application.p12"
previous_default_keychain="$(security default-keychain -d user 2>/dev/null | sed 's/[ "]//g' || true)"
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=$previous_default_keychain" >> "$GITHUB_ENV"
if [ -z "$DEVELOPER_ID_APPLICATION_P12_BASE64" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required" >&2
@@ -65,23 +72,40 @@ jobs:
fi
printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path"
rm -f "$keychain_path"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | sed 's/[ "]//g')
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -A -T /usr/bin/codesign -T /usr/bin/security
existing_keychains=()
while IFS= read -r existing_keychain; do
existing_keychain="$(printf '%s' "$existing_keychain" | sed 's/[ "]//g')"
if [ -n "$existing_keychain" ] && [ -e "$existing_keychain" ] && [[ "$existing_keychain" != *"/syncthing-release-signing-"*".keychain-db" ]]; then
existing_keychains+=("$existing_keychain")
fi
done < <(security list-keychains)
security list-keychains -s "$keychain_path" "${existing_keychains[@]}"
security list-keychains -d user -s "$keychain_path" "${existing_keychains[@]}" || true
security default-keychain -d user -s "$keychain_path" || true
security list-keychains
security list-keychains -d user || true
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
identity_output="$(security find-identity -v -p codesigning "$keychain_path")"
printf '%s\n' "$identity_output"
codesign_identity="$(printf '%s\n' "$identity_output" | awk '/"Developer ID Application:/ { print $2; exit }')"
security find-identity -v -p codesigning
codesign_identity_sha1="$(printf '%s\n' "$identity_output" | awk '/"Developer ID Application:/ { print $2; exit }')"
codesign_identity="$(printf '%s\n' "$identity_output" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$codesign_identity" ]; then
echo "Developer ID Application signing identity is required in DEVELOPER_ID_APPLICATION_P12_BASE64" >&2
exit 1
fi
probe_binary="$RUNNER_TEMP/codesign-probe"
cp /usr/bin/true "$probe_binary"
codesign --force --dryrun --sign "$codesign_identity" --keychain "$keychain_path" --options runtime --timestamp "$probe_binary"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY=$codesign_identity" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=$codesign_identity_sha1" >> "$GITHUB_ENV"
env:
DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }}
DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }}
@@ -96,12 +120,16 @@ jobs:
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_BUILDS: "darwin/arm64/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U"
CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "1"
CUSTOM_RELEASE_TEA_REPO: felixfoertsch/syncthing
- name: Delete temporary keychain
if: always()
run: |
if [ -n "${CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN:-}" ] && [ -e "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" ]; then
security default-keychain -d user -s "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" || true
fi
if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then
security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true
fi
+80
View File
@@ -0,0 +1,80 @@
name: custom release
permissions:
contents: write
on:
push:
branches:
- main
paths:
- ".github/workflows/custom-release.yml"
- "patches/**"
- "scripts/update-custom-release.sh"
workflow_dispatch:
inputs:
upstream_tag:
description: "Optional upstream Syncthing tag, for example v2.1.3"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
jobs:
build-custom-release:
runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Configure Git author
run: |
git config user.name "GitHub Actions"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Build patched Syncthing release
run: |
case "$(uname -m)" in
arm64) darwin_arch=arm64 ;;
x86_64) darwin_arch=amd64 ;;
*) echo "Unsupported macOS runner architecture: $(uname -m)" >&2; exit 1 ;;
esac
export CUSTOM_RELEASE_BUILDS="darwin/$darwin_arch/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
./scripts/update-custom-release.sh
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
CUSTOM_RELEASE_PUSH: "1"
CUSTOM_RELEASE_PUSH_BRANCH: "0"
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"
GH_TOKEN: ${{ github.token }}
- name: Publish GitHub release
run: |
tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-$(git ls-remote --refs --tags --sort='version:refname' https://github.com/syncthing/syncthing.git 'v[0-9]*' | awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }')}-$CUSTOM_RELEASE_SUFFIX"
if gh release view "$tag" >/dev/null 2>&1; then
echo "GitHub release $tag already exists; nothing to do."
exit 0
fi
assets=()
for asset in dist/*; do
[ -f "$asset" ] || continue
[ "$(basename "$asset")" = release-notes.md ] && continue
assets+=("$asset")
done
gh release create "$tag" "${assets[@]}" --title "$tag" --notes-file dist/release-notes.md --verify-tag
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix || 'stignore.7' }}
GH_TOKEN: ${{ github.token }}
+8 -8
View File
@@ -16,7 +16,7 @@ The automated release flow uses:
By default the script finds the latest stable upstream tag, creates a local
`custom/<version>-<suffix>` branch, applies all local patches, removes upstream
GitHub/Gitea workflow files from the release commit, tags
`<upstream>-stignore.6`, regenerates embedded GUI assets, runs focused tests,
`<upstream>-stignore.7`, regenerates embedded GUI assets, runs focused tests,
and writes build artifacts to `dist/`.
Useful options:
@@ -31,15 +31,15 @@ CUSTOM_RELEASE_CREATE_GITEA_RELEASE=1 CUSTOM_RELEASE_TEA_REPO=felixfoertsch/sync
CUSTOM_RELEASE_BUILDS="darwin/amd64/zip darwin/arm64/zip linux/amd64/tar linux/arm64/tar" ./scripts/update-custom-release.sh
```
The Gitea workflow in `.gitea/workflows/custom-release.yml` runs the script when
the local patchset changes on `felix/release-automation`, on a schedule, and on
manual dispatch. The repository's `main` branch stays a clean upstream mirror.
The Gitea and GitHub workflows run the script on their respective CI hosts when
the local patchset changes on `main`, on a schedule, and on manual dispatch.
The repository's `upstream` branch stays a clean upstream mirror.
The workflow detects the latest upstream Syncthing stable tag, exits if the
matching custom tag already exists, pushes only the `<upstream>-stignore.6` tag
by default, and publishes the Gitea release assets.
matching custom tag already exists, pushes only the `<upstream>-stignore.7` tag
by default, and publishes release assets on the CI host running the workflow.
The local `custom/<version>` branch is only pushed when
`CUSTOM_RELEASE_PUSH_BRANCH=1` is set. The workflow builds macOS amd64, macOS
arm64, Linux amd64, and Linux arm64 archives.
`CUSTOM_RELEASE_PUSH_BRANCH=1` is set. The workflow builds macOS arm64, Linux
amd64, and Linux arm64 archives.
The patch makes root-level `.stignore` sync like regular folder content while
keeping `.stfolder` and `.stversions` protected as internal Syncthing paths.
@@ -3,9 +3,24 @@
setup() {
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKFLOW="$REPO_ROOT/.gitea/workflows/custom-release.yml"
GITHUB_WORKFLOW="$REPO_ROOT/.github/workflows/custom-release.yml"
RELEASE_SCRIPT="$REPO_ROOT/scripts/update-custom-release.sh"
}
@test "github custom release uses a github macos runner and github releases" {
run rg -n 'runs-on:[[:space:]]*macos-14' "$GITHUB_WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'gh release create' "$GITHUB_WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"' "$GITHUB_WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'tea|ffmini_macos_arm64' "$GITHUB_WORKFLOW"
[ "$status" -ne 0 ]
}
@test "custom release runs as one job on ffmini macos runner" {
run rg -n 'runs-on:[[:space:]]*ffmini_macos_arm64' "$WORKFLOW"
[ "$status" -eq 0 ]
@@ -41,21 +56,72 @@ setup() {
run rg -n 'security import .* -P "\$DEVELOPER_ID_APPLICATION_P12_PASSWORD"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security import .* -A ' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security find-identity -v -p codesigning "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security default-keychain -d user -s "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'previous_default_keychain=' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=\$previous_default_keychain' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_PREVIOUS_DYNAMIC_DEFAULT_KEYCHAIN' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security default-keychain -s "\$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN"' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security default-keychain -d dynamic' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security find-identity -v -p codesigning$' "$WORKFLOW" "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'codesign_identity_sha1=' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'sed -n .*Developer ID Application' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY=\$codesign_identity' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=\$codesign_identity_sha1' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_KEYCHAIN_PASSWORD=\$keychain_password' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'codesign --force --dryrun --sign "\$codesign_identity" --keychain "\$keychain_path" --options runtime --timestamp "\$probe_binary"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY: "Developer ID Application' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security create-keychain' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'keychain_dir="\$HOME/Library/Keychains"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'rm -f "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'existing_keychains=\(\)' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security list-keychains -s "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security list-keychains -d dynamic' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security import' "$WORKFLOW"
[ "$status" -eq 0 ]
@@ -75,12 +141,15 @@ setup() {
}
@test "custom release signs darwin assets with hardened runtime and timestamp" {
run rg -n 'codesign_args\+=\(--sign "\$codesign_identity" --options runtime --timestamp\)' "$RELEASE_SCRIPT"
run rg -n 'codesign_args=\(--force --sign "\$codesign_identity"\)' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n -- '--keychain "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'codesign_args\+=\(--options runtime --timestamp\)' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'security unlock-keychain -p "\$CUSTOM_RELEASE_KEYCHAIN_PASSWORD" "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
@@ -88,10 +157,10 @@ setup() {
[ "$status" -eq 0 ]
run awk '
/codesign_args=\(--force\)/ { base = NR }
/codesign_args=\(--force --sign "\$codesign_identity"\)/ { sign = NR }
/codesign_args\+=\(--keychain "\$CUSTOM_RELEASE_KEYCHAIN_PATH"\)/ { keychain = NR }
/codesign_args\+=\(--sign "\$codesign_identity" --options runtime --timestamp\)/ { sign = NR }
END { exit !(base && keychain && sign && base < keychain && keychain < sign) }
/codesign_args\+=\(--options runtime --timestamp\)/ { options = NR }
END { exit !(sign && keychain && options && sign < keychain && keychain < options) }
' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
@@ -102,7 +171,7 @@ setup() {
[ "$status" -eq 0 ]
}
@test "custom release validates darwin binaries before publishing" {
@test "custom release validates signed darwin binaries before publishing" {
run rg -n 'modernc-sqlite' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
@@ -112,6 +181,15 @@ setup() {
run rg -n 'TeamIdentifier=NG5W75WE8U|TeamIdentifier.*NG5W75WE8U' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'require_gatekeeper_assessment="\$\{CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT:-0\}"' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'if \[\[ "\$require_gatekeeper_assessment" == "1" \]\]; then' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'spctl -a -vv --type execute' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
}
+10 -4
View File
@@ -6,7 +6,7 @@ set -euo pipefail
upstream_url="${CUSTOM_RELEASE_UPSTREAM_URL:-https://github.com/syncthing/syncthing.git}"
upstream_tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-}"
suffix="${CUSTOM_RELEASE_SUFFIX:-stignore.6}"
suffix="${CUSTOM_RELEASE_SUFFIX:-stignore.7}"
branch_prefix="${CUSTOM_RELEASE_BRANCH_PREFIX:-custom}"
dist_dir="${CUSTOM_RELEASE_DIST_DIR:-dist}"
target="${CUSTOM_RELEASE_TARGET:-syncthing}"
@@ -15,6 +15,7 @@ build_specs="${CUSTOM_RELEASE_BUILDS:-}"
default_cgo_enabled="${CUSTOM_RELEASE_CGO_ENABLED:-0}"
codesign_identity="${CUSTOM_RELEASE_CODESIGN_IDENTITY:-}"
codesign_team_id="${CUSTOM_RELEASE_CODESIGN_TEAM_ID:-NG5W75WE8U}"
require_gatekeeper_assessment="${CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT:-0}"
push_release="${CUSTOM_RELEASE_PUSH:-0}"
push_branch="${CUSTOM_RELEASE_PUSH_BRANCH:-0}"
push_remote="${CUSTOM_RELEASE_REMOTE:-origin}"
@@ -302,15 +303,16 @@ sign_and_validate_darwin_binary() {
[[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds"
codesign_args=(--force)
codesign_args=(--force --sign "$codesign_identity")
if [[ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]]; then
if [[ -n "${CUSTOM_RELEASE_KEYCHAIN_PASSWORD:-}" ]]; then
security unlock-keychain -p "$CUSTOM_RELEASE_KEYCHAIN_PASSWORD" "$CUSTOM_RELEASE_KEYCHAIN_PATH"
fi
security find-identity -v -p codesigning "$CUSTOM_RELEASE_KEYCHAIN_PATH"
security find-identity -v -p codesigning
codesign_args+=(--keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH")
fi
codesign_args+=(--sign "$codesign_identity" --options runtime --timestamp)
codesign_args+=(--options runtime --timestamp)
codesign "${codesign_args[@]}" "$binary"
version_output="$("$binary" --version)"
@@ -328,7 +330,11 @@ sign_and_validate_darwin_binary() {
printf '%s\n' "$codesign_details" >&2
die "darwin build is not signed by TeamIdentifier=$codesign_team_id"
fi
spctl -a -vv --type execute "$binary"
if [[ "$require_gatekeeper_assessment" == "1" ]]; then
spctl -a -vv --type execute "$binary"
else
log "Skipping Gatekeeper assessment because CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT=$require_gatekeeper_assessment"
fi
}
push_refs() {