Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 70 additions & 12 deletions .github/workflows/cms.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Execution stays disabled until the operator records Blacksmith qualification.
# See webdock/tools/plugin-ci/BLACKSMITH.md; runner labels alone do not select a region.
name: Independent CMS
on:
push:
Expand All @@ -10,34 +12,94 @@ concurrency:
cancel-in-progress: false
jobs:
check:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ${{ fromJSON(vars.WEBDOCK_PLUGIN_CI_RUNNER) }}
if: vars.WEBDOCK_BLACKSMITH_READY == 'true' && vars.WEBDOCK_BLACKSMITH_REGION != '' && vars.WEBDOCK_BLACKSMITH_BUILDER_READY == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 35
env:
WEBDOCK_OCI_BACKEND: blacksmith-buildx
WEBDOCK_BLACKSMITH_REGION: ${{ vars.WEBDOCK_BLACKSMITH_REGION }}
steps:
- name: Require reviewed Blacksmith execution
shell: bash
env:
QUALIFIED_BLACKSMITH_REGION: ${{ vars.WEBDOCK_BLACKSMITH_REGION }}
run: |
set -euo pipefail
case "${BLACKSMITH_REGION:-}" in us-east|us-west|eu-central|eu-west) ;; *) echo 'Recorded Blacksmith region required' >&2; exit 1 ;; esac
test "$BLACKSMITH_REGION" = "$QUALIFIED_BLACKSMITH_REGION"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
- name: Reject unexpected existing BuildKit before provider setup
shell: bash
run: |
set -euo pipefail
test ! -e buildkitd.toml
python3 scripts/plugin-release/build_oci.py --before-blacksmith-setup
- name: Set up the managed Blacksmith builder
uses: useblacksmith/setup-docker-builder@19215110ab936351210feebdfa5b440b4493e184 # reviewed v2
with:
cache-key: Dockerfile
nofallback: true
buildx-version: v0.23.0
platforms: linux/amd64
- name: Verify provider builder and keep generated configuration outside source
shell: bash
run: |
set -euo pipefail
python3 scripts/plugin-release/build_oci.py --assert-blacksmith-builder
mv -- buildkitd.toml "$RUNNER_TEMP/blacksmith-buildkitd.toml"
- name: Resolve source image identity
shell: bash
run: printf 'IMAGE_REPOSITORY=ghcr.io/%s\n' "${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
- name: Check, unit test and build both components on the qualified EU builder
env:
WEBDOCK_BUILDKIT_ADDR: ${{ vars.WEBDOCK_BUILDKIT_ADDR }}
WEBDOCK_BUILDCTL_SHA256: ${{ vars.WEBDOCK_BUILDCTL_SHA256 }}
- name: Check, unit test and build both components on the qualified Blacksmith builder
run: python3 scripts/plugin-release/build_oci.py --check < /dev/null
release:
needs: check
if: startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push'
runs-on: ${{ fromJSON(vars.WEBDOCK_PLUGIN_CI_RUNNER) }}
if: vars.WEBDOCK_BLACKSMITH_READY == 'true' && vars.WEBDOCK_BLACKSMITH_REGION != '' && vars.WEBDOCK_BLACKSMITH_BUILDER_READY == 'true' && (startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push')
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 45
environment: plugin-release
permissions:
contents: write
packages: write
env:
WEBDOCK_OCI_BACKEND: blacksmith-buildx
WEBDOCK_BLACKSMITH_REGION: ${{ vars.WEBDOCK_BLACKSMITH_REGION }}
steps:
- name: Require reviewed Blacksmith execution
shell: bash
env:
QUALIFIED_BLACKSMITH_REGION: ${{ vars.WEBDOCK_BLACKSMITH_REGION }}
run: |
set -euo pipefail
case "${BLACKSMITH_REGION:-}" in us-east|us-west|eu-central|eu-west) ;; *) echo 'Recorded Blacksmith region required' >&2; exit 1 ;; esac
test "$BLACKSMITH_REGION" = "$QUALIFIED_BLACKSMITH_REGION"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.3
- name: Reject unexpected existing BuildKit before provider setup
shell: bash
run: |
set -euo pipefail
test ! -e buildkitd.toml
python3 scripts/plugin-release/build_oci.py --before-blacksmith-setup
- name: Set up the managed Blacksmith builder
uses: useblacksmith/setup-docker-builder@19215110ab936351210feebdfa5b440b4493e184 # reviewed v2
with:
cache-key: Dockerfile
nofallback: true
buildx-version: v0.23.0
platforms: linux/amd64
- name: Verify provider builder and keep generated configuration outside source
shell: bash
run: |
set -euo pipefail
python3 scripts/plugin-release/build_oci.py --assert-blacksmith-builder
mv -- buildkitd.toml "$RUNNER_TEMP/blacksmith-buildkitd.toml"
- name: Require exact source version and preinstalled Bun
shell: bash
run: |
Expand All @@ -50,8 +112,6 @@ jobs:
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WEBDOCK_BUILDKIT_ADDR: ${{ vars.WEBDOCK_BUILDKIT_ADDR }}
WEBDOCK_BUILDCTL_SHA256: ${{ vars.WEBDOCK_BUILDCTL_SHA256 }}
WEBDOCK_PLUGIN_DOCKERFILE: extensions/managed-cms/Dockerfile
run: |
set -euo pipefail
Expand All @@ -61,8 +121,6 @@ jobs:
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WEBDOCK_BUILDKIT_ADDR: ${{ vars.WEBDOCK_BUILDKIT_ADDR }}
WEBDOCK_BUILDCTL_SHA256: ${{ vars.WEBDOCK_BUILDCTL_SHA256 }}
WEBDOCK_PLUGIN_DOCKERFILE: apps/cms-instance/Dockerfile
run: |
set -euo pipefail
Expand Down
1 change: 1 addition & 0 deletions .tabularium
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"managed-cms","version":"0.1.0-alpha.1","kind":"extension","description":"Create independent website CMS instances and edit pages, drafts and media in Webdock.","category":"content","tags":["cms","content","websites"],"license":"AGPL-3.0-only","readme":"README.md","documentation_url":"https://github.com/spitzli/cloud-plugin-managed-cms/blob/HEAD/README.md","homepage":"https://github.com/spitzli/webdock/tree/main/extensions/managed-cms","protocolVersion":1,"sdkVersion":1,"dataProtocolVersion":1,"coreVersion":">=0.1.0 <1.0.0","image":"ghcr.io/spitzli/cloud-plugin-managed-cms@sha256:9fffb3662a4d4e87c5a54c43bea9070dd7162822708480997dded12467b1b025","dependencies":[],"capabilities":["plugin.apps.read","plugin.apps.provision","plugin.http.read","plugin.http.request"],"schemaVersion":1,"readableSchema":{"min":1,"max":1},"migrations":[],"tables":[],"services":{"required":[{"name":"http","version":1}],"optional":[{"name":"apps","version":1}]},"configuration":{"type":"object","properties":{},"required":[],"additionalProperties":false},"actions":[{"name":"cms.instances","intent":"read","capabilities":["plugin.apps.read"],"input":{"type":"object","properties":{},"required":[],"additionalProperties":false}},{"name":"cms.status","intent":"read","capabilities":["plugin.apps.read"],"input":{"type":"object","properties":{"provisioning_id":{"type":"string","minLength":1,"maxLength":20}},"required":["provisioning_id"],"additionalProperties":false}},{"name":"cms.create","intent":"write","capabilities":["plugin.apps.provision"],"input":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":160},"request_id":{"type":"string","minLength":1,"maxLength":128}},"required":["name","request_id"],"additionalProperties":false}},{"name":"cms.read","intent":"read","capabilities":["plugin.http.read"],"input":{"type":"object","properties":{"path":{"type":"string","minLength":1,"maxLength":2048}},"required":["path"],"additionalProperties":false},"httpBinding":"cms"},{"name":"cms.write","intent":"write","capabilities":["plugin.http.request"],"input":{"type":"object","properties":{"op":{"type":"string","enum":["begin","append","commit"]},"transfer":{"type":"string","minLength":1,"maxLength":128},"size":{"type":"integer","minimum":1,"maximum":15728640},"sha256":{"type":"string","minLength":1,"maxLength":64},"kind":{"type":"string","enum":["json","media"]},"offset":{"type":"integer","minimum":0,"maximum":15728640},"data":{"type":"string","minLength":0,"maxLength":21848}},"required":["op"],"additionalProperties":false},"httpBinding":"cms"}],"jobs":[],"navigation":[{"id":"cms","label":"CMS","path":"/ui/cms"}],"slots":[],"events":[],"secrets":[],"httpBindings":["cms"],"api":[],"mcp":[],"icon":"assets/icon.svg","screenshots":[{"url":"screenshots/cms-draft-editor.png","caption":"Actual CMS editor with synthetic draft content and a read-only fixture transport. No content was saved or published.","alt":"CMS page editor with title, page address, introduction and draft controls"},{"url":"screenshots/cms-draft-mobile.png","caption":"Actual CMS editor at 320px with synthetic content; no save or publish operation.","alt":"Responsive CMS page editor and Spitzli Cloud wordmark at 320px"}]}
58 changes: 53 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
# Managed CMS

![Managed CMS icon](assets/icon.svg)

Edit website content in Spitzli Cloud while each project keeps its own CMS application and data.

## What you can do

Create independent website CMS instances through the hosting integration, then manage pages, drafts and media through the native CMS editor.

## Setup and access

Requires an operator-installed protocol-v1 plugin, apps service, a scoped CMS HTTP binding and separately deployed CMS instance. The plugin image and per-project CMS application image are distinct.

## Data and security

Core authorizes the project and the bridge enforces CMS permissions. Draft editing and publication are separate. Preserve each instance database, retained media and bridge credentials; no cross-project shared content store is introduced.

## Status and limits

Beta. Registry admission, runtime binding and database/image/backup acceptance must be verified separately. Existing integration fixtures use disposable data; source publication is not customer activation.

Catalog publication does not activate the plugin, enable public signup or grant
access to any customer resource. A new presentation release must use a new version;
previously signed releases and their assets stay immutable.

## Gallery

Actual CMS editor with synthetic draft content and a read-only fixture transport. No content was saved or published.

![CMS page editor with title, page address, introduction and draft controls](screenshots/cms-draft-editor.png)

![The actual CMS editor at 320px with synthetic content](screenshots/cms-draft-mobile.png)

## Catalog source

`.tabularium` is the complete catalog-refresh source. It retains the immutable
runtime image and full protocol/access contract of the currently released
`0.1.0-alpha.1` and adds the prepared presentation assets. Refresh it only from a
reviewed immutable source commit. This does not replace signed release assets
or activate the runtime.

The Spitzli Cloud CMS branding in this source checkout and gallery requires
a new runtime release. The existing `0.1.0-alpha.1` image retained in
`.tabularium` does not contain that change.

## Technical reference

The retained implementation notes below include platform integration procedures.
Commands referring to Core, `tools/` or `apps/console` require the [platform source
checkout](https://github.com/spitzli/webdock); they are not standalone setup steps.

Independent source workspace for the existing managed-cms TypeScript protocol-v1
plugin and its separate per-project CMS application. This is not a Rust v2 release.

Expand All @@ -26,9 +76,7 @@ operator links; those paths are not part of this independent repository.

## Independent source and release

Source: https://github.com/spitzli/cloud-plugin-managed-cms. Maintainer pushes and
same-repository PRs use the verified persistent EU runner; fork PRs never run on
it. The root Dockerfile `check` target runs typechecking, unit tests and complete
Source: https://github.com/spitzli/cloud-plugin-managed-cms. The Blacksmith GitHub App is installed for the exact 16 Spitzli platform, plugin and SDK repositories. The configured and observed region is `us-west`, under the owner’s EU-only exception for Spitzli’s own GitHub CI. The official managed-builder probe passed with `nofallback: true`. These prepared workflows use only Blacksmith; actual per-repository source checks and publication of the regular workflows remain pending. Provider and customer credentials are not used for source checks, and no builder fallback is allowed. The root Dockerfile `check` target runs typechecking, unit tests and complete
production builds of both components. Tests needing a real database or deployed
image remain explicit opt-in acceptance checks; no tenant data is used by CI.

Expand All @@ -38,8 +86,8 @@ A signed `v<version>` source tag builds/pushes separate images:
repository/revision label. Release assets include identical TS-v1 `tabularium.json`
and `managed-cms-universal.json`, SHA-256 checksums and a separate immutable CMS
instance image binding. The release does not call the registry API, invent a
registry signature or activate any customer instance. Registry admission stays
pending until the owner publishes and verifies its actual signed receipt.
registry signature or activate any customer instance. Registry admission is a separate step; verify the actual signed receipt
before installing a release.

`export-lock.json` preserves the initial export/standalone source snapshots; later
changes are recorded by verified signed Git commits and release image digests.
Expand Down
14 changes: 14 additions & 0 deletions assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Managed CMS presentation assets

`icon.svg` is an original Spitzli Cloud pictogram, created for this repository.
Purple `#6515D5` and white match the Spitzli Cloud identity; UI text uses the
platform charcoal. It depicts the capability and does not reproduce a provider
trademark or imply provider endorsement. The repository license applies.

Screenshots, where present, show actual product components or integration views.
Their README captions state whether data and operations are synthetic. They are
not evidence of a deployed provider, customer activation or delivery.

Presentation changes require a new immutable release version. Keep existing
signed manifests, release assets and image references unchanged. Resolve relative
asset paths against the new source tag or immutable commit, never `main`.
1 change: 1 addition & 0 deletions assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion extensions/managed-cms/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ export function createManifest(image:string){
protocolVersion:1,sdkVersion:1,dataProtocolVersion:1,coreVersion:">=0.1.0 <1.0.0",schemaVersion:1,readableSchema:{min:1,max:1},
capabilities:["plugin.apps.read","plugin.apps.provision","plugin.http.read","plugin.http.request"],services:{required:[{name:"http",version:1}],optional:[{name:"apps",version:1}]},httpBindings:["cms"],
configuration:object({}),actions,navigation:[{id:"cms",label:"CMS",path:"/ui/cms"}],api:[],mcp:[],jobs:[],events:[],slots:[],tables:[],migrations:[],dependencies:[],secrets:[],
homepage:"https://github.com/spitzli/cloud-plugin-managed-cms",documentation_url:"https://docs.webdock.dev/guides/managed-cms",readme:"README.md",
homepage:"https://github.com/spitzli/cloud-plugin-managed-cms",documentation_url:"https://github.com/spitzli/cloud-plugin-managed-cms/blob/HEAD/README.md",readme:"README.md",icon:"assets/icon.svg",screenshots:[{"url":"screenshots/cms-draft-editor.png","caption":"Actual CMS editor with synthetic draft content and a read-only fixture transport. No content was saved or published.","alt":"CMS page editor with title, page address, introduction and draft controls"},{"url":"screenshots/cms-draft-mobile.png","caption":"Actual CMS editor at 320px with synthetic content; no save or publish operation.","alt":"Responsive CMS page editor and Spitzli Cloud wordmark at 320px"}],
});
}
4 changes: 2 additions & 2 deletions extensions/managed-cms/ui/cms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const stepLabels:Record<string,string>={application:"Prepare the CMS",database:"
const stateLabels:Record<string,string>={pending:"Waiting",running:"In progress",succeeded:"Done",skipped:"Not needed",blocked:"Needs attention",failed:"Failed"};
function openChannel(onReady:(call:Call)=>void,onError:(error:string)=>void){
const nonce=location.hash.slice(1),pending=new Map<string,{resolve:(value:any)=>void;reject:(error:Error)=>void;timeout:ReturnType<typeof setTimeout>}>();let port:MessagePort|undefined;
const timeout=setTimeout(()=>onError("Open CMS from your Webdock project to connect."),10000);
const timeout=setTimeout(()=>onError("Open CMS from your Spitzli Cloud project to connect."),10000);
const receive=(event:MessageEvent)=>{
if(port||event.source!==parent||event.data?.type!=="webdock:init"||!nonce||event.data.nonce!==nonce||event.ports.length!==1)return;
clearTimeout(timeout);port=event.ports[0];port.onmessage=(event)=>{
Expand Down Expand Up @@ -57,7 +57,7 @@ function App(){
if(site&&transport)return <CMSApp embedded siteName={site.name} siteURL={site.origin} accountURL={site.origin} accessURL={site.origin} transport={transport} uiLocale={navigator.language.startsWith("de")?"de":"en"}/>;
return <main className="managed-cms-setup">
<p className="eyebrow">PROJECT CONTENT</p><h1>Your website’s CMS</h1>
<p className="intro">Create pages, manage media and publish from Webdock. Each project gets its own CMS, database and media storage.</p>
<p className="intro">Create pages, manage media and publish from Spitzli Cloud. Each project gets its own CMS, database and media storage.</p>
{error&&<p className="error" role="alert">{error}</p>}
{!loaded&&!error&&<p role="status">Connecting to your project…</p>}
{loaded&&!instance&&<form onSubmit={create}>
Expand Down
6 changes: 3 additions & 3 deletions packages/cms-ui/src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ function CMSAppContent(props: CMSAppProps) {
className="cms-wordmark"
onClick={() => void navigate(manifest?.modules[0]?.slug || "")}
>
{"webdock"}
{"Spitzli Cloud"}
<span>.</span> <strong>{"CMS"}</strong>
</button>
) : (
Expand All @@ -867,7 +867,7 @@ function CMSAppContent(props: CMSAppProps) {
if (await leave()) window.location.assign("/cms");
}}
>
{"webdock"}
{"Spitzli Cloud"}
<span>.</span> <strong>{"CMS"}</strong>
</a>
)}
Expand Down Expand Up @@ -1329,7 +1329,7 @@ function CMSAppContent(props: CMSAppProps) {
))
)}
</main>
<footer className="cms-footer">{t("CMS by Webdock")} <span>{t("Content and access, kept in your control.")}</span>
<footer className="cms-footer">{t("CMS by Spitzli Cloud")} <span>{t("Content and access, kept in your control.")}</span>
</footer>
</div>
</div>
Expand Down
16 changes: 8 additions & 8 deletions packages/cms-ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
color-scheme: light;
--cms-bg: #f5f7fa;
--cms-surface: #fff;
--cms-ink: #18293c;
--cms-ink: #141318;
--cms-muted: #607084;
--cms-line: #dce4ed;
--cms-accent: #ff975b;
--cms-accent: #6515D5;
--cms-soft: #edf2f7;
--cms-nav: #192b40;
--cms-nav-ink: #edf2f8;
--cms-nav: #141318;
--cms-nav-ink: #fff;
--cms-danger: #ac3342;
--cms-font: Arial, Helvetica, sans-serif;
--cms-display: var(--font-space, Arial), Helvetica, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--cms-bg: #101d2c;
--cms-surface: #19293d;
--cms-bg: #141318;
--cms-surface: #201e26;
--cms-ink: #edf3fa;
--cms-muted: #a3b4c9;
--cms-line: #33465e;
Expand Down Expand Up @@ -129,9 +129,9 @@ button:not(.cms-button) {
.cms-wordmark {
text-decoration: none;
font-family: var(--cms-display);
font-size: 26px;
font-size: 20px;
font-weight: 700;
letter-spacing: -1.2px;
letter-spacing: -0.6px;
white-space: nowrap;
}
.cms-wordmark span {
Expand Down
Loading
Loading