Skip to content

feat: optional FTPS transfer protocol #1341

feat: optional FTPS transfer protocol

feat: optional FTPS transfer protocol #1341

Workflow file for this run

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