diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 316b135..589bca8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: # FIXME: can't use env object in reusable workflow inputs: https://github.com/orgs/community/discussions/26671 - run: echo "Exposing env vars for reusable workflow" build: - uses: docker/github-builder/.github/workflows/build.yml@v1.15.0 + uses: docker/github-builder/.github/workflows/build.yml@v1.18.1 permissions: contents: read packages: write # required to push to GHCR diff --git a/Dockerfile b/Dockerfile index 58459fe..ccd63e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 -ARG webchanges_tag=v3.37.0 +ARG webchanges_tag=v3.38.0 -FROM python:3.14.6-alpine3.24 AS builder +FROM python:3.15.0-alpine3.24 AS builder ARG webchanges_tag ENV PYTHONUTF8=1 @@ -26,7 +26,7 @@ ADD https://github.com/mborsetti/webchanges.git#${webchanges_tag} /webchanges WORKDIR /webchanges # Install requirements and webchanges from source -RUN python3 -m pip install -r requirements.txt \ +RUN python3 -m pip install -r requirements_free-threaded.txt \ && python3 -m pip install . # Install some additional packages used by webchanges (optional) @@ -61,7 +61,7 @@ RUN python3 -m PyInstaller -F --strip webchanges.py -FROM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS deploy +FROM alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS deploy ENV APP_USER=webchanges ENV PYTHONUTF8=1 RUN apk add --no-cache tini diff --git a/README.md b/README.md index 52299f8..dbb70a8 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ services: - /etc/localtime:/etc/localtime:ro restart: "unless-stopped" networks: - - webchangess + - webchanges ``` ### Migrating from `webchanges` pre-v3.22 (April 2024) diff --git a/data/config.schema.json b/data/config.schema.json index e720c37..5b04e42 100644 --- a/data/config.schema.json +++ b/data/config.schema.json @@ -1,9 +1,10 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/mborsetti/webchanges/refs/heads/main/webchanges/_resources/config.schema.json", "title": "Webchanges Configuration File", "description": "JSON Schema for the webchanges config.yaml configuration file.", "type": "object", - "required": ["display", "report", "job_defaults", "differ_defaults", "database", "footnote"], + "required": ["display", "report", "job_defaults", "differ_defaults", "database", "footnote", "version_check"], "properties": { "display": { "$ref": "#/$defs/_ConfigDisplay", @@ -15,7 +16,7 @@ }, "job_defaults": { "$ref": "#/$defs/_ConfigJobDefaults", - "description": "Default directives applied to jobs by kind (all, url, browser, command)." + "description": "Default directives applied to jobs by kind (all_jobs, url_jobs, browser_jobs, command_jobs)." }, "differ_defaults": { "$ref": "#/$defs/_ConfigDifferDefaults", @@ -29,6 +30,20 @@ "type": ["string", "null"], "description": "Optional text appended to the footer of every report (text, html and markdown). Set to null to disable.", "default": null + }, + "version_check": { + "$ref": "#/$defs/_ConfigVersionCheck", + "description": "Settings for the once-a-day check against PyPi for a newer release of webchanges." + }, + "jobs": { + "anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 1}, {"type": "null"}], + "description": "The jobs file(s) to use instead of the default jobs.yaml: a file, a glob pattern (e.g. 'jobs/**/*.yaml') or a directory (for all its *.yaml files, recursively), or a list of these. Relative paths are taken from the directory of this configuration file. Overridden by the --jobs command line argument. Set to null for the default.", + "default": null + }, + "hooks": { + "anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 1}, {"type": "null"}], + "description": "The hooks file(s) to use instead of the default hooks.py: a file, a glob pattern or a directory (for all its *.py files, recursively), or a list of these. Relative paths are taken from the directory of this configuration file. Overridden by the --hooks command line argument. Set to null for the default.", + "default": null } }, "additionalProperties": false, @@ -37,7 +52,7 @@ "title": "Display", "description": "Which job outcomes appear in reports. Changes are always reported.", "type": "object", - "required": ["new", "error", "unchanged", "empty-diff"], + "required": ["new", "error", "unchanged"], "properties": { "new": { "type": "boolean", @@ -55,9 +70,8 @@ "default": false }, "empty-diff": { - "type": "boolean", - "description": "Deprecated. Whether to report a 'changed' job whose diff was reduced to an empty string by a diff_filter. Use the additions_only job directive instead.", - "default": false, + "enum": [false], + "description": "Deprecated; use additions_only in the job instead.", "deprecated": true } } @@ -156,8 +170,11 @@ "title": "stdout reporter", "description": "Display a text report on standard output (the console).", "type": "object", - "required": ["enabled", "color"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the stdout reporter.", @@ -174,8 +191,11 @@ "title": "Browser reporter", "description": "Open the HTML report in the system's default web browser.", "type": "object", - "required": ["enabled"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the browser reporter.", @@ -187,8 +207,11 @@ "title": "Discord reporter", "description": "Send the report as a message in a Discord channel via a webhook.", "type": "object", - "required": ["enabled", "webhook_url", "embed", "subject", "colored", "max_message_length"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Discord reporter.", @@ -286,8 +309,11 @@ "title": "Email reporter", "description": "Send the report via email, using either SMTP or the external sendmail program.", "type": "object", - "required": ["enabled", "html", "to", "from", "subject", "method", "smtp", "sendmail"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the email reporter.", @@ -326,20 +352,11 @@ "title": "GitHub Issue reporter", "description": "Submit the Markdown report as a new issue on a GitHub repository. Requires a personal access token with 'public_repo' (public) or 'repo' (private) scope.", "type": "object", - "required": [ - "enabled", - "token", - "owner", - "repo", - "title", - "labels", - "format_dt", - "format_content", - "assignees", - "type", - "milestone" - ], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the GitHub Issue reporter.", @@ -403,8 +420,11 @@ "title": "Gotify reporter", "description": "Send the Markdown report to a Gotify server. Requires an application token created on the Gotify server's Web-UI.", "type": "object", - "required": ["enabled", "priority", "server_url", "title", "token"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Gotify reporter.", @@ -420,11 +440,16 @@ "description": "Base URL of the Gotify server.", "default": "" }, - "title": { + "subject": { "type": "string", - "description": "Message title.", + "description": "Message title; can contain the placeholders {count}, {jobs} and {jobs_files}. Empty for no title.", "default": "" }, + "title": { + "type": "string", + "deprecated": true, + "description": "DEPRECATED: Replace key with 'subject'." + }, "token": { "type": "string", "description": "Gotify application token.", @@ -436,8 +461,11 @@ "title": "IFTTT reporter", "description": "Send the text report as an IFTTT event via the Maker Webhooks service.", "type": "object", - "required": ["enabled", "key", "event"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the IFTTT reporter.", @@ -459,8 +487,11 @@ "title": "Mailgun reporter", "description": "Send the text report via email using the Mailgun service.", "type": "object", - "required": ["enabled", "region", "api_key", "domain", "from_mail", "from_name", "to", "subject"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Mailgun reporter.", @@ -507,8 +538,11 @@ "title": "Matrix reporter", "description": "Send the report to a room using the Matrix protocol.", "type": "object", - "required": ["enabled", "homeserver", "access_token", "room_id"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Matrix reporter.", @@ -563,8 +597,11 @@ "title": "ntfy reporter", "description": "Send the text report to an ntfy server (https://ntfy.sh).", "type": "object", - "required": ["enabled", "topic_url", "authorization", "priorities"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the ntfy reporter.", @@ -587,8 +624,11 @@ "title": "Prowl reporter", "description": "Send the text report via Prowl (iOS push notifications).", "type": "object", - "required": ["enabled", "api_key", "priority", "application", "subject"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Prowl reporter.", @@ -620,8 +660,11 @@ "title": "Pushbullet reporter", "description": "Send the text report via Pushbullet. Note: the underlying 'pushbullet' Python package has not been maintained since 2022.", "type": "object", - "required": ["enabled", "api_key"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Pushbullet reporter.", @@ -638,8 +681,11 @@ "title": "Pushover reporter", "description": "Send the text report via Pushover (https://pushover.net).", "type": "object", - "required": ["enabled", "app", "device", "sound", "user", "priority"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Pushover reporter.", @@ -677,8 +723,11 @@ "title": "run_command reporter", "description": "Run a local command receiving the text report. Substitutes {count}, {jobs}, and {text} in the command string and exposes the WEBCHANGES_REPORT_CONFIG_JSON and WEBCHANGES_CHANGED_JOBS_JSON environment variables.", "type": "object", - "required": ["enabled", "command"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the run_command reporter.", @@ -695,8 +744,11 @@ "title": "Telegram reporter", "description": "Send the Markdown report via Telegram using the Bot API.", "type": "object", - "required": ["enabled", "bot_token", "chat_id", "silent"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the Telegram reporter.", @@ -732,8 +784,11 @@ "title": "Webhook reporter", "description": "Send the report to a webhook endpoint (e.g. Slack, Mattermost).", "type": "object", - "required": ["enabled", "markdown", "webhook_url", "rich_text", "max_message_length"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the webhook reporter.", @@ -765,8 +820,11 @@ "title": "XMPP reporter", "description": "Send the text report via the XMPP protocol. Use a dedicated XMPP account, never a primary one.", "type": "object", - "required": ["enabled", "sender", "recipient", "insecure_password"], "properties": { + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Restrict this reporter to the jobs having any of these tags in their 'tags' directive. If omitted, the reporter reports on every job." + }, "enabled": { "type": "boolean", "description": "Enable the XMPP reporter.", @@ -825,49 +883,159 @@ "text": { "$ref": "#/$defs/_ConfigReportText" }, "html": { "$ref": "#/$defs/_ConfigReportHtml" }, "markdown": { "$ref": "#/$defs/_ConfigReportMarkdown" }, - "stdout": { "$ref": "#/$defs/_ConfigReportStdout" }, - "browser": { "$ref": "#/$defs/_ConfigReportBrowser" }, - "discord": { "$ref": "#/$defs/_ConfigReportDiscord" }, - "email": { "$ref": "#/$defs/_ConfigReportEmail" }, - "github_issue": { "$ref": "#/$defs/_ConfigReportGithubIssue" }, - "gotify": { "$ref": "#/$defs/_ConfigReportGotify" }, - "ifttt": { "$ref": "#/$defs/_ConfigReportIfttt" }, - "mailgun": { "$ref": "#/$defs/_ConfigReportMailgun" }, - "matrix": { "$ref": "#/$defs/_ConfigReportMatrix" }, - "ntfy": { "$ref": "#/$defs/_ConfigReportNtfy" }, - "prowl": { "$ref": "#/$defs/_ConfigReportProwl" }, - "pushbullet": { "$ref": "#/$defs/_ConfigReportPushbullet" }, - "pushover": { "$ref": "#/$defs/_ConfigReportPushover" }, - "run_command": { "$ref": "#/$defs/_ConfigReportRunCommand" }, - "telegram": { "$ref": "#/$defs/_ConfigReportTelegram" }, - "webhook": { "$ref": "#/$defs/_ConfigReportWebhook" }, - "xmpp": { "$ref": "#/$defs/_ConfigReportXmpp" } + "stdout": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportStdout" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportStdout" }, "minItems": 1 } + ] + }, + "browser": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportBrowser" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportBrowser" }, "minItems": 1 } + ] + }, + "discord": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportDiscord" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportDiscord" }, "minItems": 1 } + ] + }, + "email": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportEmail" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportEmail" }, "minItems": 1 } + ] + }, + "github_issue": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportGithubIssue" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportGithubIssue" }, "minItems": 1 } + ] + }, + "gotify": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportGotify" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportGotify" }, "minItems": 1 } + ] + }, + "ifttt": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportIfttt" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportIfttt" }, "minItems": 1 } + ] + }, + "mailgun": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportMailgun" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportMailgun" }, "minItems": 1 } + ] + }, + "matrix": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportMatrix" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportMatrix" }, "minItems": 1 } + ] + }, + "ntfy": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportNtfy" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportNtfy" }, "minItems": 1 } + ] + }, + "prowl": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportProwl" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportProwl" }, "minItems": 1 } + ] + }, + "pushbullet": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportPushbullet" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportPushbullet" }, "minItems": 1 } + ] + }, + "pushover": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportPushover" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportPushover" }, "minItems": 1 } + ] + }, + "run_command": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportRunCommand" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportRunCommand" }, "minItems": 1 } + ] + }, + "telegram": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportTelegram" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportTelegram" }, "minItems": 1 } + ] + }, + "webhook": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportWebhook" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportWebhook" }, "minItems": 1 } + ] + }, + "xmpp": { + "anyOf": [ + { "$ref": "#/$defs/_ConfigReportXmpp" }, + { "type": "array", "items": { "$ref": "#/$defs/_ConfigReportXmpp" }, "minItems": 1 } + ] + } } }, "_ConfigJobDefaults": { "title": "Job defaults", - "description": "Default directives applied to jobs by kind. All keys are optional; any directive set under 'url', 'browser', or 'command' overrides the same directive set under 'all' (the 'headers' directive merges header-by-header).", + "description": "Default directives applied to jobs by kind. All keys are optional; any directive set under 'url_jobs', 'browser_jobs', or 'command_jobs' overrides the same directive set under 'all_jobs' (the 'headers' directive merges header-by-header).", "type": "object", "properties": { "_note": { "type": "string", "description": "Free-form note (ignored by webchanges; keys starting with '_' are not interpreted)." }, - "all": { + "all_jobs": { "type": "object", "description": "Directives applied to every job, including those defined in hooks.py." }, - "url": { + "url_jobs": { "type": "object", "description": "Directives applied to 'url' jobs without 'use_browser'." }, - "browser": { + "browser_jobs": { "type": "object", "description": "Directives applied to 'url' jobs with 'use_browser: true'." }, - "command": { + "command_jobs": { "type": "object", "description": "Directives applied to 'command' jobs." + }, + "all": { + "type": "object", + "description": "Deprecated; renamed 'all_jobs'.", + "deprecated": true + }, + "url": { + "type": "object", + "description": "Deprecated; renamed 'url_jobs'.", + "deprecated": true + }, + "browser": { + "type": "object", + "description": "Deprecated; renamed 'browser_jobs'.", + "deprecated": true + }, + "command": { + "type": "object", + "description": "Deprecated; renamed 'command_jobs'.", + "deprecated": true + }, + "shell": { + "type": "object", + "description": "Deprecated; renamed 'command_jobs'.", + "deprecated": true } } }, @@ -928,6 +1096,19 @@ "default": 4 } } + }, + "_ConfigVersionCheck": { + "title": "Version Check", + "description": "Settings for the once-a-day check against PyPi for a newer release of webchanges.", + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether to query PyPi for a newer release. The result is cached on disk for 24 hours, so this hits the network at most once per day. Set to false to disable the check entirely (no network call, no upgrade nudge in reports).", + "default": true + } + } } } -} +} \ No newline at end of file diff --git a/data/config.yaml b/data/config.yaml index 8cd5f53..b093b88 100644 --- a/data/config.yaml +++ b/data/config.yaml @@ -1,5 +1,5 @@ # webchanges configuration file. See https://webchanges.readthedocs.io/en/stable/configuration.html -# Originally written on 2026-08-02T18:51:58+00:00Z by version 3.37.0. +# Originally written on 2026-10-25T15:52:52+00:00Z by version 3.38.0. # yaml-language-server: $schema=config.schema.json display: @@ -70,7 +70,7 @@ report: enabled: false priority: 0 server_url: '' - title: '' + subject: '' token: '' ifttt: enabled: false @@ -136,13 +136,13 @@ report: insecure_password: '' job_defaults: _note: Default directives that are applied to jobs. - all: + all_jobs: _note: These are used for all type of jobs, including those in hooks.py. - url: + url_jobs: _note: These are used for 'url' jobs without 'use_browser'. - browser: + browser_jobs: _note: 'These are used for ''url'' jobs with ''use_browser: true''.' - command: + command_jobs: _note: These are used for 'command' jobs. differ_defaults: _note: Default directives that are applied to individual differs. @@ -158,4 +158,6 @@ database: max_snapshots: 4 footnote: null version_check: - enabled: true \ No newline at end of file + enabled: true +jobs: null +hooks: null \ No newline at end of file diff --git a/data/jobs.schema.json b/data/jobs.schema.json index da9a6b8..8fb48b8 100644 --- a/data/jobs.schema.json +++ b/data/jobs.schema.json @@ -1,7 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/mborsetti/webchanges/refs/heads/main/webchanges/_resources/jobs.schema.json", "title": "Webchanges Jobs Configuration", - "description": "JSON Schema for each job in Webchanges' jobs.yaml configuration file", + "description": "JSON Schema for each job in webchanges' jobs.yaml configuration file", "type": "object", "oneOf": [{ "required": ["url"] }, { "required": ["command"] }], "properties": { @@ -26,6 +27,10 @@ "type": "string", "description": "The URL or text to display in reports instead of the actual checked URL." }, + "tags": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "One tag, or a list of tags, used to select the job with the --tags command line argument and/or the 'tags' directive of a reporter." + }, "kind": { "type": "string", "pattern": "^(url|browser|command|hooks_.+)$", @@ -45,10 +50,16 @@ "type": "boolean", "description": "If true, sends data as JSON and sets Content-Type to application/json." }, + "json": { + "type": "boolean", + "description": "Deprecated: use 'data_as_json' instead.", + "deprecated": true, + "deprecationMessage": "Use 'data_as_json' instead of 'json'." + }, "headers": { "type": "object", - "additionalProperties": { "type": "string" }, - "description": "Custom HTTP headers." + "additionalProperties": { "type": ["string", "null"] }, + "description": "Custom HTTP headers, merged header by header with the defaults set in the configuration's job_defaults; a header set to null is not sent, even if set in job_defaults." }, "encoding": { "type": "string", @@ -58,10 +69,19 @@ "type": ["integer", "number"], "description": "Request timeout in seconds." }, + "same_site_delay": { + "type": ["integer", "number"], + "description": "Number of seconds to wait before retrieving the URL when an earlier job in the list already targets the same site (network location). The first (or only) job for a given site is never delayed. Use it to stagger jobs that would otherwise hit the same site at the same instant and get rate-limited or blocked." + }, "ignore_connection_errors": { "type": "boolean", "description": "If true, connection errors are ignored (no report sent)." }, + "empty_as_error": { + "oneOf": [{ "type": "boolean" }, { "type": "string", "enum": ["ignore"] }], + "description": "If true, an empty (or whitespace-only) response or command output, or one from which the filters leave nothing, is treated as an error (HTTP status code 999) instead of as valid content, preserving the last non-empty snapshot. Works in all job types; with use_browser, an empty response means the server returned an empty document (the rendered page is never empty). Combine with max_tries to be notified only of persistent empty data. If 'ignore', the error is never reported: the last non-empty snapshot is silently kept.", + "default": false + }, "no_redirects": { "type": "boolean", "description": "If true, do not follow HTTP redirects." @@ -73,12 +93,14 @@ }, "retries": { "type": "integer", - "description": "Number of retries for failed requests." + "minimum": 0, + "description": "Number of times to retry a request that fails with a connection error (e.g. a connection reset), a timeout, or HTTP status code 429, 500, 502, 503 or 504, before reporting the error. Waits the time requested by the server's Retry-After header (up to 60 seconds) or else 1, 2, 4... seconds (up to 30) between attempts. url jobs without use_browser only.", + "default": 0 }, "cookies": { "type": "object", - "additionalProperties": { "type": "string" }, - "description": "Cookies to send with the request." + "additionalProperties": { "type": ["string", "null"] }, + "description": "Cookies to send with the request, merged cookie by cookie with the defaults set in the configuration's job_defaults; a cookie set to null is not sent, even if set in job_defaults." }, "ignore_cached": { "type": "boolean", @@ -124,9 +146,27 @@ "description": "Browser TLS fingerprint to impersonate when http_client is curl_cffi (e.g. 'chrome', 'chrome124', 'safari17_0', 'firefox133'). Defaults to 'chrome'. Ignored by other HTTP clients.", "default": "chrome" }, + "http_version": { + "type": "string", + "enum": ["v1", "v2", "v2tls", "v2_prior_knowledge", "v3", "v3only"], + "description": "HTTP protocol version: v1 (HTTP/1.1), v2 (HTTP/2; with httpx requires the h2 package), and, with http_client: curl_cffi only, v2tls, v2_prior_knowledge, v3 (HTTP/3 with fallback) and v3only. Not supported by http_client: requests." + }, + "fingerprints": { + "type": "object", + "properties": { + "ja3": { "type": "string", "description": "JA3 TLS fingerprint string." }, + "akamai": { "type": "string", "description": "Akamai HTTP/2 fingerprint string." }, + "extra_fp": { + "type": "object", + "description": "Additional fingerprint overrides (e.g. tls_min_version, tls_grease, http2_stream_weight)." + } + }, + "additionalProperties": false, + "description": "Low-level TLS/HTTP fingerprint overrides for http_client: curl_cffi (ignored, with a warning, by other HTTP clients)." + }, "ignore_dh_key_too_small": { "type": "boolean", - "description": "If true, works around weak DH key errors.", + "description": "If true, accepts a server's weak (1024-bit) Diffie-Hellman key by lowering the OpenSSL security level (certificates are still verified). Supported by http_client httpx and requests, not by curl_cffi.", "default": false }, "params": { @@ -143,6 +183,18 @@ "type": "string", "description": "Proxy URL to use for the request." }, + "http_proxy": { + "type": "string", + "description": "Deprecated: use 'proxy' instead.", + "deprecated": true, + "deprecationMessage": "Use 'proxy' instead of 'http_proxy'." + }, + "https_proxy": { + "type": "string", + "description": "Deprecated: use 'proxy' instead.", + "deprecated": true, + "deprecationMessage": "Use 'proxy' instead of 'https_proxy'." + }, "use_browser": { "oneOf": [{ "type": "boolean" }, { "type": "string", "pattern": "^(chrome|msedge|firefox$|webkit$)" }], "description": "If true, uses a headless browser (Playwright) via the 'chrome' channel. May also be a browser name: 'firefox', 'webkit', or a value starting with 'chrome' or 'msedge' (e.g. 'chrome-beta', 'msedge-dev').", @@ -170,6 +222,11 @@ }, "description": "Resource types to block from loading (browser jobs)." }, + "connect_over_cdp": { + "oneOf": [{ "type": "boolean" }, { "type": "string", "format": "uri" }], + "description": "Attach Playwright to an already-running Chromium browser via the Chrome DevTools Protocol (browser jobs). 'true' uses the default endpoint ws://127.0.0.1:58489/devtools/browser; a string is used as the CDP WebSocket URL.", + "default": false + }, "evaluate": { "type": "string", "description": "JavaScript code to execute in the page context (browser jobs)." @@ -180,7 +237,7 @@ }, "ignore_default_args": { "oneOf": [{ "type": "boolean" }, { "type": "array", "items": { "type": "string" } }], - "description": "Override Chrome default arguments (browser jobs).", + "description": "Override browser default arguments (browser jobs).", "default": false }, "ignore_https_errors": { @@ -207,11 +264,11 @@ "switches": { "type": "array", "items": { "type": "string" }, - "description": "Additional Chrome command-line switches (browser jobs)." + "description": "Additional browser command-line switches (browser jobs)." }, "user_data_dir": { "type": "string", - "description": "Path to a Chrome user data directory (for persistence/auth with use_browser)." + "description": "Path to a browser user data directory (for persistence/auth with use_browser)." }, "wait_for_function": { "oneOf": [ @@ -300,6 +357,35 @@ "deprecated": true, "deprecationMessage": "Use 'diff_filters' instead of 'diff_filter'." }, + "diff_tool": { + "type": "string", + "description": "Deprecated: use 'differ' with name 'command' instead.", + "deprecated": true, + "deprecationMessage": "Use 'differ: {name: command, command: ...}' instead of 'diff_tool'." + }, + "navigate": { + "type": "string", + "description": "Deprecated: use 'url' with 'use_browser: true' instead.", + "deprecated": true, + "deprecationMessage": "Use 'url' with 'use_browser: true' instead of 'navigate'." + }, + "wait_for": { + "type": ["integer", "string"], + "description": "No longer supported (Pyppeteer): use 'wait_for_function', 'wait_for_selector' or 'wait_for_timeout' instead.", + "deprecated": true, + "deprecationMessage": "Use 'wait_for_function', 'wait_for_selector' or 'wait_for_timeout' instead of 'wait_for'." + }, + "wait_for_navigation": { + "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }], + "description": "Deprecated (Pyppeteer): use 'wait_for_url' instead.", + "deprecated": true, + "deprecationMessage": "Use 'wait_for_url' instead of 'wait_for_navigation'." + }, + "stderr": { + "type": "string", + "description": "Ignored; accepted for compatibility with urlwatch (command jobs).", + "deprecated": true + }, "differ": { "oneOf": [ { @@ -444,6 +530,27 @@ "type": "boolean", "description": "Use compact output format.", "default": false + }, + "cutoff_distance_for_pairs": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "With ignore_order, two changed items are paired and compared in depth only if their deep distance is below this value; otherwise they are reported as one item removed plus one added (deepdiff library's own default is 0.3).", + "default": 1.0 + }, + "cutoff_intersection_for_pairs": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "With ignore_order, pairing of changed items is attempted only if the intersection distance of the two iterables is below this value; otherwise each changed item is reported in its entirety (deepdiff library's own default is 0.7).", + "default": 1.0 + }, + "threshold_to_diff_deeper": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "The fraction of keys two dictionaries must share for them to be compared key by key instead of being reported as one whole new value (deepdiff library's own default is 0.33).", + "default": 0 } }, "required": ["name"], @@ -603,6 +710,21 @@ "description": "If true, treats the data as Markdown for rendering in reports.", "default": false }, + "is_rst": { + "type": "boolean", + "description": "If true, treats the data as reStructuredText for rendering in reports.", + "default": false + }, + "markdown_extras": { + "type": "object", + "description": "markdown2 library 'extras' used when rendering Markdown data as HTML, as name: options (null if it takes none); see https://github.com/trentm/python-markdown2/wiki/Extras. Applied over the defaults strike, target-blank-links and middle-word-em: false; e.g. middle-word-em: true restores emphasis within words.", + "default": { "strike": null, "target-blank-links": null, "middle-word-em": false } + }, + "markdown_padded_tables": { + "type": "boolean", + "description": "Show the rows of Markdown tables in a monospace font in HTML reports, so that the columns of tables padded with spaces line up. Set automatically by the html2text filter's pad_tables sub-directive.", + "default": false + }, "monospace": { "type": "boolean", "description": "If true, uses monospace font in reports.", @@ -627,38 +749,15 @@ "description": "Number of versions to keep/compare." }, "hook_data": { - "description": "Any custom data to be passed to a custom job type defined in hooks.py." + "description": "Any data (e.g. a mapping of names to values) for the code in hooks.py: a custom job class reads it as self.hook_data, and a custom filter, differ or reporter as self.job.hook_data. Not used by webchanges itself." } }, "additionalProperties": false, - "if": { - "properties": { - "kind": { "pattern": "^hooks_.+" } - }, - "required": ["kind"] - }, - "else": { - "not": { - "required": ["hook_data"] - } - }, "definitions": { "filterList": { - "oneOf": [ - { - "type": "string", - "description": "A single filter name (e.g. 'html2text', 'sha1sum')." - }, - { - "type": "array", - "items": { "$ref": "#/definitions/filterItem" }, - "description": "List of filters to apply in order." - }, - { - "$ref": "#/definitions/filterItemObject", - "description": "A single filter as an object." - } - ] + "type": "array", + "items": { "$ref": "#/definitions/filterItem" }, + "description": "List of filters to apply in order." }, "filterItem": { "oneOf": [ @@ -675,12 +774,20 @@ "html2text", "ical2text", "jsontoyaml", + "ocr", + "pdf2text", "pretty-xml", + "pypdf", + "remove-duplicate-lines", "remove_repeated", "reverse", "sha1sum", + "sha256sum", "sort", - "strip" + "strip", + "striplines", + "xmltojson", + "xmltoyaml" ], "description": "A filter name with no parameters (uses defaults)." }, @@ -716,6 +823,7 @@ "beautify": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": ["integer", "string"], "description": "Shorthand for the 'indent' sub-directive." }, { "type": "object", "properties": { @@ -735,6 +843,21 @@ ], "description": "Beautifies HTML using BeautifulSoup." }, + "between": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Regular expression matching the line that opens the block (this line is not kept)." + }, + "end": { + "type": "string", + "description": "Regular expression matching the line that closes the block (this line is not kept)." + } + }, + "additionalProperties": false, + "description": "Extracts the block of lines between a starting and an ending regular expression." + }, "css": { "oneOf": [ { "type": "string", "description": "CSS selector expression." }, @@ -775,31 +898,36 @@ "default": false } }, - "required": ["selector"], + "anyOf": [{ "required": ["selector"] }, { "required": ["exclude"] }], "additionalProperties": false } ], "description": "Selects HTML/XML elements matching a CSS selector." }, "csv2text": { - "type": "object", - "properties": { - "format_message": { - "type": "string", - "description": "Python format string with replacement fields for each column." - }, - "has_header": { - "type": "boolean", - "description": "Whether the CSV has a header row (auto-detected if not specified)." - }, - "ignore_header": { - "type": "boolean", - "description": "If true, skip the header row in output.", - "default": false + "oneOf": [ + { "type": "string", "description": "Shorthand for the 'format_message' sub-directive." }, + { + "type": "object", + "properties": { + "format_message": { + "type": "string", + "description": "Python format string with replacement fields for each column." + }, + "has_header": { + "type": "boolean", + "description": "Whether the CSV has a header row (auto-detected if not specified)." + }, + "ignore_header": { + "type": "boolean", + "description": "If true, skip the header row in output.", + "default": false + } + }, + "required": ["format_message"], + "additionalProperties": false } - }, - "required": ["format_message"], - "additionalProperties": false, + ], "description": "Converts CSV data to text using a format string." }, "delete_lines_containing": { @@ -823,20 +951,72 @@ "description": "Removes lines containing the specified text or matching a regex pattern." }, "element-by-class": { - "type": "string", - "description": "Selects HTML elements matching the specified class name." + "oneOf": [ + { "type": "string", "description": "Shorthand for the 'class' sub-directive." }, + { + "type": "object", + "properties": { + "class": { + "type": "string", + "description": "The class to match." + } + }, + "required": ["class"], + "additionalProperties": false + } + ], + "description": "Selects all the HTML elements whose class is the one specified." }, "element-by-id": { - "type": "string", - "description": "Selects the HTML element with the specified id." + "oneOf": [ + { "type": "string", "description": "Shorthand for the 'id' sub-directive." }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The id to match." + } + }, + "required": ["id"], + "additionalProperties": false + } + ], + "description": "Selects all the HTML elements whose id is the one specified." }, "element-by-style": { - "type": "string", - "description": "Selects HTML elements matching the specified style attribute value." + "oneOf": [ + { "type": "string", "description": "Shorthand for the 'style' sub-directive." }, + { + "type": "object", + "properties": { + "style": { + "type": "string", + "description": "The style attribute value to match." + } + }, + "required": ["style"], + "additionalProperties": false + } + ], + "description": "Selects all the HTML elements whose style attribute value is the one specified." }, "element-by-tag": { - "type": "string", - "description": "Selects HTML elements matching the specified tag name." + "oneOf": [ + { "type": "string", "description": "Shorthand for the 'tag' sub-directive." }, + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "description": "The tag name to match." + } + }, + "required": ["tag"], + "additionalProperties": false + } + ], + "description": "Selects all the HTML elements whose tag name is the one specified." }, "execute": { "oneOf": [ @@ -863,6 +1043,7 @@ "format-json": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": ["integer", "string"], "description": "Shorthand for the 'indentation' sub-directive." }, { "type": "object", "properties": { @@ -875,6 +1056,11 @@ "type": "boolean", "description": "Sort object keys alphabetically.", "default": false + }, + "compact": { + "type": "boolean", + "description": "Remove all unnecessary whitespace (overrides indentation).", + "default": false } }, "additionalProperties": false @@ -889,44 +1075,38 @@ }, "grep": { "oneOf": [ - { "type": "string", "description": "Literal text to match." }, + { "type": "string", "description": "Shorthand for the 're' sub-directive." }, { "type": "object", "properties": { - "text": { - "type": "string", - "description": "Literal text to match." - }, "re": { "type": "string", "description": "Regular expression pattern to match." } }, + "required": ["re"], "additionalProperties": false } ], - "description": "Deprecated: use keep_lines_containing instead.", + "description": "Deprecated: use keep_lines_containing with the 're' sub-directive instead.", "deprecated": true }, "grepi": { "oneOf": [ - { "type": "string", "description": "Literal text to match." }, + { "type": "string", "description": "Shorthand for the 're' sub-directive." }, { "type": "object", "properties": { - "text": { - "type": "string", - "description": "Literal text to match." - }, "re": { "type": "string", "description": "Regular expression pattern to match." } }, + "required": ["re"], "additionalProperties": false } ], - "description": "Deprecated: use delete_lines_containing instead.", + "description": "Deprecated: use delete_lines_containing with the 're' sub-directive instead.", "deprecated": true }, "hexdump": { @@ -937,6 +1117,7 @@ "html2text": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": "string", "description": "Shorthand for the 'method' sub-directive." }, { "type": "object", "properties": { @@ -1027,18 +1208,37 @@ ], "description": "Converts HTML to plain text." }, + "striplines": { + "type": ["boolean", "null"], + "description": "Deprecated: use strip with 'splitlines: true' instead.", + "deprecated": true + }, "ical2text": { "type": ["boolean", "null"], "description": "Converts iCalendar data to text.", "default": true }, "jq": { - "type": "string", + "oneOf": [ + { "type": "string", "description": "Shorthand for the 'query' sub-directive." }, + { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "jq filter expression." + } + }, + "required": ["query"], + "additionalProperties": false + } + ], "description": "Applies a jq filter expression to JSON data." }, "jsontoyaml": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": ["integer", "string"], "description": "Shorthand for the 'indentation' sub-directive." }, { "type": "object", "properties": { @@ -1098,6 +1298,7 @@ "pdf2text": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": "string", "description": "Shorthand for the 'password' sub-directive." }, { "type": "object", "properties": { @@ -1122,13 +1323,27 @@ "description": "Extracts text from PDF using pdftotext." }, "pretty-xml": { - "type": ["boolean", "null"], - "description": "Pretty-prints XML using Python xml.dom.minidom.", - "default": true + "oneOf": [ + { "type": ["boolean", "null"] }, + { "type": "integer", "description": "Shorthand for the 'indentation' sub-directive." }, + { + "type": "object", + "properties": { + "indentation": { + "type": "integer", + "description": "Number of spaces for indentation.", + "default": 2 + } + }, + "additionalProperties": false + } + ], + "description": "Pretty-prints XML using Python xml.dom.minidom." }, "pypdf": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": "string", "description": "Shorthand for the 'password' sub-directive." }, { "type": "object", "properties": { @@ -1191,9 +1406,28 @@ ], "description": "Replaces text matching a regex pattern." }, + "remove-duplicate-lines": { + "oneOf": [ + { "type": ["boolean", "null"] }, + { "type": "string", "description": "Shorthand for the 'separator' sub-directive." }, + { + "type": "object", + "properties": { + "separator": { + "type": "string", + "description": "Item separator (default: newline)." + } + }, + "additionalProperties": false + } + ], + "description": "Deprecated: use remove_repeated with 'adjacent: false' instead.", + "deprecated": true + }, "remove_repeated": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": "string", "description": "Shorthand for the 'separator' sub-directive." }, { "type": "object", "properties": { @@ -1221,6 +1455,7 @@ "reverse": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": "string", "description": "Shorthand for the 'separator' sub-directive." }, { "type": "object", "properties": { @@ -1240,6 +1475,11 @@ "description": "Computes the SHA-1 hash of the content.", "default": true }, + "sha256sum": { + "type": ["boolean", "null"], + "description": "Computes the SHA-256 hash of the content.", + "default": true + }, "shellpipe": { "oneOf": [ { "type": "string", "description": "Shell command to execute." }, @@ -1265,6 +1505,7 @@ "sort": { "oneOf": [ { "type": ["boolean", "null"] }, + { "type": "string", "description": "Shorthand for the 'separator' sub-directive." }, { "type": "object", "properties": { @@ -1311,6 +1552,52 @@ ], "description": "Strips leading/trailing whitespace or specified characters." }, + "xmltojson": { + "oneOf": [ + { "type": ["boolean", "null"] }, + { "type": "integer", "description": "Shorthand for the 'indentation' sub-directive." }, + { + "type": "object", + "properties": { + "indentation": { + "type": "integer", + "description": "Number of spaces for indentation.", + "default": 4 + }, + "sort_keys": { + "type": "boolean", + "description": "Sort object keys alphabetically.", + "default": false + }, + "compact": { + "type": "boolean", + "description": "Remove all unnecessary whitespace (overrides indentation).", + "default": false + } + }, + "additionalProperties": false + } + ], + "description": "Converts XML to JSON (attributes become '@' keys and text '#text'). Requires the xmltodict package." + }, + "xmltoyaml": { + "oneOf": [ + { "type": ["boolean", "null"] }, + { "type": "integer", "description": "Shorthand for the 'indentation' sub-directive." }, + { + "type": "object", + "properties": { + "indentation": { + "type": "integer", + "description": "Number of spaces for indentation.", + "default": 2 + } + }, + "additionalProperties": false + } + ], + "description": "Converts XML to YAML (attributes become '@' keys and text '#text'). Requires the xmltodict package." + }, "xpath": { "oneOf": [ { "type": "string", "description": "XPath 1.0 expression." }, @@ -1351,7 +1638,7 @@ "default": false } }, - "required": ["path"], + "anyOf": [{ "required": ["path"] }, { "required": ["exclude"] }], "additionalProperties": false } ], @@ -1360,4 +1647,4 @@ } } } -} +} \ No newline at end of file