Skip to content

Bring Devvit and downstream target workflow to main - #4

Merged
imjlk merged 7 commits into
mainfrom
codex/devvit-adapter-workflow
Jul 5, 2026
Merged

imjlk merged 7 commits into
mainfrom
codex/devvit-adapter-workflow

Conversation

@imjlk

@imjlk imjlk commented Jul 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Brings the Devvit target workflow and downstream game target config support into main.
  • Includes stricter downstream artifact smoke validation for web, Android, iOS, AIT, and Reddit/Devvit targets.
  • Adds local Node 22 mise pinning for AIT packaging compatibility and simulator-oriented iOS smoke output support.

Validation

  • PR Support downstream platform target configs #3 checks passed before merge into codex/devvit-adapter-workflow: validate, build-android, build-ios, build-ait, build-devvit.
  • Downstream game-a check passed locally after the merge.
  • Downstream game-a target smoke passed locally for web-preview, android, ios, ait, and reddit.

Notes

  • Devvit upload/playtest still requires a local devvit login.
  • iOS App Store archive/signing remains a separate release-machine gate; the current downstream smoke validates a Release simulator .app.

Summary by CodeRabbit

  • New Features

    • Added a new Reddit/Devvit target with build, release, and smoke-test support.
    • Introduced a Devvit adapter and wrapper app so the game can run in Reddit’s Devvit Web environment.
    • Added Reddit-specific platform behavior for storage, leaderboards, and supported actions.
  • Bug Fixes

    • Improved unsupported-target handling so unavailable purchases and rewarded ads fail gracefully.
    • Updated leaderboard behavior to avoid opening unsupported UI in Reddit.
  • Documentation

    • Expanded setup, build, release, and validation docs for the new Reddit Devvit flow.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@imjlk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 198eeb2e-4532-4c7f-80e9-337d878fae4f

📥 Commits

Reviewing files that changed from the base of the PR and between 1d802ea and 6927409.

📒 Files selected for processing (20)
  • .github/workflows/build-target.yml
  • .github/workflows/ci.yml
  • .github/workflows/release-ait.yml
  • .github/workflows/release-android.yml
  • .github/workflows/release-devvit.yml
  • .github/workflows/release-ios.yml
  • .github/workflows/release.yml
  • .mise.toml
  • apps/mobile-capacitor/ios/App/App.xcodeproj/project.pbxproj
  • apps/mobile-capacitor/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
  • native-plugins/capacitor-game-services/Package.swift
  • package.json
  • tools/smoke/embedded-target-config.ts
  • tools/smoke/verify-target-artifacts.ts
  • tools/target/build-target.ts
  • tools/target/effective-config.ts
  • tools/target/generate-release-manifest.ts
  • tools/target/platform-targets.ts
  • tools/target/validate-target-config.ts
  • tools/validate-target-config.ts
📝 Walkthrough

Walkthrough

This PR adds a new "reddit" Devvit Web build target across the monorepo: a new @mpgd/adapter-devvit package implementing a PlatformGateway bridge with localStorage fallback, a apps/target-devvit Express/Redis server wrapper, game-phaser/game-services/leaderboard-ledger integration, refactored build/release tooling with config-driven path resolution, expanded smoke tests, updated CI/CD workflows, and documentation. Unrelated iOS Xcode project and packaging tool changes are also included.

Changes

Reddit Devvit Web Target Implementation

