Skip to content
20 changes: 20 additions & 0 deletions src/lib/helpers/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Parses environment variable parameter string.
* Supports both KEY and KEY=value formats.
* @param envParam - Comma-separated string of env vars (e.g., "KEY1,KEY2=value2")
* @returns Array of objects with key and value properties
*/
export function parseEnvParam(envParam: string | null): Array<{ key: string; value: string }> {
if (!envParam) return [];
return 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)
};
});
}
38 changes: 36 additions & 2 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 @@ -34,7 +39,6 @@ export async function getLatestTag(owner: string, name: string): Promise<string

return null;
} catch (error) {
console.error('Failed to fetch tags from GitHub:', error);
return null;
}
}
Expand All @@ -50,7 +54,37 @@ export async function getDefaultBranch(owner: string, name: string): Promise<str
const repo = await repoResponse.json();
return repo.default_branch || null;
} catch (error) {
console.error('Failed to fetch default branch from GitHub:', error);
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) {
return null;
}
}

export async function validateBranch(
owner: string,
repo: string,
branch: string
): Promise<boolean> {
try {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`
);
return response.ok;
} catch (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,8 +64,8 @@
})) || []
);

onMount(() => {
const runtimeParam = data.runtime || page.url.searchParams.get('runtime') || 'node-18.0';
onMount(async () => {
const runtimeParam = data.runtime || page.url.searchParams.get('runtime') || Runtime.Node22;
runtime = runtimeParam as Runtime;

entrypoint = page.url.searchParams.get('entrypoint') || '';
Expand All @@ -76,23 +77,81 @@
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) {
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) {
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 +185,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 +279,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 +318,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 +351,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
Loading