custom-content-github

Custom Content Management

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:

  1. 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.
  2. 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.
  3. Portal Content Editor + 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.

Portal workflow

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.

Engine configuration (per shard)

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 configuration

game:
  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:

Branch and base-SHA resolution

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:

  1. config.branchName if explicitly set (not blank, not "UNCONFIGURED")
  2. Branch parsed from the deployed Content artifact version string (format <timestamp>_<pragmaVersion>_<branch>_<gitsha>) — gives you "PRs against whatever branch and gitsha the content artifact was deployed to this shard"
  3. config.localDevFallbackBranch: you usually get here when running pragma locally

If 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.

GitHub token

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:

Troubleshooting