From dbd4da0d3b1c8177db829cca5b36add6c01e85a8 Mon Sep 17 00:00:00 2001 From: Terraform Date: Fri, 13 Feb 2026 00:28:14 +0000 Subject: [PATCH 1/7] Fix release workflow to trigger production deployment --- .github/workflows/aws_auto_release.yml | 68 +++++++++++++------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/aws_auto_release.yml b/.github/workflows/aws_auto_release.yml index c265155..f536d63 100644 --- a/.github/workflows/aws_auto_release.yml +++ b/.github/workflows/aws_auto_release.yml @@ -8,7 +8,7 @@ on: concurrency: group: ${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - + permissions: contents: write pull-requests: read @@ -30,10 +30,10 @@ jobs: run: | merged_by="${{ github.event.pull_request.merged_by.login }}" echo "PR was merged by: $merged_by" - + # Get authorized users from CODEOWNERS file authorized_users=() - + # Read CODEOWNERS file if it exists if [[ -f ".github/CODEOWNERS" ]]; then echo "[INFO] Reading CODEOWNERS file..." @@ -46,21 +46,21 @@ jobs: else echo "[WARN] No CODEOWNERS file found" fi - + # Get repository collaborators with admin/maintain permissions using GitHub API echo "[CHECK] Checking repository permissions..." - + # Check if user has admin or maintain permissions user_permission=$(curl -s -H "Authorization: token ${{ secrets.PAT }}" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/collaborators/$merged_by/permission" | \ jq -r '.permission // "none"') - + echo "User $merged_by has permission level: $user_permission" - + # Check if user is authorized is_authorized=false - + # Check if user is in CODEOWNERS for user in "${authorized_users[@]}"; do if [[ "$user" == "$merged_by" ]]; then @@ -69,37 +69,37 @@ jobs: break fi done - + # Check if user has admin or maintain permissions if [[ "$user_permission" == "admin" || "$user_permission" == "maintain" ]]; then is_authorized=true echo "[OK] User $merged_by is authorized via repository permissions ($user_permission)" fi - + # Check if user is organization owner (for metaversecloud-com org) org_response=$(curl -s -H "Authorization: token ${{ secrets.PAT }}" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/orgs/metaversecloud-com/members/$merged_by" \ -w "%{http_code}") - + # Extract HTTP status code from the response http_code=${org_response: -3} - + if [[ "$http_code" == "200" ]]; then # Check if user is an owner owner_status=$(curl -s -H "Authorization: token ${{ secrets.PAT }}" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/orgs/metaversecloud-com/memberships/$merged_by" | \ jq -r '.role // "none"') - + if [[ "$owner_status" == "admin" ]]; then is_authorized=true echo "[OK] User $merged_by is authorized as organization owner" fi fi - + echo "is_authorized=$is_authorized" >> $GITHUB_OUTPUT - + if [[ "$is_authorized" == "false" ]]; then echo "[ERROR] User $merged_by is not authorized to trigger releases" echo "[TIP] Authorized users include:" @@ -117,16 +117,16 @@ jobs: run: | labels='${{ toJson(github.event.pull_request.labels.*.name) }}' echo "PR Labels: $labels" - + has_release_label=false has_major=false has_minor=false has_patch=false - + # Check if release label exists if echo "$labels" | grep -q "release"; then has_release_label=true - + # Check for each type of version bump if echo "$labels" | grep -q "major"; then has_major=true @@ -137,13 +137,13 @@ jobs: if echo "$labels" | grep -q "patch"; then has_patch=true fi - + # If no specific version type is specified, default to patch if [[ "$has_major" == "false" && "$has_minor" == "false" && "$has_patch" == "false" ]]; then has_patch=true fi fi - + echo "should_release=$has_release_label" >> $GITHUB_OUTPUT echo "has_major=$has_major" >> $GITHUB_OUTPUT echo "has_minor=$has_minor" >> $GITHUB_OUTPUT @@ -163,19 +163,19 @@ jobs: run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" - + # Get the latest tag from git latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") echo "Latest git tag: $latest_tag" - + # Remove 'v' prefix if present current_version=${latest_tag#v} echo "Current version: $current_version" - + # Parse current version IFS='.' read -r major minor patch <<< "$current_version" echo "Parsed version - Major: $major, Minor: $minor, Patch: $patch" - + # Apply cumulative version bumps if [[ "${{ steps.check.outputs.has_major }}" == "true" ]]; then major=$((major + 1)) @@ -183,7 +183,7 @@ jobs: patch=0 # Reset patch when major is bumped echo "Applied major bump: $major.0.0" fi - + if [[ "${{ steps.check.outputs.has_minor }}" == "true" ]]; then minor=$((minor + 1)) if [[ "${{ steps.check.outputs.has_major }}" != "true" ]]; then @@ -191,23 +191,23 @@ jobs: fi echo "Applied minor bump: $major.$minor.$patch" fi - + if [[ "${{ steps.check.outputs.has_patch }}" == "true" ]]; then patch=$((patch + 1)) echo "Applied patch bump: $major.$minor.$patch" fi - + new_version="$major.$minor.$patch" echo "Final calculated version: $new_version" - + # Create package.json if it doesn't exist if [[ ! -f "package.json" ]]; then echo '{"version": "0.0.0"}' > package.json fi - + # Update package.json with new version npm version $new_version --no-git-tag-version --allow-same-version - + echo "NEW_VERSION=v$new_version" >> $GITHUB_ENV echo "New version will be: v$new_version" @@ -222,18 +222,18 @@ jobs: make_latest: true body: | ## ? Release ${{ env.NEW_VERSION }} - + **Version Bumps Applied:** - Major: ${{ steps.check.outputs.has_major }} - Minor: ${{ steps.check.outputs.has_minor }} - Patch: ${{ steps.check.outputs.has_patch }} - + **Triggered by:** PR #${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }} **Merged by:** @${{ github.event.pull_request.merged_by.login }} - + ### Changes in this PR ${{ github.event.pull_request.body }} - + --- *This release was automatically created by the Auto Release workflow* From aefc291b4b30a36b9c93f4da5831df81c5708673 Mon Sep 17 00:00:00 2001 From: Terraform Date: Fri, 13 Feb 2026 00:30:10 +0000 Subject: [PATCH 2/7] Add production release CICD --- .github/workflows/aws_dev_release.yml | 47 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/workflows/aws_dev_release.yml b/.github/workflows/aws_dev_release.yml index 46ddb74..25971ed 100644 --- a/.github/workflows/aws_dev_release.yml +++ b/.github/workflows/aws_dev_release.yml @@ -4,7 +4,7 @@ on: branches: - dev workflow_dispatch: - + env: REPOSITORY: 'sdk-example' ECS_Cluster: "topia-dev-sdk-apps" @@ -14,7 +14,7 @@ env: concurrency: group: ${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - + permissions: id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: get servicename id: sername run: | @@ -42,27 +42,35 @@ jobs: echo "service=$service_value" >> "$GITHUB_OUTPUT" echo "Service value: $(echo $service_value | jq -c )" fi - + - uses: actions/setup-node@v4 with: node-version: 20.10 cache: 'npm' - run: npm i - run: CI=false npm run build - + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - + with: + platforms: arm64,amd64 + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::368076259134:role/github-actions-role aws-region: us-east-1 - + - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + buildkitd-flags: --debug + driver-opts: image=moby/buildkit:latest + - name: Image Metadata id: metadata uses: docker/metadata-action@v5 @@ -71,15 +79,28 @@ jobs: tags: | type=raw,value=${{ github.event.repository.name }} - - name: Build and tag - run: | - docker build --build-arg COMMIT_HASH=${{ github.sha }} --build-arg REF=${{ github.ref }} -t ${{ steps.metadata.outputs.tags }} . - + + + - name: Build and push multi-platform images to ECR + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + build-args: | + REF=${{ github.ref }} + COMMIT_HASH=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + sbom: false + - name: push docker image to Amazon ECR run: | docker push ${{ steps.metadata.outputs.tags }} - + deploy_matrix: runs-on: ubuntu-latest needs: Build @@ -96,4 +117,4 @@ jobs: - name: deploy run: | aws ecs update-service --cluster ${{ env.ECS_Cluster }} --service topia-${{ env.ENV }}-${{ matrix.service }}0 --force-new-deployment - + From 4cf89acc23decc0e242b20ee969ec8b7020cba19 Mon Sep 17 00:00:00 2001 From: Terraform Date: Fri, 13 Feb 2026 00:32:14 +0000 Subject: [PATCH 3/7] Add production release CICD --- .github/workflows/aws_prod_release.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/aws_prod_release.yml b/.github/workflows/aws_prod_release.yml index 6f60adf..dd2191f 100644 --- a/.github/workflows/aws_prod_release.yml +++ b/.github/workflows/aws_prod_release.yml @@ -12,22 +12,22 @@ env: concurrency: group: ${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - + permissions: id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout - jobs: Build: runs-on: ubuntu-latest outputs: service: ${{ steps.sername.outputs.service }} steps: + - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.release.tag_name }} - + - name: get servicename id: sername run: | @@ -53,18 +53,18 @@ jobs: - run: npm version --no-git-tag-version --workspaces --include-workspace-root true ${{ github.event.release.tag_name }} - run: npm i - run: CI=false npm run build - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64,amd64 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + with: + platforms: arm64,amd64 + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::368076259134:role/github-actions-role aws-region: us-east-1 - + - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 @@ -98,7 +98,7 @@ jobs: cache-to: type=gha,mode=max provenance: false sbom: false - + deploy_matrix: runs-on: ubuntu-latest needs: Build @@ -114,4 +114,5 @@ jobs: - name: deploy run: | - aws ecs update-service --cluster ${{ env.ECS_Cluster }} --service topia-${{ env.ENV }}-${{ matrix.service }}0 --force-new-deployment \ No newline at end of file + aws ecs update-service --cluster ${{ env.ECS_Cluster }} --service topia-${{ env.ENV }}-${{ matrix.service }}0 --force-new-deployment + From 1333c3a6bfffd7bb49574bf346bdac48fa4b9d79 Mon Sep 17 00:00:00 2001 From: Terraform Date: Fri, 13 Feb 2026 01:25:41 +0000 Subject: [PATCH 4/7] Add production release CICD --- .github/workflows/aws_prod_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/aws_prod_release.yml b/.github/workflows/aws_prod_release.yml index dd2191f..670d7b2 100644 --- a/.github/workflows/aws_prod_release.yml +++ b/.github/workflows/aws_prod_release.yml @@ -56,8 +56,8 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3.0.0 - with: - platforms: arm64,amd64 + with: + platforms: arm64,amd64 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 From fae51bee12d2f87fdcba4f77d5135347b953df02 Mon Sep 17 00:00:00 2001 From: Terraform Date: Fri, 13 Feb 2026 01:25:44 +0000 Subject: [PATCH 5/7] Add production release CICD --- .github/workflows/aws_dev_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/aws_dev_release.yml b/.github/workflows/aws_dev_release.yml index 25971ed..9d7a871 100644 --- a/.github/workflows/aws_dev_release.yml +++ b/.github/workflows/aws_dev_release.yml @@ -52,8 +52,8 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - with: - platforms: arm64,amd64 + with: + platforms: arm64,amd64 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 From 77bf35b598b368cdd2ad0b486a561511e83cfc6d Mon Sep 17 00:00:00 2001 From: Terraform Date: Fri, 13 Feb 2026 02:03:09 +0000 Subject: [PATCH 6/7] Add production release CICD --- .github/workflows/aws_dev_release.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/aws_dev_release.yml b/.github/workflows/aws_dev_release.yml index 9d7a871..602a41e 100644 --- a/.github/workflows/aws_dev_release.yml +++ b/.github/workflows/aws_dev_release.yml @@ -95,11 +95,6 @@ jobs: cache-to: type=gha,mode=max provenance: false sbom: false - - - name: push docker image to Amazon ECR - run: | - docker push ${{ steps.metadata.outputs.tags }} - deploy_matrix: runs-on: ubuntu-latest From cbc9d386b19a3dfbde74ffa87520572df34a7e48 Mon Sep 17 00:00:00 2001 From: Lina Date: Tue, 17 Feb 2026 16:28:41 -0800 Subject: [PATCH 7/7] Add inventory cache with 6h TTL and forceRefreshInventory support Cache ecosystem inventory items in memory to reduce API calls. Both getInventoryItems and awardBadge now use the shared cache. The client reads forceRefreshInventory from URL search params and passes it through to the server, which bypasses the cache when set to true. Co-Authored-By: Claude Opus 4.6 --- client/src/pages/Home.jsx | 7 +++- client/src/utils/loadGameState.js | 4 +- server/controllers/handleLoadGameState.js | 3 +- server/utils/badges/awardBadge.js | 8 ++-- server/utils/badges/getInventoryItems.js | 23 +++--------- server/utils/inventoryCache.js | 45 +++++++++++++++++++++++ 6 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 server/utils/inventoryCache.js diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx index 7433535..9c8a562 100644 --- a/client/src/pages/Home.jsx +++ b/client/src/pages/Home.jsx @@ -1,4 +1,5 @@ import { useContext, useState, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; // components import { @@ -22,13 +23,17 @@ import { backendAPI, loadGameState } from "@utils"; function Home() { const dispatch = useContext(GlobalDispatchContext); const { screenManager } = useContext(GlobalStateContext); + + const [searchParams] = useSearchParams(); + const forceRefreshInventory = searchParams.get("forceRefreshInventory") === "true"; + const [loading, setLoading] = useState(true); useEffect(() => { const fetchGameState = async () => { try { setLoading(true); - await loadGameState(dispatch); + await loadGameState(dispatch, forceRefreshInventory); } catch (error) { console.error("error in loadGameState action"); } finally { diff --git a/client/src/utils/loadGameState.js b/client/src/utils/loadGameState.js index 2127347..ab59e7c 100644 --- a/client/src/utils/loadGameState.js +++ b/client/src/utils/loadGameState.js @@ -1,9 +1,9 @@ import { backendAPI, getErrorMessage } from "@utils"; import { LOAD_GAME_STATE, SCREEN_MANAGER, SET_ERROR } from "@context/types"; -export const loadGameState = async (dispatch) => { +export const loadGameState = async (dispatch, forceRefreshInventory) => { try { - const result = await backendAPI?.post("/race/game-state"); + const result = await backendAPI?.post("/race/game-state", { forceRefreshInventory }); if (result?.data?.success) { const { checkpointsCompleted, diff --git a/server/controllers/handleLoadGameState.js b/server/controllers/handleLoadGameState.js index 295f27e..48f9662 100644 --- a/server/controllers/handleLoadGameState.js +++ b/server/controllers/handleLoadGameState.js @@ -12,6 +12,7 @@ export const handleLoadGameState = async (req, res) => { try { const credentials = getCredentials(req.query); const { profileId, urlSlug, sceneDropId } = credentials; + const forceRefresh = req.body?.forceRefreshInventory === true; const now = Date.now(); const world = await World.create(urlSlug, { credentials }); @@ -70,7 +71,7 @@ export const handleLoadGameState = async (req, res) => { const { visitor, visitorProgress, visitorInventory } = await getVisitor(credentials, true); let { checkpoints, startTimestamp } = visitorProgress; - const { badges } = await getInventoryItems(credentials); + const { badges } = await getInventoryItems(credentials, { forceRefresh }); return res.json({ checkpointsCompleted: checkpoints, diff --git a/server/utils/badges/awardBadge.js b/server/utils/badges/awardBadge.js index d36c9c4..fdc6c22 100644 --- a/server/utils/badges/awardBadge.js +++ b/server/utils/badges/awardBadge.js @@ -1,13 +1,11 @@ -import { Ecosystem } from "../topiaInit.js"; +import { getCachedInventoryItems } from "../inventoryCache.js"; export const awardBadge = async ({ credentials, visitor, visitorInventory, badgeName, redisObj, profileId }) => { try { if (visitorInventory[badgeName]) return { success: true }; - const ecosystem = await Ecosystem.create({ credentials }); - await ecosystem.fetchInventoryItems(); - - const inventoryItem = ecosystem.inventoryItems?.find((item) => item.name === badgeName); + const inventoryItems = await getCachedInventoryItems({ credentials }); + const inventoryItem = inventoryItems?.find((item) => item.name === badgeName); if (!inventoryItem) throw new Error(`Inventory item ${badgeName} not found in ecosystem`); await visitor.grantInventoryItem(inventoryItem, 1); diff --git a/server/utils/badges/getInventoryItems.js b/server/utils/badges/getInventoryItems.js index 55777a7..bd08e63 100644 --- a/server/utils/badges/getInventoryItems.js +++ b/server/utils/badges/getInventoryItems.js @@ -1,13 +1,11 @@ -import { Ecosystem } from "../index.js"; +import { getCachedInventoryItems } from "../inventoryCache.js"; -export const getInventoryItems = async (credentials) => { +export const getInventoryItems = async (credentials, { forceRefresh = false } = {}) => { try { - const ecosystem = await Ecosystem.create({ credentials }); - await ecosystem.fetchInventoryItems(); + const items = await getCachedInventoryItems({ credentials, forceRefresh }); const badges = {}; - - for (const item of ecosystem.inventoryItems) { + for (const item of items) { badges[item.name] = { id: item.id, name: item.name || "Unknown", @@ -16,18 +14,7 @@ export const getInventoryItems = async (credentials) => { }; } - // Sort items by sortOrder while keeping them as objects - const sortedBadges = {}; - - Object.values(badges) - .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)) - .forEach((badge) => { - sortedBadges[badge.name] = badge; - }); - - return { - badges: sortedBadges, - }; + return { badges }; } catch (error) { return standardizeError(error); } diff --git a/server/utils/inventoryCache.js b/server/utils/inventoryCache.js new file mode 100644 index 0000000..e3bd2d9 --- /dev/null +++ b/server/utils/inventoryCache.js @@ -0,0 +1,45 @@ +import { Ecosystem } from "./topiaInit.js"; + +// Cache duration: 6 hours in milliseconds +const CACHE_DURATION_MS = 6 * 60 * 60 * 1000; + +// In-memory cache +let inventoryCache = null; + +/** + * Get ecosystem inventory items with caching. + * - Returns cached data if available and not expired. + * - Refreshes cache if expired or missing. + * - Falls back to stale cache on API failure. + */ +export const getCachedInventoryItems = async ({ credentials, forceRefresh = false }) => { + try { + const now = Date.now(); + const isCacheValid = inventoryCache !== null && !forceRefresh && now - inventoryCache.timestamp < CACHE_DURATION_MS; + + if (isCacheValid) { + return inventoryCache.items; + } + + console.log("Fetching fresh inventory items from ecosystem"); + const ecosystem = await Ecosystem.create({ credentials }); + await ecosystem.fetchInventoryItems(); + + inventoryCache = { + items: [...ecosystem.inventoryItems].sort((a, b) => (a.metadata?.sortOrder ?? 0) - (b.metadata?.sortOrder ?? 0)), + timestamp: now, + }; + + return inventoryCache.items; + } catch (error) { + if (inventoryCache !== null) { + console.warn("Failed to fetch fresh inventory, using stale cache", error); + return inventoryCache.items; + } + throw error; + } +}; + +export const clearInventoryCache = () => { + inventoryCache = null; +};