mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-14 07:50:48 +00:00
* ci: pin third party actions to commit SHAs A tag or branch ref is a movable pointer, whoever controls the action repository can repoint it and the next run executes their code with this repo's secrets, here the Docker Hub tokens and the container publish path. The March 2025 tj-actions/changed-files incident (CVE-2025-30066) worked exactly that way. Each pin keeps the resolved tag in a comment and dependabot's weekly github-actions config bumps SHA pins like tags, so they self-maintain. The pypi publish action moves from the floating release/v1 branch to its current release. * ci: declare least-privilege workflow permissions Without a permissions block every job token inherits the repository default scope. Each workflow now declares what it uses. The test and build workflows get read-only tokens, CodeQL keeps its code scanning upload, the container publish job keeps packages write for the ghcr push, and the pypi publish job already carried its own id-token block for trusted publishing, untouched. * ci: pass release metadata to inline scripts through env The two debug steps interpolated the release tag name and the workflow_run branch directly into bash. Those strings are event metadata that can carry shell syntax, and the workflow runs on release events with the publish credentials in reach. Routed through env they arrive as plain data, output unchanged. * ci: stop persisting the checkout token in .git/config actions/checkout writes the job token into .git/config so later steps can push. None of these 21 jobs push with git: the containers are published with registry credentials, PyPI uses trusted publishing, and the test and CodeQL jobs only read the tree. A token left in .git/config is one more credential for a compromised step or a packed artifact to pick up, so each checkout now sets persist-credentials: false.
898 lines
35 KiB
YAML
898 lines
35 KiB
YAML
name: ChangeDetection.io App Test
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
python-version:
|
|
description: 'Python version to use'
|
|
required: true
|
|
type: string
|
|
default: '3.11'
|
|
skip-pypuppeteer:
|
|
description: 'Skip PyPuppeteer (not supported in 3.11/3.12)'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# Build the Docker image once and share it with all test jobs
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
|
|
- name: Cache pip packages
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: ~/.cache/pip
|
|
key: ${{ runner.os }}-pip-py${{ env.PYTHON_VERSION }}-${{ hashFiles('requirements.txt') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-pip-py${{ env.PYTHON_VERSION }}-
|
|
${{ runner.os }}-pip-
|
|
|
|
- name: Get current date for cache key
|
|
id: date
|
|
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
|
|
|
- name: Build changedetection.io container for testing under Python ${{ env.PYTHON_VERSION }}
|
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
|
with:
|
|
context: ./
|
|
file: ./Dockerfile
|
|
build-args: |
|
|
PYTHON_VERSION=${{ env.PYTHON_VERSION }}
|
|
LOGGER_LEVEL=TRACE
|
|
tags: test-changedetectionio
|
|
load: true
|
|
cache-from: type=gha,scope=build-${{ github.ref_name }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('requirements.txt', 'Dockerfile') }}-${{ steps.date.outputs.date }}
|
|
cache-to: type=gha,mode=max,scope=build-${{ github.ref_name }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('requirements.txt', 'Dockerfile') }}-${{ steps.date.outputs.date }}
|
|
|
|
- name: Verify build
|
|
run: |
|
|
echo "---- Built for Python ${{ env.PYTHON_VERSION }} -----"
|
|
docker run test-changedetectionio bash -c 'pip list'
|
|
|
|
- name: License compliance - no strong copyleft in shipped image
|
|
run: |
|
|
# Runs against the built image, i.e. exactly what ships, with the entrypoint
|
|
# bypassed so EXTRA_PACKAGES cannot pull in the opt-in AGPL osint plugin.
|
|
# --partial-match is required: plain --fail-on is an exact string match and
|
|
# would silently miss declarations like "GPL-2.0-or-later".
|
|
docker run --rm --entrypoint /bin/bash test-changedetectionio -c '
|
|
pip install --quiet pip-licenses &&
|
|
pip-licenses --partial-match \
|
|
--fail-on="GPL-2.0;GPL-3.0;AGPL;GNU General Public License;GNU Affero General Public License"
|
|
'
|
|
|
|
- name: We should be Python ${{ env.PYTHON_VERSION }} ...
|
|
run: |
|
|
docker run test-changedetectionio bash -c 'python3 --version'
|
|
|
|
- name: Save Docker image
|
|
run: |
|
|
docker save test-changedetectionio -o /tmp/test-changedetectionio.tar
|
|
|
|
- name: Upload Docker image artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp/test-changedetectionio.tar
|
|
retention-days: 1
|
|
|
|
# Unit tests (lightweight, no ancillary services needed)
|
|
unit-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Run Unit Tests
|
|
run: |
|
|
docker run test-changedetectionio bash -c 'cd changedetectionio;pytest tests/unit/ tests/llm/'
|
|
|
|
# Basic pytest tests with ancillary services
|
|
basic-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 25
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Test built container with Pytest
|
|
run: |
|
|
docker network inspect changedet-network >/dev/null 2>&1 || docker network create changedet-network
|
|
docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
|
|
|
|
- name: Test CLI options
|
|
run: |
|
|
docker network inspect changedet-network >/dev/null 2>&1 || docker network create changedet-network
|
|
docker run --name test-cdio-cli-opts --network changedet-network test-changedetectionio bash -c 'changedetectionio/test_cli_opts.sh' &> cli-opts-output.txt
|
|
echo "=== CLI Options Test Output ==="
|
|
cat cli-opts-output.txt
|
|
|
|
- name: CLI Memory Test
|
|
run: |
|
|
echo "=== Checking CLI batch mode memory usage ==="
|
|
# Extract RSS memory value from output
|
|
RSS_MB=$(grep -oP "Memory consumption before worker shutdown: RSS=\K[\d.]+" cli-opts-output.txt | head -1 || echo "0")
|
|
echo "RSS Memory: ${RSS_MB} MB"
|
|
|
|
# Check if RSS is less than 100MB
|
|
if [ -n "$RSS_MB" ]; then
|
|
if (( $(echo "$RSS_MB < 100" | bc -l) )); then
|
|
echo "✓ Memory usage is acceptable: ${RSS_MB} MB < 100 MB"
|
|
else
|
|
echo "✗ Memory usage too high: ${RSS_MB} MB >= 100 MB"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "⚠ Could not extract memory usage, skipping check"
|
|
fi
|
|
|
|
- name: Extract memory report and logs
|
|
if: always()
|
|
uses: ./.github/actions/extract-memory-report
|
|
with:
|
|
container-name: test-cdio-basic-tests
|
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
|
|
- name: Store test artifacts
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: test-cdio-basic-tests-output-py${{ env.PYTHON_VERSION }}
|
|
path: output-logs
|
|
|
|
- name: Store CLI test output
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: test-cdio-cli-opts-output-py${{ env.PYTHON_VERSION }}
|
|
path: cli-opts-output.txt
|
|
|
|
# Playwright tests
|
|
playwright-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Spin up ancillary services
|
|
run: |
|
|
docker network create changedet-network
|
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
|
|
|
|
- name: Playwright - Specific tests in built container
|
|
run: |
|
|
# Deliberately fail fast - the first failure is nearly always the real problem and it
|
|
# keeps the run short. Each file is wrapped in its own log group and the failing one is
|
|
# named explicitly, because everything after it is SKIPPED rather than run, and that is
|
|
# otherwise easy to misread as "the whole browser suite broke".
|
|
for t in tests/fetchers/test_content.py tests/test_errorhandling.py tests/visualselector/test_fetch_data.py tests/fetchers/test_custom_js_before_content.py tests/fetchers/test_renavigation.py; do
|
|
echo "::group::pytest $t"
|
|
if ! docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio \
|
|
bash -c "cd changedetectionio;pytest -vv --capture=tee-sys --showlocals --tb=long --live-server-host=0.0.0.0 --live-server-port=5004 $t"; then
|
|
echo "::endgroup::"
|
|
echo "::error::$t FAILED - stopping here. Any later test file and the remaining steps of this job were SKIPPED, not failed."
|
|
exit 1
|
|
fi
|
|
echo "::endgroup::"
|
|
done
|
|
|
|
- name: Playwright - Headers and requests
|
|
run: |
|
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'find .; cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py; pwd;find .'
|
|
|
|
- name: Playwright - Restock detection
|
|
run: |
|
|
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
|
|
|
# Pyppeteer tests
|
|
pyppeteer-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
if: ${{ inputs.skip-pypuppeteer == false }}
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Spin up ancillary services
|
|
run: |
|
|
docker network create changedet-network
|
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
|
|
|
- name: Pyppeteer - Specific tests in built container
|
|
run: |
|
|
# Fail fast, but name the file that failed - see the note in the playwright job above
|
|
for t in tests/fetchers/test_content.py tests/test_errorhandling.py tests/visualselector/test_fetch_data.py tests/fetchers/test_custom_js_before_content.py tests/fetchers/test_renavigation.py; do
|
|
echo "::group::pytest $t"
|
|
if ! docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio \
|
|
bash -c "cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 $t"; then
|
|
echo "::endgroup::"
|
|
echo "::error::$t FAILED - stopping here. Any later test file and the remaining steps of this job were SKIPPED, not failed."
|
|
exit 1
|
|
fi
|
|
echo "::endgroup::"
|
|
done
|
|
|
|
- name: Pyppeteer - Headers and requests checks
|
|
run: |
|
|
docker run --name "changedet" --hostname changedet --rm -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
|
|
- name: Pyppeteer - Restock detection
|
|
run: |
|
|
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
|
|
|
# Selenium tests
|
|
selenium-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Spin up ancillary services
|
|
run: |
|
|
docker network create changedet-network
|
|
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
|
sleep 3
|
|
|
|
- name: Specific tests for headers and requests checks with Selenium
|
|
run: |
|
|
|
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
|
|
- name: Specific tests in built container for Selenium
|
|
run: |
|
|
docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
|
|
|
|
|
|
# SMTP tests
|
|
smtp-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Spin up SMTP test server
|
|
run: |
|
|
docker network create changedet-network
|
|
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'pip3 install aiosmtpd && python changedetectionio/tests/smtp/smtp-test-server.py'
|
|
|
|
- name: Test SMTP notification mime types
|
|
run: |
|
|
docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
|
|
|
|
nginx-reverse-proxy:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Spin up services
|
|
run: |
|
|
docker network create changedet-network
|
|
|
|
# Start changedetection.io container with X-Forwarded headers support
|
|
docker run --name changedet-app --hostname changedet-app --network changedet-network \
|
|
-e USE_X_SETTINGS=true \
|
|
-d test-changedetectionio
|
|
sleep 3
|
|
|
|
- name: Start nginx reverse proxy
|
|
run: |
|
|
# Start nginx with our test configuration
|
|
docker run --name nginx-proxy --network changedet-network -d -p 8080:80 --rm \
|
|
-v ${{ github.workspace }}/.github/nginx-reverse-proxy-test.conf:/etc/nginx/conf.d/default.conf:ro \
|
|
nginx:alpine
|
|
sleep 2
|
|
|
|
- name: Test reverse proxy - root path
|
|
run: |
|
|
echo "=== Testing nginx reverse proxy at root path ==="
|
|
curl --retry-connrefused --retry 6 -s http://localhost:8080/ > /tmp/nginx-test-root.html
|
|
|
|
# Check for changedetection.io UI elements
|
|
if grep -q "checkbox-uuid" /tmp/nginx-test-root.html; then
|
|
echo "✓ Found checkbox-uuid in response"
|
|
else
|
|
echo "ERROR: checkbox-uuid not found in response"
|
|
cat /tmp/nginx-test-root.html
|
|
exit 1
|
|
fi
|
|
|
|
# Check for watchlist content
|
|
if grep -q -i "watch" /tmp/nginx-test-root.html; then
|
|
echo "✓ Found watch/watchlist content in response"
|
|
else
|
|
echo "ERROR: watchlist content not found"
|
|
cat /tmp/nginx-test-root.html
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ Root path reverse proxy working correctly"
|
|
|
|
- name: Test reverse proxy - subpath with X-Forwarded-Prefix
|
|
run: |
|
|
echo "=== Testing nginx reverse proxy at subpath /changedet-sub/ ==="
|
|
curl --retry-connrefused --retry 6 -s http://localhost:8080/changedet-sub/ > /tmp/nginx-test-subpath.html
|
|
|
|
# Check for changedetection.io UI elements
|
|
if grep -q "checkbox-uuid" /tmp/nginx-test-subpath.html; then
|
|
echo "✓ Found checkbox-uuid in subpath response"
|
|
else
|
|
echo "ERROR: checkbox-uuid not found in subpath response"
|
|
cat /tmp/nginx-test-subpath.html
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ Subpath reverse proxy working correctly"
|
|
|
|
- name: Test API through reverse proxy subpath
|
|
run: |
|
|
echo "=== Testing API endpoints through nginx subpath /changedet-sub/ ==="
|
|
|
|
# Extract API key from the changedetection.io datastore
|
|
API_KEY=$(docker exec changedet-app cat /datastore/changedetection.json | grep -o '"api_access_token": *"[^"]*"' | cut -d'"' -f4)
|
|
|
|
if [ -z "$API_KEY" ]; then
|
|
echo "ERROR: Could not extract API key from datastore"
|
|
docker exec changedet-app cat /datastore/changedetection.json
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ Extracted API key: ${API_KEY:0:8}..."
|
|
|
|
# Create a watch via API through nginx proxy subpath
|
|
echo "Creating watch via POST to /changedet-sub/api/v1/watch"
|
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:8080/changedet-sub/api/v1/watch" \
|
|
-H "x-api-key: ${API_KEY}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"url": "https://example.com/test-nginx-proxy",
|
|
"tag": "nginx-test"
|
|
}')
|
|
|
|
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
BODY=$(echo "$RESPONSE" | head -n-1)
|
|
|
|
if [ "$HTTP_CODE" != "201" ]; then
|
|
echo "ERROR: Expected HTTP 201, got $HTTP_CODE"
|
|
echo "Response: $BODY"
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ Watch created successfully (HTTP 201)"
|
|
|
|
# Extract the watch UUID from response
|
|
WATCH_UUID=$(echo "$BODY" | grep -o '"uuid": *"[^"]*"' | cut -d'"' -f4)
|
|
echo "✓ Watch UUID: $WATCH_UUID"
|
|
|
|
# Update the watch via PUT through nginx proxy subpath
|
|
echo "Updating watch via PUT to /changedet-sub/api/v1/watch/${WATCH_UUID}"
|
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT "http://localhost:8080/changedet-sub/api/v1/watch/${WATCH_UUID}" \
|
|
-H "x-api-key: ${API_KEY}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"paused": true
|
|
}')
|
|
|
|
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
BODY=$(echo "$RESPONSE" | head -n-1)
|
|
|
|
if [ "$HTTP_CODE" != "200" ]; then
|
|
echo "ERROR: Expected HTTP 200, got $HTTP_CODE"
|
|
echo "Response: $BODY"
|
|
exit 1
|
|
fi
|
|
|
|
if echo "$BODY" | grep -q 'OK'; then
|
|
echo "✓ Watch updated successfully (HTTP 200, response: OK)"
|
|
else
|
|
echo "ERROR: Expected response 'OK', got: $BODY"
|
|
echo "Response: $BODY"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify the watch is paused via GET
|
|
echo "Verifying watch is paused via GET"
|
|
RESPONSE=$(curl -s "http://localhost:8080/changedet-sub/api/v1/watch/${WATCH_UUID}" \
|
|
-H "x-api-key: ${API_KEY}")
|
|
|
|
if echo "$RESPONSE" | grep -q '"paused": *true'; then
|
|
echo "✓ Watch is paused as expected"
|
|
else
|
|
echo "ERROR: Watch paused state not confirmed"
|
|
echo "Response: $RESPONSE"
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ API tests through nginx subpath completed successfully"
|
|
|
|
- name: Cleanup nginx test
|
|
if: always()
|
|
run: |
|
|
docker logs nginx-proxy || true
|
|
docker logs changedet-app || true
|
|
docker stop nginx-proxy changedet-app || true
|
|
docker rm nginx-proxy changedet-app || true
|
|
|
|
|
|
|
|
# Proxy tests
|
|
proxy-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Spin up services
|
|
run: |
|
|
docker network create changedet-network
|
|
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
|
|
|
|
- name: Test proxy Squid style interaction
|
|
run: |
|
|
cd changedetectionio
|
|
./run_proxy_tests.sh
|
|
docker ps
|
|
cd ..
|
|
|
|
- name: Test proxy SOCKS5 style interaction
|
|
run: |
|
|
cd changedetectionio
|
|
./run_socks_proxy_tests.sh
|
|
cd ..
|
|
|
|
# Custom browser URL tests
|
|
custom-browser-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Spin up ancillary services
|
|
run: |
|
|
docker network create changedet-network
|
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
|
|
|
|
- name: Test custom browser URL
|
|
run: |
|
|
cd changedetectionio
|
|
./run_custom_browser_url_tests.sh
|
|
|
|
processor-plugin-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 20
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Basic processor plugin registration and checks
|
|
run: |
|
|
docker run -e EXTRA_PACKAGES=changedetection.io-osint-processor test-changedetectionio bash -c 'cd changedetectionio;pytest -vvv -s tests/plugins/test_processor.py::test_check_plugin_processor'
|
|
|
|
- name: Plugin get_html_head_extras hook injects into base.html
|
|
run: |
|
|
docker run test-changedetectionio bash -c 'cd changedetectionio;pytest -vvv -s tests/plugins/test_html_head_extras.py'
|
|
|
|
# Container startup tests
|
|
container-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Test container starts+runs basically without error
|
|
run: |
|
|
docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
|
|
sleep 3
|
|
curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
|
|
curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
|
|
docker logs test-changedetectionio 2>/dev/null | grep 'TRACE log is enabled' || exit 1
|
|
docker logs test-changedetectionio 2>/dev/null | grep 'DEBUG' || exit 1
|
|
docker kill test-changedetectionio
|
|
|
|
- name: Test HTTPS SSL mode
|
|
run: |
|
|
openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
|
|
docker run --name test-changedetectionio-ssl --rm -e SSL_CERT_FILE=cert.pem -e SSL_PRIVKEY_FILE=privkey.pem -p 5000:5000 -v ./cert.pem:/app/cert.pem -v ./privkey.pem:/app/privkey.pem -d test-changedetectionio
|
|
sleep 3
|
|
curl --retry-connrefused --retry 6 -k https://localhost:5000 -v|grep -q checkbox-uuid
|
|
docker kill test-changedetectionio-ssl
|
|
|
|
- name: Test IPv6 Mode
|
|
run: |
|
|
docker run --name test-changedetectionio-ipv6 --rm -p 5000:5000 -e LISTEN_HOST=:: -d test-changedetectionio
|
|
sleep 3
|
|
curl --retry-connrefused --retry 6 http://[::1]:5000 -v|grep -q checkbox-uuid
|
|
docker kill test-changedetectionio-ipv6
|
|
|
|
# Signal tests
|
|
signal-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 10
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download Docker image artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: test-changedetectionio-${{ env.PYTHON_VERSION }}
|
|
path: /tmp
|
|
|
|
- name: Load Docker image
|
|
run: |
|
|
docker load -i /tmp/test-changedetectionio.tar
|
|
|
|
- name: Test SIGTERM and SIGINT signal shutdown
|
|
run: |
|
|
echo SIGINT Shutdown request test
|
|
docker run --name sig-test -d test-changedetectionio
|
|
sleep 3
|
|
echo ">>> Sending SIGINT to sig-test container"
|
|
docker kill --signal=SIGINT sig-test
|
|
sleep 3
|
|
docker ps
|
|
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGINT' || exit 1
|
|
test -z "`docker ps|grep sig-test`"
|
|
if [ $? -ne 0 ]; then
|
|
echo "Looks like container was running when it shouldnt be"
|
|
docker ps
|
|
exit 1
|
|
fi
|
|
docker rm sig-test
|
|
|
|
echo SIGTERM Shutdown request test
|
|
docker run --name sig-test -d test-changedetectionio
|
|
sleep 3
|
|
echo ">>> Sending SIGTERM to sig-test container"
|
|
docker kill --signal=SIGTERM sig-test
|
|
sleep 3
|
|
docker ps
|
|
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGTERM' || exit 1
|
|
test -z "`docker ps|grep sig-test`"
|
|
if [ $? -ne 0 ]; then
|
|
echo "Looks like container was running when it shouldnt be"
|
|
docker ps
|
|
exit 1
|
|
fi
|
|
docker rm sig-test
|
|
|
|
# Upgrade path test
|
|
upgrade-path-test:
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 25
|
|
env:
|
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0 # Fetch all history and tags for upgrade testing
|
|
|
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
|
|
- name: Check upgrade works without error
|
|
run: |
|
|
echo "=== Testing upgrade path from 0.49.1 to ${{ github.ref_name }} (${{ github.sha }}) ==="
|
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends \
|
|
g++ \
|
|
gcc \
|
|
libc-dev \
|
|
libffi-dev \
|
|
libjpeg-dev \
|
|
libssl-dev \
|
|
libxslt-dev \
|
|
make \
|
|
patch \
|
|
pkg-config \
|
|
zlib1g-dev
|
|
|
|
# Checkout old version and create datastore
|
|
git checkout 0.49.1
|
|
python3 -m venv .venv
|
|
source .venv/bin/activate
|
|
pip install -r requirements.txt
|
|
pip install 'pyOpenSSL>=23.2.0'
|
|
|
|
echo "=== Running version 0.49.1 to create datastore ==="
|
|
ALLOW_IANA_RESTRICTED_ADDRESSES=true python3 ./changedetection.py -C -d /tmp/data &
|
|
APP_PID=$!
|
|
|
|
# Wait for app to be ready
|
|
echo "Waiting for 0.49.1 to be ready..."
|
|
sleep 6
|
|
|
|
# Extract API key from datastore (0.49.1 uses url-watches.json)
|
|
API_KEY=$(jq -r '.settings.application.api_access_token // empty' /tmp/data/url-watches.json)
|
|
echo "API Key: ${API_KEY:0:8}..."
|
|
|
|
# Create a watch with tag "github-group-test" via API
|
|
echo "Creating test watch with tag via API..."
|
|
curl -X POST "http://127.0.0.1:5000/api/v1/watch" \
|
|
-H "x-api-key: ${API_KEY}" \
|
|
-H "Content-Type: application/json" \
|
|
--show-error --fail \
|
|
--retry 6 --retry-delay 1 --retry-connrefused \
|
|
-d '{
|
|
"url": "https://example.com/upgrade-test",
|
|
"tag": "github-group-test"
|
|
}'
|
|
|
|
echo "✓ Created watch with tag 'github-group-test'"
|
|
|
|
# Create a specific test URL watch
|
|
echo "Creating test URL watch via API..."
|
|
curl -X POST "http://127.0.0.1:5000/api/v1/watch" \
|
|
-H "x-api-key: ${API_KEY}" \
|
|
-H "Content-Type: application/json" \
|
|
--show-error --fail \
|
|
-d '{
|
|
"url": "http://localhost/test.txt"
|
|
}'
|
|
|
|
echo "✓ Created watch for 'http://localhost/test.txt' in version 0.49.1"
|
|
|
|
# Stop the old version gracefully
|
|
kill $APP_PID
|
|
wait $APP_PID || true
|
|
echo "✓ Version 0.49.1 stopped"
|
|
|
|
# Upgrade to current version (use commit SHA since we're in detached HEAD)
|
|
echo "Upgrading to commit ${{ github.sha }}"
|
|
git checkout ${{ github.sha }}
|
|
pip install -r requirements.txt
|
|
|
|
echo "=== Running current version (commit ${{ github.sha }}) with old datastore (testing mode) ==="
|
|
ALLOW_IANA_RESTRICTED_ADDRESSES=true TESTING_SHUTDOWN_AFTER_DATASTORE_LOAD=1 python3 ./changedetection.py -d /tmp/data > /tmp/upgrade-test.log 2>&1
|
|
|
|
echo "=== Upgrade test output ==="
|
|
cat /tmp/upgrade-test.log
|
|
echo "✓ Datastore upgraded successfully"
|
|
|
|
# Now start the current version normally to verify the tag survived
|
|
echo "=== Starting current version to verify tag exists after upgrade ==="
|
|
ALLOW_IANA_RESTRICTED_ADDRESSES=true timeout 20 python3 ./changedetection.py -d /tmp/data > /tmp/ui-test.log 2>&1 &
|
|
APP_PID=$!
|
|
|
|
# Wait for app to be ready and fetch UI
|
|
echo "Waiting for current version to be ready..."
|
|
sleep 5
|
|
curl --retry 6 --retry-delay 1 --retry-connrefused --silent http://127.0.0.1:5000 > /tmp/ui-output.html
|
|
|
|
# Verify tag exists in UI
|
|
if grep -q "github-group-test" /tmp/ui-output.html; then
|
|
echo "✓ Tag 'github-group-test' found in UI after upgrade"
|
|
else
|
|
echo "ERROR: Tag 'github-group-test' not found in UI after upgrade"
|
|
echo "=== UI Output ==="
|
|
cat /tmp/ui-output.html
|
|
echo "=== App Log ==="
|
|
cat /tmp/ui-test.log
|
|
kill $APP_PID || true
|
|
exit 1
|
|
fi
|
|
|
|
# Verify test URL exists in UI
|
|
if grep -q "http://localhost/test.txt" /tmp/ui-output.html; then
|
|
echo "✓ Watch URL 'http://localhost/test.txt' found in UI after upgrade"
|
|
else
|
|
echo "ERROR: Watch URL 'http://localhost/test.txt' not found in UI after upgrade"
|
|
echo "=== UI Output ==="
|
|
cat /tmp/ui-output.html
|
|
echo "=== App Log ==="
|
|
cat /tmp/ui-test.log
|
|
kill $APP_PID || true
|
|
exit 1
|
|
fi
|
|
|
|
# Cleanup
|
|
kill $APP_PID || true
|
|
wait $APP_PID || true
|
|
|
|
echo ""
|
|
echo "✓✓✓ Upgrade test passed: 0.49.1 → ${{ github.ref_name }} ✓✓✓"
|
|
echo " - Commit: ${{ github.sha }}"
|
|
echo " - Datastore migrated successfully"
|
|
echo " - Tag 'github-group-test' survived upgrade"
|
|
echo " - Watch URL 'http://localhost/test.txt' survived upgrade"
|
|
|
|
echo "✓ Upgrade test passed: 0.49.1 → ${{ github.ref_name }}"
|
|
|
|
- name: Upload upgrade test logs
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: upgrade-test-logs-py${{ env.PYTHON_VERSION }}
|
|
path: /tmp/upgrade-test.log
|