# Task 020: Demo Publishing S3 Assets Compatibility Development description: Fix publish dry-run/commit flow so approved assets stored in object storage (`s3://...`) are correctly bundled for Git publishing in demo Compose mode. ## Implementation Details - Root cause: - Publishing bundle loader accepted only `file://` asset URLs. - In Compose demo mode, approved assets are stored in MinIO and exposed as `s3://bucket/key`. - Required backend changes: - Extend object storage client with read capability (`get_bytes`). - Update publishing asset collector to support both `file://` and `s3://` sources. - Use `asset.object_key` (or URL-derived key fallback) for object-storage fetch. - Preserve existing manifest structure and publish workflow statuses. - Validation: - Add regression test proving dry-run works when approved assets use `s3://` URLs with valid object keys. ## Public Interface - No API contract changes. - Existing endpoints must behave identically, except they no longer fail on `s3://` approved assets: - `POST /api/articles/{article_id}/publishing/dry-run` - `POST /api/articles/{article_id}/publishing/create-commit` ## Acceptance Criteria - [x] Dry-run no longer fails with `Approved asset file is unavailable: s3://...` when asset object exists. - [x] Publish bundle copies approved assets from object storage into target repo workspace. - [x] Existing file-based publishing tests continue to pass. - [x] New regression test covers `s3://` asset URL compatibility. ## Verification - Run publishing integration suite: - `PYTHONPATH=/private/tmp/pupline-backend-deps python3 -m unittest apps/backend/tests/integration/test_publishing_git_flow_public_api.py` - Run task 019 demo smoke: - `PYTHONPATH=/private/tmp/pupline-backend-deps python3 -m unittest apps/backend/tests/integration/test_end_to_end_demo_stack_smoke_public_api.py` - Run clean compose checklist manually (`down -v` -> `up --build`) and verify publish dry-run/commit steps. ## Result - Status: Implemented. - Files changed: - `apps/backend/src/infrastructure/object_storage.py` - `apps/backend/src/application/publishing.py` - `apps/backend/tests/integration/test_publishing_git_flow_public_api.py` - Green evidence: - `PYTHONPATH=/private/tmp/pupline-backend-deps python3 -m unittest apps/backend/tests/integration/test_publishing_git_flow_public_api.py` -> `Ran 7 tests ... OK` - `PYTHONPATH=/private/tmp/pupline-backend-deps python3 -m unittest apps/backend/tests/integration/test_end_to_end_demo_stack_smoke_public_api.py` -> `Ran 1 test ... OK` - Clean compose check (`docker compose down -v` -> `docker compose up --build -d`) + manual public API flow: - final approval: `200 PUBLISH_DRY_RUN_REQUIRED` - publishing dry-run: `201 PUBLISH_COMMIT_READY` - create-commit: `201 PUBLISH_COMMIT_CREATED` - Refactor notes: - Added storage read path to reuse existing object storage abstractions instead of introducing publish-specific S3 calls.