diff --git a/.github/workflows/aws_auto_release.yml b/.github/workflows/aws_auto_release.yml new file mode 100644 index 0000000..d00ba20 --- /dev/null +++ b/.github/workflows/aws_auto_release.yml @@ -0,0 +1,239 @@ +name: Auto Release on Main Merge +on: + pull_request: + types: [closed] + branches: + - main + +concurrency: + group: ${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: read + +jobs: + auto_release: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + token: ${{ secrets.PAT }} + + - name: Check if user is authorized + id: auth_check + 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 "📋 Reading CODEOWNERS file..." + # Extract usernames from CODEOWNERS (remove @ prefix) + codeowners=$(grep -v '^#' .github/CODEOWNERS | grep -o '@[a-zA-Z0-9_-]*' | sed 's/@//' | sort -u) + for user in $codeowners; do + authorized_users+=("$user") + echo " - CODEOWNER: $user" + done + else + echo "⚠️ No CODEOWNERS file found" + fi + + # Get repository collaborators with admin/maintain permissions using GitHub API + echo "🔍 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 + is_authorized=true + echo "✅ User $merged_by is authorized via CODEOWNERS" + break + fi + done + + # Check if user has admin or maintain permissions + if [[ "$user_permission" == "admin" || "$user_permission" == "maintain" ]]; then + is_authorized=true + echo "✅ 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 "✅ User $merged_by is authorized as organization owner" + fi + fi + + echo "is_authorized=$is_authorized" >> $GITHUB_OUTPUT + + if [[ "$is_authorized" == "false" ]]; then + echo "❌ User $merged_by is not authorized to trigger releases" + echo "💡 Authorized users include:" + echo " - CODEOWNERS: ${authorized_users[*]}" + echo " - Repository admins and maintainers" + echo " - Organization owners" + exit 0 + else + echo "🎉 User $merged_by is authorized to trigger releases" + fi + + - name: Check for release labels and determine version bumps + if: steps.auth_check.outputs.is_authorized == 'true' + id: check + 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 + fi + if echo "$labels" | grep -q "minor"; then + has_minor=true + fi + 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 + echo "has_patch=$has_patch" >> $GITHUB_OUTPUT + echo "Should release: $has_release_label" + echo "Has major: $has_major, minor: $has_minor, patch: $has_patch" + + - name: Setup Node.js + if: steps.auth_check.outputs.is_authorized == 'true' && steps.check.outputs.should_release == 'true' + uses: actions/setup-node@v4 + with: + node-version: 20.10 + + - name: Calculate new version with cumulative bumps + if: steps.auth_check.outputs.is_authorized == 'true' && steps.check.outputs.should_release == 'true' + id: version + 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)) + minor=0 # Reset minor when major is bumped + 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 + patch=0 # Reset patch when minor is bumped (only if major wasn't bumped) + 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" + + - name: Create Release + if: steps.auth_check.outputs.is_authorized == 'true' && steps.check.outputs.should_release == 'true' + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.PAT }} # Use PAT to trigger other workflows + tag_name: ${{ env.NEW_VERSION }} + name: "Release ${{ env.NEW_VERSION }}" + generate_release_notes: true + 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* + diff --git a/.github/workflows/aws_prod_release.yml b/.github/workflows/aws_prod_release.yml index 43caaf6..3550e1a 100644 --- a/.github/workflows/aws_prod_release.yml +++ b/.github/workflows/aws_prod_release.yml @@ -50,7 +50,7 @@ jobs: cache: 'npm' - run: git config --global user.email devops@topia.io - run: git config --global user.name Devops - - run: npm version --workspaces --include-workspace-root true ${{ github.event.release.tag_name }} + - 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 diff --git a/package-lock.json b/package-lock.json index fc312e5..1938be2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "server" ], "dependencies": { - "@rtsdk/topia": "^0.15.8", + "@rtsdk/topia": "^0.17.6", "axios": "^1.6.8", "concurrently": "^8.2.2" }, @@ -1107,9 +1107,9 @@ ] }, "node_modules/@rtsdk/topia": { - "version": "0.15.8", - "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.15.8.tgz", - "integrity": "sha512-T0+pZxqMn6OcDzNIGk50B3lgRWoDBOXSmwShOepNGUyM9t7Yuu7NAMsYHKYEUfXN7X948bW5CetfwWk9Cd2K4w==" + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.17.6.tgz", + "integrity": "sha512-cEGPNW1k8cuBg+7COJhy+ga6jxWm/BD27eGXAE0YCuhky9VRJ/sP1dTtB6PFONlBq9wfbzDP7lqMcAaFr+Jz5Q==" }, "node_modules/@scene-swapper/client": { "resolved": "client", diff --git a/package.json b/package.json index 3c99b1b..c1ec080 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dev-client": "npm run dev --prefix client" }, "dependencies": { - "@rtsdk/topia": "^0.15.8", + "@rtsdk/topia": "^0.17.6", "axios": "^1.6.8", "concurrently": "^8.2.2" }, diff --git a/server/controllers/handleGetGameState.ts b/server/controllers/handleGetGameState.ts index 21c36db..bbb5096 100644 --- a/server/controllers/handleGetGameState.ts +++ b/server/controllers/handleGetGameState.ts @@ -7,10 +7,7 @@ import { DataObjectType, SceneType } from "../types.js"; export const handleGetGameState = async (req: Request, res: Response) => { try { const credentials = getCredentials(req.query); - const { assetId, urlSlug, visitorId } = credentials; - - const visitor: VisitorInterface = await Visitor.get(visitorId, urlSlug, { credentials }); - const { isAdmin } = visitor; + const { assetId, profileId, urlSlug, visitorId } = credentials; const droppedAsset = await DroppedAsset.get(assetId, urlSlug, { credentials }); if (!droppedAsset.dataObject || Object.keys(droppedAsset.dataObject).length === 0) { @@ -26,6 +23,18 @@ export const handleGetGameState = async (req: Request, res: Response) => { description, } = droppedAsset.dataObject as DataObjectType; + const visitor: VisitorInterface = await Visitor.get(visitorId, urlSlug, { credentials }); + const { isAdmin } = visitor; + + visitor.updatePublicKeyAnalytics([ + { + analyticName: `${allowNonAdmins ? "allowNonAdmins" : "adminsOnly"}-${isAdmin ? "admin" : "nonAdmin"}-starts`, + profileId, + uniqueKey: profileId, + urlSlug, + }, + ]); + const results = await Promise.allSettled(droppableSceneIds.map((sceneId) => Scene.get(sceneId, { credentials }))); const scenes: SceneType[] = results diff --git a/server/controllers/handleRemoveScene.ts b/server/controllers/handleRemoveScene.ts index 6734842..85f0bec 100644 --- a/server/controllers/handleRemoveScene.ts +++ b/server/controllers/handleRemoveScene.ts @@ -1,12 +1,28 @@ import { Request, Response } from "express"; import { errorHandler, getCredentials, removeScene } from "../utils/index.js"; +import { VisitorInterface } from "@rtsdk/topia"; +import { DroppedAsset, Visitor } from "../topiaInit.js"; +import { DataObjectType } from "../types.js"; export const handleRemoveScene = async (req: Request, res: Response) => { try { const credentials = getCredentials(req.query); + const { assetId, urlSlug, visitorId } = credentials; await removeScene(credentials); + const droppedAsset = await DroppedAsset.get(assetId, urlSlug, { credentials }); + const { allowNonAdmins } = droppedAsset.dataObject as DataObjectType; + + const visitor: VisitorInterface = await Visitor.get(visitorId, urlSlug, { credentials }); + const { isAdmin } = visitor; + visitor.updatePublicKeyAnalytics([ + { + analyticName: `${allowNonAdmins ? "allowNonAdmins" : "adminsOnly"}-${isAdmin ? "admin" : "nonAdmin"}-updates`, + urlSlug, + }, + ]); + return res.json({ success: true }); } catch (error) { return errorHandler({ diff --git a/server/utils/removeScene.ts b/server/utils/removeScene.ts index 9689ed2..abb2c0f 100644 --- a/server/utils/removeScene.ts +++ b/server/utils/removeScene.ts @@ -5,7 +5,7 @@ import { Credentials } from "../types.js"; export const removeScene = async (credentials: Credentials, persistentDroppedAssets?: string[]) => { try { - const { assetId, interactivePublicKey, sceneDropId, urlSlug } = credentials; + const { assetId, sceneDropId, urlSlug } = credentials; if (!sceneDropId) throw "A sceneDropId is required to remove a scene."; diff --git a/server/utils/swapScene.ts b/server/utils/swapScene.ts index 659b958..8f997fd 100644 --- a/server/utils/swapScene.ts +++ b/server/utils/swapScene.ts @@ -1,7 +1,7 @@ -import { World } from "../topiaInit.js"; +import { Visitor, World } from "../topiaInit.js"; import { errorHandler, removeScene } from "../utils/index.js"; import { Credentials, DataObjectType } from "../types.js"; -import { DroppedAssetInterface } from "@rtsdk/topia"; +import { DroppedAssetInterface, VisitorInterface } from "@rtsdk/topia"; export const swapScene = async ( credentials: Credentials, @@ -9,15 +9,27 @@ export const swapScene = async ( selectedSceneId?: string, ) => { try { - const { assetId, sceneDropId, urlSlug } = credentials; + const { assetId, profileId, sceneDropId, urlSlug, visitorId } = credentials; const { + allowNonAdmins, currentSceneIndex = 0, droppableSceneIds, persistentDroppedAssets = [], positionOffset = { x: 0, y: 0 }, } = droppedAsset.dataObject as DataObjectType; + const visitor: VisitorInterface = await Visitor.get(visitorId, urlSlug, { credentials }); + const { isAdmin } = visitor; + visitor.updatePublicKeyAnalytics([ + { + analyticName: `${allowNonAdmins ? "allowNonAdmins" : "adminsOnly"}-${isAdmin ? "admin" : "nonAdmin"}-updates`, + profileId, + uniqueKey: profileId, + urlSlug, + }, + ]); + const promises = []; const world = World.create(urlSlug, { credentials });