Skip to content

web publish: migrate static site publishing to the My Pages backend (/mypages API) #152

Closed

Description

@birme

Current behavior (read from source, EyevinnOSC/client-ts @ main)

osaas web publish <name> <dir> (packages/cli/src/web/cmd.ts) calls publish() in packages/web/src/publish.ts, which in turn calls createStorageBucket() in packages/web/src/bucket.ts. Concretely, today it:

  1. Provisions (or reuses) a minio-minio service instance per site name via @osaas/client-services' createMinioMinioInstance, with a random root password.
  2. Creates an S3 bucket named after the site and applies a world-readable bucket policy (s3:GetObject / s3:ListBucket for Principal: *).
  3. Uploads every file under <dir> (recursively, skipping .git) directly to that bucket via the minio SDK's putObject.
  4. Prints the published URL as {minio endpoint}/{bucket name}/index.html — note it points directly at index.html, not at the bucket root, precisely because the bare root 403s (no directory-index support on a raw MinIO bucket).
  5. No list, delete, or status command exists for these sites — the CLI has no memory of what it published, so cleanup/removal is entirely manual (delete the minio-minio instance yourself).
  6. --sync clears the whole bucket via removeBucketContents before re-uploading (no incremental diff).

This is the same backend approach used by the now-deprecated MCP tool publish-static-site (Eyevinn/osaas-ai#1644, closing #1638) — both provision a dedicated minio-minio instance per site with the same limitations. Per specs/my-page.md (Eyevinn/agent-team-osc-dev) §Overview:

OSC already ships a working static-publish path (@osaas/cli web publish, and its MCP port publish-static-site ...). Feasibility is settled ... My Page is the delta on top: a presentable URL, a proper lifecycle, and a UI, plus fixing two limitations #1638 explicitly left out of scope:

  • The URL is https://{tenant}-{site}.minio-minio.auto.prod.osaas.io/{site}/index.html (tenant prefix + doubled site name)
  • create-my-domain maps a custom domain to the MinIO instance, not to a bucket sub-path, so the bare apex returns S3 AccessDenied.

Proposed change

Add (not replace, see "Migration" below) a My Pages-backed publish path that talks to osaas-deploy-manager's /mypages API family instead of provisioning MinIO directly. Contract, verified by reading Eyevinn/osaas-deploy-manager/src/api_mypages.ts on main directly (there is no specs/my-page-upload-urls-contract.md file inside osaas-deploy-manager itself — that file lives in Eyevinn/agent-team-osc-dev/specs/, cited below for the upload-policy design rationale):

  • POST {deploy.svc.{env}.osaas.io}/mypages { name } → 201 { id, name, url, status: 'draft' } (name: 3–52 chars, lowercase letters/digits/hyphens, must start/end alphanumeric; globally unique — 409 if taken; 503 + Retry-After if the tenant's mypages MinIO instance is still starting up).
  • POST /mypages/{id}/upload-urls { files: [{ path, size }] } (max 500 files/request, max ~25 MiB/file — see MAX_PAGE_FILE_SIZE_BYTES / MAX_UPLOAD_FILES_PER_REQUEST in src/apps/mypage.ts) → 200 { uploads: [{ path, url, method: 'POST', contentType, fields }], expiresAt, maxFileSizeBytes }. Each uploads[] entry is a presigned S3 POST policy (MinIO presignedPostPolicy, not a presigned PUT) — fields must be appended to a multipart form in order, with the file appended last under the field name file, then POSTed to url.
  • POST /mypages/{id}/publish optional { files?: string[], prune?: boolean } → 200 { id, name, url, status: 'live', prunedCount? }. This is what fixes the directory-index/403 problem: it server-side copies each dir/index.html to key dir/ so directory URLs resolve. Passing the complete file manifest with prune: true deletes any bucket object not in the manifest (full-replace semantics) — omit both to publish additively.
  • DELETE /mypages/{id} → 200, removes the Service/Ingress pair and the bucket.
  • GET /mypages / GET /mypages/{id} → list/get, for a future web pages or web list command.

Auth: all five endpoints require x-pat-jwt: Bearer <PAT> (verified via fastifyJwt + extractToken: extractPat in api_mypages.ts). The CLI's existing Context class (packages/core/src/context.ts) already carries a personalAccessToken (constructor option or OSC_ACCESS_TOKEN env var) and exposes getPersonalAccessToken() — this is the exact mechanism osaas-ai's My Page MCP tools use (ctx.osc.getPersonalAccessToken()), so no new auth plumbing is needed in the CLI; web publish already builds a Context the same way.

The presigned-POST consideration (CLI ≠ MCP tool)

The MCP tools in osaas-ai (src/mcp/tools/mypages.ts, #1770–#1774) take contentBase64 in the tool schema and do the multipart POST server-side inside the MCP tool handler, because an MCP client has no browser FormData/fetch runtime to hit a presigned URL directly. The CLI does not have this constraint — it runs in Node.js and can call POST /mypages/{id}/upload-urls, then build a FormData/Blob (or use form-data + node-fetch/native fetch) and POST each file directly against the returned presigned target, exactly like the MCP tool does internally but without the base64 round-trip. Read local files as streams directly; no base64 relay needed.

Suggested CLI shape (additive, not a breaking change)

Do not remove or repoint the current web publish in this change. Recommended sequencing, consistent with how the MCP-side deprecation was staged (osaas-ai#1775 deprecates publish-static-site only after its My Page tool replacements shipped; osaas-ai#1776 removal is explicitly gated/deferred):

  1. Add a new flag or subcommand, e.g. osaas web publish <name> <dir> --backend mypages (or a distinct osaas pages publish <name> <dir> command family mirroring web config-create/config-delete), that drives POST /mypages → POST /mypages/{id}/upload-urls (+ direct S3 POST per file) → POST /mypages/{id}/publish.
  2. Leave today's web publish (direct MinIO) as the default/legacy path until the new path is proven in real use.
  3. Once proven, flip the default and only then consider deprecating/removing the legacy path in a follow-up, separately-tracked change — do not couple that decision to this ticket.

Known gap to flag, not silently work around

Per specs/my-page.md D2 (Eyevinn/agent-team-osc-dev), v1 of My Pages explicitly excludes: SPA history fallback, a branded 404, per-site Cache-Control, redirect rules, custom headers, and per-page custom domains. Custom-domain support is tracked separately and unimplemented (Eyevinn/osaas-deploy-manager#1409, closed as a design-needed follow-up, not shipped). If today's CLI users rely on web cdn-create / a custom domain pointed at their published bucket, that is not yet achievable via /mypages — the new path is a strict improvement for the default *.pages.osaas.io / *.pages.liivo.io case only, and should not be pitched as a full replacement until #1409 lands.

Cross-references

  • Epic: Eyevinn/osaas-deploy-manager#1380 (My Page — upload a folder of static files, get a live URL on *.pages.osaas.io / *.pages.liivo.io), closed.
  • Backend contract: Eyevinn/osaas-deploy-manager/src/api_mypages.ts (main), and Eyevinn/agent-team-osc-dev/specs/my-page.md + specs/my-page-upload-urls-contract.md for design rationale (why POST-policy over presigned PUT, etc).
  • Parallel MCP-side migration: Eyevinn/osaas-ai#1770–#1774 (My Page MCP tools, implemented), #1775 (deprecate publish-static-site), #1776 (removal, gated). This CLI ticket is the third leg of the same migration — the direct-API/SDK/CLI-consumer surface — alongside the UI (osaas-app, in progress) and MCP (osaas-ai, done).
  • Custom domains gap: Eyevinn/osaas-deploy-manager#1409 (open design question, not yet implemented).

Lessons for whoever picks this up

  • Don't assume the CLI's static-site publish and the MCP tool's are identical just because they share a "MinIO-per-site" description — read packages/web/src/{publish,bucket}.ts directly; the URL construction and bucket-policy details matter for a correct migration.
  • specs/my-page-upload-urls-contract.md lives in Eyevinn/agent-team-osc-dev, not inside osaas-deploy-manager — don't go looking for a specs/ folder in that repo.

Activity

  1. self-assigned this
    on Aug 31, 2026
  2. birme commented on Aug 31, 2026

    @birme
    MemberAuthor

    Scope update (birme, 2026-08-31): replace the default web publish implementation with the My Pages backend, rather than the additive/opt-in --backend mypages flag originally proposed above. The API contract, auth, and error-handling details in the ticket body still apply — only the invocation model changes (no flag, this becomes the one publish path). Implementation in progress.

  3. birme commented on Aug 31, 2026

    @birme
    MemberAuthor

    Design corrected on PR #153: `osc web publish` now unconditionally publishes via the My Pages backend (no `--backend` flag, no branching) — matching the scope update above. The old direct-MinIO implementation (`publish.ts`/`bucket.ts`) has been removed as dead code since nothing else in the repo depended on it. CI is green and the PR is ready for review: #153

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions