-
Notifications
You must be signed in to change notification settings - Fork 202
Switch to branch deployments in one-click deployments #2647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
0f3aaba
d5436e1
2bb3f66
5aae015
f5b6f99
20ca407
2d3699c
6c22ff3
449ec78
ef1a4c9
7bc21f2
6e2724b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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; | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export async function validateBranch( | ||
| owner: string, | ||
| repo: string, | ||
| branch: string | ||
| ): Promise<boolean> { | ||
| try { | ||
| const response = await fetch( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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) => ({ | ||
|
|
@@ -63,7 +64,7 @@ | |
| })) || [] | ||
| ); | ||
| onMount(() => { | ||
| onMount(async () => { | ||
| const runtimeParam = data.runtime || page.url.searchParams.get('runtime') || 'node-18.0'; | ||
| runtime = runtimeParam as Runtime; | ||
|
|
@@ -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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
atharvadeosthale marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
|
|
@@ -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 | ||
| }); | ||
|
|
@@ -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 | ||
|
|
@@ -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} | ||
|
|
@@ -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> | ||
|
|
||
| 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'); | ||
|
|
||
|
|
@@ -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'; | ||
atharvadeosthale marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 tsRepository: 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 -80Repository: 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 -20Repository: 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 -150Repository: 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 5Repository: 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 3Repository: 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 -35Repository: 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 -20Repository: 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 -20Repository: 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 2Repository: 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 1Repository: 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.tsRepository: 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 -40Repository: appwrite/console Length of output: 1653 Based on my verification, I've found:
Minor Fixes Required The hardcoded specification slug 🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤔
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}` | ||
atharvadeosthale marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ); | ||
| } 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); | ||
atharvadeosthale marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| let installations = vcsInstallations || null; | ||
| if (!installations) { | ||
| try { | ||
|
|
@@ -35,7 +148,7 @@ export const load: PageLoad = async ({ url, params, parent }) => { | |
| } | ||
|
|
||
| return { | ||
| envKeys, | ||
| envVars, | ||
| runtime, | ||
| runtimesList, | ||
| installations, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.