From 558b7f8d4907870211085495680dab340f2aa73e Mon Sep 17 00:00:00 2001 From: danciaclara Date: Thu, 18 Sep 2025 14:59:47 +0530 Subject: [PATCH] build errors --- docs/apps/build-plane-app.mdx | 682 +++++++++--------- .../methods/airgapped-edition-kubernetes.mdx | 12 +- 2 files changed, 337 insertions(+), 357 deletions(-) diff --git a/docs/apps/build-plane-app.mdx b/docs/apps/build-plane-app.mdx index 7a86fb7..f198247 100644 --- a/docs/apps/build-plane-app.mdx +++ b/docs/apps/build-plane-app.mdx @@ -132,81 +132,81 @@ Plane will make a GET request to the Redirect URI with below parameters: #### Examples - - - -```python -import os -import time -from plane.oauth.api import OAuthApi -from plane.oauth.models import OAuthConfig - -# Initialize OAuth API -def get_oauth_api(): - oauth_config = OAuthConfig( - client_id=os.getenv("PLANE_CLIENT_ID"), - client_secret=os.getenv("PLANE_CLIENT_SECRET"), - redirect_uri=os.getenv("PLANE_REDIRECT_URI"), - ) - return OAuthApi( - oauth_config=oauth_config, - base_url=os.getenv("PLANE_BASE_URL", "https://api.plane.so"), - ) + + -# Get bot token using app installation ID -oauth_api = get_oauth_api() -token_response = oauth_api.get_bot_token(app_installation_id) + ```python + import os + import time + from plane.oauth.api import OAuthApi + from plane.oauth.models import OAuthConfig + + # Initialize OAuth API + def get_oauth_api(): + oauth_config = OAuthConfig( + client_id=os.getenv("PLANE_CLIENT_ID"), + client_secret=os.getenv("PLANE_CLIENT_SECRET"), + redirect_uri=os.getenv("PLANE_REDIRECT_URI"), + ) + return OAuthApi( + oauth_config=oauth_config, + base_url=os.getenv("PLANE_BASE_URL", "https://api.plane.so"), + ) + + # Get bot token using app installation ID + oauth_api = get_oauth_api() + token_response = oauth_api.get_bot_token(app_installation_id) + + # Get app installation details + app_installations = oauth_api.get_app_installations( + token_response.access_token, + app_installation_id, + ) -# Get app installation details -app_installations = oauth_api.get_app_installations( - token_response.access_token, - app_installation_id, -) + if not app_installations: + raise Exception(f"No app installations found for app installation ID {app_installation_id}") -if not app_installations: - raise Exception(f"No app installations found for app installation ID {app_installation_id}") + app_installation = app_installations[0] + bot_token = token_response.access_token + expires_in = token_response.expires_in + ``` -app_installation = app_installations[0] -bot_token = token_response.access_token -expires_in = token_response.expires_in -``` + + + + ```typescript + import axios from 'axios'; - - - -```typescript -import axios from 'axios'; - -// Prepare basic auth header using client_id and client_secret -const clientId = "your_client_id"; -const clientSecret = "your_client_secret"; -const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); - -// Prepare request data -const payload = { - grant_type: "client_credentials", - app_installation_id: appInstallationId -}; - -// Make a POST request to fetch bot token -const response = await axios.post( - "https://api.plane.so/auth/o/token/", - payload, - { - headers: { - Authorization: `Basic ${basicAuth}`, - "Content-Type": "application/x-www-form-urlencoded" + // Prepare basic auth header using client_id and client_secret + const clientId = "your_client_id"; + const clientSecret = "your_client_secret"; + const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + + // Prepare request data + const payload = { + grant_type: "client_credentials", + app_installation_id: appInstallationId + }; + + // Make a POST request to fetch bot token + const response = await axios.post( + "https://api.plane.so/auth/o/token/", + payload, + { + headers: { + Authorization: `Basic ${basicAuth}`, + "Content-Type": "application/x-www-form-urlencoded" + } } - } -); + ); -// Parse the response -const responseData = response.data; -const botToken = responseData.access_token; -const expiresIn = responseData.expires_in; // Token expiry in seconds -``` + // Parse the response + const responseData = response.data; + const botToken = responseData.access_token; + const expiresIn = responseData.expires_in; // Token expiry in seconds + ``` - + ### User-Authorized Actions (Authorization Code Flow) @@ -222,62 +222,62 @@ Plane will make a GET request to the Redirect URI with below parameters: #### Examples - - + + -```python -from plane.oauth.api import OAuthApi -from plane.oauth.models import OAuthConfig + ```python + from plane.oauth.api import OAuthApi + from plane.oauth.models import OAuthConfig -# Initialize OAuth API -oauth_api = get_oauth_api() # Using the helper function from above + # Initialize OAuth API + oauth_api = get_oauth_api() # Using the helper function from above -# Exchange authorization code for access and refresh tokens -code = "authorization_code_from_callback" -token_response = oauth_api.exchange_code_for_token( - code, - "authorization_code", -) + # Exchange authorization code for access and refresh tokens + code = "authorization_code_from_callback" + token_response = oauth_api.exchange_code_for_token( + code, + "authorization_code", + ) -# Parse the response -access_token = token_response.access_token -refresh_token = token_response.refresh_token -expires_in = token_response.expires_in -``` + # Parse the response + access_token = token_response.access_token + refresh_token = token_response.refresh_token + expires_in = token_response.expires_in + ``` - - - -```typescript -import axios from 'axios'; - -// Exchange authorization code for access and refresh tokens -const code = "authorization_code_from_callback"; -const clientId = "your_client_id"; -const clientSecret = "your_client_secret"; -const redirectUri = "your_redirect_uri"; -const payload = { - grant_type: "authorization_code", - code: code, - client_id: clientId, - client_secret: clientSecret, - redirect_uri: redirectUri -}; -const response = await axios.post( - "https://api.plane.so/auth/o/token/", - payload, - { - headers: { - "Content-Type": "application/x-www-form-urlencoded" + + + + ```typescript + import axios from 'axios'; + + // Exchange authorization code for access and refresh tokens + const code = "authorization_code_from_callback"; + const clientId = "your_client_id"; + const clientSecret = "your_client_secret"; + const redirectUri = "your_redirect_uri"; + const payload = { + grant_type: "authorization_code", + code: code, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: redirectUri + }; + const response = await axios.post( + "https://api.plane.so/auth/o/token/", + payload, + { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } } - } -); -const responseData = response.data; -const accessToken = responseData.access_token; -const refreshToken = responseData.refresh_token; -const expiresIn = responseData.expires_in; -``` - + ); + const responseData = response.data; + const accessToken = responseData.access_token; + const refreshToken = responseData.refresh_token; + const expiresIn = responseData.expires_in; + ``` + ### Fetching App Installation Details @@ -286,45 +286,45 @@ In both user-authorized and app-authorized flows, the `app_installation_id` iden #### Examples - - - -```python -# Using the OAuth API to fetch app installation details -oauth_api = get_oauth_api() -app_installations = oauth_api.get_app_installations( - token, # Either access token or bot token - app_installation_id, -) - -if app_installations: - workspace_details = app_installations[0] - print(f"Workspace: {workspace_details.workspace_detail.name}") - print(f"Workspace Slug: {workspace_details.workspace_detail.slug}") - print(f"Bot User ID: {workspace_details.app_bot}") -``` + + - - + ```python + # Using the OAuth API to fetch app installation details + oauth_api = get_oauth_api() + app_installations = oauth_api.get_app_installations( + token, # Either access token or bot token + app_installation_id, + ) -```typescript -import axios from 'axios'; + if app_installations: + workspace_details = app_installations[0] + print(f"Workspace: {workspace_details.workspace_detail.name}") + print(f"Workspace Slug: {workspace_details.workspace_detail.slug}") + print(f"Bot User ID: {workspace_details.app_bot}") + ``` -// Set authorization header with either access token or bot token -const headers = { - Authorization: `Bearer ${token}`, -}; + + -// Make GET request to fetch installation/workspace details -const response = await axios.get( - `https://api.plane.so/auth/o/app-installation/?id=${app_installation_id}`, - { headers } -); + ```typescript + import axios from 'axios'; -const workspaceDetails = response.data[0]; -``` + // Set authorization header with either access token or bot token + const headers = { + Authorization: `Bearer ${token}`, + }; - + // Make GET request to fetch installation/workspace details + const response = await axios.get( + `https://api.plane.so/auth/o/app-installation/?id=${app_installation_id}`, + { headers } + ); + + const workspaceDetails = response.data[0]; + ``` + + #### Sample Response @@ -469,103 +469,104 @@ When an issue is created, updated, or deleted: Here's how to process webhooks in your application: - - - -```typescript -interface WebhookPayload { - event: string; - action: string; - webhook_id: string; - workspace_id: string; - data: any; - activity: { - actor: { - id: string; - display_name: string; - email?: string; - }; - field?: string; - new_value?: any; - old_value?: any; - }; -} + + -// Process incoming webhook -async function handleWebhook(payload: WebhookPayload) { - console.log(`Received ${payload.event} ${payload.action} event`); - - // Get workspace credentials - const credentials = await getCredentialsForWorkspace(payload.workspace_id); - if (!credentials) { - throw new Error(`No credentials found for workspace ${payload.workspace_id}`); + ```typescript + interface WebhookPayload { + event: string; + action: string; + webhook_id: string; + workspace_id: string; + data: any; + activity: { + actor: { + id: string; + display_name: string; + email?: string; + }; + field?: string; + new_value?: any; + old_value?: any; + }; } - - // Process specific event types - if (payload.event === 'issue_comment' && payload.action === 'created') { - const comment = payload.data.comment_stripped; - if (comment.includes('/your-command')) { - // Handle your custom command - await processCommand(payload.data, credentials); + + // Process incoming webhook + async function handleWebhook(payload: WebhookPayload) { + console.log(`Received ${payload.event} ${payload.action} event`); + + // Get workspace credentials + const credentials = await getCredentialsForWorkspace(payload.workspace_id); + if (!credentials) { + throw new Error(`No credentials found for workspace ${payload.workspace_id}`); + } + + // Process specific event types + if (payload.event === 'issue_comment' && payload.action === 'created') { + const comment = payload.data.comment_stripped; + if (comment.includes('/your-command')) { + // Handle your custom command + await processCommand(payload.data, credentials); + } } } -} -``` + ``` - - - -```python -from typing import Dict, Any -from pydantic import ValidationError - -def handle_webhook(payload_data: Dict[str, Any]): - """Process incoming webhook from Plane with Pydantic validation""" - try: - # Validate webhook payload using Pydantic models - webhook = WebhookEvent(**payload_data) - print(f"Received {webhook.event} {webhook.action} event") - - # Get workspace credentials (implement your own storage) - credentials = get_credentials_for_workspace(webhook.workspace_id) - if not credentials: - raise Exception(f"No credentials found for workspace {webhook.workspace_id}") - - # Process specific event types with validated data - if webhook.event == 'issue_comment' and webhook.action == 'created': - comment_data = CommentEventData(**webhook.data) - comment_text = comment_data.comment_stripped or "" - - if '/your-command' in comment_text: - process_command(comment_data, credentials) - - elif webhook.event == 'issue' and webhook.action == 'updated': - issue_data = IssueEventData(**webhook.data) - - if webhook.activity.field == 'assignees': - handle_assignment_change(issue_data, credentials) - - except ValidationError as e: - print(f"Invalid webhook payload: {e}") - except Exception as e: - print(f"Error processing webhook: {e}") - -def process_command(comment_data: CommentEventData, credentials): - """Process custom commands from issue comments""" - from plane.api import WorkItemsApi - from plane.models import PatchedIssueRequest - - # Use the Plane API to respond to commands - # Implementation depends on your specific command logic - pass - -def handle_assignment_change(issue_data: IssueEventData, credentials): - """Handle issue assignment changes""" - # Your custom logic for handling assignments - pass -``` + + + - + ```python + from typing import Dict, Any + from pydantic import ValidationError + + def handle_webhook(payload_data: Dict[str, Any]): + """Process incoming webhook from Plane with Pydantic validation""" + try: + # Validate webhook payload using Pydantic models + webhook = WebhookEvent(**payload_data) + print(f"Received {webhook.event} {webhook.action} event") + + # Get workspace credentials (implement your own storage) + credentials = get_credentials_for_workspace(webhook.workspace_id) + if not credentials: + raise Exception(f"No credentials found for workspace {webhook.workspace_id}") + + # Process specific event types with validated data + if webhook.event == 'issue_comment' and webhook.action == 'created': + comment_data = CommentEventData(**webhook.data) + comment_text = comment_data.comment_stripped or "" + + if '/your-command' in comment_text: + process_command(comment_data, credentials) + + elif webhook.event == 'issue' and webhook.action == 'updated': + issue_data = IssueEventData(**webhook.data) + + if webhook.activity.field == 'assignees': + handle_assignment_change(issue_data, credentials) + + except ValidationError as e: + print(f"Invalid webhook payload: {e}") + except Exception as e: + print(f"Error processing webhook: {e}") + + def process_command(comment_data: CommentEventData, credentials): + """Process custom commands from issue comments""" + from plane.api import WorkItemsApi + from plane.models import PatchedIssueRequest + + # Use the Plane API to respond to commands + # Implementation depends on your specific command logic + pass + + def handle_assignment_change(issue_data: IssueEventData, credentials): + """Handle issue assignment changes""" + # Your custom logic for handling assignments + pass + ``` + + ## Obtain and store access tokens securely @@ -595,64 +596,64 @@ Token refresh works differently depending on the type of token you're using: Bot tokens obtained through the client credentials flow don't use refresh tokens. Instead, when a bot token expires, you simply request a new one using the same `app_installation_id`: - - + + + + ```python + # When bot token expires, request a new one using the same app_installation_id + from plane.oauth.api import OAuthApi + + def refresh_bot_token(app_installation_id: str): + """Refresh an expired bot token""" + oauth_api = get_oauth_api() # Using helper function from earlier examples + + # Get new bot token using the same app_installation_id + token_response = oauth_api.get_bot_token(app_installation_id) + + # Store the new token securely in your database + new_bot_token = token_response.access_token + expires_in = token_response.expires_in + + return new_bot_token, expires_in + + # Usage example + new_token, expires_in = refresh_bot_token(app_installation_id) + ``` -```python -# When bot token expires, request a new one using the same app_installation_id -from plane.oauth.api import OAuthApi + + -def refresh_bot_token(app_installation_id: str): - """Refresh an expired bot token""" - oauth_api = get_oauth_api() # Using helper function from earlier examples - - # Get new bot token using the same app_installation_id - token_response = oauth_api.get_bot_token(app_installation_id) - - # Store the new token securely in your database - new_bot_token = token_response.access_token - expires_in = token_response.expires_in - - return new_bot_token, expires_in + ```typescript + // When bot token expires, request a new one using the same app_installation_id + import axios from 'axios'; -# Usage example -new_token, expires_in = refresh_bot_token(app_installation_id) -``` + const clientId = "your_client_id"; + const clientSecret = "your_client_secret"; + const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); - - - -```typescript -// When bot token expires, request a new one using the same app_installation_id -import axios from 'axios'; - -const clientId = "your_client_id"; -const clientSecret = "your_client_secret"; -const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); - -const payload = { - grant_type: "client_credentials", - app_installation_id: appInstallationId // Same ID used during initial setup -}; - -const response = await axios.post( - "https://api.plane.so/auth/o/token/", - payload, - { - headers: { - Authorization: `Basic ${basicAuth}`, - "Content-Type": "application/x-www-form-urlencoded" + const payload = { + grant_type: "client_credentials", + app_installation_id: appInstallationId // Same ID used during initial setup + }; + + const response = await axios.post( + "https://api.plane.so/auth/o/token/", + payload, + { + headers: { + Authorization: `Basic ${basicAuth}`, + "Content-Type": "application/x-www-form-urlencoded" + } } - } -); + ); -// Parse the response -const responseData = response.data; -const newBotToken = responseData.access_token; -const expiresIn = responseData.expires_in; -``` + // Parse the response + const responseData = response.data; + const newBotToken = responseData.access_token; + const expiresIn = responseData.expires_in; + ``` - + ### User Token Refresh (Authorization Code Flow) @@ -661,71 +662,46 @@ When user access tokens expire, you can use the refresh token to get a new acces #### Examples - - - -```python -# When access token expires, use refresh token to get a new access token -from plane.oauth.api import OAuthApi - -def refresh_user_token(refresh_token: str): - """Refresh an expired user access token""" - oauth_api = get_oauth_api() # Using helper function from earlier examples - - # Use refresh token to get new access token - token_response = oauth_api.exchange_code_for_token( - refresh_token, - "refresh_token", - ) - - # Store the new tokens securely - new_access_token = token_response.access_token - new_refresh_token = token_response.refresh_token # May be the same or new - expires_in = token_response.expires_in - - return new_access_token, new_refresh_token, expires_in - -# Usage example -new_access_token, new_refresh_token, expires_in = refresh_user_token(stored_refresh_token) -``` + + - - - -```typescript -// When access token expires, use refresh token to get a new access token -const refreshPayload = { - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: clientId, - client_secret: clientSecret -}; - -const refreshResponse = await axios.post( - "https://api.plane.so/auth/o/token/", - refreshPayload, - { - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - } - refresh_response = requests.post( - url="https://api.plane.so/auth/o/token/", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=refresh_payload - ) - refresh_response_data = refresh_response.json() - access_token = refresh_response_data["access_token"] + ```python + # When access token expires, use refresh token to get a new access token + from plane.oauth.api import OAuthApi + + def refresh_user_token(refresh_token: str): + """Refresh an expired user access token""" + oauth_api = get_oauth_api() # Using helper function from earlier examples + + # Use refresh token to get new access token + token_response = oauth_api.exchange_code_for_token( + refresh_token, + "refresh_token", + ) + + # Store the new tokens securely + new_access_token = token_response.access_token + new_refresh_token = token_response.refresh_token # May be the same or new + expires_in = token_response.expires_in + + return new_access_token, new_refresh_token, expires_in + + # Usage example + new_access_token, new_refresh_token, expires_in = refresh_user_token(stored_refresh_token) ``` + + ```typescript + // When access token expires, use refresh token to get a new access token const refreshPayload = { grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId, client_secret: clientSecret }; + const refreshResponse = await axios.post( "https://api.plane.so/auth/o/token/", refreshPayload, @@ -734,11 +710,15 @@ const refreshResponse = await axios.post( "Content-Type": "application/x-www-form-urlencoded" } } - ); - const refreshResponseData = refreshResponse.data; - const accessToken = refreshResponseData.access_token; - ``` - + refresh_response = requests.post( + url="https://api.plane.so/auth/o/token/", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data=refresh_payload + ) + refresh_response_data = refresh_response.json() + access_token = refresh_response_data["access_token"] + ``` + ## Listing Your App on Plane Marketplace diff --git a/docs/self-hosting/methods/airgapped-edition-kubernetes.mdx b/docs/self-hosting/methods/airgapped-edition-kubernetes.mdx index c39f716..60109a0 100644 --- a/docs/self-hosting/methods/airgapped-edition-kubernetes.mdx +++ b/docs/self-hosting/methods/airgapped-edition-kubernetes.mdx @@ -60,9 +60,9 @@ Before starting, ensure you have: - `rabbitmq-3.13.6-management-alpine.tar` - Plane-mq service image - `valkey-7.2.5-alpine.tar` - Plane-redis service image - + :::info For this installation, you can ignore the extra files in this folder (e.g., `docker-compose.yml`, `install.sh`, `plane.env`, etc.). - + ::: 5. Load the images into your local Docker registry or private registry: @@ -179,9 +179,9 @@ For more advanced Plane configuration options, refer to the [Kubernetes document ## Activate your license Once your air-gapped installation is running, you'll need to activate your workspace with the provided license file. - +:::info You should have received the `license_key.json` file as part of your air-gapped package. If you don't have this file, contact our support team. - +::: 1. Go to your [Workspace Settings](https://docs.plane.so/core-concepts/workspaces/overview#workspace-settings) in the Plane application. 2. Select **Billing and plans** on the right pane. @@ -191,7 +191,7 @@ You should have received the `license_key.json` file as part of your air-gapped You now have Plane running in your air-gapped environment. If you run into any issues, check the logs using the commands above, or reach out to our support team for assistance. - +:::tip *Optional* Once everything is working, you can safely delete the `airgapped` folder that contains the installation script and image files to free up space. - +:::