Operators can edit game content (JSON files) directly from the
portal, validate it against what's deployed, package it into a
downloadable archive, and open a pull request against the source
repository — all without touching the filesystem of a running shard or
invoking ./pragma content apply.
There are three logical layers:
ContentDataNodeService: the engine's
content layer. With enableDynamicContent turned on, it
pulls overrides from the configured DynamicContentPlugin in
addition to the on-disk content/src and
content/package files.DatabaseDynamicContentPlugin: backs
the dynamic content with a MySQL table. Game and social shards point at
the same database via ContentDataDaoConfig, so an edit made
through one is visible to both.CustomContentGithubService: the operator-facing
surface. The editor reads on-disk src, writes overrides to the database,
builds archives, and (via the GitHub service) opens a PR.The Content Editor portlet (in operator-game) exposes four user-facing actions:
| Button | What it does |
|---|---|
| Stage (Update) | Writes the edited JSON to the machine's src dir. The change is made on a single machine's src dir. |
| Publish | Writes to the DB using the
DatabaseDynamicContentPlugin. |
| Download Archive | Calls DownloadContentArchiveOperatorV1, which assembles
a zip containing both content/src and
content/package with current overrides applied. Use this
when you want to commit by hand after running publish. The button will
be disabled approx 15 seconds after running publish to ensure all nodes
applied the changes. |
| Publish to GitHub | Calls CommitContentToGithubOperatorV1 on
CustomContentGithubService. The service uses the GitHub Git
Data API to construct an atomic commit containing all current content
files (from both src and pkg), pushes it to a new branch, and opens a
PR. |
Behind the scenes the editor also calls
CheckContentDisparityOperatorV1 to highlight files where
the database override differs from what's on disk. The check is
structural JSON comparison (Jackson
JsonNode equality after parsing) meaning that whitespace,
key ordering, and pretty-printing differences are ignored; array element
order and value differences are detected.
game:
core:
serviceConfigs:
ContentDataDaoConfig:
databaseConfig:
identifierSchema:
identifier: "defaultIdentifier"
schema: "custom_content" # <-- This schema needs to be the same on both game and social
ContentDataNodeServiceConfig:
enableDynamicContent: true
CustomContentGithubServiceConfig:
repositoryUrl: "https://github.com/pragmaplatform/axolotl-pragma-engine"
githubTokenSecret: "UNCONFIGURED"
localDevFallbackBranch: "deploy/simpletitle-clean"
contentRepoPath: "platform/simplegame/content"
pluginConfigs:
DynamicContentService.dynamicContentPlugin:
class: "pragma.content.DatabaseDynamicContentPlugin"
social:
core:
serviceConfigs:
ContentDataDaoConfig:
databaseConfig:
identifierSchema:
identifier: "defaultIdentifier"
schema: "custom_content" # <-- This schema needs to be the same on both game and social
ContentDataNodeServiceConfig:
enableDynamicContent: true
pluginConfigs:
DynamicContentService.dynamicContentPlugin:
class: "pragma.content.DatabaseDynamicContentPlugin"CustomContentGithubService
configurationgame:
serviceConfigs:
CustomContentGithubServiceConfig:
repositoryUrl: "https://github.com/pragmaplatform/axolotl-pragma-engine"
contentRepoPath: "pragma-engine/platform/<gameModule-or-5-ext>/content"
branchName: "" # optional, see "Branch resolution"
localDevFallbackBranch: "main" # fallback when running off a non-deployed build (eg: locally)
githubTokenSecret: "<encrypted PAT>" # see "GitHub token"Field-by-field:
repositoryUrl —
https://github.com/owner/repo or
git@github.com:owner/repo (with or without
.git). We use Github's REST API.contentRepoPath — required. Path
inside the repository where content lives. Files are
committed under <contentRepoPath>/src/... and
<contentRepoPath>/package/....branchName — optional
override for the PR's base branch. Leave blank (or
"UNCONFIGURED") to derive automatically.localDevFallbackBranch — used as the
base branch when running the service locally.githubTokenSecret — encrypted GitHub
Personal Access Token. Scopes detailed below.When you click Commit to GitHub, the service has to answer two questions: which branch should the PR target? and which commit should the new content commit be parented to?
Branch (target of the PR), in priority order:
config.branchName if explicitly set (not blank, not
"UNCONFIGURED")<timestamp>_<pragmaVersion>_<branch>_<gitsha>)
— gives you "PRs against whatever branch and gitsha the content artifact
was deployed to this shard"config.localDevFallbackBranch: you usually get here
when running pragma locallyIf none of the above resolves the service fails fast with
Cannot determine base branch.
Base SHA (parent of the new commit):
Once the commit lands, the service creates an ephemeral branch named
pragma-content-<yyyyMMdd-HHmmss> from that commit and
opens a PR with head = pragma-content-…,
base = <resolved branch>. The branch lives on the
remote until you merge or delete it; the PR title and body come from the
operator portal.
The token is a fine-grained personal access token scoped to the target repository.
Repository access: Only select repositories: the
repo in repositoryUrl.
Repository permissions:
| Permission | Level | Used for |
|---|---|---|
| Contents | Read and write | GET /git/commits, GET /git/refs,
POST /git/blobs, POST /git/trees,
POST /git/commits, POST /git/refs |
| Pull requests | Read and write | POST /pulls |
| Metadata | Read-only | Mandatory; GitHub auto-selects when any repo permission is granted |
Caveats:
pragma-content-* branch itself is fine because that branch
isn't protected.field: "base" code: "invalid" —
the resolved base branch doesn't exist on the remote. Either
branchName points at a non-existent branch, or the
artifact-extracted branch / localDevFallbackBranch was
never pushed.GET /git/commits/<sha> —
the artifact-reported SHA isn't reachable on the remote (orphan local
commit, force-pushed away, wrong repo). Either push the commit, fix
repositoryUrl, or rebuild the Content artifact.GET /git/refs/heads/<branch> — same as above
but for the resolved branch when no artifact SHA is available.--content-data-directory isn't pointing where you think it
is. The service walks
<contentDataDirectory>/{src,package}; both subdirs
missing is logged as
Content subdir 'X' not found at ….pragma-content-<timestamp>
branches on the remote — created when commit succeeds but PR
creation fails. Delete via
git push origin --delete pragma-content-<timestamp>
or in the GitHub UI.