diff --git a/package/calendar-plugin-0.1.15.manifest.sha256 b/package/calendar-plugin-0.1.15.manifest.sha256 new file mode 100644 index 0000000..fb05654 --- /dev/null +++ b/package/calendar-plugin-0.1.15.manifest.sha256 @@ -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 diff --git a/package/calendar-plugin-0.1.15.zip b/package/calendar-plugin-0.1.15.zip new file mode 100644 index 0000000..1e69edd Binary files /dev/null and b/package/calendar-plugin-0.1.15.zip differ diff --git a/requirements/api.md b/requirements/api.md index 075a6d6..43c28de 100644 --- a/requirements/api.md +++ b/requirements/api.md @@ -36,7 +36,7 @@ This document covers: ### Create Event - `POST /events` - 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. ### List Events @@ -70,6 +70,16 @@ This document covers: - Must not split the underlying recurring series. - 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 ### Register @@ -139,3 +149,4 @@ Acceptance should verify: - Validation and error payloads are consistent. - Single-occurrence delete creates recurrence exception rather than split series. - Authz rules enforce role/access constraints. +- Privacy redaction rules are enforced consistently across public API payloads and ICS output. diff --git a/requirements/authorization.md b/requirements/authorization.md index 85e826e..14d3a25 100644 --- a/requirements/authorization.md +++ b/requirements/authorization.md @@ -40,9 +40,15 @@ This document covers: ### ICS Access - 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. +### 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 - 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. diff --git a/requirements/caldav.md b/requirements/caldav.md index 7d267dc..9284547 100644 --- a/requirements/caldav.md +++ b/requirements/caldav.md @@ -74,6 +74,7 @@ The endpoint must support these operations at minimum. ### Create and Update - `PUT`: create new event resource or replace an existing event resource. - `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. ### Delete @@ -101,6 +102,12 @@ Minimum mapping expectations: - Recurrence rules -> `RRULE` - 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: - 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. - 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. +- Privacy values round-trip through CalDAV edit flows (including Thunderbird) without losing event detail fidelity. diff --git a/requirements/caldav_endpoints.md b/requirements/caldav_endpoints.md index 07770e0..f89084a 100644 --- a/requirements/caldav_endpoints.md +++ b/requirements/caldav_endpoints.md @@ -96,6 +96,7 @@ If a report is unsupported for a resource, server returns standards-appropriate ## Content Handling - 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. - Server should normalize line endings/content as required by iCalendar compatibility. diff --git a/requirements/data.md b/requirements/data.md index 3c838a5..8b0e186 100644 --- a/requirements/data.md +++ b/requirements/data.md @@ -33,6 +33,7 @@ Requirements: The stored model must support these fields (direct columns or normalized equivalents): - Event identifier (internal primary key) +- `visibility` (`public` or `private`) - `title` - `location` - `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 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/ICS/sidebar must include private events with privacy redaction (time/date shown, non-time content masked). ## Local Testing Data Requirements The local compatibility-harness test environment must support deterministic data setup and teardown. diff --git a/requirements/data_schema.md b/requirements/data_schema.md index fda8f31..a5a913b 100644 --- a/requirements/data_schema.md +++ b/requirements/data_schema.md @@ -22,6 +22,7 @@ This document covers: Required fields: - `id` (PK) - `uid` (stable iCalendar UID) +- `visibility` (`public`, `private`; default `public`) - `title` - `description` - `location` @@ -42,6 +43,11 @@ Constraints: - `title`, `start_datetime` required - `end_datetime >= start_datetime` - 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 Required fields: diff --git a/requirements/editor.md b/requirements/editor.md index 34ec166..4d4d94b 100644 --- a/requirements/editor.md +++ b/requirements/editor.md @@ -35,6 +35,7 @@ Behavior requirements: Supported fields: - `title` (required) +- `visibility` (`public` or `private`; default `public`) - `description` - `location` - `category` @@ -55,6 +56,7 @@ UI behavior requirements: - Time controls hide when `all_day_event` is selected. - Floating/persistent labels keep field purpose visible. - Internal event ID is hidden from users. +- Editor must provide a control to mark events as `public` or `private`. ## Recurrence Model Supported `repeat_type` values: @@ -91,3 +93,4 @@ Acceptance should verify: - Deleting one occurrence creates an exception while preserving the series. - Monthly ordinal patterns save and reload correctly. - Validation blocks invalid start/end ranges. +- Privacy visibility setting saves and reloads correctly. diff --git a/requirements/ics.md b/requirements/ics.md index d07fbde..9d96f8d 100644 --- a/requirements/ics.md +++ b/requirements/ics.md @@ -49,6 +49,7 @@ Field mapping requirements: - Plugin end date/time -> `DTEND` - Plugin category -> `CATEGORIES` - Stable unique event identifier -> `UID` +- Plugin `visibility` controls public-output masking for private events - Last update timestamp -> `DTSTAMP` Additional mapping guidance: @@ -57,6 +58,15 @@ Additional mapping guidance: - Timed events must use `DTSTART;TZID=Europe/London` and `DTEND;TZID=Europe/London`. - `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 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. ## 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. ## Verification Requirements diff --git a/requirements/ics_endpoint.md b/requirements/ics_endpoint.md index b252bbf..dad6a48 100644 --- a/requirements/ics_endpoint.md +++ b/requirements/ics_endpoint.md @@ -26,13 +26,14 @@ This document covers: ## Access Policy - 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 - Default mode: `public_read` unless overridden by policy requirements. ## Data Scope - ICS output includes only events intended for the selected endpoint audience. - 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`. ## Stability and Subscription diff --git a/requirements/web-ui.md b/requirements/web-ui.md index c4f5589..efbc96b 100644 --- a/requirements/web-ui.md +++ b/requirements/web-ui.md @@ -83,6 +83,16 @@ Minimum display requirements: - Category (if provided) - 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) 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. - Form controls use persistent inline/floating labels so field purpose remains visible after input. - 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. - `start date/time` and `end date/time` 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 - `/calendar` must support two user modes: public (not logged in) and logged-in. - 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. - Login dialog must support credential login. - 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. - Empty-state behavior is clear and user-friendly. - ICS link is present and returns valid calendar payload. +- Privacy redaction behavior is correct in public views, sidebar, and logged-in views. diff --git a/scripts/deploy_remote.sh b/scripts/deploy_remote.sh new file mode 100755 index 0000000..58aac1f --- /dev/null +++ b/scripts/deploy_remote.sh @@ -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 +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}" diff --git a/scripts/package_plugin.sh b/scripts/package_plugin.sh new file mode 100755 index 0000000..054db76 --- /dev/null +++ b/scripts/package_plugin.sh @@ -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}"