Skip to content

MWPW-192311: Add bulk publish runtime action for IO Studio#758

Merged
afmicka merged 15 commits intomainfrom
MWPW-192311
Apr 30, 2026
Merged

MWPW-192311: Add bulk publish runtime action for IO Studio#758
afmicka merged 15 commits intomainfrom
MWPW-192311

Conversation

@Axelcureno
Copy link
Copy Markdown
Member

@Axelcureno Axelcureno commented Apr 10, 2026

Resolves https://jira.corp.adobe.com/browse/MWPW-192311
QA Checklist: https://wiki.corp.adobe.com/display/adobedotcom/M@S+Engineering+QA+Use+Cases

New IO Runtime action for bulk-publishing content fragments, plus a CLI toolchain for discovering and batching paths. Backend only - no UI changes. The Runtime layer exists for logging/observability; batching is delegated to Odin's bulk endpoint.

What this does

Action (io/studio/src/bulk-publish/):

  • index.js - validates input, groups paths by locale, chunks at 50, runs chunks sequentially. Limits: 500 paths, 50 locales, 5000 resolved.
  • resolver.js - expands paths[] x locales[] via getTargetPath from common.js.
  • publisher.js - single bulk POST /adobe/sites/cf/fragments/publish per chunk with scheduled_activation_with_references workflow; 3-retry exponential backoff on HTTP 0/429/5xx.
  • Registered in app.config.yaml (runtime: nodejs:22, web: yes, final: true). IMS-validated against allowedClientId.

CLI (scripts/content/): bulk-publish-discover.mjs (query Odin search by surface), bulk-publish-segment.mjs (split into <= 500-path files), bulk-publish.mjs (invoke the action), debug-search-pagination.mjs. Docs in scripts/content/README.md.

E2E validation

  • Sandbox: fresh publish + locale expansion (fr_FR, de_DE) verified on author-p22655-e59433.
  • Stage: 13,885 items across 62 locale segments, zero failures.
  • Prod: 17,829 items across 107 locale segments, zero failures.

Tests

  • 30 unit tests (io/studio/test/bulk-publish/), full studio suite green (167 passing).

Test URLs

Checklist

  • C1. Unit tests
  • C2. Nala test (N/A - backend IO Runtime action, no UI surface)
  • C3. All checks green
  • C4. Test URLs in PR description
  • C5. Ready for demo
  • C6. Re-read Jira to confirm ACs

@Axelcureno Axelcureno requested a review from npeltier April 10, 2026 19:43
@aem-code-sync
Copy link
Copy Markdown

aem-code-sync Bot commented Apr 10, 2026

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 93.46154% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.46%. Comparing base (26266ec) to head (f2a13f6).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
io/studio/src/bulk-publish/index.js 92.61% 11 Missing ⚠️
io/studio/src/bulk-publish/publisher.js 93.33% 6 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #758      +/-   ##
==========================================
+ Coverage   87.44%   87.46%   +0.02%     
==========================================
  Files         211      214       +3     
  Lines       63410    63670     +260     
==========================================
+ Hits        55447    55690     +243     
- Misses       7963     7980      +17     
Files with missing lines Coverage Δ
io/studio/src/bulk-publish/resolver.js 100.00% <100.00%> (ø)
io/studio/src/bulk-publish/publisher.js 93.33% <93.33%> (ø)
io/studio/src/bulk-publish/index.js 92.61% <92.61%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 26266ec...f2a13f6. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Create a new bulk-publish IO Runtime action that accepts fragment paths
and optional locales, publishes them to Odin with skip-already-published
checks, exponential backoff retry, concurrency limiting, and in-memory
queuing. Backend only — no UI in this ticket.

- resolver.js: expands paths × locales using getTargetPath from common.js
- skip-check.js: compares fragment.published.at vs fragment.modified.at
- publisher.js: per-path publish with 3 retries via fetchOdin
- queue.js: in-memory serial mutex for concurrent invocation safety
- index.js: orchestrates via processBatchWithConcurrency (default 5)
- 27 unit tests (proxyquire + sinon + chai), 83/83 full io/studio suite
- E2E verified against prod Odin (sandbox surface): fresh publish,
  skip-already-published, and locale expansion all confirmed
- Add path prefix validation (must start with /content/dam/mas/)
- Add input size caps: max 500 paths, 50 locales, 5000 resolved
- Clamp concurrencyLimit to max 20 (prevent caller bypass)
- Standardize all logging to structured JSON format
- Add STATUS constants to eliminate stringly-typed comparisons
- Remove dead PUBLISH_URI/WORKFLOW_MODEL_ID exports from publisher
- Sanitize error response (no internal details in 500 body)
- Add 3 new security validation tests (30 total, 86 full suite)
Comment thread io/studio/app.config.yaml Outdated
@Axelcureno Axelcureno self-assigned this Apr 13, 2026
…dation

- app.config.yaml: add limits.timeout: 300000 (5 min) to bulk-publish action so
  large batches (up to 5000 resolved paths) do not silently hit the 60s OpenWhisk
  default web action timeout
- publisher.js: skip exponential backoff on non-retryable 4xx errors; extract HTTP
  status from error message via regex and break immediately on anything that is not
  429 or 5xx; wrap publishPath body in outer try/catch so an unexpected throw from
  fetchFragmentByPath returns a structured failed result instead of rejecting the
  entire Promise.all batch
- index.js: tighten path validation predicate so non-string entries (null, number,
  object) produce a clear 400 error instead of silently falling through to a
  confusing "No valid paths after resolution" message
