name: Build Syncthing on: pull_request: push: schedule: # Run nightly build at 05:00 UTC - cron: '00 05 * * *' workflow_dispatch: env: # The go version to use for builds. We set check-latest to true when # installing, so we get the latest patch version that matches the # expression. GO_VERSION: "~1.22.0" # Optimize compatibility on the slow archictures. GO386: softfloat GOARM: "5" GOMIPS: softfloat # Avoid hilarious amounts of obscuring log output when running tests. LOGGER_DISCARD: "1" # Our build metadata BUILD_USER: builder BUILD_HOST: github.syncthing.net # A note on actions and third party code... The actions under actions/ (like # `uses: actions/checkout`) are maintained by GitHub, and we need to trust # GitHub to maintain their code and infrastructure or we're in deep shit in # general. The same doesn't necessarily apply to other actions authors, so # some care needs to be taken when adding steps, especially in the paths # that lead up to code being packaged and signed. jobs: # # Tests for all platforms. Runs a matrix build on Windows, Linux and Mac, # with the list of expected supported Go versions (current, previous). # build-test: name: Build and test strategy: fail-fast: false matrix: runner: ["windows-latest", "ubuntu-latest", "macos-latest"] # The oldest version in this list should match what we have in our go.mod. # Variables don't seem to be supported here, or we could have done something nice. go: ["~1.21.7", "~1.22.0"] runs-on: ${{ matrix.runner }} steps: - name: Set git to use LF if: matrix.runner == 'windows-latest' # Without this, the Windows checkout will happen with CRLF line # endings, which is fine for the source code but messes up tests # that depend on data on disk being as expected. Ideally, those # tests should be fixed, but not today. run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} cache: true check-latest: true - name: Build run: | go run build.go - name: Install go-test-json-to-loki run: | go install calmh.dev/go-test-json-to-loki@latest - name: Test run: | go version go run build.go test | go-test-json-to-loki env: GOFLAGS: "-json" LOKI_URL: ${{ vars.LOKI_URL }} LOKI_USER: ${{ vars.LOKI_USER }} LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }} LOKI_LABELS: "go=${{ matrix.go }},runner=${{ matrix.runner }},repo=${{ github.repository }},ref=${{ github.ref }}" # # Meta checks for formatting, copyright, etc # correctness: name: Check correctness runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Check correctness run: | go test -v ./meta # # The basic checks job is a virtual one that depends on the matrix tests, # the correctness checks, and various builds that we always do. This makes # it easy to have the PR process have a single test as a gatekeeper for # merging, instead of having to add all the matrix tests and update them # each time the version changes. (The top level test is not available for # choosing there, only the matrix "children".) # basics: name: Basic checks passed runs-on: ubuntu-latest needs: - build-test - correctness - package-linux - package-cross - package-source - package-debian - govulncheck steps: - uses: actions/checkout@v4 # # Windows # package-windows: name: Package for Windows if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-')) environment: signing runs-on: windows-latest steps: - name: Set git to use LF # Without this, the checkout will happen with CRLF line endings, # which is fine for the source code but messes up tests that depend # on data on disk being as expected. Ideally, those tests should be # fixed, but not today. run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Get actual Go version run: | go version echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV - uses: actions/cache@v4 with: path: | ~\AppData\Local\go-build ~\go\pkg\mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }} - name: Install dependencies run: | go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@v1.4.0 - name: Create packages run: | go run build.go -goarch amd64 zip go run build.go -goarch arm zip go run build.go -goarch arm64 zip go run build.go -goarch 386 zip env: CGO_ENABLED: "0" CODESIGN_SIGNTOOL: ${{ secrets.CODESIGN_SIGNTOOL }} CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }} CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODESIGN_CERTIFICATE_PASSWORD }} CODESIGN_TIMESTAMP_SERVER: ${{ secrets.CODESIGN_TIMESTAMP_SERVER }} - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: packages-windows path: syncthing-windows-*.zip # # Linux # package-linux: name: Package for Linux runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Get actual Go version run: | go version echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }} - name: Create packages run: | archs=$(go tool dist list | grep linux | sed 's#linux/##') for goarch in $archs ; do go run build.go -goarch "$goarch" tar done env: CGO_ENABLED: "0" - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: packages-linux path: syncthing-linux-*.tar.gz # # macOS # package-macos: name: Package for macOS if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-')) environment: signing runs-on: macos-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Get actual Go version run: | go version echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }} - name: Import signing certificate run: | # Set up a run-specific keychain, making it available for the # `codesign` tool. umask 066 KEYCHAIN_PATH=$RUNNER_TEMP/codesign.keychain KEYCHAIN_PASSWORD=$(uuidgen) security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security default-keychain -s "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" # Import the certificate CERTIFICATE_PATH=$RUNNER_TEMP/codesign.p12 echo "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 -d -o "$CERTIFICATE_PATH" security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$DEVELOPER_ID_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign security set-key-partition-list -S apple-tool:,apple: -s -k actions "$KEYCHAIN_PATH" # Set the codesign identity for following steps echo "CODESIGN_IDENTITY=$CODESIGN_IDENTITY" >> $GITHUB_ENV env: DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }} DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }} CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }} - name: Create package (amd64) run: | go run build.go -goarch amd64 zip env: CGO_ENABLED: "1" - name: Create package (arm64 cross) run: | cat < xgo.sh #!/bin/bash CGO_ENABLED=1 \ CGO_CFLAGS="-target arm64-apple-macos10.15" \ CGO_LDFLAGS="-target arm64-apple-macos10.15" \ go "\$@" EOT chmod 755 xgo.sh go run build.go -gocmd ./xgo.sh -goarch arm64 zip env: CGO_ENABLED: "1" - name: Create package (universal) run: | rm -rf _tmp mkdir _tmp pushd _tmp unzip ../syncthing-macos-amd64-*.zip unzip ../syncthing-macos-arm64-*.zip lipo -create syncthing-macos-amd64-*/syncthing syncthing-macos-arm64-*/syncthing -o syncthing amd64=(syncthing-macos-amd64-*) universal="${amd64/amd64/universal}" mv "$amd64" "$universal" mv syncthing "$universal" zip -r "../$universal.zip" "$universal" - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: packages-macos path: syncthing-*.zip notarize-macos: name: Notarize for macOS if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-')) environment: signing needs: - package-macos - basics runs-on: macos-latest steps: - name: Download artifacts uses: actions/download-artifact@v4 with: name: packages-macos - name: Notarize binaries run: | APPSTORECONNECT_API_KEY_PATH="$RUNNER_TEMP/apikey.p8" echo "$APPSTORECONNECT_API_KEY" | base64 -d -o "$APPSTORECONNECT_API_KEY_PATH" for file in syncthing-macos-*.zip ; do xcrun notarytool submit \ -k "$APPSTORECONNECT_API_KEY_PATH" \ -d "$APPSTORECONNECT_API_KEY_ID" \ -i "$APPSTORECONNECT_API_KEY_ISSUER" \ $file done env: APPSTORECONNECT_API_KEY: ${{ secrets.APPSTORECONNECT_API_KEY }} APPSTORECONNECT_API_KEY_ID: ${{ secrets.APPSTORECONNECT_API_KEY_ID }} APPSTORECONNECT_API_KEY_ISSUER: ${{ secrets.APPSTORECONNECT_API_KEY_ISSUER }} # # Cross compile other unixes # package-cross: name: Package cross compiled runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Get actual Go version run: | go version echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-cross-${{ hashFiles('**/go.sum') }} - name: Create packages run: | platforms=$(go tool dist list \ | grep -v aix/ppc64 \ | grep -v android/ \ | grep -v darwin/ \ | grep -v ios/ \ | grep -v js/ \ | grep -v linux/ \ | grep -v nacl/ \ | grep -v plan9/ \ | grep -v windows/ \ | grep -v /wasm \ ) # Build for each platform with errors silenced, because we expect # some oddball platforms to fail. This avoids a bunch of errors in # the GitHub Actions output, instead summarizing each build # failure as a warning. for plat in $platforms; do goos="${plat%/*}" goarch="${plat#*/}" echo "::group ::$plat" if ! go run build.go -goos "$goos" -goarch "$goarch" tar 2>/dev/null; then echo "::warning ::Failed to build for $plat" fi echo "::endgroup::" done env: CGO_ENABLED: "0" - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: packages-other path: syncthing-*.tar.gz # # Source # package-source: name: Package source code runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Package source run: | version=$(go run build.go version) echo "$version" > RELEASE go mod vendor go run build.go assets cd .. tar c -z -f "syncthing-source-$version.tar.gz" \ --exclude .git \ syncthing mv "syncthing-source-$version.tar.gz" syncthing - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: packages-source path: syncthing-source-*.tar.gz # # Sign binaries for auto upgrade, generate ASC signature files # sign-for-upgrade: name: Sign for upgrade if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-')) environment: signing needs: - basics - package-windows - package-linux - package-macos - package-cross - package-source runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/checkout@v4 with: repository: syncthing/release-tools path: tools fetch-depth: 0 - name: Download artifacts uses: actions/download-artifact@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Install signing tool run: | go install ./cmd/stsigtool - name: Sign archives run: | export PRIVATE_KEY="$RUNNER_TEMP/privkey.pem" export PATH="$PATH:$(go env GOPATH)/bin" echo "$STSIGTOOL_PRIVATE_KEY" | base64 -d > "$PRIVATE_KEY" mkdir packages mv packages-*/* packages pushd packages "$GITHUB_WORKSPACE/tools/sign-only" rm -f "$PRIVATE_KEY" env: STSIGTOOL_PRIVATE_KEY: ${{ secrets.STSIGTOOL_PRIVATE_KEY }} - name: Create and sign .asc files run: | sudo apt update sudo apt -y install gnupg export SIGNING_KEY="$RUNNER_TEMP/gpg-secret.asc" echo "$GNUPG_SIGNING_KEY_BASE64" | base64 -d > "$SIGNING_KEY" gpg --import < "$SIGNING_KEY" pushd packages files=(*.tar.gz *.zip) sha1sum "${files[@]}" | gpg --clearsign > sha1sum.txt.asc sha256sum "${files[@]}" | gpg --clearsign > sha256sum.txt.asc gpg --sign --armour --detach syncthing-source-*.tar.gz popd rm -f "$SIGNING_KEY" .gnupg env: GNUPG_SIGNING_KEY_BASE64: ${{ secrets.GNUPG_SIGNING_KEY_BASE64 }} - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: packages-signed path: packages/* # # Debian # package-debian: name: Package for Debian runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Get actual Go version run: | go version echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV - uses: ruby/setup-ruby@v1 with: ruby-version: '3.0' - name: Install fpm run: | gem install fpm - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-debian-${{ hashFiles('**/go.sum') }} - name: Package for Debian run: | for arch in amd64 i386 armhf armel arm64 ; do go run build.go -no-upgrade -installsuffix=no-upgrade -goarch "$arch" deb done env: BUILD_USER: debian - name: Archive artifacts uses: actions/upload-artifact@v4 with: name: debian-packages path: "*.deb" # # Nightlies # publish-nightly: name: Publish nightly build if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly') environment: signing needs: - sign-for-upgrade - notarize-macos runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: repository: syncthing/release-tools path: tools fetch-depth: 0 - name: Download artifacts uses: actions/download-artifact@v4 with: name: packages-signed path: packages - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Create release json run: | cd packages "$GITHUB_WORKSPACE/tools/generate-release-json" "$BASE_URL" > nightly.json env: BASE_URL: https://syncthing.ams3.digitaloceanspaces.com/nightly/ - name: Push artifacts uses: docker://docker.io/rclone/rclone:latest env: RCLONE_CONFIG_SPACES_TYPE: s3 RCLONE_CONFIG_SPACES_PROVIDER: DigitalOcean RCLONE_CONFIG_SPACES_ACCESS_KEY_ID: ${{ secrets.SPACES_KEY }} RCLONE_CONFIG_SPACES_SECRET_ACCESS_KEY: ${{ secrets.SPACES_SECRET }} RCLONE_CONFIG_SPACES_ENDPOINT: ams3.digitaloceanspaces.com RCLONE_CONFIG_SPACES_ACL: public-read with: args: sync packages spaces:syncthing/nightly # # Push release artifacts to Spaces # publish-release-files: name: Publish release files if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/release' environment: signing needs: - sign-for-upgrade - package-debian runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download signed packages uses: actions/download-artifact@v4 with: name: packages-signed path: packages - name: Download debian packages uses: actions/download-artifact@v4 with: name: debian-packages path: packages - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Set version run: | version=$(go run build.go version) echo "VERSION=$version" >> $GITHUB_ENV - name: Push to Spaces (${{ env.VERSION }}) uses: docker://docker.io/rclone/rclone:latest env: RCLONE_CONFIG_SPACES_TYPE: s3 RCLONE_CONFIG_SPACES_PROVIDER: DigitalOcean RCLONE_CONFIG_SPACES_ACCESS_KEY_ID: ${{ secrets.SPACES_KEY }} RCLONE_CONFIG_SPACES_SECRET_ACCESS_KEY: ${{ secrets.SPACES_SECRET }} RCLONE_CONFIG_SPACES_ENDPOINT: ams3.digitaloceanspaces.com RCLONE_CONFIG_SPACES_ACL: public-read with: args: sync packages spaces:syncthing/release/${{ env.VERSION }} - name: Push to Spaces (latest) uses: docker://docker.io/rclone/rclone:latest env: RCLONE_CONFIG_SPACES_TYPE: s3 RCLONE_CONFIG_SPACES_PROVIDER: DigitalOcean RCLONE_CONFIG_SPACES_ACCESS_KEY_ID: ${{ secrets.SPACES_KEY }} RCLONE_CONFIG_SPACES_SECRET_ACCESS_KEY: ${{ secrets.SPACES_SECRET }} RCLONE_CONFIG_SPACES_ENDPOINT: ams3.digitaloceanspaces.com RCLONE_CONFIG_SPACES_ACL: public-read with: args: sync spaces:syncthing/release/${{ env.VERSION }} spaces:syncthing/release/latest # # Build and push to Docker Hub # docker-syncthing: name: Build and push Docker images runs-on: ubuntu-latest if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/infrastructure' || startsWith(github.ref, 'refs/heads/release-')) environment: docker strategy: matrix: pkg: - syncthing - strelaysrv - stdiscosrv include: - pkg: syncthing dockerfile: Dockerfile image: syncthing/syncthing - pkg: strelaysrv dockerfile: Dockerfile.strelaysrv image: syncthing/relaysrv - pkg: stdiscosrv dockerfile: Dockerfile.stdiscosrv image: syncthing/discosrv steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: Get actual Go version run: | go version echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-docker-${{ matrix.pkg }}-${{ hashFiles('**/go.sum') }} - name: Build binaries run: | for arch in amd64 arm64 arm; do go run build.go -goos linux -goarch "$arch" -no-upgrade build ${{ matrix.pkg }} mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch" done env: CGO_ENABLED: "0" BUILD_USER: docker - name: Check if we will be able to push images run: | if [[ "${{ secrets.DOCKERHUB_TOKEN }}" != "" ]]; then echo "DOCKER_PUSH=true" >> $GITHUB_ENV; fi - name: Login to Docker Hub uses: docker/login-action@v3 if: env.DOCKER_PUSH == 'true' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Set version tags run: | version=$(go run build.go version) version=${version#v} if [[ $version == @([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]) ]] ; then echo Release version, pushing to :latest and version tags major=${version%.*.*} minor=${version%.*} tags=${{ matrix.image }}:$version,${{ matrix.image }}:$major,${{ matrix.image }}:$minor,${{ matrix.image }}:latest elif [[ $version == *-rc.@([0-9]|[0-9][0-9]) ]] ; then echo Release candidate, pushing to :rc tags=${{ matrix.image }}:rc else echo Development version, pushing to :edge tags=${{ matrix.image }}:edge fi echo "DOCKER_TAGS=$tags" >> $GITHUB_ENV echo "VERSION=$version" >> $GITHUB_ENV - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ${{ matrix.dockerfile }} platforms: linux/amd64,linux/arm64,linux/arm/7 push: ${{ env.DOCKER_PUSH == 'true' }} tags: ${{ env.DOCKER_TAGS }} labels: | org.opencontainers.image.version=${{ env.VERSION }} org.opencontainers.image.revision=${{ github.sha }} # # Check for known vulnerabilities in Go dependencies # govulncheck: runs-on: ubuntu-latest name: Run govulncheck steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false check-latest: true - name: run govulncheck run: | go run build.go assets go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./...