From b051ffc5f4d9e3b90d41cc9a745843878d88d69e Mon Sep 17 00:00:00 2001 From: "Frank Chiarulli Jr." Date: Sun, 30 Nov 2025 16:01:24 -0500 Subject: [PATCH] add optional repo checks to the game manifest --- action-deploy/dist/main.js | 16 ++++++++++++++++ action-deploy/src/index.ts | 17 +++++++++++++++++ api/src/game/manifest.ts | 4 ++++ .../v1/deployments/[deployment_name]/+server.ts | 17 +++++++++++++++++ web/static/manifest.schema.json | 15 +++++++++++++++ 5 files changed, 69 insertions(+) diff --git a/action-deploy/dist/main.js b/action-deploy/dist/main.js index 5ad6c6b..105cbad 100644 --- a/action-deploy/dist/main.js +++ b/action-deploy/dist/main.js @@ -34950,6 +34950,10 @@ var Manifest = object({ remix_of: object({ name: string2().nonempty().regex(/[a-zA-Z0-9_-]*/), version: ZodSemverUnbranded + }).optional(), + deployment: object({ + github_owner: string2().optional(), + github_repo: string2().optional() }).optional() }); var import_semver = __toESM2(require_semver2(), 1); @@ -48076,6 +48080,18 @@ async function run() { core5.info(`Found manifest for app ${manifest.name}`); core5.info(JSON.stringify(manifest, null, 2)); core5.endGroup(); + if (manifest.deployment) { + const githubRepo = process.env.GITHUB_REPOSITORY || ""; + const [actualOwner, actualRepo] = githubRepo.split("/"); + if (manifest.deployment.github_owner && manifest.deployment.github_owner !== actualOwner) { + core5.info(`⏭️ Deployment skipped: github_owner '${manifest.deployment.github_owner}' does not match '${actualOwner}'`); + return; + } + if (manifest.deployment.github_repo && manifest.deployment.github_repo !== actualRepo) { + core5.info(`⏭️ Deployment skipped: github_repo '${manifest.deployment.github_repo}' does not match '${actualRepo}'`); + return; + } + } const absoluteArtifactPath = resolve(workspace, artifactPath); if (!fs13.existsSync(`${absoluteArtifactPath}/index.html`)) { throw new Error(`Artifact folder ${artifactPath} does not contain an index.html file`); diff --git a/action-deploy/src/index.ts b/action-deploy/src/index.ts index a1ebc75..e8b55c1 100644 --- a/action-deploy/src/index.ts +++ b/action-deploy/src/index.ts @@ -54,6 +54,23 @@ export async function run(): Promise { core.info(`Found manifest for app ${manifest.name}`); core.info(JSON.stringify(manifest, null, 2)); core.endGroup(); + + // Check deployment restrictions + if (manifest.deployment) { + const githubRepo = process.env.GITHUB_REPOSITORY || ""; + const [actualOwner, actualRepo] = githubRepo.split("/"); + + if (manifest.deployment.github_owner && manifest.deployment.github_owner !== actualOwner) { + core.info(`⏭️ Deployment skipped: github_owner '${manifest.deployment.github_owner}' does not match '${actualOwner}'`); + return; + } + + if (manifest.deployment.github_repo && manifest.deployment.github_repo !== actualRepo) { + core.info(`⏭️ Deployment skipped: github_repo '${manifest.deployment.github_repo}' does not match '${actualRepo}'`); + return; + } + } + const absoluteArtifactPath = resolve(workspace, artifactPath); // ensure artfact folder has an index.html diff --git a/api/src/game/manifest.ts b/api/src/game/manifest.ts index 684c186..3d58957 100644 --- a/api/src/game/manifest.ts +++ b/api/src/game/manifest.ts @@ -41,6 +41,10 @@ export const Manifest = z.object({ .regex(/[a-zA-Z0-9_-]*/), version: ZodSemverUnbranded, }).optional(), + deployment: z.object({ + github_owner: z.string().optional(), + github_repo: z.string().optional(), + }).optional(), }); export type Manifest = z.infer; \ No newline at end of file diff --git a/web/src/routes/api/v1/deployments/[deployment_name]/+server.ts b/web/src/routes/api/v1/deployments/[deployment_name]/+server.ts index 788a38c..0056bf1 100644 --- a/web/src/routes/api/v1/deployments/[deployment_name]/+server.ts +++ b/web/src/routes/api/v1/deployments/[deployment_name]/+server.ts @@ -149,6 +149,23 @@ export const POST: RequestHandler = async ({ params, request, platform }) => { return jsonResponse({ error: 'Deployment name does not match object name in manifest' }, 400); } + // Check deployment restrictions + if (manifest.deployment) { + const [actualOwner, actualRepo] = auth.repository.split('/'); + + if (manifest.deployment.github_owner && manifest.deployment.github_owner !== actualOwner) { + return jsonResponse({ + error: `Deployment restricted: github_owner '${manifest.deployment.github_owner}' does not match '${actualOwner}'` + }, 403); + } + + if (manifest.deployment.github_repo && manifest.deployment.github_repo !== actualRepo) { + return jsonResponse({ + error: `Deployment restricted: github_repo '${manifest.deployment.github_repo}' does not match '${actualRepo}'` + }, 403); + } + } + let game = await Game.byName(deploymentName); if (game !== undefined && !game.matchesRepo(auth.repository)) { diff --git a/web/static/manifest.schema.json b/web/static/manifest.schema.json index 960dac4..203449a 100644 --- a/web/static/manifest.schema.json +++ b/web/static/manifest.schema.json @@ -131,6 +131,21 @@ "required": ["name", "version"], "additionalProperties": false, "description": "Reference to the original game if this is a remix" + }, + "deployment": { + "type": "object", + "properties": { + "github_owner": { + "type": "string", + "description": "GitHub owner (user or org) to restrict deployments to" + }, + "github_repo": { + "type": "string", + "description": "GitHub repository name to restrict deployments to" + } + }, + "additionalProperties": false, + "description": "Optional deployment restrictions. If set, deployments will be skipped when the GitHub repository doesn't match" } }, "required": ["name", "description", "visibility", "authors"],