- Tests: add coverage for 401 no-retry, fetchFragmentByPath rejection recovery,
  and non-string path validation (158 passing, zero regressions)
Copy link
Copy Markdown
Contributor

@yesil yesil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a single api call from the frontend with the list of all the fragment IDs?

https://developer.adobe.com/experience-cloud/experience-manager-apis/api/stable/sites/#operation/fragments/publish

@3ch023
Copy link
Copy Markdown
Contributor

3ch023 commented Apr 15, 2026

Why not use a single api call from the frontend with the list of all the fragment IDs?

https://developer.adobe.com/experience-cloud/experience-manager-apis/api/stable/sites/#operation/fragments/publish

as discussed in slack, io action is beneficial for logging/monitoring/etc.
but agree that using a bulk publish request to odin instead of queing separate ones is a better way, it would also solve a potential issue of timeout in action (60 sec atm?)

@npeltier
Copy link
Copy Markdown
Contributor

Why not use a single api call from the frontend with the list of all the fragment IDs?
https://developer.adobe.com/experience-cloud/experience-manager-apis/api/stable/sites/#operation/fragments/publish

as discussed in slack, io action is beneficial for logging/monitoring/etc. but agree that using a bulk publish request to odin instead of queing separate ones is a better way, it would also solve a potential issue of timeout in action (60 sec atm?)

please also consider bulk publishing per locale or something, not all items

@Blainegunn Blainegunn self-requested a review April 15, 2026 16:57
@Axelcureno Axelcureno marked this pull request as draft April 15, 2026 20:50
@Axelcureno
Copy link
Copy Markdown
Member Author

Axelcureno commented Apr 22, 2026

Addressed in the latest push. publisher.js now issues a single Odin bulk-publish call per chunk instead of looping per-path. index.js groups resolved paths by locale first, then sub-chunks each locale into \u226450 paths (Odin's maxItems cap \u2014 FluffyJaws confirmed silent truncation past 50).

Verified end-to-end: 13,885 paths to stage and 17,829 paths to prod, zero failures. IO action kept for logging/monitoring.

@npeltier @yesil @3ch023 @honstar

@Axelcureno Axelcureno requested a review from yesil April 22, 2026 20:46
Axel Cureno Basurto added 3 commits April 24, 2026 08:27
…le chunk

- publisher.js: replace per-path publishPath with publishChunk; one
  Odin /publish POST per chunk (up to 50 paths, Odin maxItems cap)
- index.js: group resolved paths by locale first, then sub-chunk each
  locale into <=50-path pieces; one workflow instance per chunk
- index.js: accept aemOdinEndpoint body param alongside odinEndpoint
  to avoid OpenWhisk "reserved properties" 400 when the package
  input is unbound in the workspace
- Drop skip-check (Odin is idempotent for already-published fragments);
  remove per-path GET to fetchFragmentByPath
- Tests: 32 passing (added dual-key fallback coverage)
- Addresses PR #758 comments from yesil, 3ch023, npeltier
- bulk-publish.mjs: invoke the deployed action from a paths file,
  handle IMS token, summarize published/skipped/failed, four-layer
  acom/en_US guard
- bulk-publish-discover.mjs: walk MAS surfaces via Odin search API,
  filter by modelIds (card/collection/dictionary entry+index),
  group by surface/locale, guard against acom/en_US leakage
- bulk-publish-segment.mjs: split a paths file into per-locale files
  respecting the action 500-path request cap
- debug-search-pagination.mjs: cursor-follow probe for verifying
  Odin search pagination end-to-end
- README: usage for bulk-publish.mjs

Used for the MWPW-192311 rollout: 17,829 fragments published across
107 locales (prod), 13,885 across 62 (stage), zero failures.
…lize discovery

- scripts/content/common.js: export CARD/COLLECTION/DICTIONARY model
  IDs as the single source; add parseArgs() helper returning
  { getFlag, hasFlag }
- Four CLI scripts: replace per-script getFlag/hasFlag copies with
  the shared parseArgs helper; drop mirrored model-id constants
- bulk-publish-discover.mjs: parallelize per-locale Odin search via
  Promise.all (was sequential for-loop)
- io/studio/src/bulk-publish/index.js: publishOneChunk counts status
  in a single reduce pass instead of two filter scans
- Tests: 160 passing across full io/studio suite (+2 vs baseline),
  coverage on bulk-publish/index.js 92.25% -> 92.81%
@Axelcureno Axelcureno marked this pull request as ready for review April 24, 2026 15:50
Copy link
Copy Markdown
Contributor

@yesil yesil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Axelcureno has the OOTB bulk publish AOI been ruled out ? I would like to know why before I continue the review on this.

- index.js: replace processBatchWithConcurrency with sequential for-of
- delete queue.js + queue.test.js (mutex no longer needed)
- app.config.yaml: drop concurrencyLimit input

Per Ilyas's PR feedback - Odin's scheduled_activation_with_references
workflow handles its own queuing. The in-process queue + concurrency
were leftovers from the per-path implementation; now that each chunk
is one bulk POST, they don't add value.
@afmicka afmicka merged commit 6431509 into main Apr 30, 2026
14 checks passed
@afmicka afmicka deleted the MWPW-192311 branch April 30, 2026 13:29
Axelcureno pushed a commit that referenced this pull request May 5, 2026
Spec covers the 5-screen UI that pairs with the merged PR #758 backend:
overview, create/edit, post-validate, confirm modal, published state.
Scheduling intentionally out of MVP scope.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants