Privacy model added, plus some helper scripts
This commit is contained in:
parent
9b9c182dc7
commit
79a207a6f2
|
|
@ -0,0 +1,20 @@
|
||||||
|
ca4806cd3e7827b9666e6929cbcae8f3e2eba87e499f7dd7008d8edf2fcc774e staging/calendar-plugin/calendar-plugin.php
|
||||||
|
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb staging/calendar-plugin/src/Contracts/AuthAdapterInterface.php
|
||||||
|
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d staging/calendar-plugin/src/Contracts/DatabaseAdapterInterface.php
|
||||||
|
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba staging/calendar-plugin/src/Contracts/HttpAdapterInterface.php
|
||||||
|
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 staging/calendar-plugin/src/Contracts/OptionsAdapterInterface.php
|
||||||
|
47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8 staging/calendar-plugin/src/Domain/CalDavService.php
|
||||||
|
4dc3337761c97aa550896fcc377aaab8338c598f39e489742dacbfd20e1a71b1 staging/calendar-plugin/src/Domain/EventService.php
|
||||||
|
412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f staging/calendar-plugin/src/Domain/IcsService.php
|
||||||
|
5f7fb1f8c00136ad2c4a2dc8b909dabf6494a194b09aa73557a4bf68d73f4ed2 staging/calendar-plugin/src/Domain/RecurrenceExpander.php
|
||||||
|
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 staging/calendar-plugin/src/Domain/SettingsService.php
|
||||||
|
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f staging/calendar-plugin/src/Domain/UserService.php
|
||||||
|
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b staging/calendar-plugin/src/Infrastructure/ServiceContainer.php
|
||||||
|
70334df8ca06c8fe61d81f24cb0d8f19285180f9950054bd86af0083adf8b4c8 staging/calendar-plugin/src/Infrastructure/WordPress/MigrationManager.php
|
||||||
|
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 staging/calendar-plugin/src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||||
|
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c staging/calendar-plugin/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||||
|
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b staging/calendar-plugin/src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||||
|
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea staging/calendar-plugin/src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||||
|
ffc7c3eef8f3ed0873b5f614765219925d07c24ffd6ffa7422a3d862a7342450 staging/calendar-plugin/src/Plugin.php
|
||||||
|
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 staging/calendar-plugin/src/bootstrap.php
|
||||||
|
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c staging/calendar-plugin/uninstall.php
|
||||||
Binary file not shown.
|
|
@ -36,7 +36,7 @@ This document covers:
|
||||||
### Create Event
|
### Create Event
|
||||||
- `POST /events`
|
- `POST /events`
|
||||||
- Requires calendar write capability.
|
- Requires calendar write capability.
|
||||||
- Request body supports fields in `requirements/editor.md`.
|
- Request body supports fields in `requirements/editor.md`, including `visibility` (`public`/`private`).
|
||||||
- Response: `201` with created event payload and identifiers.
|
- Response: `201` with created event payload and identifiers.
|
||||||
|
|
||||||
### List Events
|
### List Events
|
||||||
|
|
@ -70,6 +70,16 @@ This document covers:
|
||||||
- Must not split the underlying recurring series.
|
- Must not split the underlying recurring series.
|
||||||
- Response: `204` on success.
|
- Response: `204` on success.
|
||||||
|
|
||||||
|
|
||||||
|
## Public Read Endpoints and Privacy Redaction
|
||||||
|
|
||||||
|
Public-read endpoints (for example `/public/events`, `/public/sidebar-events`, and ICS feed generation paths) must apply privacy masking for `private` events:
|
||||||
|
|
||||||
|
- Event date/time placement remains unchanged.
|
||||||
|
- Title is replaced with `Private Event`.
|
||||||
|
- Description/location/category and other non-time content are omitted from public responses.
|
||||||
|
- Logged-in write-capable UI and authenticated CalDAV/API reads continue to receive full event details.
|
||||||
|
|
||||||
## CalDAV User Workflow Endpoints
|
## CalDAV User Workflow Endpoints
|
||||||
|
|
||||||
### Register
|
### Register
|
||||||
|
|
@ -139,3 +149,4 @@ Acceptance should verify:
|
||||||
- Validation and error payloads are consistent.
|
- Validation and error payloads are consistent.
|
||||||
- Single-occurrence delete creates recurrence exception rather than split series.
|
- Single-occurrence delete creates recurrence exception rather than split series.
|
||||||
- Authz rules enforce role/access constraints.
|
- Authz rules enforce role/access constraints.
|
||||||
|
- Privacy redaction rules are enforced consistently across public API payloads and ICS output.
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,15 @@ This document covers:
|
||||||
|
|
||||||
### ICS Access
|
### ICS Access
|
||||||
- Public ICS endpoint visibility is governed by setup configuration.
|
- Public ICS endpoint visibility is governed by setup configuration.
|
||||||
- If endpoint is public: `public` may read only approved public events.
|
- If endpoint is public: `public` may read public events and redacted private events.
|
||||||
- If endpoint requires auth: enforce configured auth policy consistently.
|
- If endpoint requires auth: enforce configured auth policy consistently.
|
||||||
|
|
||||||
|
### Privacy Visibility Rules
|
||||||
|
- `public` viewers (not logged in) see full details for public events.
|
||||||
|
- `public` viewers see private events in normal date/time positions with title `Private Event` and no other event content.
|
||||||
|
- Logged-in write-capable users and authenticated CalDAV users may view full details for both public and private events.
|
||||||
|
- Privacy masking applies consistently across public calendar page views, sidebar, and public ICS output.
|
||||||
|
|
||||||
## Ownership and Scope Rules
|
## Ownership and Scope Rules
|
||||||
- CalDAV users operate against the single shared `public` calendar collection.
|
- CalDAV users operate against the single shared `public` calendar collection.
|
||||||
- Access control is enforced by account approval state (`pending_approval` vs `active`) rather than per-user private calendars.
|
- Access control is enforced by account approval state (`pending_approval` vs `active`) rather than per-user private calendars.
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ The endpoint must support these operations at minimum.
|
||||||
### Create and Update
|
### Create and Update
|
||||||
- `PUT`: create new event resource or replace an existing event resource.
|
- `PUT`: create new event resource or replace an existing event resource.
|
||||||
- `PUT` updates must preserve recurrence/exception semantics from iCalendar input.
|
- `PUT` updates must preserve recurrence/exception semantics from iCalendar input.
|
||||||
|
- CalDAV clients (including Thunderbird) must be able to read current event privacy and update it on edit (for example via `CLASS`).
|
||||||
- Write operations must require approved authenticated access.
|
- Write operations must require approved authenticated access.
|
||||||
|
|
||||||
### Delete
|
### Delete
|
||||||
|
|
@ -101,6 +102,12 @@ Minimum mapping expectations:
|
||||||
- Recurrence rules -> `RRULE`
|
- Recurrence rules -> `RRULE`
|
||||||
- Recurrence exceptions -> `EXDATE` and/or additional `VEVENT` with matching `UID` plus `RECURRENCE-ID`
|
- Recurrence exceptions -> `EXDATE` and/or additional `VEVENT` with matching `UID` plus `RECURRENCE-ID`
|
||||||
|
|
||||||
|
Privacy visibility mapping:
|
||||||
|
|
||||||
|
- Event visibility must round-trip through CalDAV/iCalendar semantics (for example `CLASS:PUBLIC` and `CLASS:PRIVATE` where supported).
|
||||||
|
- Authenticated CalDAV users must receive full event content for both public and private events.
|
||||||
|
- CalDAV feeds are not subject to the public redaction model used by anonymous web/ICS surfaces.
|
||||||
|
|
||||||
Recurrence behavior:
|
Recurrence behavior:
|
||||||
|
|
||||||
- Series-level recurrence remains a single logical event sequence keyed by `UID`.
|
- Series-level recurrence remains a single logical event sequence keyed by `UID`.
|
||||||
|
|
@ -188,3 +195,4 @@ Acceptance should verify:
|
||||||
- Sync collection reports provide incremental changes.
|
- Sync collection reports provide incremental changes.
|
||||||
- Recurring-event single-occurrence delete results in exception representation, not sequence split.
|
- Recurring-event single-occurrence delete results in exception representation, not sequence split.
|
||||||
- Returned iCalendar validates against RFC 5545 expectations and is accepted by target clients.
|
- Returned iCalendar validates against RFC 5545 expectations and is accepted by target clients.
|
||||||
|
- Privacy values round-trip through CalDAV edit flows (including Thunderbird) without losing event detail fidelity.
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ If a report is unsupported for a resource, server returns standards-appropriate
|
||||||
|
|
||||||
## Content Handling
|
## Content Handling
|
||||||
- Event objects use `text/calendar` payloads with RFC 5545-compatible `VCALENDAR`.
|
- Event objects use `text/calendar` payloads with RFC 5545-compatible `VCALENDAR`.
|
||||||
|
- Authenticated CalDAV object responses (`GET`, `REPORT`, `calendar-multiget`) return full event details for both public and private events.
|
||||||
- Unsupported component types should be rejected unless explicitly mapped.
|
- Unsupported component types should be rejected unless explicitly mapped.
|
||||||
- Server should normalize line endings/content as required by iCalendar compatibility.
|
- Server should normalize line endings/content as required by iCalendar compatibility.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ Requirements:
|
||||||
The stored model must support these fields (direct columns or normalized equivalents):
|
The stored model must support these fields (direct columns or normalized equivalents):
|
||||||
|
|
||||||
- Event identifier (internal primary key)
|
- Event identifier (internal primary key)
|
||||||
|
- `visibility` (`public` or `private`)
|
||||||
- `title`
|
- `title`
|
||||||
- `location`
|
- `location`
|
||||||
- `category`
|
- `category`
|
||||||
|
|
@ -55,6 +56,7 @@ The stored model must support these fields (direct columns or normalized equival
|
||||||
- Data model must support efficient retrieval by date range for day/week/month/year UI windows.
|
- Data model must support efficient retrieval by date range for day/week/month/year UI windows.
|
||||||
- Data model must support generation of recurrence occurrences for both web UI and ICS output.
|
- Data model must support generation of recurrence occurrences for both web UI and ICS output.
|
||||||
- Queries for public UI must return only intended public event data.
|
- Queries for public UI must return only intended public event data.
|
||||||
|
- Queries for public UI/ICS/sidebar must include private events with privacy redaction (time/date shown, non-time content masked).
|
||||||
|
|
||||||
## Local Testing Data Requirements
|
## Local Testing Data Requirements
|
||||||
The local compatibility-harness test environment must support deterministic data setup and teardown.
|
The local compatibility-harness test environment must support deterministic data setup and teardown.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ This document covers:
|
||||||
Required fields:
|
Required fields:
|
||||||
- `id` (PK)
|
- `id` (PK)
|
||||||
- `uid` (stable iCalendar UID)
|
- `uid` (stable iCalendar UID)
|
||||||
|
- `visibility` (`public`, `private`; default `public`)
|
||||||
- `title`
|
- `title`
|
||||||
- `description`
|
- `description`
|
||||||
- `location`
|
- `location`
|
||||||
|
|
@ -42,6 +43,11 @@ Constraints:
|
||||||
- `title`, `start_datetime` required
|
- `title`, `start_datetime` required
|
||||||
- `end_datetime >= start_datetime`
|
- `end_datetime >= start_datetime`
|
||||||
- recurrence fields internally consistent
|
- recurrence fields internally consistent
|
||||||
|
- `visibility` must be one of the supported enum values
|
||||||
|
|
||||||
|
Privacy semantics:
|
||||||
|
- `private` events are stored with full details.
|
||||||
|
- Public-facing renderers (public ICS/public calendar/sidebar) must apply redaction rules at read time, not by destructively altering stored event data.
|
||||||
|
|
||||||
### Recurrence Exceptions
|
### Recurrence Exceptions
|
||||||
Required fields:
|
Required fields:
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ Behavior requirements:
|
||||||
Supported fields:
|
Supported fields:
|
||||||
|
|
||||||
- `title` (required)
|
- `title` (required)
|
||||||
|
- `visibility` (`public` or `private`; default `public`)
|
||||||
- `description`
|
- `description`
|
||||||
- `location`
|
- `location`
|
||||||
- `category`
|
- `category`
|
||||||
|
|
@ -55,6 +56,7 @@ UI behavior requirements:
|
||||||
- Time controls hide when `all_day_event` is selected.
|
- Time controls hide when `all_day_event` is selected.
|
||||||
- Floating/persistent labels keep field purpose visible.
|
- Floating/persistent labels keep field purpose visible.
|
||||||
- Internal event ID is hidden from users.
|
- Internal event ID is hidden from users.
|
||||||
|
- Editor must provide a control to mark events as `public` or `private`.
|
||||||
|
|
||||||
## Recurrence Model
|
## Recurrence Model
|
||||||
Supported `repeat_type` values:
|
Supported `repeat_type` values:
|
||||||
|
|
@ -91,3 +93,4 @@ Acceptance should verify:
|
||||||
- Deleting one occurrence creates an exception while preserving the series.
|
- Deleting one occurrence creates an exception while preserving the series.
|
||||||
- Monthly ordinal patterns save and reload correctly.
|
- Monthly ordinal patterns save and reload correctly.
|
||||||
- Validation blocks invalid start/end ranges.
|
- Validation blocks invalid start/end ranges.
|
||||||
|
- Privacy visibility setting saves and reloads correctly.
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ Field mapping requirements:
|
||||||
- Plugin end date/time -> `DTEND`
|
- Plugin end date/time -> `DTEND`
|
||||||
- Plugin category -> `CATEGORIES`
|
- Plugin category -> `CATEGORIES`
|
||||||
- Stable unique event identifier -> `UID`
|
- Stable unique event identifier -> `UID`
|
||||||
|
- Plugin `visibility` controls public-output masking for private events
|
||||||
- Last update timestamp -> `DTSTAMP`
|
- Last update timestamp -> `DTSTAMP`
|
||||||
|
|
||||||
Additional mapping guidance:
|
Additional mapping guidance:
|
||||||
|
|
@ -57,6 +58,15 @@ Additional mapping guidance:
|
||||||
- Timed events must use `DTSTART;TZID=Europe/London` and `DTEND;TZID=Europe/London`.
|
- Timed events must use `DTSTART;TZID=Europe/London` and `DTEND;TZID=Europe/London`.
|
||||||
- `DTEND` must represent a valid end boundary and not precede `DTSTART`.
|
- `DTEND` must represent a valid end boundary and not precede `DTSTART`.
|
||||||
|
|
||||||
|
|
||||||
|
Privacy behavior:
|
||||||
|
|
||||||
|
- `public` events are exported with normal mapped content.
|
||||||
|
- `private` events remain present in the feed at normal date/time positions, but with redaction:
|
||||||
|
- `SUMMARY` must be `Private Event`
|
||||||
|
- `DTSTART`/`DTEND` remain populated
|
||||||
|
- `DESCRIPTION`, `LOCATION`, and `CATEGORIES` must be omitted from public ICS output
|
||||||
|
|
||||||
## Recurrence Mapping
|
## Recurrence Mapping
|
||||||
Recurring entries should be represented with `RRULE` where possible.
|
Recurring entries should be represented with `RRULE` where possible.
|
||||||
|
|
||||||
|
|
@ -84,7 +94,7 @@ If specific recurrence shapes cannot be represented in a single RRULE, plugin ma
|
||||||
- Generation errors should fail safely with observable error handling.
|
- Generation errors should fail safely with observable error handling.
|
||||||
|
|
||||||
## Security and Privacy
|
## Security and Privacy
|
||||||
- Export must include only events intended for public/user-facing distribution.
|
- Export must enforce visibility policy: private events are included with redacted content in public ICS output.
|
||||||
- Internal/admin-only metadata must not leak into ICS properties.
|
- Internal/admin-only metadata must not leak into ICS properties.
|
||||||
|
|
||||||
## Verification Requirements
|
## Verification Requirements
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,14 @@ This document covers:
|
||||||
|
|
||||||
## Access Policy
|
## Access Policy
|
||||||
- Access mode is configurable in `Setup`:
|
- Access mode is configurable in `Setup`:
|
||||||
- `public_read`: endpoint accessible without auth, only public events included
|
- `public_read`: endpoint accessible without auth; includes both public and private events, with private events redacted
|
||||||
- `authenticated_read`: endpoint requires authenticated access
|
- `authenticated_read`: endpoint requires authenticated access
|
||||||
- Default mode: `public_read` unless overridden by policy requirements.
|
- Default mode: `public_read` unless overridden by policy requirements.
|
||||||
|
|
||||||
## Data Scope
|
## Data Scope
|
||||||
- ICS output includes only events intended for the selected endpoint audience.
|
- ICS output includes only events intended for the selected endpoint audience.
|
||||||
- Internal/admin-only metadata must never appear in output.
|
- Internal/admin-only metadata must never appear in output.
|
||||||
|
- In `public_read` mode, private events remain in timeline position but expose only date/time and the title `Private Event`.
|
||||||
- Recurrence and exception semantics follow `requirements/ics.md` and `requirements/recurrence_exceptions.md`.
|
- Recurrence and exception semantics follow `requirements/ics.md` and `requirements/recurrence_exceptions.md`.
|
||||||
|
|
||||||
## Stability and Subscription
|
## Stability and Subscription
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,16 @@ Minimum display requirements:
|
||||||
- Category (if provided)
|
- Category (if provided)
|
||||||
- Description excerpt/summary (if configured for display)
|
- Description excerpt/summary (if configured for display)
|
||||||
|
|
||||||
|
Privacy display rules:
|
||||||
|
|
||||||
|
- Public events render full details per normal display rules.
|
||||||
|
- Private events remain visible in chronological position for all viewers.
|
||||||
|
- In public (not logged-in) UI and sidebar rendering, private events must display:
|
||||||
|
- Title as `Private Event`
|
||||||
|
- Date/time fields as normal
|
||||||
|
- No location/category/description content
|
||||||
|
- In logged-in write-capable UI, private events display full details.
|
||||||
|
|
||||||
## Web Event Editing UX (Authorized Users)
|
## Web Event Editing UX (Authorized Users)
|
||||||
Where the viewer has write permission, the web UI must support event creation/editing.
|
Where the viewer has write permission, the web UI must support event creation/editing.
|
||||||
|
|
||||||
|
|
@ -94,6 +104,7 @@ Requirements:
|
||||||
- Irrelevant recurrence fields must be hidden based on current recurrence selections.
|
- Irrelevant recurrence fields must be hidden based on current recurrence selections.
|
||||||
- Form controls use persistent inline/floating labels so field purpose remains visible after input.
|
- Form controls use persistent inline/floating labels so field purpose remains visible after input.
|
||||||
- Event ID must not be shown in the UI.
|
- Event ID must not be shown in the UI.
|
||||||
|
- Create/edit panel must show current privacy (`public`/`private`) and allow changing it before save.
|
||||||
- `location` and `category` should be rendered on one row.
|
- `location` and `category` should be rendered on one row.
|
||||||
- `start date/time` and `end date/time` should be rendered on one row.
|
- `start date/time` and `end date/time` should be rendered on one row.
|
||||||
- `repeat type` and `every` interval should be rendered on one row.
|
- `repeat type` and `every` interval should be rendered on one row.
|
||||||
|
|
@ -112,6 +123,7 @@ Requirements:
|
||||||
## Login and Access Modes
|
## Login and Access Modes
|
||||||
- `/calendar` must support two user modes: public (not logged in) and logged-in.
|
- `/calendar` must support two user modes: public (not logged in) and logged-in.
|
||||||
- Public mode is read-only.
|
- Public mode is read-only.
|
||||||
|
- In public mode, private events are redacted (title `Private Event`; time preserved; non-time fields hidden).
|
||||||
- A `Login` action must open an in-page dialog.
|
- A `Login` action must open an in-page dialog.
|
||||||
- Login dialog must support credential login.
|
- Login dialog must support credential login.
|
||||||
- Login dialog must support registration (registration is a request for approval/write access).
|
- Login dialog must support registration (registration is a request for approval/write access).
|
||||||
|
|
@ -149,3 +161,4 @@ Acceptance should verify:
|
||||||
- All five view modes are selectable and render expected event windows.
|
- All five view modes are selectable and render expected event windows.
|
||||||
- Empty-state behavior is clear and user-friendly.
|
- Empty-state behavior is clear and user-friendly.
|
||||||
- ICS link is present and returns valid calendar payload.
|
- ICS link is present and returns valid calendar payload.
|
||||||
|
- Privacy redaction behavior is correct in public views, sidebar, and logged-in views.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENV_FILE="${ROOT_DIR}/credentials/.env"
|
||||||
|
|
||||||
|
if [[ -f "${ENV_FILE}" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "${ENV_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARTIFACT=""
|
||||||
|
WP_ROOT="${REMOTE_WP_PATH:-/var/www/wordpress}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--artifact)
|
||||||
|
ARTIFACT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--wp-root)
|
||||||
|
WP_ROOT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
cat <<'USAGE'
|
||||||
|
Deploy a plugin artifact to remote WordPress.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/deploy_remote.sh [--artifact /abs/or/relative/path.zip] [--wp-root /var/www/wordpress]
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
- Artifact: latest ./package/calendar-plugin-*.zip
|
||||||
|
- Remote host settings from credentials/.env
|
||||||
|
|
||||||
|
This script enforces ownership:
|
||||||
|
- chown -R www-data:www-data <remote plugin dir>
|
||||||
|
USAGE
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[deploy] unknown argument: $1" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${ARTIFACT}" ]]; then
|
||||||
|
ARTIFACT="$(ls -1 "${ROOT_DIR}/package/calendar-plugin-"*.zip 2>/dev/null | sort -V | tail -n1 || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ARTIFACT}" ]]; then
|
||||||
|
echo "[deploy] no artifact found; run scripts/package_plugin.sh first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${ARTIFACT}" != /* ]]; then
|
||||||
|
ARTIFACT="${ROOT_DIR}/${ARTIFACT#./}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${ARTIFACT}" ]]; then
|
||||||
|
echo "[deploy] artifact not found: ${ARTIFACT}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANIFEST="${ARTIFACT%.zip}.manifest.sha256"
|
||||||
|
if [[ ! -f "${MANIFEST}" ]]; then
|
||||||
|
echo "[deploy] warning: manifest not found beside artifact (${MANIFEST}); continuing" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
required=(REMOTE_HOST REMOTE_PORT REMOTE_USER REMOTE_SSH_KEY_PATH REMOTE_APP_DIR REMOTE_WP_CLI WP_URL)
|
||||||
|
for key in "${required[@]}"; do
|
||||||
|
if [[ -z "${!key:-}" ]]; then
|
||||||
|
echo "[deploy] missing required env var: ${key}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
SSH_KEY_PATH="${REMOTE_SSH_KEY_PATH}"
|
||||||
|
if [[ "${SSH_KEY_PATH}" != /* ]]; then
|
||||||
|
SSH_KEY_PATH="${ROOT_DIR}/${SSH_KEY_PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${SSH_KEY_PATH}" ]]; then
|
||||||
|
echo "[deploy] ssh key not found: ${SSH_KEY_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
artifact_base="$(basename -- "${ARTIFACT}")"
|
||||||
|
stage_name="${artifact_base%.zip}"
|
||||||
|
STAGE_DIR="/tmp/${stage_name}"
|
||||||
|
|
||||||
|
SSH=(ssh -F /dev/null -i "${SSH_KEY_PATH}" -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new "${REMOTE_USER}@${REMOTE_HOST}")
|
||||||
|
RSYNC=(rsync -avz --delete -e "ssh -F /dev/null -i ${SSH_KEY_PATH} -p ${REMOTE_PORT} -o StrictHostKeyChecking=accept-new")
|
||||||
|
|
||||||
|
echo "[deploy] staging on remote: ${STAGE_DIR}"
|
||||||
|
"${SSH[@]}" "mkdir -p '${STAGE_DIR}'"
|
||||||
|
|
||||||
|
if [[ -f "${MANIFEST}" ]]; then
|
||||||
|
"${RSYNC[@]}" "${ARTIFACT}" "${MANIFEST}" "${REMOTE_USER}@${REMOTE_HOST}:${STAGE_DIR}/"
|
||||||
|
else
|
||||||
|
"${RSYNC[@]}" "${ARTIFACT}" "${REMOTE_USER}@${REMOTE_HOST}:${STAGE_DIR}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${SSH[@]}" "set -euo pipefail; rm -rf '${STAGE_DIR}/extracted'; mkdir -p '${STAGE_DIR}/extracted'; unzip -q '${STAGE_DIR}/${artifact_base}' -d '${STAGE_DIR}/extracted'; test -f '${STAGE_DIR}/extracted/calendar-plugin/calendar-plugin.php'"
|
||||||
|
"${SSH[@]}" "set -euo pipefail; rsync -a --delete '${STAGE_DIR}/extracted/calendar-plugin/' '${REMOTE_APP_DIR}/'"
|
||||||
|
"${SSH[@]}" "set -euo pipefail; chown -R www-data:www-data '${REMOTE_APP_DIR}'"
|
||||||
|
"${SSH[@]}" "set -euo pipefail; '${REMOTE_WP_CLI}' --path='${WP_ROOT}' plugin activate calendar-plugin --allow-root >/dev/null 2>&1 || true"
|
||||||
|
|
||||||
|
# Exact-match style checksum dry-run check
|
||||||
|
"${SSH[@]}" "set -euo pipefail; rsync -avznc --delete '${STAGE_DIR}/extracted/calendar-plugin/' '${REMOTE_APP_DIR}/' >/tmp/codex_rsync_check.out; if grep -Eq '^[^./]|^\./' /tmp/codex_rsync_check.out; then cat /tmp/codex_rsync_check.out; exit 1; fi"
|
||||||
|
|
||||||
|
# Ownership sanity check: must be zero mismatches
|
||||||
|
non_owned="$("${SSH[@]}" "set -euo pipefail; find '${REMOTE_APP_DIR}' \( ! -user www-data -o ! -group www-data \) | wc -l")"
|
||||||
|
if [[ "${non_owned}" != "0" ]]; then
|
||||||
|
echo "[deploy] ownership check failed: ${non_owned} path(s) not owned by www-data:www-data" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve slug for endpoint checks
|
||||||
|
url_slug="$("${SSH[@]}" "'${REMOTE_WP_CLI}' --path='${WP_ROOT}' option get calendar_plugin_url_slug --allow-root 2>/dev/null || true" | tr -d '\r' | tail -n1)"
|
||||||
|
if [[ -n "${url_slug}" ]]; then
|
||||||
|
ICS_URL="${WP_URL%/}/${url_slug}/calendar.ics"
|
||||||
|
CALDAV_URL="${WP_URL%/}/${url_slug}/caldav/"
|
||||||
|
else
|
||||||
|
ICS_URL="${WP_URL%/}/calendar.ics"
|
||||||
|
CALDAV_URL="${WP_URL%/}/caldav/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
health_code="$(curl -sS -o /tmp/codex_health.out -w '%{http_code}' "${WP_URL%/}/wp-json/calendar/v1/health" || true)"
|
||||||
|
ics_code="$(curl -sS -o /tmp/codex_ics.out -w '%{http_code}' "${ICS_URL}" || true)"
|
||||||
|
caldav_code="$(curl -sS -o /tmp/codex_caldav.out -w '%{http_code}' "${CALDAV_URL}" || true)"
|
||||||
|
|
||||||
|
echo "[deploy] plugin deployed: ${artifact_base}"
|
||||||
|
echo "[deploy] ownership: www-data:www-data (verified)"
|
||||||
|
echo "[deploy] checks: health=${health_code} ics=${ics_code} caldav_unauth=${caldav_code}"
|
||||||
|
echo "[deploy] urls: ${ICS_URL} ${CALDAV_URL}"
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENV_FILE="${ROOT_DIR}/credentials/.env"
|
||||||
|
|
||||||
|
if [[ -f "${ENV_FILE}" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "${ENV_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
VERSION="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
cat <<'USAGE'
|
||||||
|
Create a plugin package artifact from ./code.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/package_plugin.sh [--version X.Y.Z]
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- If --version is not provided, reads Version from code/calendar-plugin.php.
|
||||||
|
- Writes zip + manifest to ./package.
|
||||||
|
USAGE
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[package] unknown argument: $1" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${VERSION}" ]]; then
|
||||||
|
VERSION="$(sed -n 's/^ \* Version: \(.*\)$/\1/p' "${ROOT_DIR}/code/calendar-plugin.php" | head -n1 | tr -d '[:space:]')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${VERSION}" ]]; then
|
||||||
|
echo "[package] unable to detect plugin version from code/calendar-plugin.php" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SLUG="calendar-plugin"
|
||||||
|
PACKAGE_DIR="${ROOT_DIR}/package"
|
||||||
|
STAGING_DIR="${PACKAGE_DIR}/staging/${SLUG}"
|
||||||
|
ARTIFACT="${PACKAGE_DIR}/${SLUG}-${VERSION}.zip"
|
||||||
|
MANIFEST="${PACKAGE_DIR}/${SLUG}-${VERSION}.manifest.sha256"
|
||||||
|
|
||||||
|
mkdir -p "${PACKAGE_DIR}/staging"
|
||||||
|
rm -rf "${STAGING_DIR}"
|
||||||
|
mkdir -p "${STAGING_DIR}"
|
||||||
|
|
||||||
|
rsync -a --delete "${ROOT_DIR}/code/" "${STAGING_DIR}/"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${PACKAGE_DIR}/staging"
|
||||||
|
zip -qr "${ARTIFACT}" "${SLUG}"
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${STAGING_DIR}"
|
||||||
|
find . -type f -print0 | sort -z | xargs -0 sha256sum
|
||||||
|
) > "${MANIFEST}"
|
||||||
|
|
||||||
|
echo "[package] created artifact: ${ARTIFACT}"
|
||||||
|
echo "[package] created manifest: ${MANIFEST}"
|
||||||
Loading…
Reference in New Issue