feat: optional FTPS transfer protocol #1341
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [master, develop] | |
| tags: | |
| - v[0-9]+.[0-9]+.[0-9]+ | |
| pull_request: | |
| branches: [master, develop] | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| # Should we publish Docker images? True on version tags, develop pushes, or manual dispatch. | |
| SHOULD_PUBLISH: ${{ startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/develop' && github.event_name == 'push') || github.event_name == 'workflow_dispatch' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| unit-test: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: src/angular/package-lock.json | |
| - name: Install dependencies | |
| working-directory: src/angular | |
| run: npm ci | |
| - name: Run tests | |
| working-directory: src/angular | |
| run: npx ng test | |
| angular-lint: | |
| name: Angular Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: src/angular/package-lock.json | |
| - name: Install dependencies | |
| working-directory: src/angular | |
| run: npm ci | |
| - name: Lint Angular | |
| working-directory: src/angular | |
| # All lint rules are at "error" — zero warnings expected. | |
| # See #376-#382 for the cleanup history. | |
| run: npx ng lint --max-warnings 0 | |
| python-lint: | |
| name: Python Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 | |
| - name: Install Ruff | |
| run: uv pip install --system ruff | |
| - name: Ruff check | |
| working-directory: src/python | |
| run: ruff check | |
| - name: Ruff format check | |
| working-directory: src/python | |
| run: ruff format --check | |
| python-typecheck: | |
| name: Python Type Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 | |
| - name: Install dependencies | |
| working-directory: src/python | |
| run: uv pip install --system -r pyproject.toml pyright | |
| - name: Pyright | |
| working-directory: src/python | |
| run: pyright | |
| python-test: | |
| name: Python Unit Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 | |
| - name: Install dependencies | |
| working-directory: src/python | |
| run: uv pip install --system -r pyproject.toml --group test | |
| - name: Run unit tests | |
| working-directory: src/python | |
| env: | |
| PYTHONPATH: . | |
| # Only the live-server suites stay excluded: test_lftp.py and | |
| # test_sshcp.py drive a real lftp/SSH transfer against the seedsynctest | |
| # account (also gated at runtime by SEEDSYNC_LIVE_SSH_TESTS). The | |
| # mock-based lftp/ssh/scanner suites now run here (#529). | |
| # test_multiprocessing_logger stays excluded as before (out of scope). | |
| run: >- | |
| pytest tests/unittests -v --tb=short | |
| --timeout=30 | |
| --ignore=tests/unittests/test_lftp/test_lftp.py | |
| --ignore=tests/unittests/test_ssh/test_sshcp.py | |
| --ignore=tests/unittests/test_common/test_multiprocessing_logger.py | |
| python-integration-test: | |
| name: Python Integration Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 | |
| - name: Install dependencies | |
| working-directory: src/python | |
| run: uv pip install --system -r pyproject.toml --group test | |
| - name: Run web handler integration tests | |
| working-directory: src/python | |
| env: | |
| PYTHONPATH: . | |
| # Only the web-handler integration tests run in CI. The | |
| # tests/integration/test_controller and test_lftp suites drive a real | |
| # lftp/SSH transfer against the seedsynctest account (gated by | |
| # SEEDSYNC_LIVE_SSH_TESTS) and have no mock subset, so they cannot run | |
| # without that live server (#529). | |
| run: >- | |
| pytest tests/integration/test_web -v --tb=short | |
| --timeout=30 | |
| angular-build: | |
| name: Build Angular | |
| if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/develop' && github.event_name == 'push') || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: src/angular/package-lock.json | |
| - name: Install dependencies | |
| working-directory: src/angular | |
| run: npm ci | |
| - name: Build Angular frontend | |
| working-directory: src/angular | |
| run: npx ng build --configuration=production --output-path /tmp/angular-output/build | |
| - name: Upload Angular build artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: angular-build | |
| path: /tmp/angular-output | |
| retention-days: 1 | |
| build-test: | |
| name: Build and Test (amd64) | |
| if: ${{ !(startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Build Docker image | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: src/docker/build/docker-image/Dockerfile | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: seedsync:test | |
| cache-from: type=gha,scope=amd64 | |
| cache-to: type=gha,scope=amd64,mode=max | |
| - name: Start container | |
| run: | | |
| docker run -d --name test-container -p 8800:8800 -e SEEDSYNC_DISABLE_RATE_LIMIT=1 seedsync:test | |
| for i in $(seq 1 30); do | |
| curl -sf http://localhost:8800/ && break | |
| [ "$i" -eq 30 ] && { echo "Container failed to become ready"; docker logs test-container; exit 1; } | |
| sleep 1 | |
| done | |
| docker logs test-container | |
| - name: Run E2E tests in Playwright container | |
| run: | | |
| # Match the @playwright/test version pinned in the lockfile so the | |
| # container's chromium build is the one the package expects. | |
| PW_VERSION=$(jq -r '.packages["node_modules/@playwright/test"].version' src/e2e-playwright/package-lock.json) | |
| docker run --rm \ | |
| --network host \ | |
| -v "$GITHUB_WORKSPACE":/workspace \ | |
| -w /workspace/src/e2e-playwright \ | |
| "mcr.microsoft.com/playwright:v${PW_VERSION}-noble" \ | |
| sh -c "npm ci && npx playwright test" | |
| - name: Upload Playwright report | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-report | |
| path: src/e2e-playwright/playwright-report/ | |
| retention-days: 7 | |
| - name: Stop container | |
| if: always() | |
| run: docker stop test-container | |
| build-amd64: | |
| name: Build (amd64) | |
| if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/develop' && github.event_name == 'push') || github.event_name == 'workflow_dispatch' | |
| needs: [angular-build] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download Angular build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: angular-build | |
| path: /tmp/angular-output | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build amd64 image | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: src/docker/build/docker-image/Dockerfile | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: seedsync:test | |
| build-contexts: | | |
| angular-builder=/tmp/angular-output | |
| cache-from: type=gha,scope=amd64 | |
| cache-to: type=gha,scope=amd64,mode=max | |
| - name: Test container starts | |
| run: | | |
| docker run -d --name test-container -p 8800:8800 seedsync:test | |
| for i in $(seq 1 30); do | |
| curl -sf http://localhost:8800/ && break | |
| [ "$i" -eq 30 ] && { echo "Container failed to become ready"; docker logs test-container; exit 1; } | |
| sleep 1 | |
| done | |
| docker logs test-container | |
| docker stop test-container | |
| - name: Push amd64 image by digest | |
| id: push | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: src/docker/build/docker-image/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true | |
| build-contexts: | | |
| angular-builder=/tmp/angular-output | |
| cache-from: type=gha,scope=amd64 | |
| cache-to: type=gha,scope=amd64,mode=max | |
| provenance: false | |
| sbom: false | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.push.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-amd64 | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| build-arm64: | |
| name: Build (arm64) | |
| if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/develop' && github.event_name == 'push') || github.event_name == 'workflow_dispatch' | |
| needs: [angular-build] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download Angular build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: angular-build | |
| path: /tmp/angular-output | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push arm64 image by digest | |
| id: push | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: src/docker/build/docker-image/Dockerfile | |
| platforms: linux/arm64 | |
| push: true | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true | |
| build-contexts: | | |
| angular-builder=/tmp/angular-output | |
| cache-from: type=gha,scope=arm64 | |
| cache-to: type=gha,scope=arm64,mode=max | |
| provenance: false | |
| sbom: false | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.push.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-arm64 | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| publish: | |
| name: Publish | |
| if: >- | |
| always() && !cancelled() | |
| && (startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/develop' && github.event_name == 'push') || github.event_name == 'workflow_dispatch') | |
| && needs.unit-test.result == 'success' | |
| && needs.angular-lint.result == 'success' | |
| && needs.python-lint.result == 'success' | |
| && needs.python-typecheck.result == 'success' | |
| && needs.python-test.result == 'success' | |
| && needs.python-integration-test.result == 'success' | |
| && needs.build-amd64.result == 'success' | |
| && needs.build-arm64.result == 'success' | |
| needs: [unit-test, angular-lint, python-lint, python-typecheck, python-test, python-integration-test, build-amd64, build-arm64] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Download amd64 digest | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: digests-amd64 | |
| path: /tmp/digests | |
| - name: Download arm64 digest | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: digests-arm64 | |
| path: /tmp/digests | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata (release) | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| id: meta-release | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=raw,value=latest | |
| - name: Extract metadata (develop) | |
| if: github.ref == 'refs/heads/develop' | |
| id: meta-develop | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=develop | |
| - name: Extract metadata (manual dispatch) | |
| if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/develop' | |
| id: meta-dispatch | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest | |
| - name: Create multi-arch manifest and push | |
| working-directory: /tmp/digests | |
| run: | | |
| docker buildx imagetools create \ | |
| $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| release: | |
| name: Create Release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: [publish] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Create Release | |
| uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 | |
| with: | |
| generate_release_notes: true | |
| make_latest: true |