name: Joplin Continuous Integration on: [push, pull_request] jobs: Main: # We always process server or desktop release tags, because they also publish the release if: github.repository == 'laurent22/joplin' runs-on: ${{ matrix.os }} strategy: matrix: # Do not use unbuntu-latest because it causes `The operation was canceled` failures: # https://github.com/actions/runner-images/issues/6709 # # windows-11-vs2026-arm is pinned because windows-11-arm migrates to # Visual Studio 2026 in late September 2026, and that job compiles # sqlite3 from source. # https://github.com/actions/runner-images/issues/14602 os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm, windows-11-vs2026-arm] steps: - uses: actions/checkout@v6 - name: Setup build environment uses: ./.github/workflows/shared/setup-build-environment - name: Install Docker Engine if: runner.os == 'Linux' run: | sudo apt-get install -y apt-transport-https sudo apt-get install -y ca-certificates sudo apt-get install -y curl sudo apt-get install -y gnupg sudo apt-get install -y lsb-release curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update || true sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin - name: Free disk space if: runner.os == 'Linux' run: | sudo rm -rf /usr/local/lib/android || true sudo rm -rf /usr/share/dotnet || true sudo rm -rf /opt/ghc || true docker system prune -af || true docker builder prune -af || true sudo rm -rf /var/lib/docker/tmp/* || true # Login to Docker only if we're on a server release tag. If we run this on # a pull request it will fail because the PR doesn't have access to # secrets - uses: docker/login-action@v4 if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/transcribe-v')) with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} # - name: Test Windows app signing # if: runner.os == 'Windows' # env: # GH_TOKEN: ${{ secrets.GH_TOKEN }} # IS_CONTINUOUS_INTEGRATION: 1 # BUILD_SEQUENCIAL: 1 # SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }} # SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }} # SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }} # SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }} # SIGN_APPLICATION: 1 # # To ensure that the operations stop on failure, all commands # # should be on one line with "&&" in between. # run: | # yarn install && cd packages/app-desktop && yarn dist - name: Run tests, build and publish Linux and macOS apps if: runner.os == 'Linux' || runner.os == 'macOs' env: APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }} CSC_LINK: ${{ secrets.APPLE_CSC_LINK }} GH_TOKEN: ${{ secrets.GH_TOKEN }} IS_CONTINUOUS_INTEGRATION: 1 BUILD_SEQUENCIAL: 1 SERVER_REPOSITORY: joplin/server SERVER_TAG_PREFIX: server CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} run: | "${GITHUB_WORKSPACE}/.github/scripts/run_ci.sh" - name: Build and publish Windows app if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v') env: GH_TOKEN: ${{ secrets.GH_TOKEN }} IS_CONTINUOUS_INTEGRATION: 1 BUILD_SEQUENCIAL: 1 SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }} SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }} SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }} SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }} SIGN_APPLICATION: 1 # `yarn install` can fail randomly on CI when node-pre-gyp downloads # prebuilt sqlite3 binaries from GitHub Releases - retry a few times. run: | . "${env:GITHUB_WORKSPACE}/.github/scripts/build_windows.ps1" Build-WindowsApp # Build and package the Windows app, without publishing it, just to # verify that the build process hasn't been broken. - name: Build Windows app (no publishing) if: runner.os == 'Windows' && !startsWith(github.ref, 'refs/tags/v') env: IS_CONTINUOUS_INTEGRATION: 1 BUILD_SEQUENCIAL: 1 SERVER_REPOSITORY: joplin/server SERVER_TAG_PREFIX: server run: | . "${env:GITHUB_WORKSPACE}/.github/scripts/build_windows.ps1" Build-WindowsApp -DistArgs "--publish=never" - name: Publish Docker manifest if: runner.os == 'Linux' env: SERVER_REPOSITORY: joplin/server SERVER_TAG_PREFIX: server run: | chmod 700 "${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh" "${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh" ServerDockerImage: if: github.repository == 'laurent22/joplin' runs-on: ${{ matrix.os }} strategy: matrix: # Do not use unbuntu-latest because it causes `The operation was canceled` failures: # https://github.com/actions/runner-images/issues/6709 os: [ubuntu-22.04, ubuntu-22.04-arm] steps: - name: Install Docker Engine run: | sudo apt-get install -y apt-transport-https sudo apt-get install -y ca-certificates sudo apt-get install -y curl sudo apt-get install -y gnupg sudo apt-get install -y lsb-release curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update || true sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '24' - name: Free disk space if: runner.os == 'Linux' run: | sudo rm -rf /usr/local/lib/android || true sudo rm -rf /usr/share/dotnet || true sudo rm -rf /opt/ghc || true docker system prune -af || true docker builder prune -af || true sudo rm -rf /var/lib/docker/tmp/* || true - uses: ./.github/workflows/shared/install-yarn - name: Build Docker Image env: BUILD_SEQUENCIAL: 1 run: | if [ "$RUNNER_ARCH" == "ARM64" ]; then DOCKER_IMAGE_PLATFORM="linux/arm64" fi echo "RUNNER_OS=$RUNNER_OS" echo "RUNNER_ARCH=$RUNNER_ARCH" echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM" source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh" retry yarn install yarn buildServerDocker --docker-file Dockerfile.server --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server # Basic test to ensure that the created build is valid. It should exit with # code 0 if it works. docker run joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/index.js migrate list - name: Check HTTP request run: | # Need to pass environment variables: docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/index.js --env dev & # Wait for server to start sleep 120 # Check if status code is correct # if the actual_status DOES NOT include the expected_status # it exits the process with code 1 expected_status="HTTP/1.1 200 OK" actual_status=$(curl -I -X GET http://localhost:22300/api/ping | head -n 1) if [[ ! "$actual_status" =~ "$expected_status" ]]; then echo 'Failed while checking the status code after request to /api/ping' echo 'expected: ' $expected_status echo 'actual: ' $actual_status exit 1; fi # Check if the body response is correct # if the actual_body is different of expected_body exit with code 1 expected_body='{"status":"ok","message":"Joplin Server is running"}' actual_body=$(curl http://localhost:22300/api/ping) if [[ "$actual_body" != "$expected_body" ]]; then echo 'Failed while checking the body response after request to /api/ping' exit 1; fi - name: Check sync run: | yarn syncFuzzer start --use-running-server --steps 5