liara_deploy_release
Deploy applications on Liara cloud platform using uploaded source code and configure environment variables for deployment management.
Instructions
Deploy a release using a source ID
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| appName | Yes | The name of the app | |
| sourceID | Yes | Source ID from previous source upload | |
| envVars | No | Optional environment variables for this deployment |
Implementation Reference
- src/services/deployment.ts:37-49 (handler)The main handler function that performs the deploy release operation by calling the Liara API with the provided appName and DeployReleaseRequest containing sourceID and optional envVars.export async function deployRelease( client: LiaraClient, appName: string, request: DeployReleaseRequest ): Promise<DeployReleaseResponse> { validateAppName(appName); validateRequired(request.sourceID, 'Source ID'); return await client.post<DeployReleaseResponse>( `/v2/projects/${appName}/releases`, request ); }
- src/api/types.ts:124-132 (schema)Type definitions for the input (DeployReleaseRequest) and output (DeployReleaseResponse) of the deploy release operation.export interface DeployReleaseRequest { sourceID: string; envVars?: EnvironmentVariable[]; } export interface DeployReleaseResponse { releaseID: string; }
- src/services/deployment.ts:11-31 (helper)Supporting function to upload source code (tar.gz) to obtain sourceID required for deployRelease.export async function uploadSource( client: LiaraClient, appName: string, filePath: string ): Promise<DeploySourceResponse> { validateAppName(appName); validateRequired(filePath, 'File path'); // Create FormData with file stream const formData = new FormData(); const fileName = filePath.split('/').pop() || 'source.tar.gz'; formData.append('source', createReadStream(filePath), { filename: fileName, contentType: 'application/gzip', }); return await client.postFormData<DeploySourceResponse>( `/v2/projects/${appName}/sources`, formData ); }
- src/api/client.ts:18-206 (helper)The API client class used by all service functions, including post and postFormData methods for deployment.export class LiaraClient { private client: AxiosInstance; private teamId?: string; private maxRetries: number; private retryDelay: number; constructor(config: LiaraClientConfig) { const baseURL = config.baseURL || process.env.LIARA_API_BASE_URL || 'https://api.iran.liara.ir'; this.client = axios.create({ baseURL, headers: { 'Authorization': `Bearer ${config.apiToken}`, 'Content-Type': 'application/json', }, timeout: 30000, // 30 seconds }); this.teamId = config.teamId; this.maxRetries = config.maxRetries ?? 3; this.retryDelay = config.retryDelay ?? 1000; // Add response interceptor for error handling this.client.interceptors.response.use( (response) => response, (error: AxiosError<LiaraApiError>) => { return Promise.reject(this.handleError(error)); } ); } /** * Sleep for a given number of milliseconds */ private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Execute request with retry logic for rate limiting */ private async executeWithRetry<T>( requestFn: () => Promise<T>, retryCount = 0 ): Promise<T> { try { return await requestFn(); } catch (error: unknown) { const err = error as { statusCode?: number; response?: { status?: number } }; // Retry on rate limiting (429) or server errors (5xx) const statusCode = err.statusCode || err.response?.status; const isRetryable = statusCode === 429 || (statusCode !== undefined && statusCode >= 500 && statusCode < 600); if (isRetryable && retryCount < this.maxRetries) { // Exponential backoff: 1s, 2s, 4s... const delay = this.retryDelay * Math.pow(2, retryCount); await this.sleep(delay); return this.executeWithRetry(requestFn, retryCount + 1); } throw error; } } /** * Handle API errors and convert to user-friendly messages */ private handleError(error: AxiosError<LiaraApiError>): Error { if (error.response) { const status = error.response.status; const data = error.response.data; let message = data?.message || data?.error || 'Unknown API error'; switch (status) { case 401: message = 'Authentication failed. Please check your API token.'; break; case 403: message = 'Access forbidden. You may not have permission for this operation.'; break; case 404: message = data?.message || 'Resource not found.'; break; case 409: message = data?.message || 'Conflict: Resource already exists or operation cannot be completed.'; break; case 429: message = 'Rate limit exceeded. Please try again later.'; break; case 500: case 502: case 503: message = 'Liara API is temporarily unavailable. Please try again later.'; break; } const apiError = new Error(message); (apiError as any).statusCode = status; (apiError as any).originalError = data; return apiError; } else if (error.request) { return new Error('Unable to connect to Liara API. Please check your internet connection.'); } else { return new Error(error.message || 'An unexpected error occurred.'); } } /** * Add team ID parameter to request if configured */ private addTeamId(params: Record<string, unknown> = {}): Record<string, unknown> { if (this.teamId) { return { ...params, teamID: this.teamId }; } return params; } /** * GET request with automatic retry for rate limiting */ async get<T>(url: string, params?: Record<string, string | number | boolean>): Promise<T> { return this.executeWithRetry(async () => { const response = await this.client.get<T>(url, { params: this.addTeamId(params) as Record<string, string | number | boolean> }); return response.data; }); } /** * POST request with automatic retry for rate limiting */ async post<T>(url: string, data?: unknown, params?: Record<string, string | number | boolean>): Promise<T> { return this.executeWithRetry(async () => { const response = await this.client.post<T>(url, data, { params: this.addTeamId(params) as Record<string, string | number | boolean> }); return response.data; }); } /** * PUT request with automatic retry for rate limiting */ async put<T>(url: string, data?: unknown, params?: Record<string, string | number | boolean>): Promise<T> { return this.executeWithRetry(async () => { const response = await this.client.put<T>(url, data, { params: this.addTeamId(params) as Record<string, string | number | boolean> }); return response.data; }); } /** * DELETE request with automatic retry for rate limiting */ async delete<T>(url: string, params?: Record<string, string | number | boolean>): Promise<T> { return this.executeWithRetry(async () => { const response = await this.client.delete<T>(url, { params: this.addTeamId(params) as Record<string, string | number | boolean> }); return response.data; }); } /** * POST multipart/form-data request with automatic retry for rate limiting */ async postFormData<T>(url: string, formData: { getHeaders?: () => Record<string, string> }, params?: Record<string, string | number | boolean>): Promise<T> { return this.executeWithRetry(async () => { // form-data sets its own Content-Type with boundary, so we use its headers const headers = formData.getHeaders ? formData.getHeaders() : { 'Content-Type': 'multipart/form-data', }; const response = await this.client.post<T>(url, formData, { params: this.addTeamId(params), headers: { ...headers, 'Authorization': this.client.defaults.headers['Authorization'], }, maxContentLength: Infinity, maxBodyLength: Infinity, }); return response.data; }); } }