Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30c2b4fd17 | |||
| 623d697e66 | |||
| 0dba7d9d9e | |||
| ffbbd99a79 | |||
| 6ec6ce1d4e | |||
| fca9102314 | |||
| b8ea748973 | |||
| 4b71e4f324 | |||
| fcaef0869f | |||
| 4f2829ed48 | |||
| cda7f54a48 | |||
| 55f5a70ff5 | |||
| 083f8e7068 | |||
| 018d62af14 | |||
| d45edca330 | |||
| 4cac15184c | |||
| eb67464ca7 |
@@ -6,3 +6,6 @@ vendor/** -text=auto
|
||||
|
||||
# Diffs on these files are meaningless
|
||||
*.svg -diff
|
||||
|
||||
# Patch files intentionally contain diff context whitespace
|
||||
patches/*.patch -whitespace
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
name: custom release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
releases: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- felix/release-automation
|
||||
paths:
|
||||
- ".gitea/workflows/custom-release.yml"
|
||||
- "patches/**"
|
||||
- "scripts/update-custom-release.sh"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upstream_tag:
|
||||
description: "Optional upstream Syncthing tag, for example v2.1.0"
|
||||
required: false
|
||||
suffix:
|
||||
description: "Optional custom release suffix, for example stignore.7"
|
||||
required: false
|
||||
schedule:
|
||||
- cron: "17 04 * * *"
|
||||
|
||||
jobs:
|
||||
build-custom-release:
|
||||
runs-on: ffmini_macos_arm64
|
||||
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 "Gitea Actions"
|
||||
git config user.email "actions@git.felixfoertsch.de"
|
||||
|
||||
- name: Set up tea
|
||||
run: |
|
||||
go install code.gitea.io/tea@v0.14.1
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||
"$(go env GOPATH)/bin/tea" logins delete actions >/dev/null 2>&1 || true
|
||||
"$(go env GOPATH)/bin/tea" logins add --name actions --url https://git.felixfoertsch.de --token "$GITEA_TOKEN" --no-version-check
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Import Developer ID certificate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
exit 1
|
||||
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" -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"
|
||||
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_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 }}
|
||||
|
||||
- name: Build patched Syncthing release
|
||||
run: ./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_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
|
||||
@@ -18,3 +18,4 @@ deb
|
||||
/repos
|
||||
/proto/scripts/protoc-gen-gosyncthing
|
||||
/compat.json
|
||||
/dist/
|
||||
|
||||
@@ -284,8 +284,8 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
|
||||
}
|
||||
|
||||
// fs cannot import config or versioner, so we hard code .stfolder
|
||||
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath)
|
||||
var internals = []string{".stfolder", ".stignore", ".stversions"}
|
||||
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath).
|
||||
var internals = []string{".stfolder", ".stversions"}
|
||||
|
||||
// IsInternal returns true if the file, as a path relative to the folder
|
||||
// root, represents an internal file that should always be ignored. The file
|
||||
|
||||
@@ -21,10 +21,8 @@ func TestIsInternal(t *testing.T) {
|
||||
internal bool
|
||||
}{
|
||||
{".stfolder", true},
|
||||
{".stignore", true},
|
||||
{".stversions", true},
|
||||
{".stfolder/foo", true},
|
||||
{".stignore/foo", true},
|
||||
{".stversions/foo", true},
|
||||
|
||||
{".stfolderfoo", false},
|
||||
@@ -34,6 +32,8 @@ func TestIsInternal(t *testing.T) {
|
||||
{"foo.stignore", false},
|
||||
{"foo.stversions", false},
|
||||
{"foo/.stfolder", false},
|
||||
{".stignore", false},
|
||||
{".stignore/foo", false},
|
||||
{"foo/.stignore", false},
|
||||
{"foo/.stversions", false},
|
||||
}
|
||||
|
||||
@@ -60,15 +60,15 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
|
||||
must(t, m.ScanFolder("ro"))
|
||||
|
||||
// We should now have two files and two directories, with global state unchanged.
|
||||
// We should now have three files and two directories, with global state unchanged.
|
||||
|
||||
size = mustV(m.GlobalSize("ro"))
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
|
||||
}
|
||||
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
|
||||
if size.Files != 2 || size.Directories != 2 {
|
||||
t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
|
||||
if size.Files != 3 || size.Directories != 2 {
|
||||
t.Fatalf("Local: expected 3 files and 2 directories: %+v", size)
|
||||
}
|
||||
size = mustV(m.ReceiveOnlySize("ro"))
|
||||
if size.Files+size.Directories == 0 {
|
||||
|
||||
@@ -974,13 +974,15 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
file := "foobar"
|
||||
contents := []byte("test file contents\n")
|
||||
|
||||
basicCheck := func(fs []protocol.FileInfo) {
|
||||
basicCheck := func(fs []protocol.FileInfo) protocol.FileInfo {
|
||||
t.Helper()
|
||||
if len(fs) != 1 {
|
||||
t.Fatal("expected a single index entry, got", len(fs))
|
||||
} else if fs[0].Name != file {
|
||||
t.Fatalf("expected a index entry for %v, got one for %v", file, fs[0].Name)
|
||||
for _, f := range fs {
|
||||
if f.Name == file {
|
||||
return f
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected an index entry for %v, got %v", file, fs)
|
||||
return protocol.FileInfo{}
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -1001,8 +1003,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
basicCheck(fs)
|
||||
f := fs[0]
|
||||
f := basicCheck(fs)
|
||||
if !f.IsInvalid() {
|
||||
t.Errorf("Received non-invalid index update")
|
||||
}
|
||||
@@ -1022,8 +1023,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
basicCheck(fs)
|
||||
f := fs[0]
|
||||
f := basicCheck(fs)
|
||||
if f.IsInvalid() {
|
||||
t.Errorf("Received invalid index update")
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ type testfile struct {
|
||||
type testfileList []testfile
|
||||
|
||||
var testdata = testfileList{
|
||||
{".stignore", 48, "f60db5c36f642f8a6c1a636024330cd4dba6ab965083542f3629ff7d8c547911"},
|
||||
{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
|
||||
{"dir1", 128, ""},
|
||||
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Local Syncthing Patches
|
||||
|
||||
Apply the local patches manually after pulling a new upstream Syncthing release:
|
||||
|
||||
```bash
|
||||
git apply patches/sync-stignore.patch patches/webui-build-marker.patch
|
||||
go run build.go -build-out bin/syncthing-stignore build syncthing
|
||||
```
|
||||
|
||||
The automated release flow uses:
|
||||
|
||||
```bash
|
||||
./scripts/update-custom-release.sh
|
||||
```
|
||||
|
||||
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,
|
||||
and writes build artifacts to `dist/`.
|
||||
|
||||
Useful options:
|
||||
|
||||
```bash
|
||||
CUSTOM_RELEASE_UPSTREAM_TAG=v2.1.0 ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_FORCE=1 ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_PATCHES="patches/sync-stignore.patch patches/webui-build-marker.patch" ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_PUSH=1 CUSTOM_RELEASE_REMOTE=gitea ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_PUSH=1 CUSTOM_RELEASE_PUSH_BRANCH=1 CUSTOM_RELEASE_REMOTE=gitea ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_CREATE_GITEA_RELEASE=1 CUSTOM_RELEASE_TEA_REPO=felixfoertsch/syncthing ./scripts/update-custom-release.sh
|
||||
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 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.
|
||||
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.
|
||||
|
||||
The patch makes root-level `.stignore` sync like regular folder content while
|
||||
keeping `.stfolder` and `.stversions` protected as internal Syncthing paths.
|
||||
The web UI patch adds `It syncs .stignore now!` to the GUI footer so the custom
|
||||
binary is visually distinguishable from upstream builds. The release test fails
|
||||
if the generated GUI asset bundle does not contain that marker.
|
||||
@@ -0,0 +1,119 @@
|
||||
diff --git a/lib/fs/filesystem.go b/lib/fs/filesystem.go
|
||||
index 31bbb9770..dcece778e 100644
|
||||
--- a/lib/fs/filesystem.go
|
||||
+++ b/lib/fs/filesystem.go
|
||||
@@ -284,8 +284,8 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
|
||||
}
|
||||
|
||||
// fs cannot import config or versioner, so we hard code .stfolder
|
||||
-// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath)
|
||||
-var internals = []string{".stfolder", ".stignore", ".stversions"}
|
||||
+// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath).
|
||||
+var internals = []string{".stfolder", ".stversions"}
|
||||
|
||||
// IsInternal returns true if the file, as a path relative to the folder
|
||||
// root, represents an internal file that should always be ignored. The file
|
||||
diff --git a/lib/fs/filesystem_test.go b/lib/fs/filesystem_test.go
|
||||
index 08e45ff50..2d100a2b0 100644
|
||||
--- a/lib/fs/filesystem_test.go
|
||||
+++ b/lib/fs/filesystem_test.go
|
||||
@@ -21,10 +21,8 @@ func TestIsInternal(t *testing.T) {
|
||||
internal bool
|
||||
}{
|
||||
{".stfolder", true},
|
||||
- {".stignore", true},
|
||||
{".stversions", true},
|
||||
{".stfolder/foo", true},
|
||||
- {".stignore/foo", true},
|
||||
{".stversions/foo", true},
|
||||
|
||||
{".stfolderfoo", false},
|
||||
@@ -34,6 +32,8 @@ func TestIsInternal(t *testing.T) {
|
||||
{"foo.stignore", false},
|
||||
{"foo.stversions", false},
|
||||
{"foo/.stfolder", false},
|
||||
+ {".stignore", false},
|
||||
+ {".stignore/foo", false},
|
||||
{"foo/.stignore", false},
|
||||
{"foo/.stversions", false},
|
||||
}
|
||||
diff --git a/lib/model/folder_recvonly_test.go b/lib/model/folder_recvonly_test.go
|
||||
index e84d0c49d..fbd442966 100644
|
||||
--- a/lib/model/folder_recvonly_test.go
|
||||
+++ b/lib/model/folder_recvonly_test.go
|
||||
@@ -60,15 +60,15 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
|
||||
must(t, m.ScanFolder("ro"))
|
||||
|
||||
- // We should now have two files and two directories, with global state unchanged.
|
||||
+ // We should now have three files and two directories, with global state unchanged.
|
||||
|
||||
size = mustV(m.GlobalSize("ro"))
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
|
||||
}
|
||||
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
|
||||
- if size.Files != 2 || size.Directories != 2 {
|
||||
- t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
|
||||
+ if size.Files != 3 || size.Directories != 2 {
|
||||
+ t.Fatalf("Local: expected 3 files and 2 directories: %+v", size)
|
||||
}
|
||||
size = mustV(m.ReceiveOnlySize("ro"))
|
||||
if size.Files+size.Directories == 0 {
|
||||
diff --git a/lib/model/requests_test.go b/lib/model/requests_test.go
|
||||
index 288140721..6f8124e97 100644
|
||||
--- a/lib/model/requests_test.go
|
||||
+++ b/lib/model/requests_test.go
|
||||
@@ -974,13 +974,15 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
file := "foobar"
|
||||
contents := []byte("test file contents\n")
|
||||
|
||||
- basicCheck := func(fs []protocol.FileInfo) {
|
||||
+ basicCheck := func(fs []protocol.FileInfo) protocol.FileInfo {
|
||||
t.Helper()
|
||||
- if len(fs) != 1 {
|
||||
- t.Fatal("expected a single index entry, got", len(fs))
|
||||
- } else if fs[0].Name != file {
|
||||
- t.Fatalf("expected a index entry for %v, got one for %v", file, fs[0].Name)
|
||||
+ for _, f := range fs {
|
||||
+ if f.Name == file {
|
||||
+ return f
|
||||
+ }
|
||||
}
|
||||
+ t.Fatalf("expected an index entry for %v, got %v", file, fs)
|
||||
+ return protocol.FileInfo{}
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -1001,8 +1003,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
- basicCheck(fs)
|
||||
- f := fs[0]
|
||||
+ f := basicCheck(fs)
|
||||
if !f.IsInvalid() {
|
||||
t.Errorf("Received non-invalid index update")
|
||||
}
|
||||
@@ -1022,8 +1023,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
- basicCheck(fs)
|
||||
- f := fs[0]
|
||||
+ f := basicCheck(fs)
|
||||
if f.IsInvalid() {
|
||||
t.Errorf("Received invalid index update")
|
||||
}
|
||||
diff --git a/lib/scanner/walk_test.go b/lib/scanner/walk_test.go
|
||||
index d75dddaf3..3693ae4db 100644
|
||||
--- a/lib/scanner/walk_test.go
|
||||
+++ b/lib/scanner/walk_test.go
|
||||
@@ -40,6 +40,7 @@ type testfile struct {
|
||||
type testfileList []testfile
|
||||
|
||||
var testdata = testfileList{
|
||||
+ {".stignore", 48, "f60db5c36f642f8a6c1a636024330cd4dba6ab965083542f3629ff7d8c547911"},
|
||||
{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
|
||||
{"dir1", 128, ""},
|
||||
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
|
||||
@@ -0,0 +1,62 @@
|
||||
diff --git a/gui/default/index.html b/gui/default/index.html
|
||||
index e3e573c9a..5ec76e740 100644
|
||||
--- a/gui/default/index.html
|
||||
+++ b/gui/default/index.html
|
||||
@@ -1017,6 +1017,9 @@
|
||||
</div> <!-- /row -->
|
||||
|
||||
</div> <!-- /container -->
|
||||
+ <footer class="container text-center text-muted small" aria-label="Custom build marker">
|
||||
+ It syncs .stignore now!
|
||||
+ </footer>
|
||||
</div> <!-- /ng-cloak -->
|
||||
|
||||
<ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
|
||||
diff --git a/lib/api/auto/custom_marker_test.go b/lib/api/auto/custom_marker_test.go
|
||||
new file mode 100644
|
||||
index 000000000..73eae3db7
|
||||
--- /dev/null
|
||||
+++ b/lib/api/auto/custom_marker_test.go
|
||||
@@ -0,0 +1,42 @@
|
||||
+// Copyright (C) 2026 The Syncthing Authors.
|
||||
+//
|
||||
+// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
+// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
+
|
||||
+package auto
|
||||
+
|
||||
+import (
|
||||
+ "compress/gzip"
|
||||
+ "io"
|
||||
+ "strings"
|
||||
+ "testing"
|
||||
+)
|
||||
+
|
||||
+const customBuildMarker = "It syncs .stignore now!"
|
||||
+
|
||||
+func TestCustomBuildMarkerIsEmbedded(t *testing.T) {
|
||||
+ asset, ok := Assets()["default/index.html"]
|
||||
+ if !ok {
|
||||
+ t.Fatal("default/index.html is missing from embedded GUI assets")
|
||||
+ }
|
||||
+
|
||||
+ content := asset.Content
|
||||
+ if asset.Gzipped {
|
||||
+ reader, err := gzip.NewReader(strings.NewReader(asset.Content))
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ defer reader.Close()
|
||||
+
|
||||
+ data, err := io.ReadAll(reader)
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ content = string(data)
|
||||
+ }
|
||||
+
|
||||
+ if !strings.Contains(content, customBuildMarker) {
|
||||
+ t.Fatalf("embedded GUI assets do not contain custom build marker %q", customBuildMarker)
|
||||
+ }
|
||||
+}
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKFLOW="$REPO_ROOT/.gitea/workflows/custom-release.yml"
|
||||
RELEASE_SCRIPT="$REPO_ROOT/scripts/update-custom-release.sh"
|
||||
}
|
||||
|
||||
@test "custom release runs as one job on ffmini macos runner" {
|
||||
run rg -n 'runs-on:[[:space:]]*ffmini_macos_arm64' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'ubuntu-latest' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'actions/upload-artifact' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "custom release tea login setup is idempotent on persistent host runner" {
|
||||
run rg -n 'tea" logins delete actions >/dev/null 2>&1 \|\| true' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'tea" logins add --name actions' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "custom release workflow imports developer id signing material into temporary keychain" {
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_BASE64' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_PASSWORD' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_PASSWORD secret is required' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
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 ]
|
||||
|
||||
run rg -n 'security delete-keychain' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "custom release carries per-target cgo mode" {
|
||||
run rg -n 'darwin/arm64/zip/1' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'linux/amd64/tar/0' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CGO_ENABLED' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "custom release signs darwin assets with hardened runtime and timestamp" {
|
||||
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 ]
|
||||
|
||||
run rg -n 'security find-identity -v -p codesigning "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run awk '
|
||||
/codesign_args=\(--force --sign "\$codesign_identity"\)/ { sign = NR }
|
||||
/codesign_args\+=\(--keychain "\$CUSTOM_RELEASE_KEYCHAIN_PATH"\)/ { keychain = NR }
|
||||
/codesign_args\+=\(--options runtime --timestamp\)/ { options = NR }
|
||||
END { exit !(sign && keychain && options && sign < keychain && keychain < options) }
|
||||
' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'Developer ID Application' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "custom release validates signed darwin binaries before publishing" {
|
||||
run rg -n 'modernc-sqlite' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'codesign --verify --strict --verbose=2' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
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 ]
|
||||
}
|
||||
Executable
+416
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Description: create a patched Syncthing release from the latest upstream tag.
|
||||
# Usage: ./scripts/update-custom-release.sh
|
||||
|
||||
upstream_url="${CUSTOM_RELEASE_UPSTREAM_URL:-https://github.com/syncthing/syncthing.git}"
|
||||
upstream_tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-}"
|
||||
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}"
|
||||
archive_kind="${CUSTOM_RELEASE_ARCHIVE:-tar}"
|
||||
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}"
|
||||
run_tests="${CUSTOM_RELEASE_TEST:-1}"
|
||||
force="${CUSTOM_RELEASE_FORCE:-0}"
|
||||
publish_gitea_release="${CUSTOM_RELEASE_CREATE_GITEA_RELEASE:-0}"
|
||||
tea_repo="${CUSTOM_RELEASE_TEA_REPO:-}"
|
||||
patch_tmp_dir=""
|
||||
patch_files=()
|
||||
assets_rebuilt=0
|
||||
|
||||
if [[ -n "${CUSTOM_RELEASE_PATCHES:-}" ]]; then
|
||||
read -r -a patch_files <<< "$CUSTOM_RELEASE_PATCHES"
|
||||
elif [[ -n "${CUSTOM_RELEASE_PATCH:-}" ]]; then
|
||||
patch_files=("$CUSTOM_RELEASE_PATCH")
|
||||
else
|
||||
patch_files=(
|
||||
patches/sync-stignore.patch
|
||||
patches/webui-build-marker.patch
|
||||
)
|
||||
fi
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_clean_worktree() {
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
die "working tree has uncommitted changes"
|
||||
fi
|
||||
}
|
||||
|
||||
latest_stable_tag() {
|
||||
git ls-remote --refs --tags --sort='version:refname' "$upstream_url" 'v[0-9]*' \
|
||||
| awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }'
|
||||
}
|
||||
|
||||
tag_exists() {
|
||||
local tag="$1"
|
||||
|
||||
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
git ls-remote --quiet --exit-code --tags "$push_remote" "refs/tags/$tag" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
copy_patches_to_temp() {
|
||||
local patch_tmp_dir="$1"
|
||||
local patch_file
|
||||
local patch_name
|
||||
local patch_index=0
|
||||
|
||||
mkdir -p "$patch_tmp_dir"
|
||||
for patch_file in "${patch_files[@]}"; do
|
||||
[[ -f "$patch_file" ]] || die "patch file not found: $patch_file"
|
||||
printf -v patch_name '%03d-%s' "$patch_index" "$(basename "$patch_file")"
|
||||
cp "$patch_file" "$patch_tmp_dir/$patch_name"
|
||||
patch_index=$((patch_index + 1))
|
||||
done
|
||||
}
|
||||
|
||||
fetch_upstream_tag() {
|
||||
local tag="$1"
|
||||
|
||||
git fetch --force "$upstream_url" "refs/tags/$tag:refs/tags/$tag"
|
||||
}
|
||||
|
||||
create_release_commit() {
|
||||
local tag="$1"
|
||||
local custom_tag="$2"
|
||||
local branch="$3"
|
||||
local patch_tmp_dir="$4"
|
||||
local patch_file
|
||||
|
||||
git checkout -B "$branch" "$tag"
|
||||
rm -rf "$dist_dir"
|
||||
for patch_file in "$patch_tmp_dir"/*; do
|
||||
[[ -f "$patch_file" ]] || continue
|
||||
log "Applying $(basename "$patch_file")"
|
||||
git apply --3way "$patch_file"
|
||||
done
|
||||
remove_upstream_workflows
|
||||
git add -A
|
||||
git commit -m "apply local Syncthing patches for $tag"
|
||||
git tag -a "$custom_tag" -m "Syncthing $tag with local patches"
|
||||
}
|
||||
|
||||
remove_upstream_workflows() {
|
||||
local workflow_dir
|
||||
|
||||
for workflow_dir in .github/workflows .gitea/workflows; do
|
||||
if [[ -e "$workflow_dir" ]]; then
|
||||
log "Removing upstream workflow directory $workflow_dir from release commit"
|
||||
rm -rf "$workflow_dir"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
delete_local_tag_if_forced() {
|
||||
local tag="$1"
|
||||
|
||||
if [[ "$force" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
|
||||
git tag -d "$tag"
|
||||
fi
|
||||
}
|
||||
|
||||
test_release() {
|
||||
if [[ "$run_tests" != "1" ]]; then
|
||||
log "Skipping tests because CUSTOM_RELEASE_TEST=$run_tests"
|
||||
return
|
||||
fi
|
||||
|
||||
rebuild_assets_once
|
||||
go test ./lib/api/auto ./lib/fs ./lib/ignore ./lib/scanner ./lib/model
|
||||
}
|
||||
|
||||
rebuild_assets_once() {
|
||||
if [[ "$assets_rebuilt" == "1" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
log "Regenerating embedded GUI assets"
|
||||
go run build.go assets
|
||||
assets_rebuilt=1
|
||||
}
|
||||
|
||||
format_build_specs() {
|
||||
local spec
|
||||
|
||||
for spec in $build_specs; do
|
||||
printf -- '- %s\n' "$spec"
|
||||
done
|
||||
}
|
||||
|
||||
build_release() {
|
||||
local custom_tag="$1"
|
||||
|
||||
rebuild_assets_once
|
||||
rm -rf "$dist_dir"
|
||||
mkdir -p "$dist_dir"
|
||||
|
||||
if [[ -z "$build_specs" ]]; then
|
||||
build_specs="$(go env GOOS)/$(go env GOARCH)/$archive_kind"
|
||||
fi
|
||||
|
||||
local spec
|
||||
for spec in $build_specs; do
|
||||
build_one "$custom_tag" "$spec"
|
||||
done
|
||||
|
||||
cat > "$dist_dir/release-notes.md" <<EOF
|
||||
# $custom_tag
|
||||
|
||||
Syncthing $upstream_tag with the local patches applied.
|
||||
|
||||
Builds:
|
||||
$(format_build_specs)
|
||||
|
||||
Patches:
|
||||
$(printf -- '- %s\n' "${patch_files[@]}")
|
||||
|
||||
Patch behavior:
|
||||
- Root-level .stignore syncs like regular folder content.
|
||||
- .stfolder and .stversions stay protected as Syncthing internals.
|
||||
- The web GUI footer includes "It syncs .stignore now!" as a custom build marker.
|
||||
EOF
|
||||
|
||||
(
|
||||
cd "$dist_dir"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum ./* > SHA256SUMS
|
||||
else
|
||||
shasum -a 256 ./* > SHA256SUMS
|
||||
fi
|
||||
)
|
||||
}
|
||||
|
||||
build_one() {
|
||||
local custom_tag="$1"
|
||||
local spec="$2"
|
||||
local goos
|
||||
local goarch
|
||||
local kind
|
||||
local cgo_enabled
|
||||
|
||||
IFS=/ read -r goos goarch kind cgo_enabled <<< "$spec"
|
||||
[[ -n "$goos" && -n "$goarch" && -n "$kind" ]] || die "invalid build spec: $spec"
|
||||
cgo_enabled="${cgo_enabled:-$default_cgo_enabled}"
|
||||
|
||||
log "Building $target for $goos/$goarch as $kind with CGO_ENABLED=$cgo_enabled"
|
||||
|
||||
case "$kind" in
|
||||
tar|zip)
|
||||
local archive
|
||||
archive="$(CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" "$kind" "$target" | tail -n 1)"
|
||||
if [[ "$goos" == "darwin" ]]; then
|
||||
sign_and_validate_darwin_archive "$archive" "$kind"
|
||||
fi
|
||||
mv "$archive" "$dist_dir/"
|
||||
;;
|
||||
binary)
|
||||
CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" -build-out "$dist_dir/$target-$goos-$goarch" build "$target"
|
||||
if [[ "$goos" == "darwin" ]]; then
|
||||
sign_and_validate_darwin_binary "$dist_dir/$target-$goos-$goarch"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
die "unknown build archive kind in $spec"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
find_release_binary() {
|
||||
local root="$1"
|
||||
local candidate
|
||||
|
||||
while IFS= read -r candidate; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return
|
||||
fi
|
||||
done < <(find "$root" -type f -name "$target")
|
||||
|
||||
die "could not find executable $target in $root"
|
||||
}
|
||||
|
||||
sign_and_validate_darwin_archive() {
|
||||
local archive="$1"
|
||||
local kind="$2"
|
||||
local tmp
|
||||
local archive_abs
|
||||
local binary
|
||||
|
||||
[[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
archive_abs="$(cd "$(dirname "$archive")" && pwd -P)/$(basename "$archive")"
|
||||
case "$kind" in
|
||||
zip)
|
||||
unzip -q "$archive_abs" -d "$tmp"
|
||||
;;
|
||||
tar)
|
||||
tar -xf "$archive_abs" -C "$tmp"
|
||||
;;
|
||||
*)
|
||||
die "cannot sign archive kind $kind"
|
||||
;;
|
||||
esac
|
||||
|
||||
binary="$(find_release_binary "$tmp")"
|
||||
sign_and_validate_darwin_binary "$binary"
|
||||
|
||||
rm -f "$archive_abs"
|
||||
case "$kind" in
|
||||
zip)
|
||||
(
|
||||
cd "$tmp"
|
||||
zip -qr "$archive_abs" .
|
||||
)
|
||||
;;
|
||||
tar)
|
||||
(
|
||||
cd "$tmp"
|
||||
tar -czf "$archive_abs" .
|
||||
)
|
||||
;;
|
||||
esac
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
sign_and_validate_darwin_binary() {
|
||||
local binary="$1"
|
||||
local version_output
|
||||
local codesign_details
|
||||
local codesign_args
|
||||
|
||||
[[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds"
|
||||
|
||||
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+=(--options runtime --timestamp)
|
||||
codesign "${codesign_args[@]}" "$binary"
|
||||
|
||||
version_output="$("$binary" --version)"
|
||||
if [[ "$version_output" == *modernc-sqlite* ]]; then
|
||||
die "darwin build unexpectedly reports [modernc-sqlite]: $version_output"
|
||||
fi
|
||||
|
||||
codesign --verify --strict --verbose=2 "$binary"
|
||||
codesign_details="$(codesign -dv --verbose=4 "$binary" 2>&1)"
|
||||
if [[ "$codesign_team_id" == "NG5W75WE8U" && "$codesign_details" != *"TeamIdentifier=NG5W75WE8U"* ]]; then
|
||||
printf '%s\n' "$codesign_details" >&2
|
||||
die "darwin build is not signed by TeamIdentifier=NG5W75WE8U"
|
||||
fi
|
||||
if [[ "$codesign_details" != *"TeamIdentifier=$codesign_team_id"* ]]; then
|
||||
printf '%s\n' "$codesign_details" >&2
|
||||
die "darwin build is not signed by TeamIdentifier=$codesign_team_id"
|
||||
fi
|
||||
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() {
|
||||
local branch="$1"
|
||||
local custom_tag="$2"
|
||||
|
||||
if [[ "$push_release" != "1" ]]; then
|
||||
log "Skipping push because CUSTOM_RELEASE_PUSH=$push_release"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$push_branch" == "1" ]]; then
|
||||
git push "$push_remote" "$branch"
|
||||
else
|
||||
log "Skipping release branch push because CUSTOM_RELEASE_PUSH_BRANCH=$push_branch"
|
||||
fi
|
||||
git push "$push_remote" "$custom_tag"
|
||||
}
|
||||
|
||||
publish_release() {
|
||||
local custom_tag="$1"
|
||||
|
||||
if [[ "$publish_gitea_release" != "1" ]]; then
|
||||
log "Skipping Gitea release publishing because CUSTOM_RELEASE_CREATE_GITEA_RELEASE=$publish_gitea_release"
|
||||
return
|
||||
fi
|
||||
|
||||
command -v tea >/dev/null 2>&1 || die "tea is required for Gitea release publishing"
|
||||
|
||||
local args=(releases create "$custom_tag" --title "$custom_tag" --note-file "$dist_dir/release-notes.md")
|
||||
if [[ -n "$tea_repo" ]]; then
|
||||
args+=(--repo "$tea_repo")
|
||||
else
|
||||
args+=(--remote "$push_remote")
|
||||
fi
|
||||
|
||||
local asset
|
||||
for asset in "$dist_dir"/*; do
|
||||
[[ -f "$asset" ]] || continue
|
||||
[[ "$(basename "$asset")" == "release-notes.md" ]] && continue
|
||||
args+=(--asset "$asset")
|
||||
done
|
||||
|
||||
tea "${args[@]}"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_clean_worktree
|
||||
|
||||
if [[ -z "$upstream_tag" ]]; then
|
||||
upstream_tag="$(latest_stable_tag)"
|
||||
fi
|
||||
[[ -n "$upstream_tag" ]] || die "could not determine latest upstream tag"
|
||||
|
||||
local custom_tag="${upstream_tag}-${suffix}"
|
||||
local branch="${branch_prefix}/${upstream_tag#v}-${suffix}"
|
||||
|
||||
if tag_exists "$custom_tag" && [[ "$force" != "1" ]]; then
|
||||
log "Custom release $custom_tag already exists; nothing to do."
|
||||
return
|
||||
fi
|
||||
|
||||
patch_tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$patch_tmp_dir"' EXIT
|
||||
|
||||
copy_patches_to_temp "$patch_tmp_dir"
|
||||
fetch_upstream_tag "$upstream_tag"
|
||||
delete_local_tag_if_forced "$custom_tag"
|
||||
create_release_commit "$upstream_tag" "$custom_tag" "$branch" "$patch_tmp_dir"
|
||||
test_release
|
||||
build_release "$custom_tag"
|
||||
push_refs "$branch" "$custom_tag"
|
||||
publish_release "$custom_tag"
|
||||
|
||||
log "Built custom release $custom_tag from upstream $upstream_tag"
|
||||
log "Artifacts are in $dist_dir/"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user