417 lines
11 KiB
Bash
Executable File
417 lines
11 KiB
Bash
Executable File
#!/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 "$@"
|