Skip to content
Open
38 changes: 38 additions & 0 deletions src/lib/helpers/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export function getNestedRootDirectory(repository: string): string | null {
return match ? match[1] : null;
}

export function getBranchFromUrl(repository: string): string | null {
const match = repository.match(/\/tree\/([^/]+)/);
return match ? decodeURIComponent(match[1]) : null;
}

export function getRepositoryInfo(
repository: string
): { owner: string; name: string; url: string } | null {
Expand Down Expand Up @@ -54,3 +59,36 @@ export async function getDefaultBranch(owner: string, name: string): Promise<str
return null;
}
}

export async function getBranches(owner: string, name: string): Promise<string[] | null> {
try {
const branchesResponse = await fetch(
`https://api.github.com/repos/${owner}/${name}/branches`
);
if (!branchesResponse.ok) {
return null;
}

const branches = await branchesResponse.json();
return branches.map((branch) => branch.name);
} catch (error) {
console.error('Failed to fetch branches from GitHub:', error);
return null;
}
}

export async function validateBranch(
owner: string,
repo: string,
branch: string
): Promise<boolean> {
try {
const response = await fetch(
Copy link
Member

Choose a reason for hiding this comment

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

Why are we making calls to the GitHub API directly from our console? We should avoid adding business logic on the client, also we have to account for future support of other VCS providers, which the BE already has foundations for.

`https://api.github.com/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`
);
return response.ok;
} catch (error) {
console.error('Failed to validate branch from GitHub:', error);
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { iconPath } from '$lib/stores/app';
import type { PageData } from './$types';
import { getLatestTag } from '$lib/helpers/github';
import { getDefaultBranch, getBranches, validateBranch } from '$lib/helpers/github';
import { writable } from 'svelte/store';
import Link from '$lib/elements/link.svelte';
Expand All @@ -42,8 +42,9 @@
let selectedScopes = $state<string[]>([]);
let rootDir = $state(data.repository?.rootDirectory);
let variables = $state<Array<{ key: string; value: string; secret: boolean }>>([]);
let latestTag = $state(null);
let branches = $state<string[]>([]);
let selectedBranch = $state<string>('');
let loadingBranches = $state(false);
const specificationOptions = $derived(
data.specificationsList?.specifications?.map((size) => ({
Expand All @@ -63,7 +64,7 @@
})) || []
);
onMount(() => {
onMount(async () => {
const runtimeParam = data.runtime || page.url.searchParams.get('runtime') || 'node-18.0';
runtime = runtimeParam as Runtime;
Expand All @@ -76,23 +77,82 @@
specification = specificationOptions[0].value;
}
if (data.envKeys.length > 0) {
variables = data.envKeys.map((key) => ({ key, value: '', secret: false }));
// Initialize environment variables from query params (with prefilled values if provided)
if (data.envVars.length > 0) {
variables = data.envVars.map((env) => ({
key: env.key,
value: env.value,
secret: false
}));
}
getLatestTag(data.repository.owner, data.repository.name).then(
(tagName) => (latestTag = tagName)
);
// Load branches and set default branch
if (data.repository?.owner && data.repository?.name) {
Copy link
Member

Choose a reason for hiding this comment

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

This is coupling us very hard to GitHub, this logic has to be part of Utopia/VCS if its not already there, which is also very likely.

loadingBranches = true;
try {
// Check for branch param from URL
const branchParam = page.url.searchParams.get('branch');
const [branchList, defaultBranch, isBranchValid] = await Promise.all([
getBranches(data.repository.owner, data.repository.name),
getDefaultBranch(data.repository.owner, data.repository.name),
branchParam
? validateBranch(data.repository.owner, data.repository.name, branchParam)
: Promise.resolve(false)
]);
if (branchList && branchList.length > 0) {
branches = branchList;
if (branchParam && isBranchValid) {
// Use the provided branch if it's valid
selectedBranch = branchParam;
} else {
// Fall back to default branch, or first branch if default not found
selectedBranch =
defaultBranch && branchList.includes(defaultBranch)
? defaultBranch
: branchList[0];
}
} else {
// Branch list is empty or null
addNotification({
type: 'error',
message:
'Failed to load branches from repository. Please check the repository URL or try again.'
});
}
} catch (error) {
console.error('Failed to load branches:', error);
addNotification({
type: 'error',
message:
'Failed to load branches from repository. Please check the repository URL or try again.'
});
} finally {
loadingBranches = false;
}
} else {
// Repository info is missing
addNotification({
type: 'error',
message: 'Repository information is missing. Please check the repository URL.'
});
}
});
async function create() {
if (!selectedBranch || branches.length === 0) {
addNotification({
type: 'error',
message: 'Please wait for branches to load or check the repository URL.'
});
return;
}
$isSubmitting = true;
try {
if (!latestTag) {
latestTag = await getLatestTag(data.repository.owner, data.repository.name);
}
// Create function with configuration
const func = await sdk
.forProject(page.params.region, page.params.project)
Expand Down Expand Up @@ -126,16 +186,16 @@
await Promise.all(promises);
// Create deployment from GitHub repository using the latest tag
// Create deployment from GitHub repository using the selected branch
await sdk
.forProject(page.params.region, page.params.project)
.functions.createTemplateDeployment({
functionId: func.$id,
repository: data.repository.name,
owner: data.repository.owner,
rootDirectory: rootDir || '.',
type: Type.Tag,
reference: latestTag ?? '1.0.0',
type: Type.Branch,
reference: selectedBranch,
activate: true
});
Expand Down Expand Up @@ -220,6 +280,22 @@
</Layout.Stack>
</Fieldset>

<Fieldset legend="Git configuration">
<Layout.Stack gap="m">
<Input.Select
id="branch"
label="Branch"
required
placeholder={loadingBranches ? 'Loading branches...' : 'Select branch'}
bind:value={selectedBranch}
disabled={loadingBranches}
options={branches.map((branch) => ({
value: branch,
label: branch
}))} />
</Layout.Stack>
</Fieldset>

<Fieldset legend="Build configuration">
<Layout.Stack gap="m">
<Input.Text
Expand All @@ -243,7 +319,7 @@
</Layout.Stack>
</Fieldset>

{#if data.envKeys.length > 0}
{#if data.envVars.length > 0}
<Fieldset legend="Environment variables">
<Layout.Stack gap="m">
{#each variables as variable, i}
Expand Down Expand Up @@ -276,7 +352,12 @@
fullWidthMobile
submissionLoader
forceShowLoader={$isSubmitting}
disabled={!name || !runtime || !specification || $isSubmitting}>
disabled={!name ||
!runtime ||
!specification ||
!selectedBranch ||
branches.length === 0 ||
$isSubmitting}>
Deploy function
</Button>
</Layout.Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import { redirect, isRedirect } from '@sveltejs/kit';
import { base } from '$app/paths';
import type { PageLoad } from './$types';
import { getNestedRootDirectory, getRepositoryInfo } from '$lib/helpers/github';
import {
getNestedRootDirectory,
getRepositoryInfo,
getDefaultBranch,
validateBranch
} from '$lib/helpers/github';
import { ID, Runtime, Type } from '@appwrite.io/console';

export const load: PageLoad = async ({ url, params, parent }) => {
const { installations: vcsInstallations, runtimesList, specificationsList } = await parent();
const {
installations: vcsInstallations,
runtimesList,
specificationsList,
regionalConsoleVariables
} = await parent();

const repository = url.searchParams.get('repo') || url.searchParams.get('repository');

Expand All @@ -19,10 +30,112 @@ export const load: PageLoad = async ({ url, params, parent }) => {
}

const envParam = url.searchParams.get('env');
const envKeys = envParam ? envParam.split(',').map((key: string) => key.trim()) : [];

// Parse env vars - supports KEY or KEY=value format
const envVars: Array<{ key: string; value: string }> = envParam
? envParam.split(',').map((entry: string) => {
const trimmed = entry.trim();
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) {
return { key: trimmed, value: '' };
}
return {
key: trimmed.substring(0, eqIndex),
value: trimmed.substring(eqIndex + 1)
};
})
: [];

const runtime = url.searchParams.get('runtime');

// Quick mode - create function and redirect directly to function page
const quickMode = url.searchParams.get('quick') === 'true';

if (quickMode) {
try {
const runtimeParam = runtime || 'node-18.0';
const selectedRuntime = runtimeParam as Runtime;

const entrypoint = url.searchParams.get('entrypoint') || '';
const installCommand = url.searchParams.get('install') || '';
const rootDir =
getNestedRootDirectory(repository) || url.searchParams.get('rootDir') || '.';

// Get branch - validate provided or use default
const branchParam = url.searchParams.get('branch');
let selectedBranch: string;

if (branchParam) {
const isValid = await validateBranch(info.owner, info.name, branchParam);
if (isValid) {
selectedBranch = branchParam;
} else {
selectedBranch = (await getDefaultBranch(info.owner, info.name)) || 'main';
}
} else {
selectedBranch = (await getDefaultBranch(info.owner, info.name)) || 'main';
}

// Get first available specification
const specification =
specificationsList?.specifications?.[0]?.slug || 's-0.5vcpu-512mb';
Comment on lines +66 to +68
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 27, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if this specification slug is used elsewhere or defined as a constant
rg -n 's-0.5vcpu-512mb' --type ts

Repository: appwrite/console

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Read the file to see the context around lines 79-81
cat -n src/routes/\(console\)/project-\[region\]-\[project\]/functions/create-function/deploy/+page.ts | sed -n '60,100p'

Repository: appwrite/console

Length of output: 2099


🏁 Script executed:

#!/bin/bash
# Read the top of the file to see imports and variable definitions
cat -n src/routes/\(console\)/project-\[region\]-\[project\]/functions/create-function/deploy/+page.ts | head -80

Repository: appwrite/console

Length of output: 3522


🏁 Script executed:

#!/bin/bash
# Find the parent layout file - should be in src/routes/(console)
find src/routes -name '+layout.ts' -o -name '+layout.svelte' | head -20

Repository: appwrite/console

Length of output: 1394


🏁 Script executed:

#!/bin/bash
# Read the project-level layout file
cat -n src/routes/\(console\)/project-\[region\]-\[project\]/+layout.ts | head -150

Repository: appwrite/console

Length of output: 3874


🏁 Script executed:

#!/bin/bash
# Read the console-level layout file and search for specificationsList
rg -n 'specificationsList' src/routes/\(console\)/+layout.ts -A 5 -B 5

Repository: appwrite/console

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for specificationsList definition across the codebase
rg -n 'specificationsList' --type ts -B 3 -A 3

Repository: appwrite/console

Length of output: 13283


🏁 Script executed:

#!/bin/bash
# Read the functions layout file to see the full context
cat -n src/routes/\(console\)/project-\[region\]-\[project\]/functions/+layout.ts | head -35

Repository: appwrite/console

Length of output: 1165


🏁 Script executed:

#!/bin/bash
# Search for specification constants or defaults
rg -n 'specification' --type ts -i | grep -i 'const\|default\|fallback' | head -20

Repository: appwrite/console

Length of output: 1481


🏁 Script executed:

#!/bin/bash
# Search for constants files and any specification-related constants
find src -name '*const*' -type f | head -20

Repository: appwrite/console

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Read the constants file and search for specification-related content
rg -n 'specification' src/lib/constants.ts -i -A 2 -B 2

Repository: appwrite/console

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for other hardcoded values in the deploy page
rg -n 'node-18\|runtime\|slug' src/routes/\(console\)/project-\[region\]-\[project\]/functions/create-function/deploy/+page.ts -A 1 -B 1

Repository: appwrite/console

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look at the runtime fallback on line 56
sed -n '54,58p' src/routes/\(console\)/project-\[region\]-\[project\]/functions/create-function/deploy/+page.ts

Repository: appwrite/console

Length of output: 213


🏁 Script executed:

#!/bin/bash
# Search for types or documentation about specifications
rg -n 'SpecificationList|specification.*slug' --type ts -A 2 -B 2 | head -40

Repository: appwrite/console

Length of output: 1653


Based on my verification, I've found:

  1. The hardcoded slug is used only once in the codebase at line 81 of the target file
  2. It's a valid fallback pattern - the code tries to get the first available specification and falls back to 's-0.5vcpu-512mb' when none are available
  3. No constant defined - the slug is not extracted as a named constant anywhere in the codebase
  4. Consistent with the codebase pattern - similar fallbacks exist (e.g., 'node-18.0' for runtimes)
  5. The slug format appears valid - it follows Appwrite's specification naming convention and represents a 0.5 vCPU / 512MB specification

Minor Fixes Required

The hardcoded specification slug 's-0.5vcpu-512mb' should be extracted as a named constant for maintainability and easier future updates. While the slug appears valid and the fallback behavior is sound, this aligns with best practices for configuration values that might need centralized management.

🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/functions/create-function/deploy/+page.ts
around lines 79 to 81, the fallback spec slug 's-0.5vcpu-512mb' is hardcoded;
extract it to a named constant (e.g., DEFAULT_SPEC_SLUG) placed either at the
top of this file or in a shared constants module, export it if placed in a
shared module, then replace the inline string with that constant; ensure any
imports are updated and run linters to confirm no unused-vars or export issues.

Copy link
Member

Choose a reason for hiding this comment

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

@atharvadeosthale agreee, see if we have proper constant for this. I think there's a RuntimeSpecification type 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we have that yet


// Create function
const func = await sdk.forProject(params.region, params.project).functions.create({
functionId: ID.unique(),
name: info.name,
runtime: selectedRuntime,
execute: ['any'],
entrypoint: entrypoint || undefined,
commands: installCommand || undefined,
providerSilentMode: false,
specification
});

// Add auto-generated domain
await sdk.forProject(params.region, params.project).proxy.createFunctionRule({
domain: `${ID.unique()}.${regionalConsoleVariables._APP_DOMAIN_FUNCTIONS}`,
functionId: func.$id
});

// Add variables (empty values used as empty strings)
await Promise.all(
envVars.map((variable) =>
sdk.forProject(params.region, params.project).functions.createVariable({
functionId: func.$id,
key: variable.key,
value: variable.value,
secret: false
})
)
);
Comment on lines +90 to +98
Copy link
Member

Choose a reason for hiding this comment

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

unrelated but this has a very bad performance, we need a batch method for this.


// Create deployment
await sdk.forProject(params.region, params.project).functions.createTemplateDeployment({
functionId: func.$id,
repository: info.name,
owner: info.owner,
rootDirectory: rootDir,
type: Type.Branch,
reference: selectedBranch,
activate: true
});

// Redirect to function page
redirect(
302,
`${base}/project-${params.region}-${params.project}/functions/function-${func.$id}`
);
} catch (e) {
// Re-throw redirects (they're not errors)
if (isRedirect(e)) {
throw e;
}
// On error, fall through to show the wizard
console.error('Quick deploy failed:', e);
}
}

let installations = vcsInstallations || null;
if (!installations) {
try {
Expand All @@ -35,7 +148,7 @@ export const load: PageLoad = async ({ url, params, parent }) => {
}

return {
envKeys,
envVars,
runtime,
runtimesList,
installations,
Expand Down
Loading