Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fcda80297 | ||
|
|
e94a489daa | ||
|
|
df605452ba | ||
|
|
b472401d2f | ||
|
|
e4a1e400e8 | ||
|
|
2ce7074118 | ||
|
|
03bd48e399 | ||
|
|
edd1265b42 | ||
|
|
6bafaa99d7 | ||
|
|
53e3e9787b | ||
|
|
8e82de7f4a | ||
|
|
41dfc384aa | ||
|
|
66d88542f6 | ||
|
|
268b520394 | ||
|
|
3a3557ac72 | ||
|
|
b8ba9a64cd | ||
|
|
58a7955835 | ||
|
|
564af2fa3e | ||
|
|
ccaa630821 | ||
|
|
a3c53aca45 | ||
|
|
e0e3a7d09b | ||
|
|
c97cea180e | ||
|
|
5efc69d04b | ||
|
|
912713a732 | ||
|
|
f4c84ff6ac | ||
|
|
3ced9981f7 | ||
|
|
49204a84e4 | ||
|
|
da1665acb3 | ||
|
|
5f20275598 | ||
|
|
8bbd83ea3f | ||
|
|
4c847dff79 | ||
|
|
2e2aaf50ed | ||
|
|
9f8e58a0a0 | ||
|
|
6b46a328f0 | ||
|
|
b1cdac35d6 | ||
|
|
33c4801abf | ||
|
|
f464d764de | ||
|
|
d342c38998 | ||
|
|
f1f9a14601 | ||
|
|
de368a9c6a | ||
|
|
0a4249b63a |
No files matched your search
@@ -274,24 +274,21 @@ jobs:
|
||||
if: inputs.push_digests
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
- name: Verify OpenCode install with a read-only browser profile
|
||||
- name: Verify optional agent install with a read-only browser profile
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: ${{ inputs.push_digests && format('{0}@{1}', env.DOCKERHUB_IMAGE, steps.docker_build.outputs.digest) || matrix.local_tag }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
PROFILE_DIR="$(mktemp -d)"
|
||||
# shellcheck disable=SC2016
|
||||
"$DOCKER_BINARY" run --rm --platform '${{ matrix.platform }}' \
|
||||
--volume "$PROFILE_DIR:/home/archivebox/.config/chromium:ro" \
|
||||
"$IMAGE" bash -c '
|
||||
set -Eeuo pipefail
|
||||
test -w "$XDG_CONFIG_HOME"
|
||||
test ! -w "$XDG_CONFIG_HOME/chromium"
|
||||
# Check the shipped executable before the installer can repair
|
||||
# an image that accidentally omitted this opt-in dependency.
|
||||
"$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.bin/opencode" --version
|
||||
archivebox install opencode
|
||||
"$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.bin/opencode" --version
|
||||
uv run --no-project python -c "from abx_plugins.plugins.opencode.image import verify_archivebox_install; verify_archivebox_install()"
|
||||
'
|
||||
|
||||
- name: Validate exact built image version and commit
|
||||
@@ -454,7 +451,9 @@ jobs:
|
||||
"$DOCKER_BINARY" run --name www-nginx -p 80:80 -d nginx:1.29.5@sha256:0236ee02dcbce00b9bd83e0f5fbc51069e7e1161bd59d99885b3ae1734f3392e
|
||||
|
||||
"$DOCKER_BINARY" run -v "$PWD"/data:/data --network host "$ARCHIVEBOX_IMAGE" add http://www.test-nginx-1.local
|
||||
echo "http://www.test-nginx-2.local" | "$DOCKER_BINARY" run -i --network host -v "$PWD"/data:/data "$ARCHIVEBOX_IMAGE" add
|
||||
# The argument form above exercises the complete default pipeline.
|
||||
# Keep a real capture for stdin parsing without repeating every plugin.
|
||||
echo "http://www.test-nginx-2.local" | "$DOCKER_BINARY" run -i --network host -v "$PWD"/data:/data "$ARCHIVEBOX_IMAGE" add --plugins=wget,hashes
|
||||
|
||||
archive_list="$("$DOCKER_BINARY" run -v "$PWD"/data:/data "$ARCHIVEBOX_IMAGE" list)"
|
||||
[[ "$archive_list" == *"www.test-nginx-1.local"* ]] || { echo "The site 1 isn't in the list"; exit 1; }
|
||||
|
||||
@@ -392,7 +392,8 @@ jobs:
|
||||
|
||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
with:
|
||||
install: true
|
||||
# These images were already built and tested; only publish manifests.
|
||||
driver: docker
|
||||
|
||||
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
@@ -525,17 +526,27 @@ jobs:
|
||||
for digest in "${DIGESTS[@]}"; do REFS+=("${GHCR_IMAGE}@sha256:${digest}"); done
|
||||
$DOCKER_BINARY buildx imagetools create "${TAG_ARGS[@]}" "${REFS[@]}"
|
||||
|
||||
- name: Verify published Docker images run
|
||||
- name: Verify published tags contain the exact tested images
|
||||
env:
|
||||
DOCKERHUB_TAGS: ${{ steps.docker_meta.outputs.dockerhub_tags }}
|
||||
GHCR_TAGS: ${{ steps.docker_meta.outputs.ghcr_tags }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
VERSION='${{ needs.python-release.outputs.version }}'
|
||||
SHORT_SHA="${RELEASE_SHA::12}"
|
||||
for image in "${DOCKERHUB_IMAGE}:sha-${SHORT_SHA}" "${GHCR_IMAGE}:sha-${SHORT_SHA}"; do
|
||||
"$DOCKER_BINARY" buildx imagetools inspect "$image" >/dev/null
|
||||
INSTALLED_VERSION="$("$DOCKER_BINARY" run --rm "$image" --version)"
|
||||
test "$INSTALLED_VERSION" = "$VERSION"
|
||||
done
|
||||
# Both architectures already passed CLI/version/capture acceptance by
|
||||
# digest. Verify promotion preserves those exact images and attestations
|
||||
# instead of downloading and running the same image again on one arch.
|
||||
mapfile -t DIGESTS < <($UV_BINARY run --no-cache --no-project python -c 'import re; from pathlib import Path; print("\n".join(sorted(path.name for path in Path("/tmp/digests").iterdir() if path.is_file() and re.fullmatch(r"[0-9a-f]{64}", path.name))))')
|
||||
REFS=()
|
||||
for digest in "${DIGESTS[@]}"; do REFS+=("${DOCKERHUB_IMAGE}@sha256:${digest}"); done
|
||||
EXPECTED="$("$DOCKER_BINARY" buildx imagetools create --dry-run "${REFS[@]}" | "$JQ_BINARY" -ceS '.manifests | sort_by(.digest)')"
|
||||
"$JQ_BINARY" -e 'map(select(.platform.os == "linux") | .platform.architecture) | sort == ["amd64", "arm64"]' <<< "$EXPECTED"
|
||||
while IFS= read -r image; do
|
||||
[[ -n "$image" ]] || continue
|
||||
ACTUAL="$("$DOCKER_BINARY" buildx imagetools inspect --raw "$image" | "$JQ_BINARY" -ceS 'select(.schemaVersion == 2) | .manifests | sort_by(.digest)')"
|
||||
[[ "$ACTUAL" == "$EXPECTED" ]] || { echo "Published tag differs from tested images: $image" >&2; exit 1; }
|
||||
echo "Verified exact tested images: $image"
|
||||
done < <(printf '%s\n' "$DOCKERHUB_TAGS" "$GHCR_TAGS")
|
||||
|
||||
- name: Record published image source
|
||||
shell: bash
|
||||
|
||||
@@ -120,6 +120,9 @@ jobs:
|
||||
if [[ "$CI_RUNNER_ENVIRONMENT" == github-hosted ]]; then
|
||||
workers="$(nproc)"
|
||||
cache_suffix="-workers-$workers"
|
||||
# Avoid the hosted image's cold Chromium executable page faults.
|
||||
# abxpkg owns installation and reuse in the existing runtime cache.
|
||||
echo "CHROME_BINPROVIDERS=playwright,puppeteer" >> "$GITHUB_ENV"
|
||||
fi
|
||||
{
|
||||
echo "CI_TEST_WORKERS=$workers"
|
||||
|
||||
+5
-7
@@ -3,14 +3,14 @@
|
||||
# Multistage ArchiveBox Dockerfile that consumes the abx-dl runtime image.
|
||||
# WHY: abx-dl owns Python, Node, Chromium, and extraction dependencies so the
|
||||
# standalone downloader stays independent of ArchiveBox's server features.
|
||||
# ArchiveBox-only dependencies (Sonic, OpenCode, supervisor, Django) belong in
|
||||
# ArchiveBox-only dependencies (server plugins, supervisor, Django) belong in
|
||||
# the layers added by this Dockerfile, never in the shared abx-dl base image.
|
||||
# REQUIRED INSTALLATION CONTRACT — do not work around failures:
|
||||
# 1. IMAGE CONSTRUCTION: preinstall ALL plugins supported by runtime="archivebox"
|
||||
# and ALL their required_binaries, regardless of default enabled state.
|
||||
# Inherit the complete downloader dependencies from the tested abx-dl image;
|
||||
# install every remaining ArchiveBox-only dependency in this image's layers,
|
||||
# including OpenCode and Sonic. Never put server dependencies in abx-dl.
|
||||
# including optional server plugins. Never put server dependencies in abx-dl.
|
||||
# Use the normal plugin dependency pipeline: install command -> config.json
|
||||
# required_binaries -> abxpkg. Never add manual apt/pip/npm installs to hide
|
||||
# failures in that pipeline. OS/interpreter bootstrap is a separate concern,
|
||||
@@ -266,15 +266,13 @@ RUN --mount=type=cache,target=/opt/archivebox/lib/cache,sharing=locked,mode=1777
|
||||
|
||||
# Install the complete ArchiveBox catalog, including disabled server plugins.
|
||||
# Explicit plugin names make installation independent of enabled defaults.
|
||||
RUN --mount=type=cache,target=/var/tmp/abxpkg-cache,sharing=locked,mode=1777,id=archivebox-opencode-$TARGETARCH \
|
||||
RUN --mount=type=cache,target=/var/tmp/abxpkg-cache,sharing=locked,mode=1777,id=archivebox-server-plugins-$TARGETARCH \
|
||||
chown -R "$DEFAULT_ARCHIVEBOX_UID:$DEFAULT_ARCHIVEBOX_GID" /var/tmp/abxpkg-cache \
|
||||
&& chmod 1777 /var/tmp/abxpkg-cache \
|
||||
&& export ABX_DOCKER_PLUGINS="$(/venv/bin/python3 -c 'from abx_dl.catalog import PluginCatalog; print(" ".join(PluginCatalog.discover(runtime="archivebox")))')" \
|
||||
&& env ABXPKG_TMP_CACHE_DIR=/var/tmp/abxpkg-cache XDG_CACHE_HOME=/var/tmp/abxpkg-cache \
|
||||
setpriv --reuid="$ARCHIVEBOX_USER" --regid="$ARCHIVEBOX_USER" --init-groups abx-dl install $ABX_DOCKER_PLUGINS \
|
||||
# pnpm includes musl variants that Debian's glibc runtime cannot use.
|
||||
&& find "$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules" -type l -name 'opencode-linux-*-musl' -delete \
|
||||
&& find "$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.pnpm" -maxdepth 1 -type d -name 'opencode-linux-*-musl@*' -exec rm -rf {} + \
|
||||
&& UV_NO_CACHE=true /usr/bin/uv run --no-project python -c 'from abx_plugins.plugins.opencode.image import prune_incompatible_image_files; prune_incompatible_image_files()' \
|
||||
# pnpm's explicit store lives under ABXPKG_LIB_DIR, regardless of XDG_CACHE_HOME.
|
||||
# Retaining its download copies made image size depend on whether the build
|
||||
# preserved hardlinks to installed files. Remove the store in this layer;
|
||||
@@ -283,7 +281,7 @@ RUN --mount=type=cache,target=/var/tmp/abxpkg-cache,sharing=locked,mode=1777,id=
|
||||
&& rm -rf "$ABXPKG_LIB_DIR/cache/pnpm"
|
||||
RUN --network=none export ABX_DOCKER_PLUGINS="$(/venv/bin/python3 -c 'from abx_dl.catalog import PluginCatalog; print(" ".join(PluginCatalog.discover(runtime="archivebox")))')" \
|
||||
&& setpriv --reuid="$ARCHIVEBOX_USER" --regid="$ARCHIVEBOX_USER" --init-groups \
|
||||
bash -c '"$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.bin/opencode" --version && abx-dl install $ABX_DOCKER_PLUGINS'
|
||||
bash -c 'uv run --no-project python -c "from abx_plugins.plugins.opencode.image import verify_installed; verify_installed()" && abx-dl install $ABX_DOCKER_PLUGINS'
|
||||
|
||||
WORKDIR "$DATA_DIR"
|
||||
RUN echo "[+] Initializing image collection..." \
|
||||
|
||||
@@ -211,11 +211,22 @@ class ArchiveResultFilterSchema(FilterSchema):
|
||||
created_at__lt: Annotated[datetime | None, FilterLookup("created_at__lt")] = None
|
||||
|
||||
|
||||
def _with_archiveresult_processes(queryset):
|
||||
# cmd/pwd/version delegate to Process and Binary. Without this join a full
|
||||
# capture's metadata response performs two extra indexed lookups per hook
|
||||
# (roughly 100 queries), despite the initial Snapshot UUID lookup being fast.
|
||||
# Logs and the hydrated hook environment are not response fields; loading
|
||||
# those potentially large values would replace round trips with excess I/O.
|
||||
return queryset.select_related("process__binary").defer("process__stdout", "process__stderr", "process__env")
|
||||
|
||||
|
||||
@router.get("/archiveresults", response=list[ArchiveResultSchema], url_name="get_archiveresult")
|
||||
@paginate(CustomPagination)
|
||||
def get_archiveresults(request: HttpRequest, filters: Query[ArchiveResultFilterSchema]):
|
||||
"""List all ArchiveResult entries matching these filters."""
|
||||
queryset = filters.filter(ArchiveResult.objects.all())
|
||||
queryset = filters.filter(
|
||||
_with_archiveresult_processes(ArchiveResult.objects.all()).prefetch_related("snapshot__crawl__created_by", "snapshot__tags"),
|
||||
)
|
||||
if filters.search or filters.snapshot_tag:
|
||||
return queryset.distinct()
|
||||
return queryset
|
||||
@@ -242,7 +253,12 @@ def _uuid_ref_query(field_name: str, ref: str) -> Q:
|
||||
@router.get("/archiveresult/{archiveresult_id}", response=ArchiveResultSchema, url_name="get_archiveresult")
|
||||
def get_archiveresult(request: HttpRequest, archiveresult_id: str):
|
||||
"""Get a specific ArchiveResult by id."""
|
||||
return ArchiveResult.objects.get(_uuid_ref_query("id", archiveresult_id))
|
||||
return (
|
||||
_with_archiveresult_processes(ArchiveResult.objects.all())
|
||||
.select_related("snapshot__crawl__created_by")
|
||||
.prefetch_related("snapshot__tags")
|
||||
.get(_uuid_ref_query("id", archiveresult_id))
|
||||
)
|
||||
|
||||
|
||||
def _normalize_uploaded_archiveresult_plugin(plugin: str) -> str:
|
||||
@@ -689,12 +705,14 @@ class SnapshotSchema(Schema):
|
||||
|
||||
@staticmethod
|
||||
def resolve_num_archiveresults(obj, context):
|
||||
return obj.archiveresult_set.all().distinct().count()
|
||||
# This reverse FK contains each result once. DISTINCT would materialize
|
||||
# wide JSON output columns just to count rows through snapshot_id's index.
|
||||
return obj.archiveresult_set.count()
|
||||
|
||||
@staticmethod
|
||||
def resolve_archiveresults(obj, context):
|
||||
if bool(context["request"].__dict__.get("with_archiveresults", False)):
|
||||
return obj.archiveresult_set.all().distinct()
|
||||
return _with_archiveresult_processes(obj.archiveresult_set.all())
|
||||
return ArchiveResult.objects.none()
|
||||
|
||||
|
||||
|
||||
@@ -170,6 +170,18 @@ class ResourceAdmission:
|
||||
used_bytes = 0
|
||||
for child in processes:
|
||||
try:
|
||||
if sys.platform.startswith("linux"):
|
||||
try:
|
||||
memory = child.memory_full_info()
|
||||
# Chrome's processes share substantial resident
|
||||
# memory. RSS charges those pages once per process
|
||||
# and can falsely serialize captures on an idle host.
|
||||
used_bytes += memory.pss + memory.swap
|
||||
continue
|
||||
except psutil.AccessDenied:
|
||||
# Restricted /proc access still gets conservative
|
||||
# accounting through the available RSS measurement.
|
||||
pass
|
||||
used_bytes += child.memory_info().rss
|
||||
except (psutil.Error, OSError):
|
||||
continue
|
||||
|
||||
@@ -40,6 +40,7 @@ PYTEST_BASETEMP_ROOT = (REPO_ROOT / "tests" / "out").resolve()
|
||||
SESSION_DATA_DIR = Path(
|
||||
os.environ.get("ARCHIVEBOX_PYTEST_SESSION_DATA_DIR") or tempfile.mkdtemp(prefix="archivebox-pytest-session-"),
|
||||
).resolve()
|
||||
PORT_ALLOCATION_DIR = Path(os.environ.setdefault("ARCHIVEBOX_PYTEST_PORTS_DIR", str(SESSION_DATA_DIR / "ports")))
|
||||
if worker_id := os.environ.get("PYTEST_XDIST_WORKER"):
|
||||
# Workers inherit the coordinator's environment. Give each one its own
|
||||
# collection and cleanup root so a finishing worker cannot delete another's.
|
||||
@@ -1103,6 +1104,8 @@ def start_archivebox_server(
|
||||
else:
|
||||
output = log_path.read_text(encoding="utf-8", errors="replace") if log_path else ""
|
||||
raise AssertionError(f"ArchiveBox server did not open port {port} within 30 seconds:\n{output}")
|
||||
response = get_http_response(port, host=f"127.0.0.1:{port}", path="/health/", timeout=5)
|
||||
assert response.status_code == 200 and response.headers.get("X-ArchiveBox-Health") == "OK", response.text
|
||||
return proc
|
||||
|
||||
|
||||
@@ -1403,9 +1406,20 @@ def init_archive(cwd: Path) -> None:
|
||||
|
||||
|
||||
def get_free_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return sock.getsockname()[1]
|
||||
# Binding port zero only proves availability now. Another pytest worker can
|
||||
# receive that same port before either service starts. Claim it atomically
|
||||
# for this session while the socket is still bound, across all workers and
|
||||
# helper subprocesses; the session cleanup removes these small markers.
|
||||
PORT_ALLOCATION_DIR.mkdir(parents=True, exist_ok=True)
|
||||
while True:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
try:
|
||||
(PORT_ALLOCATION_DIR / str(port)).touch(exist_ok=False)
|
||||
except FileExistsError:
|
||||
continue
|
||||
return port
|
||||
|
||||
|
||||
def stop_server(cwd: Path) -> None:
|
||||
|
||||
@@ -8,10 +8,12 @@ from urllib.parse import urlsplit
|
||||
import pytest
|
||||
from abx_plugins import get_plugins_dir
|
||||
from django.test import Client
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
from archivebox.core.models import ArchiveResult, Snapshot
|
||||
from archivebox.core.routes_util import build_snapshot_url
|
||||
from archivebox.tests.conftest import cli_env, find_snapshot_dir, parse_jsonl_output, run_archivebox_cmd
|
||||
from archivebox.tests.conftest import cli_env, create_admin_and_token, find_snapshot_dir, parse_jsonl_output, run_archivebox_cmd
|
||||
|
||||
from archivebox.tests.test_orm_helpers import use_archivebox_db
|
||||
|
||||
@@ -80,6 +82,27 @@ def test_extract_runs_on_existing_snapshots(archive_with_extractors):
|
||||
assert archiveresults["wget"].output_str == "wget/example.com/index.html"
|
||||
assert archiveresults["wget"].output_files["example.com/index.html"]["size"] == wget_path.stat().st_size
|
||||
|
||||
token = create_admin_and_token(initialized_archive)
|
||||
with use_archivebox_db(initialized_archive):
|
||||
with CaptureQueriesContext(connection) as queries:
|
||||
response = Client().get(
|
||||
f"/api/v1/core/snapshot/{snapshot_id}",
|
||||
HTTP_HOST="api.archivebox.localhost",
|
||||
HTTP_X_ARCHIVEBOX_API_KEY=token,
|
||||
)
|
||||
assert response.status_code == 200, response.content
|
||||
returned = {row["plugin"]: row for row in response.json()["archiveresults"]}
|
||||
for plugin, result in archiveresults.items():
|
||||
assert returned[plugin]["cmd"] == result.cmd
|
||||
assert returned[plugin]["cmd_version"] == result.cmd_version
|
||||
assert returned[plugin]["pwd"] == result.pwd
|
||||
# Each real hook owns a Process and Binary. Metadata reads must fetch
|
||||
# them with the result rows, not run two extra queries per plugin.
|
||||
related_lookups = [
|
||||
query["sql"] for query in queries if 'FROM "machine_process"' in query["sql"] or 'FROM "machine_binary"' in query["sql"]
|
||||
]
|
||||
assert related_lookups == []
|
||||
|
||||
|
||||
def test_wget_literal_percent_output_url_serves_captured_file(initialized_archive, snapshot, live_server, browser_runtime):
|
||||
"""Replay the real HedgeDoc link-interstitial file saved by wget."""
|
||||
|
||||
@@ -52,6 +52,7 @@ def test_persona_import_real_browser_session(initialized_archive, tmp_path, http
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from archivebox.tests.conftest import resolve_abxpkg_chrome_env
|
||||
@@ -167,7 +168,9 @@ const puppeteer = require(process.argv[1]);
|
||||
assert archived.returncode == 0, archived.stderr
|
||||
launch_commands = list((initialized_archive / "archive" / "users").glob("*/crawls/**/chrome/cmd.sh"))
|
||||
assert len(launch_commands) == 1
|
||||
assert shlex.split(launch_commands[0].read_text().splitlines()[1])[0] == browser_env["CHROME_BINARY"]
|
||||
launched_browser = Path(shlex.split(launch_commands[0].read_text().splitlines()[1])[0])
|
||||
# abxpkg's environment and provider projections can name the same binary.
|
||||
assert launched_browser.samefile(browser_env["CHROME_BINARY"])
|
||||
outputs = list((initialized_archive / "archive" / "users").glob("*/snapshots/**/dom/output.html"))
|
||||
assert len(outputs) == 1
|
||||
html = outputs[0].read_text()
|
||||
|
||||
@@ -159,7 +159,7 @@ print(json.dumps({{
|
||||
assert not Path(retained["archiveresult_dir"]).exists()
|
||||
|
||||
|
||||
def test_delete_after_real_add_page_and_rest_create_paths(client):
|
||||
def test_delete_after_real_add_page_and_rest_create_paths(client, recursive_test_site):
|
||||
from archivebox.personas.models import Persona
|
||||
|
||||
# Match init/server startup; visiting /add/ must not create persona records.
|
||||
@@ -172,6 +172,7 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
|
||||
)
|
||||
|
||||
client.force_login(admin_user)
|
||||
ui_url, rest_url = recursive_test_site["child_urls"][:2]
|
||||
response = client.get(reverse("add"), HTTP_HOST=ADMIN_HOST)
|
||||
assert response.status_code == 200
|
||||
assert 'name="delete_after"' in response.content.decode()
|
||||
@@ -179,7 +180,7 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
"url": "https://example.com/delete-after-ui",
|
||||
"url": ui_url,
|
||||
"tag": "retention-ui",
|
||||
"depth": "0",
|
||||
"max_urls": "1",
|
||||
@@ -203,13 +204,13 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
|
||||
|
||||
from archivebox.crawls.models import Crawl
|
||||
|
||||
ui_crawl = Crawl.objects.get(urls__contains="https://example.com/delete-after-ui")
|
||||
ui_crawl = Crawl.objects.get(urls=ui_url)
|
||||
assert ui_crawl.config["DELETE_AFTER"] == "2h"
|
||||
assert ui_crawl.delete_at is not None
|
||||
from archivebox.services.runner import run_due_crawl
|
||||
|
||||
assert run_due_crawl(ui_crawl, lock_seconds=10)
|
||||
ui_snapshot = ui_crawl.snapshot_set.get(url="https://example.com/delete-after-ui")
|
||||
ui_snapshot = ui_crawl.snapshot_set.get(url=ui_url)
|
||||
assert ui_snapshot.delete_at is not None
|
||||
assert not ui_snapshot.output_dir.joinpath("staticfile", "stdin.txt").exists()
|
||||
|
||||
@@ -221,10 +222,12 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
|
||||
"/api/v1/crawls/crawls",
|
||||
data=json.dumps(
|
||||
{
|
||||
"urls": ["https://example.com/delete-after-rest"],
|
||||
"urls": [rest_url],
|
||||
"max_depth": 0,
|
||||
"max_urls": 1,
|
||||
"config": {"DELETE_AFTER": "3h"},
|
||||
# Exercise retention through a real capture. The full plugin
|
||||
# pipeline has its own recursive-crawl integration coverage.
|
||||
"config": {"DELETE_AFTER": "3h", "PLUGINS": "wget,hashes"},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
@@ -232,9 +235,16 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
|
||||
HTTP_X_ARCHIVEBOX_API_KEY=api_token.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
rest_crawl = Crawl.objects.get(urls__contains="https://example.com/delete-after-rest")
|
||||
rest_crawl = Crawl.objects.get(urls=rest_url)
|
||||
assert rest_crawl.config["DELETE_AFTER"] == "3h"
|
||||
assert rest_crawl.delete_at is not None
|
||||
assert run_due_crawl(rest_crawl, lock_seconds=10)
|
||||
rest_snapshot = rest_crawl.snapshot_set.get(url="https://example.com/delete-after-rest")
|
||||
rest_snapshot = rest_crawl.snapshot_set.get(url=rest_url)
|
||||
assert rest_snapshot.delete_at is not None
|
||||
from archivebox.core.models import ArchiveResult
|
||||
|
||||
for plugin in ("wget", "hashes"):
|
||||
result = rest_snapshot.archiveresult_set.get(plugin=plugin)
|
||||
assert result.status == ArchiveResult.StatusChoices.SUCCEEDED
|
||||
assert result.delete_at is not None
|
||||
assert result.output_size > 0
|
||||
@@ -215,6 +215,8 @@ def test_release_uses_registered_publisher_and_authorized_tag_credentials():
|
||||
step for step in docker_workflow["jobs"]["build"]["steps"] if step.get("name") == "Validate exact built image version and commit"
|
||||
)
|
||||
assert version_check["if"] == "inputs.full_tests"
|
||||
assert "archivebox version" in version_check["run"]
|
||||
assert "Missing exact commit marker" in version_check["run"]
|
||||
pip_workflow = yaml.safe_load(PIP_WORKFLOW.read_text())
|
||||
install_script = next(
|
||||
step["run"] for step in pip_workflow["jobs"]["build"]["steps"] if step.get("name") == "Release wheel import and CLI smoke"
|
||||
@@ -238,11 +240,20 @@ def test_release_uses_registered_publisher_and_authorized_tag_credentials():
|
||||
assert 'echo "${DOCKERHUB_IMAGE}:sha-${SHORT_SHA}"' in tag_script
|
||||
assert 'echo "${DOCKERHUB_IMAGE}:${VERSION}"' in tag_script
|
||||
|
||||
docker_verify = next(step for step in docker_release["steps"] if step.get("name") == "Verify published Docker images run")
|
||||
docker_verify = next(
|
||||
step for step in docker_release["steps"] if step.get("name") == "Verify published tags contain the exact tested images"
|
||||
)
|
||||
verify_script = docker_verify["run"]
|
||||
assert '"${DOCKERHUB_IMAGE}:sha-${SHORT_SHA}"' in verify_script
|
||||
assert '"${GHCR_IMAGE}:sha-${SHORT_SHA}"' in verify_script
|
||||
assert '"${DOCKERHUB_IMAGE}:${VERSION}"' not in verify_script
|
||||
assert docker_verify["env"] == {
|
||||
"DOCKERHUB_TAGS": "${{ steps.docker_meta.outputs.dockerhub_tags }}",
|
||||
"GHCR_TAGS": "${{ steps.docker_meta.outputs.ghcr_tags }}",
|
||||
}
|
||||
assert '"${DOCKERHUB_IMAGE}@sha256:${digest}"' in verify_script
|
||||
assert 'imagetools create --dry-run "${REFS[@]}"' in verify_script
|
||||
assert 'imagetools inspect --raw "$image"' in verify_script
|
||||
assert 'sort == ["amd64", "arm64"]' in verify_script
|
||||
assert '[[ "$ACTUAL" == "$EXPECTED" ]]' in verify_script
|
||||
assert '"$DOCKERHUB_TAGS" "$GHCR_TAGS"' in verify_script
|
||||
|
||||
release_script = (REPO_ROOT / "bin" / "release.sh").read_text()
|
||||
assert "Never create GitHub Releases for automated rc builds" in release_script
|
||||
@@ -311,7 +322,7 @@ def test_stable_publication_requires_live_acceptance_before_upload():
|
||||
def test_staging_acceptance_uses_published_image_source_not_workflow_run_head():
|
||||
release = yaml.safe_load(RELEASE_WORKFLOW.read_text())
|
||||
docker_steps = release["jobs"]["docker-release"]["steps"]
|
||||
verified = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Verify published Docker images run")
|
||||
verified = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Verify published tags contain the exact tested images")
|
||||
source = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Record published image source")
|
||||
uploaded = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Upload published image source")
|
||||
assert verified < source < uploaded
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from archivebox.services.resource_admission import ResourceAdmission, disk_backed_swap_free_bytes
|
||||
|
||||
@@ -55,6 +56,59 @@ def test_memory_observation_tracks_a_real_child_process():
|
||||
assert child.wait(timeout=10) == 0
|
||||
|
||||
|
||||
def test_memory_observation_counts_shared_pages_once_on_linux():
|
||||
admission = ResourceAdmission()
|
||||
before = admission.memory_headroom()
|
||||
assert before is not None
|
||||
size = 128 * 1024 * 1024
|
||||
child = subprocess.Popen(
|
||||
[
|
||||
"uv",
|
||||
"run",
|
||||
"python",
|
||||
"-c",
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
allocation = bytearray(128 * 1024 * 1024)
|
||||
children = []
|
||||
for _ in range(2):
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
sys.stdin.read()
|
||||
os._exit(0)
|
||||
children.append(pid)
|
||||
print(len(allocation), flush=True)
|
||||
sys.stdin.read()
|
||||
for pid in children:
|
||||
os.waitpid(pid, 0)
|
||||
""",
|
||||
],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
assert child.stdout is not None
|
||||
assert child.stdout.readline().strip() == str(size)
|
||||
during = admission.memory_headroom()
|
||||
assert during is not None
|
||||
growth = during[0] - before[0]
|
||||
assert growth > size // 2
|
||||
finite_cgroup = any(
|
||||
(path / "memory.max").is_file() and (path / "memory.max").read_text().strip() != "max" for path in admission.cgroup_paths()
|
||||
)
|
||||
if sys.platform.startswith("linux") and not finite_cgroup:
|
||||
# These three real processes share one allocation. Linux PSS and
|
||||
# summing RSS differ by two copies. A finite cgroup instead measures
|
||||
# the entire container, including unrelated concurrent test workers.
|
||||
assert growth < size * 2, f"Shared allocation counted repeatedly: {growth}"
|
||||
finally:
|
||||
assert child.stdin is not None
|
||||
child.stdin.close()
|
||||
assert child.wait(timeout=10) == 0
|
||||
|
||||
|
||||
def test_disk_backed_swap_headroom_excludes_zram_but_keeps_backing_swap():
|
||||
swaps = """Filename Type Size Used Priority
|
||||
/dev/zram0 partition 1000 100 100
|
||||
|
||||
@@ -11,6 +11,8 @@ from urllib.parse import urlsplit
|
||||
import pytest
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from .conftest import localhost_session
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_archived_srcset_recovers_only_missing_image_candidates(snapshot, live_server, browser_runtime):
|
||||
@@ -87,7 +89,9 @@ def test_archived_srcset_recovers_only_missing_image_candidates(snapshot, live_s
|
||||
if density == 1:
|
||||
assert "1x_web_48dp.png" in rendered.url
|
||||
assert all(url.startswith((origin, "data:", "blob:")) for url in requests), requests
|
||||
raw = context.request.get(f"{origin}/archivewebpage/google-drive.{archive_format}?raw=1")
|
||||
assert raw.body() == recorded
|
||||
with localhost_session() as session:
|
||||
raw = session.get(f"{origin}/archivewebpage/google-drive.{archive_format}?raw=1")
|
||||
assert raw.status_code == 200
|
||||
assert raw.content == recorded
|
||||
context.close()
|
||||
browser.close()
|
||||
@@ -659,6 +659,14 @@ def test_plugin_grid_includes_all_visible_plugins_and_timestamps_last(admin_clie
|
||||
assert len(names) == len(expected)
|
||||
for name in expected:
|
||||
assert f'data-plugin-name="{name}"'.encode() in response.content
|
||||
# The same schema drives both editors, including newly installed plugins.
|
||||
# Check every plugin's category and rendered controls, not a fixed allowlist.
|
||||
catalog = get_plugin_catalog()
|
||||
for group in form.plugin_groups:
|
||||
for card in group["plugins"]:
|
||||
assert group["field_name"] == f"{catalog[card['name']].config.category}_plugins"
|
||||
for field in card["config_fields"]:
|
||||
assert f'name="{field["input_name"]}"'.encode() in response.content
|
||||
postprocessing = next(group for group in form.plugin_groups if group["field_name"] == "postprocessing_plugins")
|
||||
assert postprocessing["plugins"][-1]["name"] == "opentimestamps"
|
||||
assert b'name="plugin_config__opentimestamps__OPENTIMESTAMPS_TIMEOUT"' in response.content
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from threading import Thread
|
||||
from datetime import datetime, timezone
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from archivebox.misc.util import download_url, find_all_urls, fix_url_from_markdown, parse_date, sanitize_extracted_url
|
||||
from archivebox.tests.conftest import get_free_port, run_python_cwd
|
||||
|
||||
|
||||
def test_port_allocations_do_not_repeat_across_processes(tmp_path):
|
||||
script = """
|
||||
import json
|
||||
from archivebox.tests.conftest import get_free_port
|
||||
print(json.dumps([get_free_port() for _ in range(256)]))
|
||||
"""
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
results = list(pool.map(lambda _: run_python_cwd(script, tmp_path), range(4)))
|
||||
ports = [get_free_port() for _ in range(256)]
|
||||
for stdout, stderr, returncode in results:
|
||||
assert returncode == 0, stderr
|
||||
ports.extend(json.loads(stdout))
|
||||
assert len(ports) == 1280
|
||||
assert len(set(ports)) == len(ports), "Parallel services must not receive the same port"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -156,7 +156,6 @@ if [[ "$HAS_USEFUL_SNAPSHOT" == "0" ]]; then
|
||||
--depth=0 \
|
||||
--overwrite \
|
||||
--tag=documentation,reference \
|
||||
--plugins=title,headers,wget,screenshot,pdf,dom,readability,htmltotext,hashes,dns \
|
||||
https://docs.sweeting.me/s/cookie-dilemma https://archivebox.io
|
||||
)
|
||||
fi
|
||||
@@ -211,17 +210,10 @@ echo "[*] Creating a temporary screenshot admin"
|
||||
CREATED_TEMP_USER=1
|
||||
|
||||
# Configure the optional UI before the server snapshots its startup config.
|
||||
OPENCODE_PORT="$(uv run --no-cache --project "$REPO_DIR" python - <<'PYPORT'
|
||||
import socket
|
||||
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
print(sock.getsockname()[1])
|
||||
PYPORT
|
||||
)"
|
||||
(
|
||||
cd "$DATA_DIR"
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox config --set "OPENCODE_PORT=$OPENCODE_PORT"
|
||||
uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'from abx_plugins.plugins.opencode.archivebox.screenshots import configure; configure()'
|
||||
)
|
||||
|
||||
echo "[*] Starting ArchiveBox on port $PORT"
|
||||
@@ -274,11 +266,11 @@ VIEWS=(
|
||||
add_supplementary_template_capture() {
|
||||
local plugin_names="$1"
|
||||
local source_url="$2"
|
||||
local prepare_plugins="$3"
|
||||
local capture_plugins="${prepare_plugins:-$plugin_names}"
|
||||
local capture_log="$CAPTURE_ROOT/${plugin_names}-supplementary-capture.log"
|
||||
local supplementary_plugins=()
|
||||
IFS=',' read -r -a supplementary_plugins <<<"$plugin_names"
|
||||
# These are real all-plugin captures. The recipe only selects which output
|
||||
# to photograph, never which extractors run or which dependencies get saved.
|
||||
echo "[*] Capturing real $plugin_names source for template coverage"
|
||||
if ! (
|
||||
cd "$DATA_DIR"
|
||||
@@ -286,7 +278,6 @@ add_supplementary_template_capture() {
|
||||
--depth=0 \
|
||||
--overwrite \
|
||||
--tag=screenshot-gallery \
|
||||
--plugins="$capture_plugins" \
|
||||
"$source_url"
|
||||
) >"$capture_log" 2>&1; then
|
||||
echo "[!] Supplementary $plugin_names capture failed" >&2
|
||||
@@ -309,20 +300,6 @@ add_supplementary_template_capture() {
|
||||
|
||||
local snapshot_id snapshot_view_url snapshot_view_path
|
||||
IFS=$'\t' read -r snapshot_id snapshot_view_url snapshot_view_path <<<"$snapshot_record"
|
||||
if [[ -n "$prepare_plugins" ]]; then
|
||||
if ! (
|
||||
cd "$DATA_DIR"
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox extract \
|
||||
--plugins="$plugin_names" \
|
||||
"$snapshot_id"
|
||||
) >>"$capture_log" 2>&1; then
|
||||
echo "[!] $plugin_names extraction failed for the prepared snapshot" >&2
|
||||
tail -100 "$capture_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
stop_background_runner
|
||||
fi
|
||||
|
||||
local discovery_report="$CAPTURE_ROOT/${plugin_names}-output-discovery.json"
|
||||
NODE_PATH="$ABXPKG_LIB_DIR/pnpm/packages/chrome/node_modules" \
|
||||
CHROME_BINARY="$SCREENSHOT_CHROME_BINARY" \
|
||||
@@ -367,6 +344,12 @@ while [[ "$capture_index" -lt "${#VIEWS[@]}" ]]; do
|
||||
[[ "$capture_mode" != wait-text:* ]] || wait_for_text="${capture_mode#wait-text:}"
|
||||
preview_wait_for_text=""
|
||||
[[ "$capture_mode" != wait-preview-text:* ]] || preview_wait_for_text="${capture_mode#wait-preview-text:}"
|
||||
config_selector=""
|
||||
config_click_selector=""
|
||||
if [[ "$capture_mode" == plugin-config:* ]]; then
|
||||
config_selector="[data-plugin-name='${capture_mode#plugin-config:}']"
|
||||
config_click_selector="$config_selector .plugin-config-marker"
|
||||
fi
|
||||
view_timing_report=""
|
||||
while IFS='|' read -r profile _viewport_width _viewport_height; do
|
||||
filename="$slug-$profile.png"
|
||||
@@ -490,6 +473,8 @@ PY
|
||||
SCREENSHOT_HEIGHT=1000 \
|
||||
SCREENSHOT_VARIANTS_JSON="$output_variants" \
|
||||
SCREENSHOT_COLLAPSE_FILTERS=1 \
|
||||
SCREENSHOT_CLICK_SELECTOR="$config_click_selector" \
|
||||
SCREENSHOT_SCROLL_SELECTOR="$config_selector" \
|
||||
SCREENSHOT_SNAPSHOT_HEADER="$snapshot_header" \
|
||||
SCREENSHOT_WAIT_FOR_TEXT="$wait_for_text" \
|
||||
SCREENSHOT_PREVIEW_WAIT_FOR_TEXT="$preview_wait_for_text" \
|
||||
@@ -589,7 +574,7 @@ PY
|
||||
VIEWS+=(
|
||||
"Add URLs|$ADMIN_BASE_URL/add/|/add/|archivebox/core/views.py"
|
||||
"Admin dashboard|$ADMIN_BASE_URL/admin/|/admin/|archivebox/core/admin_site.py"
|
||||
"AI agent|$ADMIN_BASE_URL/admin/agent/|/admin/agent/|archivebox/opencode/views.py|wait-text:ArchiveBox AI Agent"
|
||||
"$(uv run --no-cache --project "$REPO_DIR" python -c 'import sys; from abx_plugins.plugins.opencode.archivebox.screenshots import gallery_entry; print(gallery_entry(sys.argv[1]))' "$ADMIN_BASE_URL")"
|
||||
"Snapshots table|$ADMIN_BASE_URL/admin/core/snapshot/|/admin/core/snapshot/|archivebox/core/admin_snapshots.py"
|
||||
"Snapshots grid|$ADMIN_BASE_URL/admin/core/snapshot/grid/|/admin/core/snapshot/grid/|archivebox/templates/admin/snapshots_grid.html"
|
||||
"Snapshot admin detail|$ADMIN_BASE_URL/admin/core/snapshot/$SNAPSHOT_ID/change/|/admin/core/snapshot/$SNAPSHOT_ID/change/|archivebox/core/admin_snapshots.py"
|
||||
@@ -662,11 +647,18 @@ PY
|
||||
[[ -z "$plugin_name" ]] && continue
|
||||
VIEWS+=("Snapshot View ($plugin_name)|$LIVE_SNAPSHOT_VIEW_URL#$output_path|$LIVE_SNAPSHOT_VIEW_PATH|archivebox/templates/core/snapshot.html|$output_capture_mode")
|
||||
done <<<"$SNAPSHOT_OUTPUT_PLUGINS"
|
||||
while IFS='|' read -r example_plugins example_url example_prepare; do
|
||||
while IFS='|' read -r example_plugins example_url; do
|
||||
[[ -z "$example_plugins" ]] && continue
|
||||
add_supplementary_template_capture "$example_plugins" "$example_url" "$example_prepare"
|
||||
add_supplementary_template_capture "$example_plugins" "$example_url"
|
||||
done < <(uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'import json,sys; plan=json.load(open(sys.argv[1])); print("\n".join("|".join((",".join(example["plugins"]), example["url"], ",".join(example["prepare_plugins"]))) for example in plan["examples"]))' \
|
||||
'import json,sys; plan=json.load(open(sys.argv[1])); print("\n".join("|".join((",".join(example["plugins"]), example["url"])) for example in plan["examples"]))' \
|
||||
"$PLUGIN_SCREENSHOT_PLAN")
|
||||
|
||||
while IFS='|' read -r config_name config_plugin; do
|
||||
[[ -z "$config_plugin" ]] && continue
|
||||
VIEWS+=("$config_name|$ADMIN_BASE_URL/add/|/add/|archivebox/templates/plugins/plugin_config_grid.html|plugin-config:$config_plugin")
|
||||
done < <(uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'import json,sys; print("\n".join(view["name"] + "|" + view["plugin"] for view in json.load(open(sys.argv[1]))["config_views"]))' \
|
||||
"$PLUGIN_SCREENSHOT_PLAN")
|
||||
|
||||
VIEWS+=("Snapshot View (header collapsed)|$LIVE_SNAPSHOT_VIEW_URL|$LIVE_SNAPSHOT_VIEW_PATH|archivebox/templates/core/snapshot.html|snapshot-collapsed")
|
||||
|
||||
@@ -254,7 +254,7 @@ find /tmp "$TMP_DIR" -maxdepth 1 -type d -name "archivebox-chrome-profile.*" -mm
|
||||
|
||||
ensure_dir "/home/$ARCHIVEBOX_USER"
|
||||
# A nested read-only browser-profile mount can create this parent as root.
|
||||
# OpenCode's installer needs to create a sibling config directory as archivebox.
|
||||
# Plugin installers need to create sibling config directories as archivebox.
|
||||
ensure_dir "${XDG_CONFIG_HOME:-/home/$ARCHIVEBOX_USER/.config}"
|
||||
ensure_small_runtime_tree "$ABXBUS_CACHE_DIR"
|
||||
ensure_small_runtime_tree "$ABXBUS_CACHE_DIR/semaphores"
|
||||
|
||||
@@ -67,6 +67,7 @@ def plugin_screenshot_plan() -> dict:
|
||||
required = []
|
||||
examples = {}
|
||||
overrides = {}
|
||||
config_views = []
|
||||
enabled_plugins = []
|
||||
for plugin_dir in sorted(get_plugins_dir().iterdir()):
|
||||
config_path = plugin_dir / "config.json"
|
||||
@@ -81,6 +82,8 @@ def plugin_screenshot_plan() -> dict:
|
||||
enabled = True
|
||||
if recipe.get("view"):
|
||||
required.append(recipe["view"])
|
||||
if recipe.get("config"):
|
||||
config_views.append({"name": recipe["view"], "plugin": plugin})
|
||||
elif enabled and (
|
||||
recipe.get("url")
|
||||
or (plugin_dir / "templates/full.html").is_file()
|
||||
@@ -89,10 +92,16 @@ def plugin_screenshot_plan() -> dict:
|
||||
required.append(f"Snapshot View ({plugin})")
|
||||
if recipe.get("url"):
|
||||
overrides[plugin] = recipe
|
||||
key = (recipe["url"], tuple(recipe.get("prepare_plugins", [])))
|
||||
example = examples.setdefault(key, {"url": key[0], "prepare_plugins": list(key[1]), "plugins": []})
|
||||
key = recipe["url"]
|
||||
example = examples.setdefault(key, {"url": key, "plugins": []})
|
||||
example["plugins"].append(plugin)
|
||||
return {"required": required, "examples": list(examples.values()), "overrides": overrides, "enabled": enabled_plugins}
|
||||
return {
|
||||
"required": required,
|
||||
"examples": list(examples.values()),
|
||||
"overrides": overrides,
|
||||
"enabled": enabled_plugins,
|
||||
"config_views": config_views,
|
||||
}
|
||||
|
||||
|
||||
def build_provenance() -> dict[str, str]:
|
||||
@@ -259,7 +268,8 @@ def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -
|
||||
route = parsed_url.path or "/"
|
||||
if parsed_url.fragment:
|
||||
route = f"{route}#{parsed_url.fragment}"
|
||||
source_url = f"{source_base_url}{capture['source']}"
|
||||
source = capture["source"]
|
||||
source_url = source if source.startswith("https://") else f"{source_base_url}{source}"
|
||||
ttfb_values = [variant["ttfb_ms"] for variant in variants.values() if isinstance(variant.get("ttfb_ms"), int | float)]
|
||||
ttfb_text = f" · ~{round(sum(ttfb_values) / len(ttfb_values))}ms TTFB" if ttfb_values else ""
|
||||
markdown_cells = []
|
||||
|
||||
@@ -374,12 +374,7 @@ async function main() {
|
||||
if (process.env.SCREENSHOT_AFTER_CLICK_WAIT_SELECTOR) {
|
||||
await page.waitForSelector(process.env.SCREENSHOT_AFTER_CLICK_WAIT_SELECTOR, { timeout: 45000 });
|
||||
}
|
||||
if (process.env.SCREENSHOT_SCROLL_SELECTOR) {
|
||||
await page.waitForSelector(process.env.SCREENSHOT_SCROLL_SELECTOR, { timeout: 45000 }).catch(() => {});
|
||||
await page.evaluate((selector) => {
|
||||
document.querySelector(selector)?.scrollIntoView({ block: 'start', inline: 'nearest' });
|
||||
}, process.env.SCREENSHOT_SCROLL_SELECTOR);
|
||||
}
|
||||
|
||||
|
||||
if (requireLiveProgress) {
|
||||
await waitForLiveProgress(page, liveProgressDeadline, screencastResponses);
|
||||
@@ -446,6 +441,14 @@ async function main() {
|
||||
const screenshotPaths = [];
|
||||
if (requireLiveProgress) checks.liveProgressCaptures = [];
|
||||
const capture = async (screenshotPath) => {
|
||||
// Resizing can move the target to a different row/column. Re-anchor each
|
||||
// gallery variant so it photographs the requested controls at every width.
|
||||
if (process.env.SCREENSHOT_SCROLL_SELECTOR) {
|
||||
await page.waitForSelector(process.env.SCREENSHOT_SCROLL_SELECTOR, { timeout: 45000 }).catch(() => {});
|
||||
await page.evaluate((selector) => {
|
||||
document.querySelector(selector)?.scrollIntoView({ block: 'start', inline: 'nearest' });
|
||||
}, process.env.SCREENSHOT_SCROLL_SELECTOR);
|
||||
}
|
||||
await waitForVisibleCardPreviews(page, requireLiveProgress ? Math.max(1, liveProgressDeadline - Date.now()) : 45000);
|
||||
if (requireLiveProgress) {
|
||||
const liveProgress = await waitForLiveProgress(page, liveProgressDeadline, screencastResponses);
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ View: [`/admin/`](http://admin.archivebox.localhost:5797/admin/) · [View code](
|
||||
|
||||
## AI agent
|
||||
|
||||
View: [`/admin/agent/`](http://admin.archivebox.localhost:5797/admin/agent/) · [View code](https://github.com/ArchiveBox/ArchiveBox/blob/dev/abx_plugins/plugins/opencode/views.py)
|
||||
View: [`/admin/agent/`](http://admin.archivebox.localhost:5797/admin/agent/) · [View code](https://github.com/ArchiveBox/abx-plugins/blob/main/abx_plugins/plugins/opencode/archivebox/views.py)
|
||||
|
||||
<table><thead><tr>
|
||||
<th>Desktop</th><th>Tablet</th><th>Mobile</th>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "archivebox",
|
||||
"version": "0.9.72rc87",
|
||||
"version": "0.9.72rc105",
|
||||
"repository": "github:ArchiveBox/ArchiveBox",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "archivebox"
|
||||
version = "0.9.72rc87"
|
||||
version = "0.9.72rc105"
|
||||
requires-python = ">=3.13"
|
||||
description = "Self-hosted internet archiving solution."
|
||||
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
|
||||
@@ -78,9 +78,9 @@ dependencies = [
|
||||
### Extractor dependencies (runtime binaries resolved through abxpkg)
|
||||
### Binary/Package Management
|
||||
"abxbus==2.5.74", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abxpkg==1.13.28", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abx-plugins[opencode]==1.13.140", # optional AI runtime clients are owned by the plugin
|
||||
"abx-dl==1.13.114", # shared ArchiveBox downloader package
|
||||
"abxpkg==1.13.33", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abx-plugins[opencode]==1.13.161", # optional AI runtime clients are owned by the plugin
|
||||
"abx-dl==1.13.126", # shared ArchiveBox downloader package
|
||||
### UUID7 backport for Python <3.14
|
||||
"uuid7>=0.1.0; python_version < '3.14'", # provides the uuid_extensions module on Python 3.13
|
||||
"browser-cookie3>=0.20.1",
|
||||
@@ -344,7 +344,7 @@ Donate = "https://github.com/ArchiveBox/ArchiveBox/wiki/Donations"
|
||||
|
||||
|
||||
[tool.bumpver]
|
||||
current_version = "v0.9.72rc87"
|
||||
current_version = "v0.9.72rc105"
|
||||
version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]"
|
||||
commit_message = "bump version {old_version} -> {new_version}"
|
||||
tag_message = "{new_version}"
|
||||
|
||||
@@ -13,18 +13,18 @@ supported-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-09-29T23:03:11.928013552Z"
|
||||
exclude-newer = "2026-10-01T07:11:37.418401316Z"
|
||||
exclude-newer-span = "P5D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
abxbus = { timestamp = "2026-10-04T23:03:10.928027688Z", span = "PT1S" }
|
||||
abxbus = { timestamp = "2026-10-06T07:11:36.418414Z", span = "PT1S" }
|
||||
abx-plugins = "2100-01-01T00:00:00Z"
|
||||
abx-dl = "2100-01-01T00:00:00Z"
|
||||
abxpkg = "2100-01-01T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "abx-dl"
|
||||
version = "1.13.114"
|
||||
version = "1.13.126"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "abx-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -38,14 +38,14 @@ dependencies = [
|
||||
{ name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/69/78cefa05329200e994bf6005f0aba63398381135a5a832e55feba7c3dd98/abx_dl-1.13.114.tar.gz", hash = "sha256:30158758e6a73cdc2d61b66b21d93d5081d21e53d5a15ac78a712738512a0e2b", size = 98199, upload-time = "2026-10-04T23:00:59.231Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/80/f30434e2e081d33c1b46233e9b284c98cff9eb8895b5fd546204953849b7/abx_dl-1.13.126.tar.gz", hash = "sha256:bdd29b626fba7a35f39865e5769b3b6ea1145a9953f2cf3aa14379eae192c248", size = 98185, upload-time = "2026-10-06T07:10:19.966Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/78/8e4c702985b8b69436a45c8034bfcf695e6760850d58eb381ca1d28e51f2/abx_dl-1.13.114-py3-none-any.whl", hash = "sha256:e99e84d8f006c1bc9892dcbfab08e843968ea1804514cc98042039fc729b13db", size = 103548, upload-time = "2026-10-04T23:00:57.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3e/f666f8294a31a8acd9189d790585f453e0ccecbb0a9ed9c9163c28ba6a10/abx_dl-1.13.126-py3-none-any.whl", hash = "sha256:7eb1e7295b4d11c5225d6fcd100e51069dc1022818e0c8f50abdf7d24ba4434d", size = 103547, upload-time = "2026-10-06T07:10:21.456Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "abx-plugins"
|
||||
version = "1.13.140"
|
||||
version = "1.13.161"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -54,9 +54,9 @@ dependencies = [
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/34/4c3c74bead11db31e651121778fc4cb107615bc1d6670fd8a1c0d478ae64/abx_plugins-1.13.140.tar.gz", hash = "sha256:138e4288d7b742d5ebc16792f6f3b84e685d6af184a161dfd112bbf0e5653324", size = 487932, upload-time = "2026-10-04T22:51:51.016Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/9d/9e442f90b30eb34fe4be44444d399685f453d3e452961018f7f91eaca173/abx_plugins-1.13.161.tar.gz", hash = "sha256:eb80af30d259716d4c713c708197328ca7fe0ad0b7fe05c27ef3bcb8112913e3", size = 509069, upload-time = "2026-10-06T07:02:15.305Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/6b/197297bb156c264da4b194ec28123150ff63d7f17be015c24578d3aa712a/abx_plugins-1.13.140-py3-none-any.whl", hash = "sha256:6e7855c2f558b506911994a365d620dd040acb7cf95acee7425e493bdc018160", size = 709033, upload-time = "2026-10-04T22:51:52.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/2b/da4c917bd49e92ba059b5a93b21f49499b5d385c15db23e2bc4ced834eb9/abx_plugins-1.13.161-py3-none-any.whl", hash = "sha256:1a88ec1580fa522718cf77049fd841441bd041cea63d8e38d4d1947e8b5b7b6a", size = 739583, upload-time = "2026-10-06T07:02:12.819Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -82,7 +82,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "abxpkg"
|
||||
version = "1.13.28"
|
||||
version = "1.13.33"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -90,9 +90,9 @@ dependencies = [
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/eb/3f802d6c788582683c5534e9d0683b871ee72b7f1d5026f6554f88890cdb/abxpkg-1.13.28.tar.gz", hash = "sha256:1daf4664c1d1645218bd52bd9356536df199a8ddd862032bab451bf6c367645d", size = 244592, upload-time = "2026-10-04T21:50:38.202Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/6f/30d3decb924862273bf19a6674c2b8cc408424fbe05c1f5d19869226bdd4/abxpkg-1.13.33.tar.gz", hash = "sha256:ca9ecb3b37a312599573e191facf75e178394b7067e7df327acf99c32bef0c40", size = 245179, upload-time = "2026-10-05T00:16:06.849Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/0c/bb90b796016dabc804ac4f06fd1e96615c681a43f7f5ccf5ab19de0bb490/abxpkg-1.13.28-py3-none-any.whl", hash = "sha256:94c0879a11ac121c2cf6cf05c783ba697dd3270af181d5b8fd7da525af06690b", size = 255914, upload-time = "2026-10-04T21:50:36.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/ef/0213490bc963f473078a56f0d553deacbcef00dfa5ee80d04ca9fafb73be/abxpkg-1.13.33-py3-none-any.whl", hash = "sha256:70c089fb42a2ff06affa97fc5d2cf190b9c2a4ae9561e81e57dce3e0e2933d52", size = 256428, upload-time = "2026-10-05T00:16:05.411Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -127,7 +127,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "archivebox"
|
||||
version = "0.9.72rc87"
|
||||
version = "0.9.72rc105"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -228,10 +228,10 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "abx-dl", specifier = "==1.13.114" },
|
||||
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.13.140" },
|
||||
{ name = "abx-dl", specifier = "==1.13.126" },
|
||||
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.13.161" },
|
||||
{ name = "abxbus", specifier = "==2.5.74" },
|
||||
{ name = "abxpkg", specifier = "==1.13.28" },
|
||||
{ name = "abxpkg", specifier = "==1.13.33" },
|
||||
{ name = "archivebox", extras = ["sonic", "ldap", "debug"], marker = "extra == 'all'" },
|
||||
{ name = "atomicwrites", specifier = "==1.4.1" },
|
||||
{ name = "base32-crockford", specifier = ">=0.3.0" },
|
||||
|
||||
Reference in new issue
Block a user