Layer / File(s) Summary
Target config schema and platform config foundation
tools/target/schemas.ts, tools/target/platform-targets.ts, packages/target-config/src/runtime.ts, packages/target-config/targets.json, platform.targets.json
Adds devvit-web target kind, DevvitTargetConfig, new runtime/release literals, reddit target entries, and a new config loader/validator module.
Devvit adapter package
adapters/devvit/src/index.ts, adapters/devvit/src/index.test.ts, adapters/devvit/package.json, adapters/devvit/tsconfig.json, adapters/devvit/CHANGELOG.md
Implements createDevvitPlatformGateway, fetch/sandbox bridges, and namespaced localStorage fallback storage, with a full test suite.
target-devvit wrapper app and server
apps/target-devvit/*
Adds the Devvit wrapper app config, Vite server build, README, and an Express server exposing /api/mpgd/bridge with Redis-backed leaderboard/storage handling.
game-phaser reddit integration
apps/game-phaser/src/platform/*, apps/game-phaser/src/scenes/ResultScene.ts, apps/game-phaser/package.json
Wires the reddit target into installPlatform, runtimeDetector, demoGameServices, and gates leaderboard-open UI for reddit.
game-services & leaderboard ledger reddit support
packages/game-services/src/*, backend/leaderboard-ledger/src/index.ts
Adds reddit to ledger target types and rejects unsupported purchase/rewarded-ad flows client-side.
Build/release tooling refactor
tools/target/*
Refactors build/effective-config/manifest scripts to use the new platform-targets loader and config-relative path resolution, adding a devvit-web build case.
Smoke tests and validation scripts
tools/smoke/*, tools/validate-*, .mcp.json, examples/phaser-starter/agent/game.manifest.json
Extends smoke coverage and starter-workflow/readiness validation for the reddit target and .mcp.json.
CI/CD workflows, scripts, and docs
.github/workflows/*, package.json, README.md, docs/*, .gitignore, .mise.toml, tsconfig.graph.json
Adds devvit build/release workflow steps, bumps action versions, adds build:devvit scripts, and documents the Reddit Devvit target.

Unrelated iOS project and packaging tool changes

Layer / File(s) Summary
iOS Xcode project and scheme updates
apps/mobile-capacitor/ios/App/App.xcodeproj/*, native-plugins/capacitor-game-services/Package.swift
Adds SUPPORTED_PLATFORMS build settings, a new scheme file, and a Cordova dependency.
Package build/workspace tooling cleanup
tools/package/build-packages.ts, tools/package/workspace.ts
Adds generated-source cleanup and sorts publishable package discovery.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • imjlk/mpgd-kit#1: Extends the same tools/validate-starter-workflow.ts validation with .mcp.json/Devvit MCP requirements added in this PR.
  • imjlk/mpgd-kit#2: Overlaps directly with this PR's @mpgd/adapter-devvit, apps/target-devvit, and CI/build/release workflow additions for the reddit target.

Poem

A rabbit hops through Reddit's burrow deep,
Building bridges while the servers sleep. 🐇
Storage falls back, the leaderboard climbs,
CI pipelines hum in newer times.
Devvit dreams now build and run —
Hop, hop, hooray, the target's done!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: bringing the Devvit/downstream target workflow into main.
Description check ✅ Passed The description matches the required Summary, Validation, and Notes sections and includes the main implementation and validation details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/devvit-adapter-workflow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@imjlk
imjlk force-pushed the codex/devvit-adapter-workflow branch from 1d802ea to a037b77 Compare July 5, 2026 03:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (7)
native-plugins/capacitor-game-services/Package.swift (1)

19-20: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Drop the unused Cordova dependency native-plugins/capacitor-game-services/ios/Sources/CapacitorGameServices/CapacitorGameServicesPlugin.swift only imports Capacitor and Foundation, so Cordova in Package.swift looks unnecessary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@native-plugins/capacitor-game-services/Package.swift` around lines 19 - 20,
The Package.swift dependency list includes an unused Cordova product for the
CapacitorGameServices plugin. Remove the Cordova package product from the target
dependencies in Package.swift, keeping only the Capacitor dependency used by
CapacitorGameServicesPlugin.swift, and verify the package still builds with the
remaining imports.
.github/workflows/build-target.yml (1)

68-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Devvit dist glob uploaded for every target, not just reddit.

apps/target-devvit/dist/** is now included in the upload-artifact path unconditionally, so android/ios/ait/web-preview runs will also attempt to match this glob even though it's never populated for those targets. Harmless with default if-no-files-found: warn, but adds noise to every non-devvit build log.

Suggested scoping
-      - uses: actions/upload-artifact@v6
+      - uses: actions/upload-artifact@v6
+        if: always()
         with:
           name: ${{ inputs.target }}-${{ inputs.profile }}
           path: |
             artifacts/**
             release-output/**
-            apps/target-devvit/dist/**
+            ${{ inputs.target == 'reddit' && 'apps/target-devvit/dist/**' || '' }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-target.yml around lines 68 - 74, The upload-artifact
step in the build target workflow always includes the Devvit dist glob, which
causes unnecessary warnings on non-reddit targets. Scope the
`apps/target-devvit/dist/**` path so it is only added when the target is
`reddit`, keeping the existing `actions/upload-artifact@v6` step and other
artifact paths unchanged.
.github/workflows/release-devvit.yml (1)

41-57: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant preflight validation before build:target.

Lines 42-49 already run validate:catalog, validate:ads, validate:target-config, validate:effective-config, validate:targets (and build-packages runs as part of pnpm check/earlier steps). Since MPGD_SKIP_BUILD_TARGET_PREFLIGHT isn't set here, pnpm build:target reddit (line 54) reruns the same validate steps and build-packages again internally (per tools/target/build-target.ts's preflight guard). This duplicates work the release.yml workflow explicitly avoids via MPGD_SKIP_BUILD_TARGET_PREFLIGHT=1.

♻️ Suggested fix
       - run: pnpm build:target reddit "${PROFILE}"
         env:
           PROFILE: ${{ inputs.profile }}
+          MPGD_SKIP_BUILD_TARGET_PREFLIGHT: '1'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-devvit.yml around lines 41 - 57, The release
workflow is doing duplicate preflight validation before build:target because
buildTarget still runs its own checks. In the release-devvit workflow, set
MPGD_SKIP_BUILD_TARGET_PREFLIGHT=1 for the pnpm build:target reddit step so the
already-run validations are not repeated. Use the buildTarget path in
tools/target/build-target.ts and the existing release workflow build step as the
key places to align behavior.
packages/game-services/src/types.ts (1)

10-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider centralizing the ledger-target literal set.

This union/assert pair duplicates LeaderboardLedgerTarget/assertLedgerTarget in backend/leaderboard-ledger/src/index.ts almost verbatim (same literals, same manual !== chain, same error message text). Both needed manual edits in this PR to add 'reddit'; a future target change risks updating only one side. Since both are already subsets of the canonical PlatformTarget (packages/platform/src/index.ts), consider exporting a single ledger-target literal array/type (and shared assertion) from @mpgd/platform for both packages to consume.

Also applies to: 243-250

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/game-services/src/types.ts` around lines 10 - 13, The ledger-target
set is duplicated between GameServicesLedgerTarget and the
LeaderboardLedgerTarget/assertLedgerTarget logic, so future platform changes can
drift. Centralize the canonical ledger-target literals in `@mpgd/platform` by
exporting a shared literal array/type and shared assertion, then update the
types.ts definition and the backend/leaderboard-ledger/src/index.ts target check
to consume those shared symbols instead of maintaining separate literal
unions/manual comparisons.
tools/target/build-target.ts (1)

130-141: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move the archive/simulator mutual-exclusion check before the side-effecting cap sync.

The invariant check for MPGD_RUN_IOS_ARCHIVE + MPGD_RUN_IOS_SIMULATOR_BUILD runs after ensureCapacitorPlatform/cap sync (Line 134-135) already executed. A misconfigured env would waste that (potentially slow, native-tooling) work before failing.

♻️ Proposed reordering
   case 'capacitor-ios': {
     const webDir = targetPath(requireString(target.webDir, `${targetName}.webDir`));
     const shellApp = targetPath(requireString(target.shellApp, `${targetName}.shellApp`));
+    if (process.env.MPGD_RUN_IOS_ARCHIVE === '1' && process.env.MPGD_RUN_IOS_SIMULATOR_BUILD === '1') {
+      throw new Error('Set only one of MPGD_RUN_IOS_ARCHIVE or MPGD_RUN_IOS_SIMULATOR_BUILD.');
+    }
     replaceDirectory(`${gameApp}/dist`, webDir);
     ensureCapacitorPlatform(shellApp, 'ios', env);
     run('pnpm', ['--dir', shellApp, 'cap', 'sync', 'ios'], env);

     let releaseArtifact = requireString(target.shellApp, `${targetName}.shellApp`) + '/ios';
-
-    if (process.env.MPGD_RUN_IOS_ARCHIVE === '1' && process.env.MPGD_RUN_IOS_SIMULATOR_BUILD === '1') {
-      throw new Error('Set only one of MPGD_RUN_IOS_ARCHIVE or MPGD_RUN_IOS_SIMULATOR_BUILD.');
-    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/target/build-target.ts` around lines 130 - 141, Move the
MPGD_RUN_IOS_ARCHIVE and MPGD_RUN_IOS_SIMULATOR_BUILD mutual-exclusion check in
the capacitor-ios branch of build-target.ts so it runs before any side effects
like ensureCapacitorPlatform and run('pnpm', ['--dir', shellApp, 'cap', 'sync',
'ios'], env). Keep the check near the top of the case block, using the existing
targetName/shellApp logic only after the invariant is validated, so
misconfigured env fails fast before native tooling work starts.
tools/smoke/adapter-effective-config.ts (1)

174-212: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider asserting disabled capabilities are actually not delegated for reddit.

verifyDevvitAdapter only exercises leaderboard.submitScore (the enabled path) and infers the disabled path from omission. The capacitor test (Lines 103-123) explicitly exercises purchase/showRewarded to prove they are delegated when enabled; an analogous explicit attempt-and-assert-rejection for gateway.commerce.purchase/gateway.ads.showRewarded on reddit would give stronger confidence that withTargetAvailability actually blocks these for the disabled effective config, rather than relying on them simply never being called.

Also applies to: 307-320

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/smoke/adapter-effective-config.ts` around lines 174 - 212,
verifyDevvitAdapter only checks the enabled leaderboard.submitScore path, so add
explicit assertions that disabled reddit capabilities are blocked and not
delegated. In verifyDevvitAdapter, use gateway.commerce.purchase and
gateway.ads.showRewarded with reddit-effective config, then assert they reject
and that createRecordingBridge does not record those method calls; this should
mirror the enabled-path coverage already used in the capacitor test and validate
withTargetAvailability behavior directly.
tools/validate-public-readiness.ts (1)

49-49: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Also add .devvit/ to blockedTrackedGeneratedPrefixes. It holds local Devvit auth state (~/.devvit/token), so a committed copy should fail the public-readiness check too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/validate-public-readiness.ts` at line 49, The public-readiness check
currently ignores `.devvit/` in the tracked generated paths, so add `.devvit/`
to blockedTrackedGeneratedPrefixes in validate-public-readiness.ts. Update the
logic around the prefix list so committed copies of local Devvit auth state are
treated as blocked by the public-readiness validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release-ait.yml:
- Around line 19-21: The release-ait workflow’s actions/checkout step is missing
the credential-hardening setting used elsewhere in this PR. Update the checkout
configuration in the release workflow to include persist-credentials: false
alongside fetch-depth: 0 so the GitHub token is not left in local git config;
use the existing actions/checkout step in the release-ait workflow as the place
to apply this change.

In @.github/workflows/release-android.yml:
- Around line 19-22: The checkout step in the Android release workflow is
missing the same credential-hardening used elsewhere. Update the
`actions/checkout` usage in this workflow to set `persist-credentials: false`,
matching the pattern already used in `release-devvit.yml` and `release.yml`, so
the `checkout` step does not retain credentials after cloning.

In @.github/workflows/release-ios.yml:
- Around line 22-26: The checkout step in the release-ios workflow is missing
the same credential hardening used in the other release workflows. Update the
actions/checkout invocation in this workflow to set persist-credentials to
false, matching the existing checkout configuration pattern in the related
release workflows.

In `@apps/target-devvit/src/server/index.ts`:
- Around line 533-547: `currentDisplayName()` is reading a server-context
`username` field that does not exist, so it always falls back instead of
returning the real display name. Update the display-name lookup in
`currentDisplayName` to use `context.reddit.getCurrentUsername()` and make the
function async if needed, then adjust the `identity.getPlayer` call path to
await this lookup and pass the fetched username through instead of relying on
`context.username`.
- Around line 21-63: The /internal/menu/create-post handler in app.post is
exposed over HTTP and currently calls reddit.submitCustomPost without verifying
the caller is authorized. Add an explicit authorization gate before the
create-post logic runs, using the request context available in this route and
the existing currentSubredditName check to ensure only valid menu invocations or
moderators can proceed. If the caller is not authorized, return a neutral
UiResponse and skip the reddit.submitCustomPost call. Keep the existing success
and error handling in the same handler after the authz check.
- Around line 446-458: The leaderboard lock setup in acquireLeaderboardLock
currently passes a Date from leaderboardLockExpirationDate into redis.set, but
Devvit Redis expects a numeric TTL in seconds for expiration. Update the lock
acquisition flow to pass the TTL as a number (using leaderboardLockTtlSeconds
directly or an equivalent numeric helper) and remove the Date-based helper so
the lock lifetime is applied correctly.

In `@tools/smoke/embedded-target-config.ts`:
- Around line 131-137: The failure check in the command execution path is gating
on stdout instead of the exit code, which can miss real unzip errors. Update the
error handling around the command/result check in embedded-target-config.ts to
treat nonzero status as failure by default and only allow the specific benign
warning exit codes you intentionally want to ignore; use the existing
result.status, result.stdout, and result.stderr handling in this block to narrow
the tolerated cases.

---

Nitpick comments:
In @.github/workflows/build-target.yml:
- Around line 68-74: The upload-artifact step in the build target workflow
always includes the Devvit dist glob, which causes unnecessary warnings on
non-reddit targets. Scope the `apps/target-devvit/dist/**` path so it is only
added when the target is `reddit`, keeping the existing
`actions/upload-artifact@v6` step and other artifact paths unchanged.

In @.github/workflows/release-devvit.yml:
- Around line 41-57: The release workflow is doing duplicate preflight
validation before build:target because buildTarget still runs its own checks. In
the release-devvit workflow, set MPGD_SKIP_BUILD_TARGET_PREFLIGHT=1 for the pnpm
build:target reddit step so the already-run validations are not repeated. Use
the buildTarget path in tools/target/build-target.ts and the existing release
workflow build step as the key places to align behavior.

In `@native-plugins/capacitor-game-services/Package.swift`:
- Around line 19-20: The Package.swift dependency list includes an unused
Cordova product for the CapacitorGameServices plugin. Remove the Cordova package
product from the target dependencies in Package.swift, keeping only the
Capacitor dependency used by CapacitorGameServicesPlugin.swift, and verify the
package still builds with the remaining imports.

In `@packages/game-services/src/types.ts`:
- Around line 10-13: The ledger-target set is duplicated between
GameServicesLedgerTarget and the LeaderboardLedgerTarget/assertLedgerTarget
logic, so future platform changes can drift. Centralize the canonical
ledger-target literals in `@mpgd/platform` by exporting a shared literal
array/type and shared assertion, then update the types.ts definition and the
backend/leaderboard-ledger/src/index.ts target check to consume those shared
symbols instead of maintaining separate literal unions/manual comparisons.

In `@tools/smoke/adapter-effective-config.ts`:
- Around line 174-212: verifyDevvitAdapter only checks the enabled
leaderboard.submitScore path, so add explicit assertions that disabled reddit
capabilities are blocked and not delegated. In verifyDevvitAdapter, use
gateway.commerce.purchase and gateway.ads.showRewarded with reddit-effective
config, then assert they reject and that createRecordingBridge does not record
those method calls; this should mirror the enabled-path coverage already used in
the capacitor test and validate withTargetAvailability behavior directly.

In `@tools/target/build-target.ts`:
- Around line 130-141: Move the MPGD_RUN_IOS_ARCHIVE and
MPGD_RUN_IOS_SIMULATOR_BUILD mutual-exclusion check in the capacitor-ios branch
of build-target.ts so it runs before any side effects like
ensureCapacitorPlatform and run('pnpm', ['--dir', shellApp, 'cap', 'sync',
'ios'], env). Keep the check near the top of the case block, using the existing
targetName/shellApp logic only after the invariant is validated, so
misconfigured env fails fast before native tooling work starts.

In `@tools/validate-public-readiness.ts`:
- Line 49: The public-readiness check currently ignores `.devvit/` in the
tracked generated paths, so add `.devvit/` to blockedTrackedGeneratedPrefixes in
validate-public-readiness.ts. Update the logic around the prefix list so
committed copies of local Devvit auth state are treated as blocked by the
public-readiness validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b558905-718c-438c-a9dc-baf8bf771391

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6b17f and 1d802ea.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (64)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/workflows/build-target.yml
  • .github/workflows/ci.yml
  • .github/workflows/release-ait.yml
  • .github/workflows/release-android.yml
  • .github/workflows/release-devvit.yml
  • .github/workflows/release-ios.yml
  • .github/workflows/release.yml
  • .gitignore
  • .mcp.json
  • .mise.toml
  • README.md
  • adapters/devvit/CHANGELOG.md
  • adapters/devvit/package.json
  • adapters/devvit/src/index.test.ts
  • adapters/devvit/src/index.ts
  • adapters/devvit/tsconfig.json
  • apps/game-phaser/package.json
  • apps/game-phaser/src/platform/demoGameServices.ts
  • apps/game-phaser/src/platform/installPlatform.ts
  • apps/game-phaser/src/platform/runtimeDetector.test.ts
  • apps/game-phaser/src/platform/runtimeDetector.ts
  • apps/game-phaser/src/scenes/ResultScene.ts
  • apps/mobile-capacitor/ios/App/App.xcodeproj/project.pbxproj
  • apps/mobile-capacitor/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
  • apps/target-devvit/README.md
  • apps/target-devvit/devvit.json
  • apps/target-devvit/package.json
  • apps/target-devvit/src/server/index.ts
  • apps/target-devvit/tsconfig.json
  • apps/target-devvit/vite.server.config.ts
  • backend/leaderboard-ledger/src/index.ts
  • docs/AGENTIC_GAME_WORKFLOW.md
  • docs/GAME_DEVELOPMENT.md
  • docs/PUBLIC_RELEASE_CHECKLIST.md
  • docs/ROADMAP_CHECKLIST.md
  • examples/phaser-starter/agent/game.manifest.json
  • native-plugins/capacitor-game-services/Package.swift
  • package.json
  • packages/game-services/src/client.test.ts
  • packages/game-services/src/client.ts
  • packages/game-services/src/server.test.ts
  • packages/game-services/src/types.ts
  • packages/target-config/src/runtime.ts
  • packages/target-config/targets.json
  • platform.targets.json
  • tools/package/build-packages.ts
  • tools/package/workspace.ts
  • tools/smoke/adapter-effective-config.ts
  • tools/smoke/build-target-artifacts.ts
  • tools/smoke/effective-target-config.ts
  • tools/smoke/embedded-target-config.ts
  • tools/smoke/target-config-runtime.ts
  • tools/smoke/verify-target-artifacts.ts
  • tools/target/build-target.ts
  • tools/target/effective-config.ts
  • tools/target/generate-release-manifest.ts
  • tools/target/platform-targets.ts
  • tools/target/schemas.ts
  • tools/target/validate-target-config.ts
  • tools/validate-public-readiness.ts
  • tools/validate-starter-workflow.ts
  • tools/validate-target-config.ts
  • tsconfig.graph.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 7

🧹 Nitpick comments (7)
native-plugins/capacitor-game-services/Package.swift (1)

19-20: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Drop the unused Cordova dependency native-plugins/capacitor-game-services/ios/Sources/CapacitorGameServices/CapacitorGameServicesPlugin.swift only imports Capacitor and Foundation, so Cordova in Package.swift looks unnecessary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@native-plugins/capacitor-game-services/Package.swift` around lines 19 - 20,
The Package.swift dependency list includes an unused Cordova product for the
CapacitorGameServices plugin. Remove the Cordova package product from the target
dependencies in Package.swift, keeping only the Capacitor dependency used by
CapacitorGameServicesPlugin.swift, and verify the package still builds with the
remaining imports.
.github/workflows/build-target.yml (1)

68-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Devvit dist glob uploaded for every target, not just reddit.

apps/target-devvit/dist/** is now included in the upload-artifact path unconditionally, so android/ios/ait/web-preview runs will also attempt to match this glob even though it's never populated for those targets. Harmless with default if-no-files-found: warn, but adds noise to every non-devvit build log.

Suggested scoping
-      - uses: actions/upload-artifact@v6
+      - uses: actions/upload-artifact@v6
+        if: always()
         with:
           name: ${{ inputs.target }}-${{ inputs.profile }}
           path: |
             artifacts/**
             release-output/**
-            apps/target-devvit/dist/**
+            ${{ inputs.target == 'reddit' && 'apps/target-devvit/dist/**' || '' }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-target.yml around lines 68 - 74, The upload-artifact
step in the build target workflow always includes the Devvit dist glob, which
causes unnecessary warnings on non-reddit targets. Scope the
`apps/target-devvit/dist/**` path so it is only added when the target is
`reddit`, keeping the existing `actions/upload-artifact@v6` step and other
artifact paths unchanged.
.github/workflows/release-devvit.yml (1)

41-57: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant preflight validation before build:target.

Lines 42-49 already run validate:catalog, validate:ads, validate:target-config, validate:effective-config, validate:targets (and build-packages runs as part of pnpm check/earlier steps). Since MPGD_SKIP_BUILD_TARGET_PREFLIGHT isn't set here, pnpm build:target reddit (line 54) reruns the same validate steps and build-packages again internally (per tools/target/build-target.ts's preflight guard). This duplicates work the release.yml workflow explicitly avoids via MPGD_SKIP_BUILD_TARGET_PREFLIGHT=1.

♻️ Suggested fix
       - run: pnpm build:target reddit "${PROFILE}"
         env:
           PROFILE: ${{ inputs.profile }}
+          MPGD_SKIP_BUILD_TARGET_PREFLIGHT: '1'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-devvit.yml around lines 41 - 57, The release
workflow is doing duplicate preflight validation before build:target because
buildTarget still runs its own checks. In the release-devvit workflow, set
MPGD_SKIP_BUILD_TARGET_PREFLIGHT=1 for the pnpm build:target reddit step so the
already-run validations are not repeated. Use the buildTarget path in
tools/target/build-target.ts and the existing release workflow build step as the
key places to align behavior.
packages/game-services/src/types.ts (1)

10-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider centralizing the ledger-target literal set.

This union/assert pair duplicates LeaderboardLedgerTarget/assertLedgerTarget in backend/leaderboard-ledger/src/index.ts almost verbatim (same literals, same manual !== chain, same error message text). Both needed manual edits in this PR to add 'reddit'; a future target change risks updating only one side. Since both are already subsets of the canonical PlatformTarget (packages/platform/src/index.ts), consider exporting a single ledger-target literal array/type (and shared assertion) from @mpgd/platform for both packages to consume.

Also applies to: 243-250

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/game-services/src/types.ts` around lines 10 - 13, The ledger-target
set is duplicated between GameServicesLedgerTarget and the
LeaderboardLedgerTarget/assertLedgerTarget logic, so future platform changes can
drift. Centralize the canonical ledger-target literals in `@mpgd/platform` by
exporting a shared literal array/type and shared assertion, then update the
types.ts definition and the backend/leaderboard-ledger/src/index.ts target check
to consume those shared symbols instead of maintaining separate literal
unions/manual comparisons.
tools/target/build-target.ts (1)

130-141: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move the archive/simulator mutual-exclusion check before the side-effecting cap sync.

The invariant check for MPGD_RUN_IOS_ARCHIVE + MPGD_RUN_IOS_SIMULATOR_BUILD runs after ensureCapacitorPlatform/cap sync (Line 134-135) already executed. A misconfigured env would waste that (potentially slow, native-tooling) work before failing.

♻️ Proposed reordering
   case 'capacitor-ios': {
     const webDir = targetPath(requireString(target.webDir, `${targetName}.webDir`));
     const shellApp = targetPath(requireString(target.shellApp, `${targetName}.shellApp`));
+    if (process.env.MPGD_RUN_IOS_ARCHIVE === '1' && process.env.MPGD_RUN_IOS_SIMULATOR_BUILD === '1') {
+      throw new Error('Set only one of MPGD_RUN_IOS_ARCHIVE or MPGD_RUN_IOS_SIMULATOR_BUILD.');
+    }
     replaceDirectory(`${gameApp}/dist`, webDir);
     ensureCapacitorPlatform(shellApp, 'ios', env);
     run('pnpm', ['--dir', shellApp, 'cap', 'sync', 'ios'], env);

     let releaseArtifact = requireString(target.shellApp, `${targetName}.shellApp`) + '/ios';
-
-    if (process.env.MPGD_RUN_IOS_ARCHIVE === '1' && process.env.MPGD_RUN_IOS_SIMULATOR_BUILD === '1') {
-      throw new Error('Set only one of MPGD_RUN_IOS_ARCHIVE or MPGD_RUN_IOS_SIMULATOR_BUILD.');
-    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/target/build-target.ts` around lines 130 - 141, Move the
MPGD_RUN_IOS_ARCHIVE and MPGD_RUN_IOS_SIMULATOR_BUILD mutual-exclusion check in
the capacitor-ios branch of build-target.ts so it runs before any side effects
like ensureCapacitorPlatform and run('pnpm', ['--dir', shellApp, 'cap', 'sync',
'ios'], env). Keep the check near the top of the case block, using the existing
targetName/shellApp logic only after the invariant is validated, so
misconfigured env fails fast before native tooling work starts.
tools/smoke/adapter-effective-config.ts (1)

174-212: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider asserting disabled capabilities are actually not delegated for reddit.

verifyDevvitAdapter only exercises leaderboard.submitScore (the enabled path) and infers the disabled path from omission. The capacitor test (Lines 103-123) explicitly exercises purchase/showRewarded to prove they are delegated when enabled; an analogous explicit attempt-and-assert-rejection for gateway.commerce.purchase/gateway.ads.showRewarded on reddit would give stronger confidence that withTargetAvailability actually blocks these for the disabled effective config, rather than relying on them simply never being called.

Also applies to: 307-320

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/smoke/adapter-effective-config.ts` around lines 174 - 212,
verifyDevvitAdapter only checks the enabled leaderboard.submitScore path, so add
explicit assertions that disabled reddit capabilities are blocked and not
delegated. In verifyDevvitAdapter, use gateway.commerce.purchase and
gateway.ads.showRewarded with reddit-effective config, then assert they reject
and that createRecordingBridge does not record those method calls; this should
mirror the enabled-path coverage already used in the capacitor test and validate
withTargetAvailability behavior directly.
tools/validate-public-readiness.ts (1)

49-49: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Also add .devvit/ to blockedTrackedGeneratedPrefixes. It holds local Devvit auth state (~/.devvit/token), so a committed copy should fail the public-readiness check too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/validate-public-readiness.ts` at line 49, The public-readiness check
currently ignores `.devvit/` in the tracked generated paths, so add `.devvit/`
to blockedTrackedGeneratedPrefixes in validate-public-readiness.ts. Update the
logic around the prefix list so committed copies of local Devvit auth state are
treated as blocked by the public-readiness validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release-ait.yml:
- Around line 19-21: The release-ait workflow’s actions/checkout step is missing
the credential-hardening setting used elsewhere in this PR. Update the checkout
configuration in the release workflow to include persist-credentials: false
alongside fetch-depth: 0 so the GitHub token is not left in local git config;
use the existing actions/checkout step in the release-ait workflow as the place
to apply this change.

In @.github/workflows/release-android.yml:
- Around line 19-22: The checkout step in the Android release workflow is
missing the same credential-hardening used elsewhere. Update the
`actions/checkout` usage in this workflow to set `persist-credentials: false`,
matching the pattern already used in `release-devvit.yml` and `release.yml`, so
the `checkout` step does not retain credentials after cloning.

In @.github/workflows/release-ios.yml:
- Around line 22-26: The checkout step in the release-ios workflow is missing
the same credential hardening used in the other release workflows. Update the
actions/checkout invocation in this workflow to set persist-credentials to
false, matching the existing checkout configuration pattern in the related
release workflows.

In `@apps/target-devvit/src/server/index.ts`:
- Around line 533-547: `currentDisplayName()` is reading a server-context
`username` field that does not exist, so it always falls back instead of
returning the real display name. Update the display-name lookup in
`currentDisplayName` to use `context.reddit.getCurrentUsername()` and make the
function async if needed, then adjust the `identity.getPlayer` call path to
await this lookup and pass the fetched username through instead of relying on
`context.username`.
- Around line 21-63: The /internal/menu/create-post handler in app.post is
exposed over HTTP and currently calls reddit.submitCustomPost without verifying
the caller is authorized. Add an explicit authorization gate before the
create-post logic runs, using the request context available in this route and
the existing currentSubredditName check to ensure only valid menu invocations or
moderators can proceed. If the caller is not authorized, return a neutral
UiResponse and skip the reddit.submitCustomPost call. Keep the existing success
and error handling in the same handler after the authz check.
- Around line 446-458: The leaderboard lock setup in acquireLeaderboardLock
currently passes a Date from leaderboardLockExpirationDate into redis.set, but
Devvit Redis expects a numeric TTL in seconds for expiration. Update the lock
acquisition flow to pass the TTL as a number (using leaderboardLockTtlSeconds
directly or an equivalent numeric helper) and remove the Date-based helper so
the lock lifetime is applied correctly.

In `@tools/smoke/embedded-target-config.ts`:
- Around line 131-137: The failure check in the command execution path is gating
on stdout instead of the exit code, which can miss real unzip errors. Update the
error handling around the command/result check in embedded-target-config.ts to
treat nonzero status as failure by default and only allow the specific benign
warning exit codes you intentionally want to ignore; use the existing
result.status, result.stdout, and result.stderr handling in this block to narrow
the tolerated cases.

---

Nitpick comments:
In @.github/workflows/build-target.yml:
- Around line 68-74: The upload-artifact step in the build target workflow
always includes the Devvit dist glob, which causes unnecessary warnings on
non-reddit targets. Scope the `apps/target-devvit/dist/**` path so it is only
added when the target is `reddit`, keeping the existing
`actions/upload-artifact@v6` step and other artifact paths unchanged.

In @.github/workflows/release-devvit.yml:
- Around line 41-57: The release workflow is doing duplicate preflight
validation before build:target because buildTarget still runs its own checks. In
the release-devvit workflow, set MPGD_SKIP_BUILD_TARGET_PREFLIGHT=1 for the pnpm
build:target reddit step so the already-run validations are not repeated. Use
the buildTarget path in tools/target/build-target.ts and the existing release
workflow build step as the key places to align behavior.

In `@native-plugins/capacitor-game-services/Package.swift`:
- Around line 19-20: The Package.swift dependency list includes an unused
Cordova product for the CapacitorGameServices plugin. Remove the Cordova package
product from the target dependencies in Package.swift, keeping only the
Capacitor dependency used by CapacitorGameServicesPlugin.swift, and verify the
package still builds with the remaining imports.

In `@packages/game-services/src/types.ts`:
- Around line 10-13: The ledger-target set is duplicated between
GameServicesLedgerTarget and the LeaderboardLedgerTarget/assertLedgerTarget
logic, so future platform changes can drift. Centralize the canonical
ledger-target literals in `@mpgd/platform` by exporting a shared literal
array/type and shared assertion, then update the types.ts definition and the
backend/leaderboard-ledger/src/index.ts target check to consume those shared
symbols instead of maintaining separate literal unions/manual comparisons.

In `@tools/smoke/adapter-effective-config.ts`:
- Around line 174-212: verifyDevvitAdapter only checks the enabled
leaderboard.submitScore path, so add explicit assertions that disabled reddit
capabilities are blocked and not delegated. In verifyDevvitAdapter, use
gateway.commerce.purchase and gateway.ads.showRewarded with reddit-effective
config, then assert they reject and that createRecordingBridge does not record
those method calls; this should mirror the enabled-path coverage already used in
the capacitor test and validate withTargetAvailability behavior directly.

In `@tools/target/build-target.ts`:
- Around line 130-141: Move the MPGD_RUN_IOS_ARCHIVE and
MPGD_RUN_IOS_SIMULATOR_BUILD mutual-exclusion check in the capacitor-ios branch
of build-target.ts so it runs before any side effects like
ensureCapacitorPlatform and run('pnpm', ['--dir', shellApp, 'cap', 'sync',
'ios'], env). Keep the check near the top of the case block, using the existing
targetName/shellApp logic only after the invariant is validated, so
misconfigured env fails fast before native tooling work starts.

In `@tools/validate-public-readiness.ts`:
- Line 49: The public-readiness check currently ignores `.devvit/` in the
tracked generated paths, so add `.devvit/` to blockedTrackedGeneratedPrefixes in
validate-public-readiness.ts. Update the logic around the prefix list so
committed copies of local Devvit auth state are treated as blocked by the
public-readiness validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b558905-718c-438c-a9dc-baf8bf771391

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6b17f and 1d802ea.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (64)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/workflows/build-target.yml
  • .github/workflows/ci.yml
  • .github/workflows/release-ait.yml
  • .github/workflows/release-android.yml
  • .github/workflows/release-devvit.yml
  • .github/workflows/release-ios.yml
  • .github/workflows/release.yml
  • .gitignore
  • .mcp.json
  • .mise.toml
  • README.md
  • adapters/devvit/CHANGELOG.md
  • adapters/devvit/package.json
  • adapters/devvit/src/index.test.ts
  • adapters/devvit/src/index.ts
  • adapters/devvit/tsconfig.json
  • apps/game-phaser/package.json
  • apps/game-phaser/src/platform/demoGameServices.ts
  • apps/game-phaser/src/platform/installPlatform.ts
  • apps/game-phaser/src/platform/runtimeDetector.test.ts
  • apps/game-phaser/src/platform/runtimeDetector.ts
  • apps/game-phaser/src/scenes/ResultScene.ts
  • apps/mobile-capacitor/ios/App/App.xcodeproj/project.pbxproj
  • apps/mobile-capacitor/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
  • apps/target-devvit/README.md
  • apps/target-devvit/devvit.json
  • apps/target-devvit/package.json
  • apps/target-devvit/src/server/index.ts
  • apps/target-devvit/tsconfig.json
  • apps/target-devvit/vite.server.config.ts
  • backend/leaderboard-ledger/src/index.ts
  • docs/AGENTIC_GAME_WORKFLOW.md
  • docs/GAME_DEVELOPMENT.md
  • docs/PUBLIC_RELEASE_CHECKLIST.md
  • docs/ROADMAP_CHECKLIST.md
  • examples/phaser-starter/agent/game.manifest.json
  • native-plugins/capacitor-game-services/Package.swift
  • package.json
  • packages/game-services/src/client.test.ts
  • packages/game-services/src/client.ts
  • packages/game-services/src/server.test.ts
  • packages/game-services/src/types.ts
  • packages/target-config/src/runtime.ts
  • packages/target-config/targets.json
  • platform.targets.json
  • tools/package/build-packages.ts
  • tools/package/workspace.ts
  • tools/smoke/adapter-effective-config.ts
  • tools/smoke/build-target-artifacts.ts
  • tools/smoke/effective-target-config.ts
  • tools/smoke/embedded-target-config.ts
  • tools/smoke/target-config-runtime.ts
  • tools/smoke/verify-target-artifacts.ts
  • tools/target/build-target.ts
  • tools/target/effective-config.ts
  • tools/target/generate-release-manifest.ts
  • tools/target/platform-targets.ts
  • tools/target/schemas.ts
  • tools/target/validate-target-config.ts
  • tools/validate-public-readiness.ts
  • tools/validate-starter-workflow.ts
  • tools/validate-target-config.ts
  • tsconfig.graph.json
🛑 Comments failed to post (7)
.github/workflows/release-ait.yml (1)

19-21: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Checkout step missing persist-credentials: false.

Static analysis (zizmor) flags credential persistence through artifacts here. Every other updated workflow in this PR (build-target.yml, ci.yml) sets persist-credentials: false on actions/checkout, but this one only sets fetch-depth: 0, leaving the GitHub token persisted in the local git config for the remainder of the job.

Suggested fix
       - uses: actions/checkout@v5
         with:
           fetch-depth: 0
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      - uses: actions/checkout@v5
        with:
          fetch-depth: 0
          persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 19-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-ait.yml around lines 19 - 21, The release-ait
workflow’s actions/checkout step is missing the credential-hardening setting
used elsewhere in this PR. Update the checkout configuration in the release
workflow to include persist-credentials: false alongside fetch-depth: 0 so the
GitHub token is not left in local git config; use the existing actions/checkout
step in the release-ait workflow as the place to apply this change.

Source: Linters/SAST tools

.github/workflows/release-android.yml (1)

19-22: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Missing persist-credentials: false on checkout.

Sibling workflows touched in this same PR (release-devvit.yml, release.yml) explicitly set persist-credentials: false on every actions/checkout step, but this one doesn't. zizmor also flags this as a credential-persistence risk (artipacked).

🔒 Proposed fix
       - uses: actions/checkout@v5
         with:
           fetch-depth: 0
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      - uses: actions/checkout@v5
        with:
          fetch-depth: 0
          persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 19-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-android.yml around lines 19 - 22, The checkout
step in the Android release workflow is missing the same credential-hardening
used elsewhere. Update the `actions/checkout` usage in this workflow to set
`persist-credentials: false`, matching the pattern already used in
`release-devvit.yml` and `release.yml`, so the `checkout` step does not retain
credentials after cloning.

Source: Linters/SAST tools

.github/workflows/release-ios.yml (1)

22-26: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Missing persist-credentials: false on checkout.

Flagged by zizmor (artipacked): the credential is left persisted in the workspace after checkout, unlike the sibling release-devvit.yml and release.yml workflows updated in this PR which explicitly set persist-credentials: false on every checkout step.

🔒 Suggested fix
       - uses: actions/checkout@v5
         with:
           fetch-depth: 0
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0
          persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 23-25: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-ios.yml around lines 22 - 26, The checkout step in
the release-ios workflow is missing the same credential hardening used in the
other release workflows. Update the actions/checkout invocation in this workflow
to set persist-credentials to false, matching the existing checkout
configuration pattern in the related release workflows.

Source: Linters/SAST tools

apps/target-devvit/src/server/index.ts (3)

21-63: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Devvit's @devvit/web server framework restrict "/internal/*" routes to internal menu-action invocations only, or are they publicly reachable HTTP endpoints?

💡 Result:

Routes under the /internal/* path in a Devvit Web application are not automatically restricted to platform invocations and are publicly reachable HTTP endpoints [1]. While the Devvit platform uses these /internal/* endpoints to invoke app-specific logic—such as menu actions, form submissions, triggers, and scheduled tasks—they are exposed as standard HTTP routes on your server [2][3][4][5]. Because they are reachable via HTTP, any user or external actor capable of reaching your app's server URL can theoretically attempt to hit these endpoints [1]. Security best practices for Devvit applications dictate that you must manually implement authentication and authorization checks within your server-side handlers to ensure they are only executed by authorized users or the platform itself [1]. For example, if a menu action is intended for moderators only, the handler must explicitly verify the requester's permissions (e.g., calling a requireModerator function) rather than relying on the route's path or the app's configuration [1]. Relying solely on the platform's configuration or the route name for security is insufficient [1].

Citations:


Add an explicit authz check before creating the post. apps/target-devvit/src/server/index.ts:21-63 is reachable over HTTP, so anyone who can hit this endpoint can trigger reddit.submitCustomPost() unless the handler validates that the caller is an authorized menu invocation/moderator.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/target-devvit/src/server/index.ts` around lines 21 - 63, The
/internal/menu/create-post handler in app.post is exposed over HTTP and
currently calls reddit.submitCustomPost without verifying the caller is
authorized. Add an explicit authorization gate before the create-post logic
runs, using the request context available in this route and the existing
currentSubredditName check to ensure only valid menu invocations or moderators
can proceed. If the caller is not authorized, return a neutral UiResponse and
skip the reddit.submitCustomPost call. Keep the existing success and error
handling in the same handler after the authz check.

446-458: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@devvit/web redis SetOptions expiration field: does it accept a Date object or a duration in seconds/ms?

💡 Result:

In the Devvit Redis client, key expiration is handled using a duration in seconds, not a Date object [1]. When setting an expiration for a key, the expire method specifically expects a number representing the duration in seconds (e.g., await redis.expire(key, 3600) for one hour) [1]. Regarding the set operation, while the standard Devvit Redis interface provides a set(key: string, value: string, options?: SetOptions) method [1], the underlying implementation typically uses integer values for expiration arguments (often representing seconds) [2]. There is no indication that the Devvit SetOptions supports a Date object for expiration [1]. Developers should rely on providing numeric durations in seconds to meet the expire requirements [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate Redis SetOptions and related expiration handling in the repository.
git ls-files | rg -n '(^|/)(redis|Redis|setOptions|SetOptions|expiration)\.(ts|tsx|js|jsx|md)$|(^|/)server/index\.ts$' || true

echo '--- SEARCH: SetOptions definitions and expiration usage ---'
rg -n "interface SetOptions|type SetOptions|expiration\b|redis\.set\(" apps packages . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' || true

Repository: imjlk/mpgd-kit

Length of output: 857


🌐 Web query:

Devvit Redis SetOptions expiration Date set(key value options) type definition documentation

💡 Result:

In the Reddit Devvit framework, the redis.set method is used to store string values, and it supports optional parameters—including an expiration time—via an options object [1]. Based on implementation patterns in the Devvit Redis package, the set method signature generally follows this structure: await redis.set(key, value, options?); The options object supports an expiration field, which defines the time-to-live (TTL) for the key in seconds [1]. Example usage: await redis.set('myKey', 'myValue', { expiration: 60 }); // Key expires in 60 seconds [1] For managing expiration separately or updating it after a key has been created, Devvit also provides a dedicated expire method [2]: await redis.expire(key, seconds); Key technical details: - Expiration: When provided in the set options, the expiration value is interpreted in seconds [1]. - Alternatives: If you need to set expiration conditions (such as NX or XX, which are common in standard Redis), the current Devvit API usage commonly utilizes the explicit expire method for lifecycle management [2] or standard key-value setting [1]. While the internal Redis mocks in the Devvit codebase reference nx and xx flags, the primary documented approach for developers is to use the provided Redis client methods (set and expire) as exposed in the Devvit context [2][1]. Always ensure you are importing or accessing redis through the Devvit context (e.g., const { redis } = context) to ensure it is correctly scoped to your installation [2].

Citations:


🌐 Web query:

site:github.com/reddit/devvit "interface SetOptions" redis expiration Date seconds

💡 Result:

In the context of the Reddit Devvit framework's Redis client implementation, SetOptions is an interface used within the Redis abstraction [1]. When using this client, expiration for a value is typically set by providing a numeric value for the expiration property (representing seconds) in the request object [1]. For example, the internal mock implementation handles expiration as follows: if (request.expiration && request.expiration > 0) { args.push('EX', request.expiration); } The Devvit Redis client uses standard Redis commands (such as SET with EX options) [1], where the expiration duration is specified in seconds. While the framework supports standard Date objects in other areas of its API (such as scheduling jobs with runAt [2]), Redis expiration within the SetOptions pattern relies on a numeric integer representing seconds, not a Date object [1].

Citations:


🌐 Web query:

site:github.com/reddit/devvit packages/redis/src/test/mocks/RedisMock.ts expiration number EX request.expiration

💡 Result:

In the Reddit Devvit project's Redis mock implementation located at packages/redis/src/test/mocks/RedisMock.ts, the logic for handling key expiration during set operations is implemented as follows [1]: if (request.expiration && request.expiration > 0) { args.push('EX', request.expiration); } This code snippet checks if a valid expiration value (a number greater than 0) exists in the request object [1]. If it does, it appends the 'EX' command and the corresponding expiration value to the arguments array, effectively simulating the Redis EXPIRE behavior for the mocked command [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for the Redis client type and any expiration examples/usages.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  "expiration:\s*|interface\s+SetOptions|type\s+SetOptions|redis\.set\(" .

echo '--- potential Devvit redis package files ---'
fd -a 'RedisMock.ts|redis.*ts|SetOptions.*ts|index.d.ts' . || true

Repository: imjlk/mpgd-kit

Length of output: 501


Use a numeric TTL for expiration. Devvit Redis set expects the expiration value in seconds (EX), so passing a Date here can set the lock to the wrong lifetime or prevent it from expiring as intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/target-devvit/src/server/index.ts` around lines 446 - 458, The
leaderboard lock setup in acquireLeaderboardLock currently passes a Date from
leaderboardLockExpirationDate into redis.set, but Devvit Redis expects a numeric
TTL in seconds for expiration. Update the lock acquisition flow to pass the TTL
as a number (using leaderboardLockTtlSeconds directly or an equivalent numeric
helper) and remove the Date-based helper so the lock lifetime is applied
correctly.

533-547: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@devvit/web server context object: does it expose userId and username fields directly, or is a separate Reddit API call required to get the current username?

💡 Result:

In the Devvit framework, the context object does not expose username information directly. While context.userId is available on the context object (and is present only when a user is logged in), the username is not [1]. To retrieve the current user's username, you must perform an asynchronous call to the Reddit API using context.reddit.getCurrentUsername() [2][3]. The pattern typically involves checking the current user's identity only when needed, as shown in the following example: const username = await context.reddit.getCurrentUsername; if (username) { // Use the username }

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the reported lines.
sed -n '500,580p' apps/target-devvit/src/server/index.ts

# Find where `context` is typed or imported in this file.
rg -n "context|username|userId|currentPlayerId|currentDisplayName|reddit.getCurrentUsername" apps/target-devvit/src/server/index.ts

# Search the repo for explicit Devvit context typing and username access patterns.
rg -n "getCurrentUsername|context\.username|context\.userId|readonly username\?:|readonly userId\?:|Context" apps/target-devvit/src -g '!**/dist/**' -g '!**/build/**'

Repository: imjlk/mpgd-kit

Length of output: 3384


Replace context.username with context.reddit.getCurrentUsername()
context.userId is fine, but context.username isn’t a server-context field. As written, currentDisplayName() will fall back to playerId, so identity.getPlayer can only return a real display name if you fetch it asynchronously from context.reddit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/target-devvit/src/server/index.ts` around lines 533 - 547,
`currentDisplayName()` is reading a server-context `username` field that does
not exist, so it always falls back instead of returning the real display name.
Update the display-name lookup in `currentDisplayName` to use
`context.reddit.getCurrentUsername()` and make the function async if needed,
then adjust the `identity.getPlayer` call path to await this lookup and pass the
fetched username through instead of relying on `context.username`.
tools/smoke/embedded-target-config.ts (1)

131-137: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

unzip exit status codes meaning warning error severe

💡 Result:

The unzip utility uses specific exit status codes to indicate the outcome of an operation. On most systems (excluding VMS), these codes generally approximate the standards defined by PKWARE [1][2][3]. Common exit status codes include: 0: The operation was successful [4][5]. 1: One or more files were processed successfully, but some errors or warnings occurred [4][5]. 2: A generic error in the zipfile format was detected; processing may have completed successfully, but a warning was generated [4][5]. 3: A severe error in the zipfile format was detected; processing likely failed immediately [4][5]. 4: Unable to allocate memory for buffers during initialization [4][5]. 5: Unable to allocate memory or obtain a TTY to read a decryption password [4][5]. 6: Unable to allocate memory during decompression to disk [4][5]. 9: The specified zipfiles were not found [4][5]. 10: Invalid options were specified on the command line [6][4][5]. 11: No matching files were found [6][4][5]. 50: The disk is, or was, full during extraction [6][4][5]. 51: The end of the ZIP archive was encountered prematurely [6][4][5]. 80: The user aborted the operation (e.g., via Ctrl+C) [6][4][5]. 81: Testing or extraction failed due to unsupported compression methods or decryption [6][4][5]. 82: No files were found due to an incorrect decryption password [4][5]. Note that some distributions or compilation configurations may provide additional human-readable explanations if the RETURN_CODES option is defined [1][2][3]. VMS systems map these standard codes to specific VMS-style status codes [1][2][3].

Citations:


🏁 Script executed:

sed -n '1,220p' tools/smoke/embedded-target-config.ts

Repository: imjlk/mpgd-kit

Length of output: 4031


Zip failures need to be gated on the exit code, not stdout

unzip can emit partial output before fatal failures, so status !== 0 && stdout.length > 0 still lets corrupt archives, disk-full, EOF, abort, and decryption failures slip through. Narrow this to the specific benign warning codes you intend to tolerate.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/smoke/embedded-target-config.ts` around lines 131 - 137, The failure
check in the command execution path is gating on stdout instead of the exit
code, which can miss real unzip errors. Update the error handling around the
command/result check in embedded-target-config.ts to treat nonzero status as
failure by default and only allow the specific benign warning exit codes you
intentionally want to ignore; use the existing result.status, result.stdout, and
result.stderr handling in this block to narrow the tolerated cases.

@imjlk

imjlk commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Patched the post-merge PR #3 review follow-up in 6927409:

  • Tightened embedded target config zip extraction so non-zero unzip results are rejected unless they match the narrow Apps in Toss .ait prefix-warning case: extra bytes at beginning or within zipfile plus attempting to process anyway.
  • Kept corrupt archive/CRC style failures as hard smoke failures even if partial stdout is emitted.
  • Changed web-like directory artifact smoke validation to read from the release manifest artifact path instead of targetConfig.webDir:
    • AIT skip-mode directory artifacts now validate the advertised manifest artifact itself.
    • Devvit client/server checks now validate under the manifest artifact directory.
  • Ran open-code-review on the local patch; it raised one clarity suggestion around spawnSync.status, which was folded in by handling signal/null status explicitly and typing the warning helper with number.

Validation run:

  • pnpm exec ttsc --noEmit -p tsconfig.tools.json
  • MPGD_PLATFORM_TARGETS_FILE=/Users/imjlk/repos/imjlk/game-a/mpgd.targets.json pnpm smoke:target ait reddit
  • pnpm check
  • PR4 CI passed: validate, build-android, build-ios, build-ait, build-devvit, CodeRabbit.

Current PR state: clean and mergeable.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 6927409473

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant