Compare commits

..
Author SHA1 Message Date
sriram veeraghanta 621fcfc041 removing trubo 2023-04-21 19:37:13 -04:00
sriram veeraghanta 1538b99a28 removing trubo 2023-04-21 19:30:36 -04:00
13826 changed files with 92019 additions and 1690914 deletions
-54
View File
@@ -1,54 +0,0 @@
---
name: pr-description
description: Generate a PR description following the project's GitHub PR template. Analyzes the current branch's changes against the base branch to produce a complete, filled-out PR description.
user_invocable: true
---
# PR Description Generator
Generate a pull request description based on the project's PR template at `.github/pull_request_template.md`.
## Steps
1. **Determine the base branch**: Use `preview` as the default base branch unless the user specifies otherwise.
2. **Analyze changes**: Run the following to understand what changed:
- `git log <base>...HEAD --oneline` to see all commits on this branch
- `git diff <base>...HEAD --stat` to see which files changed
- `git diff <base>...HEAD` to read the actual diff (use `--no-color`)
- If the diff is very large, focus on the most important files first
3. **Fill out the PR template** with the following sections:
### Description
Write a clear, concise summary of what the PR does and why. Focus on the "what" and "why", not line-by-line changes. Mention any important implementation decisions.
### Type of Change
Check the appropriate box(es) based on the changes:
- Bug fix (non-breaking change which fixes an issue)
- Feature (non-breaking change which adds functionality)
- Improvement (change that would cause existing functionality to not work as expected)
- Code refactoring
- Performance improvements
- Documentation update
### Screenshots and Media
Leave this section for the user to fill in, with a note: `<!-- Add screenshots here -->`
### Test Scenarios
Based on the code changes, suggest specific test scenarios that should be verified. Be concrete (e.g., "Navigate to project settings and verify the new toggle works") rather than generic.
### References
- If commit messages or branch name reference a work item identifier (e.g., `WEB-1234`), include it
- If the user provides a linked issue, include it
- If Sentry issue links or IDs (e.g., `SENTRY-ABC123`, Sentry URLs) were mentioned earlier in the conversation, include them as references
4. **Output format**: Print the filled-out markdown template so the user can copy it directly. Do NOT wrap it in a code fence — output the raw markdown.
## Guidelines
- Keep the description concise but informative
- Use bullet points for multiple changes
- Focus on user-facing impact, not implementation details
- If the branch has a Plane work item ID in its name (e.g., `WEB-1234`), reference it
- Don't fabricate test scenarios that aren't relevant to the actual changes
-163
View File
@@ -1,163 +0,0 @@
---
name: release-notes
description: "Generate release notes for a Plane release PR in either `makeplane/plane-cloud` (date-based versioning, e.g. `release: vYY.MM.DD-N`) or `makeplane/plane-ee` (semver, e.g. `release: vX.Y.Z`). Reads PR commits, filters out noise, categorizes by conventional-commit type, optionally enriches via Plane MCP, and writes the result as the PR description."
user_invocable: true
---
# Release Notes Generator
Generate structured release notes from a Plane release PR by parsing its commit list, then update the PR description. Works for both `makeplane/plane-cloud` and `makeplane/plane-ee`.
## Repo-specific versioning
Plane uses **different version schemes** across its two release repos. Detect which repo the PR belongs to and use the matching format.
| Repo | Version scheme | Example PR title | Source branch | Target branch |
| ----------------------- | -------------- | ---------------------- | ------------- | -------------------- |
| `makeplane/plane-cloud` | Date-based | `release: v26.04.13-1` | `uat` | `master` |
| `makeplane/plane-ee` | Semver | `release: v1.12.0` | `uat` | `master` / `preview` |
- **plane-cloud** ships daily — version is `vYY.MM.DD-N` where `N` is the counter for that date's release.
- **plane-ee** ships on a versioned cadence — version is `vX.Y.Z` (major.minor.patch) following semver.
- Detect the repo with `gh pr view <PR_NUM> --json headRepository,baseRepository` or from the URL the user shared. Never mix the two formats in one set of notes.
## When to Use
- User links/mentions a Plane release PR (e.g. `release: v26.04.13-1` for cloud or `release: v1.12.0` for EE) and asks for release notes
- User asks to "create release notes" / "update PR description" for a PR in `makeplane/plane-cloud` or `makeplane/plane-ee`
- The branch is named `uat` or `release/x.y.z` and the base is `master` or `preview`
## Steps
### 1. Fetch commits
```bash
gh pr view <PR_NUM> --json title,body,baseRefName,headRefName,commits \
--jq '.commits[] | .messageHeadline + "\n---BODY---\n" + .messageBody + "\n===END==="'
```
For a quick scan first:
```bash
gh pr view <PR_NUM> --json commits \
--jq '.commits[] | {oid: .oid[0:10], message: .messageHeadline}'
```
### 2. Filter out noise
**Always exclude** these commits — mechanical, not user-facing:
| Pattern | Reason |
| -------------------------------------------- | ------------------------------------- |
| `Sync: Enterprise Changes #NNNN` | Cross-repo sync, no functional change |
| `fix: merge conflicts` | Merge artifact |
| `Merge branch '...' of github.com:...` | Merge artifact |
| `Revert "..."` (when immediately re-applied) | Internal churn |
### 3. Parse work item IDs
Most meaningful commits begin with a Plane work item identifier in brackets:
- `[WEB-XXXX]` — web/frontend product items
- `[SILO-XXXX]` — Silo (integrations: Slack, GitHub, GitLab, Jira/Linear)
- `[MOBILE-XXXX]`, `[API-XXXX]`, etc.
Always preserve these IDs in the release notes — they let readers click through to the source ticket.
### 4. (Optional) Enrich via Plane MCP
For larger features where the commit headline is terse, fetch the work item:
```
mcp__plane__retrieve_work_item_by_identifier(project_identifier="WEB", issue_identifier=6874)
```
Use the returned `name` and `description_stripped` to flesh out the bullet. Skip this for routine fixes — commit body is usually enough. Don't enrich every item (slow + work item descriptions are often empty).
### 5. Categorize by conventional-commit type
| Commit prefix | Section |
| -------------------------------- | ------------------- |
| `feat:`, `feat(scope):` | ✨ New Features |
| `fix:`, `fix(scope):` | 🐛 Bug Fixes |
| `refactor:` | 🔧 Refactor & Chore |
| `chore:`, `chore(scope):` | 🔧 Refactor & Chore |
| `chore(deps):`, dependabot bumps | 📦 Dependencies |
### 6. Format
Use the version format that matches the repo (see **Repo-specific versioning** above):
- `plane-cloud``# Release vYY.MM.DD-N`
- `plane-ee``# Release vX.Y.Z`
```markdown
# Release <version>
## ✨ New Features
- **<Short title>** — [WEB-XXXX] (#PR_NUM)
Optional 12 sentence elaboration drawn from commit body.
## 🐛 Bug Fixes
- **<Short title>** — [WEB-XXXX] (#PR_NUM)
## 🔧 Refactor & Chore
- **<Short title>** — [WEB-XXXX] (#PR_NUM)
## 📦 Dependencies
- Bump `<package>` X.Y.Z → A.B.C (#PR_NUM)
```
Rules:
- Lead with a bold human-readable title (rewrite the commit subject if cryptic)
- Always include the work item ID in brackets and the merge PR number in parens
- Add a sub-line elaboration only when the commit body has substance worth surfacing (acceptance criteria, scope notes, gotchas like "behind feature flag", "requires migration", "requires Vercel setting")
- Drop empty sections
### 7. Update the PR description
```bash
gh pr edit <PR_NUM> --body "$(cat <<'EOF'
<release notes markdown>
EOF
)"
```
Always use a HEREDOC with single-quoted `'EOF'` so backticks/dollars in the notes are preserved.
## Quick Reference: end-to-end
```bash
PR=2498
gh pr view $PR --json commits --jq '.commits[] | .messageHeadline + "\n---\n" + .messageBody + "\n==="' > /tmp/commits.txt
# read /tmp/commits.txt, filter, categorize, draft notes
gh pr edit $PR --body "$(cat <<'EOF'
... release notes ...
EOF
)"
```
## Common Mistakes
- **Including `Sync: Enterprise Changes` commits** — these are sync PRs, never user-visible changes
- **Including `fix: merge conflicts`** — merge artifact, no functional content
- **Dropping the work item ID** — readers rely on `[WEB-XXXX]` to navigate to the ticket
- **Over-enriching with MCP lookups** — work item descriptions are often empty; commit body is usually richer
- **Missing the merge PR number** — always include `(#NNNN)` from the commit subject so reviewers can audit the source PR
- **Using `--body` without HEREDOC** — backticks/dollar signs get shell-interpreted and corrupt the notes
- **Editing the title** — release PR titles are version markers; only edit the body
- **Using the wrong version scheme** — `plane-cloud` is date-based (`vYY.MM.DD-N`), `plane-ee` is semver (`vX.Y.Z`). Check the repo before drafting the `# Release <version>` heading
## Plane-Specific Conventions
- Release PRs go from `uat``master` (or `preview`)
- PR title format:
- `plane-cloud`: `release: vYY.MM.DD-N` where N is the daily release counter for that date
- `plane-ee`: `release: vX.Y.Z` semver (major.minor.patch)
- Commits coming from feature branches always carry a work item ID; commits without one are usually infra/chores
- `Sync: Enterprise Changes #NNNN` are automated cross-repo syncs and are _always_ skipped in release notes
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+2 -65
View File
@@ -2,68 +2,5 @@
*.pyc
.env
venv
.venv
node_modules/
**/node_modules/
npm-debug.log
.next/
**/.next/
.turbo/
**/.turbo/
build/
**/build/
out/
**/out/
dist/
**/dist/
# Logs
npm-debug.log*
pnpm-debug.log*
.pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS junk
.DS_Store
Thumbs.db
# Editor settings
.vscode
.idea
# Coverage and test output
coverage/
**/coverage/
*.lcov
.junit/
test-results/
# Caches and build artifacts
.cache/
**/.cache/
storybook-static/
*storybook.log
*.tsbuildinfo
# Local env and secrets
.env.local
.env.development.local
.env.test.local
.env.production.local
.secrets
tmp/
temp/
# Database/cache dumps
*.rdb
*.rdb.gz
# Misc
*.pem
*.key
# React Router - https://github.com/remix-run/react-router-templates/blob/dc79b1a065f59f3bfd840d4ef75cc27689b611e6/default/.dockerignore
.react-router/
build/
node_modules/
README.md
node_modules
npm-debug.log
-45
View File
@@ -1,45 +0,0 @@
# Database Settings
POSTGRES_USER="plane"
POSTGRES_PASSWORD="plane"
POSTGRES_DB="plane"
PGDATA="/var/lib/postgresql/data"
# Redis Settings
REDIS_HOST="plane-redis"
REDIS_PORT="6379"
# RabbitMQ Settings
RABBITMQ_HOST="plane-mq"
RABBITMQ_PORT="5672"
RABBITMQ_USER="plane"
RABBITMQ_PASSWORD="plane"
RABBITMQ_VHOST="plane"
LISTEN_HTTP_PORT=80
LISTEN_HTTPS_PORT=443
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
# Changing this requires change in the proxy config for uploads if using minio setup
AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
FILE_SIZE_LIMIT=5242880
# Settings related to Docker
DOCKERIZED=1 # deprecated
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# If SSL Cert to be generated, set CERT_EMAIl="email <EMAIL_ADDRESS>"
CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
TRUSTED_PROXIES=0.0.0.0/0
SITE_ADDRESS=:80
CERT_EMAIL=
# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
# CERT_ACME_DNS="acme_dns <CERT_DNS_PROVIDER> <CERT_DNS_PROVIDER_API_KEY>"
CERT_ACME_DNS=
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
root: true,
settings: {
next: {
rootDir: ["app/"],
},
},
};
-1
View File
@@ -1 +0,0 @@
*.sh text eol=lf
+4 -12
View File
@@ -1,8 +1,7 @@
name: Bug report
description: Create a bug report to help us improve Plane
title: "[bug]: "
labels: [🐛bug, plane]
assignees: [vihar, pushya22]
labels: [bug, need testing]
body:
- type: markdown
attributes:
@@ -45,7 +44,7 @@ body:
- Deploy preview
validations:
required: true
- type: dropdown
type: dropdown
id: browser
attributes:
label: Browser
@@ -55,19 +54,12 @@ body:
- Safari
- Other
- type: dropdown
id: variant
id: version
attributes:
label: Variant
label: Version
options:
- Cloud
- Self-hosted
- Local
validations:
required: true
- type: input
id: version
attributes:
label: Version
placeholder: v0.17.0-dev
validations:
required: true
@@ -1,8 +1,7 @@
name: Feature request
description: Suggest a feature to improve Plane
title: "[feature]: "
labels: [feature, plane]
assignees: [vihar, pushya22]
labels: [feature]
body:
- type: markdown
attributes:
+1 -1
View File
@@ -1,6 +1,6 @@
contact_links:
- name: Help and support
about: Reach out to us on our Forum or GitHub discussions.
about: Reach out to us on our Discord server or GitHub discussions.
- name: Dedicated support
url: mailto:support@plane.so
about: Write to us if you'd like dedicated support using Plane
-3
View File
@@ -1,3 +0,0 @@
self-hosted-runner:
labels:
- ubuntu-22.04-4core
-17
View File
@@ -1,17 +0,0 @@
See the root `AGENTS.md` for comprehensive project instructions including tech stack, monorepo structure, commands, and architecture.
Each app and package has its own `AGENTS.md` with module-specific context.
## Quick Reference
- **Package manager**: pnpm (never use npm or yarn)
- **Build**: Turbo (`turbo.json`)
- **Lint/types**: `pnpm check` from root
- **Auto-fix**: `pnpm fix` from root
- **Frontend**: React 18, React Router 7, TypeScript, MobX, Tailwind CSS
- **Backend**: Django, DRF, Celery, PostgreSQL, Redis
- **Code style**: camelCase for variables/functions, PascalCase for components/types
- **TypeScript**: Strict mode, `workspace:*` for internal packages, `catalog:` for external deps
- **Python**: Ruff for linting/formatting, line length 120
- **Formatting**: Prettier with Tailwind plugin
- **Linting**: ESLint 9 with typed linting
-63
View File
@@ -1,63 +0,0 @@
---
description: Guidelines for bash commands and tooling in the monorepo
applyTo: "**/*.sh"
---
# Bash & Tooling Instructions
This document outlines the standard tools and commands used in this monorepo.
## Package Manager
We use **pnpm** for package management.
- **Do not use `npm` or `yarn`.**
- Lockfile: `pnpm-lock.yaml`
- Workspace configuration: `pnpm-workspace.yaml`
### Common Commands
- Install dependencies: `pnpm install`
- Run a script in a specific package: `pnpm --filter <package_name> run <script>`
- Run a script in all packages: `pnpm -r run <script>`
## Monorepo Tooling
We use **Turbo** for build system orchestration.
- Configuration: `turbo.json`
## Project Structure
- `apps/`: Contains application services (admin, api, live, proxy, space, web).
- `packages/`: Contains shared packages and libraries.
- `deployments/`: Deployment configurations.
## Running Tests
- To run tests in a specific package (e.g., codemods):
```bash
cd packages/codemods
pnpm run test
```
- Or from root:
```bash
pnpm --filter @plane/codemods run test
```
## Docker
- Local development uses `docker-compose-local.yml`.
- Production/Staging uses `docker-compose.yml`.
### Docker Compose Profiles
The local compose file supports profiles for selective service startup:
| Profile | Services | Command |
| ---------- | ------------------------------------------------ | ------------------------------------------------------------------ |
| `all` | All services | `docker compose -f docker-compose-local.yml --profile all up` |
| `services` | External only (postgres, redis, rabbitmq, minio) | `docker compose -f docker-compose-local.yml --profile services up` |
| `api` | External + api, worker, beat-worker, migrator | `docker compose -f docker-compose-local.yml --profile api up` |
To set a default profile, add `COMPOSE_PROFILES=all` to your `.env` file.
@@ -1,129 +0,0 @@
---
description: Guidelines for using modern TypeScript features (v5.0-v5.8)
applyTo: "**/*.{ts,tsx,mts,cts}"
---
# TypeScript Coding Guidelines & Modern Features (v5.0 - v5.8)
When writing TypeScript code, prioritize using modern features and best practices introduced in recent versions (up to 5.8).
## Global Themes Across 5.x
1. **Standard decorators are here; legacy decorators are legacy.**
New TC39-compliant decorators landed in 5.0 and were extended in 5.2 (metadata). Old `experimentalDecorators`-style behavior is still supported but should be treated as legacy.
2. **Type system is more precise and less noisy.**
Major work went into narrowing, control flow analysis, error messages, and new helpers like `NoInfer`, inferred predicates, and better `undefined`/`never`/uninitialized checks.
3. **Module / runtime interop has been modernized.**
Options like `--moduleResolution bundler`, `--module nodenext`/`node18`, `--rewriteRelativeImportExtensions`, `--erasableSyntaxOnly`, and `--verbatimModuleSyntax` are about playing nicely with ESM, Node 18+/22+, direct TypeScript execution, and bundlers.
4. **The standard library keeps tracking modern JS.**
Support for new ES features (iterator helpers, `Object.groupBy`/`Map.groupBy`, new Set/ES2024 APIs) shows up as type declarations and sometimes extra checks (regex syntax checking, etc.).
When generating or refactoring code, prefer these newer idioms, and avoid patterns that conflict with updated checks.
## Modern Features to Utilize
### Type System & Inference
- **`const` Type Parameters (5.0)**: Use `const` type parameters for more precise literal inference.
```typescript
declare function names<const T extends string[]>(...names: T): void;
```
- **`@satisfies` Operator (5.0)**: Use `satisfies` to validate types without widening them.
- **Inferred Type Predicates (5.5)**: Allow TypeScript to infer type predicates for functions that filter arrays or check types, reducing the need for explicit `is` return types.
- **`NoInfer` Utility (5.4)**: Use `NoInfer<T>` to block inference for specific type arguments when you want them to be determined by other arguments.
- **Narrowing**:
- **Switch(true) (5.3)**: Utilize narrowing in `switch(true)` blocks.
- **Boolean Comparisons (5.3)**: Rely on narrowing from direct boolean comparisons.
- **Closures (5.4)**: Trust preserved narrowing in closures when variables aren't modified after the check.
- **Constant Indexed Access (5.5)**: Use constant indices to narrow object/array properties.
### Syntax & Control Flow
- **Decorators (5.0)**: Use standard ECMAScript decorators (Stage 3).
- **`using` Declarations (5.2)**: Use `using` for explicit resource management (Disposable pattern) instead of manual cleanup.
```typescript
using resource = new Resource();
```
- **Import Attributes (5.3/5.8)**: Use `with { type: "json" }` for import attributes. Avoid the deprecated `assert` syntax.
- **`switch` Exhaustiveness**: Rely on TypeScript's exhaustiveness checking in switch statements.
### Modules & Imports
- **`verbatimModuleSyntax` (5.0)**: Respect this flag by using `import type` explicitly when importing types to ensure they are erased during compilation.
- **Type-Only Imports with Extensions (5.2)**: You can use `.ts`, `.mts`, `.cts` extensions in `import type` statements.
- **`resolution-mode` (5.3)**: Use `import type { Type } from "mod" with { "resolution-mode": "import" }` if needed for specific module resolution contexts.
- **JSDoc `@import` (5.5)**: Use `@import` tags in JSDoc for cleaner type imports in JS files if working in a mixed codebase.
### Standard Library & Built-ins
- **Iterator Helpers (5.6)**: Use new iterator methods (map, filter, etc.) if targeting modern environments.
- **Set Methods (5.5)**: Utilize new `Set` methods like `union`, `intersection`, etc., when available.
- **`Object.groupBy` / `Map.groupBy` (5.4)**: Use these standard methods for grouping instead of external libraries like Lodash when appropriate.
- **`Promise.withResolvers` (5.7)**: Use `Promise.withResolvers()` for creating promises with exposed resolve/reject functions.
### Configuration & Tooling
- **`--moduleResolution bundler` (5.0)**: Assume this resolution strategy for modern web projects (Vite, Next.js, etc.).
- **`--erasableSyntaxOnly` (5.8)**: Be aware of this flag; avoid TypeScript-specific syntax that cannot be simply erased (like `enum`s or `namespaces`) if the project aims for maximum compatibility with tools like Node.js's `--strip-types`. Prefer `const` objects or unions over `enum`s if requested.
## Specific Coding Patterns
### Arrays & Collections
- Use **Copying Array Methods (5.2)** (`toSorted`, `toSpliced`, `with`) for immutable array operations.
- **TypedArrays (5.7)**: Be aware that TypedArrays are now generic over `ArrayBufferLike`.
### Classes
- **Parameter Decorators (5.0/5.2)**: Use modern standard decorators.
- **`super` Property Access (5.3)**: Avoid accessing instance fields via `super`.
### Error Handling
- **Checks for Never-Initialized Variables (5.7)**: Ensure variables are initialized before use to avoid new errors.
## Deprecations to Avoid
- Avoid `import ... assert` (use `with`).
- Avoid implicit `any` returns in `undefined`-returning functions (though 5.1 makes this easier, explicit is better).
- Avoid `enum`s if the project prefers erasable syntax (5.8).
## Version-Specific Highlights
### TypeScript 5.0
- **Decorators**: Use standard decorators unless `experimentalDecorators` is explicitly enabled.
- **`const` Type Parameters**: Use for literal inference.
- **Enums**: All enums are union enums.
- **Modules**: `--moduleResolution bundler` and `--verbatimModuleSyntax` are key for modern bundlers.
### TypeScript 5.1
- **Returns**: `undefined`-returning functions don't need explicit returns.
- **Getters/Setters**: Can have unrelated types with explicit annotations.
### TypeScript 5.2
- **Resource Management**: `using` declarations for `Symbol.dispose`.
- **Decorator Metadata**: Use `context.metadata` for design-time metadata.
### TypeScript 5.3
- **Import Attributes**: Use `with { type: "json" }`.
- **Switch(true)**: Narrowing works in `switch(true)`.
### TypeScript 5.4
- **Closures**: Narrowing preserved in closures if last assignment is before creation.
- **`NoInfer`**: Block inference for specific arguments.
- **Grouping**: `Object.groupBy` / `Map.groupBy`.
### TypeScript 5.5
- **Inferred Predicates**: Functions checking types often don't need explicit `is` return types.
- **Constant Index Access**: Better narrowing for constant keys.
- **Regex**: Syntax checking for regex literals.
### TypeScript 5.6
- **Truthiness Checks**: Errors on always-truthy/falsy conditions (e.g., `if (/regex/)`).
- **Iterator Helpers**: `.map`, `.filter` on iterators.
### TypeScript 5.7
- **Uninitialized Variables**: Stricter checks for never-initialized variables.
- **Relative Imports**: `--rewriteRelativeImportExtensions` for `.ts` imports in output.
- **ES2024**: Support for `Promise.withResolvers`, `Atomics.waitAsync`.
### TypeScript 5.8
- **Return Checks**: Granular checks for conditional returns.
- **Node Modules**: `--module node18` stable; `require()` of ESM allowed in `nodenext`.
- **Erasable Syntax**: `--erasableSyntaxOnly` forbids enums, namespaces, etc.
When generating code, always prefer the most modern, standard, and type-safe approach available in TypeScript 5.8.
-20
View File
@@ -1,20 +0,0 @@
### Description
<!-- Provide a detailed description of the changes in this PR -->
### Type of Change
<!-- Put an 'x' in the boxes that apply -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Feature (non-breaking change which adds functionality)
- [ ] Improvement (change that would cause existing functionality to not work as expected)
- [ ] Code refactoring
- [ ] Performance improvements
- [ ] Documentation update
### Screenshots and Media (if applicable)
<!-- Add screenshots to help explain your changes, ideally showcasing before and after -->
### Test Scenarios
<!-- Please describe the tests that you ran to verify your changes -->
### References
<!-- Link related issues if there are any -->
-305
View File
@@ -1,305 +0,0 @@
name: AMI Build - AWS
on:
workflow_dispatch:
inputs:
ami_prefix:
description: 'AMI Prefix'
required: true
default: 'plane-commercial'
prime_host:
description: 'Prime Host'
required: true
default: 'https://prime.plane.so'
ami_publish_region:
description: 'AMI Publish Regions (comma separated)'
type: string
required: true
default: 'us-east-1'
mark_manifest_latest:
description: 'Mark manifest as latest'
type: boolean
required: false
default: false
env:
# Inputs
AMI_PREFIX: ${{ inputs.ami_prefix || 'plane-commercial' }}
PRIME_HOST: ${{ inputs.prime_host || 'https://prime.plane.so' }}
AMI_PUBLISH_REGION: ${{ inputs.ami_publish_region || 'us-east-1' }}
# Inputs by Devops
AWS_MANIFEST_BUCKET: 'plane-terraform-marketplace'
AWS_VPC_CIDR: '10.34.0.0/16'
AWS_SUBNET_CIDR: '10.34.1.0/24'
AWS_VPC_REGION: 'us-east-1'
AWS_BASE_IMAGE_OWNER: '099720109477'
# Secrets
AWS_ACCESS_KEY: ${{ secrets.MARKETPLACE_AWS_ACCESS_KEY_ID }}
AWS_SECRET_KEY: ${{ secrets.MARKETPLACE_AWS_SECRET_ACCESS_KEY }}
# Constants
CURRENT_MANIFEST_FILE: 'ee-docker-aws-ami-manifest.json'
EE_PACKER_FILE: 'ee-docker-aws-ami.pkr.hcl'
CF_TEMPLATE_FILE: deployments/ami/commercial/ee-cloudformation-template.yaml
CF_OUTPUT_FILE: deployments/ami/commercial/plane-commercial-cloudformation.yaml
MARK_MANIFEST_LATEST: ${{ inputs.mark_manifest_latest || false }}
jobs:
build_ami:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ env.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ env.AWS_SECRET_KEY }}
aws-region: ${{ env.AWS_VPC_REGION }}
- name: Setup `packer`
uses: hashicorp/setup-packer@v3
id: setup
with:
version: latest
- name: Copy Upload Assets
run: |
mkdir -p plane-dist
cp deployments/ami/commercial/cloudinit-ee/* plane-dist/
- name: Run `packer init`
id: init
run: "packer init ./deployments/ami/commercial/${{ env.EE_PACKER_FILE }}"
- name: Run `packer validate`
id: validate
run: "packer validate ./deployments/ami/commercial/${{ env.EE_PACKER_FILE }}"
- name: Make Variables File
id: make_variables_file
run: |
touch variables.pkrvars.hcl
echo "aws_region = \"${AWS_VPC_REGION}\"" >> variables.pkrvars.hcl
echo "ami_name_prefix = \"${AMI_PREFIX}\"" >> variables.pkrvars.hcl
echo "vpc_cidr = \"${AWS_VPC_CIDR}\"" >> variables.pkrvars.hcl
echo "subnet_cidr = \"${AWS_SUBNET_CIDR}\"" >> variables.pkrvars.hcl
echo "base_image_owner = \"${AWS_BASE_IMAGE_OWNER}\"" >> variables.pkrvars.hcl
echo "prime_host = \"${PRIME_HOST}\"" >> variables.pkrvars.hcl
echo "instance_type = \"t3a.xlarge\"" >> variables.pkrvars.hcl
echo "manifest_file_name = \"${{ env.CURRENT_MANIFEST_FILE }}\"" >> variables.pkrvars.hcl
# split AMI_PUBLISH_REGION by comma and add to ami_regions
ami_regions=$(echo "${AMI_PUBLISH_REGION}" | sed 's/[[:space:]]*,[[:space:]]*/\n/g' | jq -R . | jq -s -c .)
echo "ami_regions = ${ami_regions}" >> variables.pkrvars.hcl
cat variables.pkrvars.hcl
- name: Run `packer build`
id: build
run: |
packer build \
-var "aws_access_key=${{ env.AWS_ACCESS_KEY }}" \
-var "aws_secret_key=${{ env.AWS_SECRET_KEY }}" \
-var-file=variables.pkrvars.hcl \
./deployments/ami/commercial/${{ env.EE_PACKER_FILE }}
- name: Extract AMI Information and Create Summary
id: ami_info
run: |
# Extract AMI details from manifest
AMI_STRING=$(jq -r '.builds[-1].artifact_id' ${{env.CURRENT_MANIFEST_FILE}})
AMI_NAME=$(jq -r '.builds[-1].custom_data.ami_name' ${{env.CURRENT_MANIFEST_FILE}})
BUILD_TIME=$(jq -r '.builds[-1].custom_data.build_time' ${{env.CURRENT_MANIFEST_FILE}})
# Create array of AMI information
declare -a AMI_INFO
IFS=',' read -ra AMI_ARRAY <<< "$AMI_STRING"
for ami in "${AMI_ARRAY[@]}"; do
REGION=$(echo "$ami" | cut -d ":" -f1)
AMI_ID=$(echo "$ami" | cut -d ":" -f2)
AMI_INFO+=("$REGION:$AMI_ID")
done
# Add git information to manifest
jq --arg branch "${{ github.ref_name }}" \
--arg commit "${{ github.sha }}" \
--arg build_time "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
'.builds[-1].custom_data += {git_branch: $branch, git_commit: $commit, build_timestamp: $build_time}' \
${{env.CURRENT_MANIFEST_FILE}} > temp-manifest.json
mv temp-manifest.json ${{env.CURRENT_MANIFEST_FILE}}
- name: Store Manifest in S3
run: |
# Also store a versioned copy
aws s3 cp ${{env.CURRENT_MANIFEST_FILE}} "s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-manifest-${{ github.sha }}.json"
- name: Store Manifest in S3 as latest
if: ${{ env.MARK_MANIFEST_LATEST == 'true' }}
run: |
# Store the current manifest as latest
aws s3 cp ${{env.CURRENT_MANIFEST_FILE}} s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-manifest-latest.json || true
- name: Upload Build Manifest as Artifact
uses: actions/upload-artifact@v7
with:
name: ee-docker-aws-ami-manifest
path: ${{env.CURRENT_MANIFEST_FILE}}
retention-days: 30
- name: Tag AMIs
run: |
# Extract AMI string again
AMI_STRING=$(jq -r '.builds[-1].artifact_id' ${{env.CURRENT_MANIFEST_FILE}})
# Process and tag each AMI in its respective region
IFS=',' read -ra AMI_ARRAY <<< "$AMI_STRING"
for ami in "${AMI_ARRAY[@]}"; do
REGION=$(echo "$ami" | cut -d ":" -f1)
AMI_ID=$(echo "$ami" | cut -d ":" -f2)
echo "Tagging AMI ${AMI_ID} in region ${REGION}"
aws ec2 create-tags \
--region "$REGION" \
--resources "$AMI_ID" \
--tags \
Key=GitBranch,Value=${{ github.ref_name }} \
Key=GitCommit,Value=${{ github.sha }} \
Key=BuildTime,Value=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
done
- name: Update CloudFormation Template
run: |
# Install yq if not present
if ! command -v yq &> /dev/null; then
echo "Installing yq..."
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
fi
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed. Please install jq to continue."
exit 1
fi
ARTIFACT_ID=$(jq -r '.builds[0].artifact_id' "${{ env.CURRENT_MANIFEST_FILE }}")
if [[ "$ARTIFACT_ID" == "null" || -z "$ARTIFACT_ID" ]]; then
echo "Error: Could not extract artifact_id from manifest file"
exit 1
fi
echo "Found artifact_id: $ARTIFACT_ID"
REGIONS=()
AMIS=()
# Split by comma and process each region:ami pair
IFS=',' read -ra PAIRS <<< "$ARTIFACT_ID"
for pair in "${PAIRS[@]}"; do
# Trim whitespace
pair=$(echo "$pair" | xargs)
# Split by colon to get region and ami
IFS=':' read -ra REGION_AMI <<< "$pair"
if [[ ${#REGION_AMI[@]} -eq 2 ]]; then
region="${REGION_AMI[0]}"
ami="${REGION_AMI[1]}"
REGIONS+=("$region")
AMIS+=("$ami")
echo " $region -> $ami"
fi
done
# Check if we found any AMI mappings
if [[ ${#REGIONS[@]} -eq 0 ]]; then
echo "Error: No valid region:ami pairs found in artifact_id"
exit 1
fi
# Copy the original template to output file
cp "${{ env.CF_TEMPLATE_FILE }}" "${{ env.CF_OUTPUT_FILE }}"
echo "Regions: ${REGIONS[@]}"
echo "AMIs: ${AMIS[@]}"
echo "Updating AMI IDs in template..."
REGION_RESTRICTIONS=()
# Update AMI IDs for each region found in the manifest
REGIONS_STRING=""
for i in "${!REGIONS[@]}"; do
region="${REGIONS[$i]}"
ami_id="${AMIS[$i]}"
echo " Updating $region with AMI: $ami_id"
REGIONS_STRING+="${region}, "
yq eval ".Parameters.AMIId.Default = \"${ami_id}\"" -i "${{ env.CF_OUTPUT_FILE }}"
done
cat "${{ env.CF_OUTPUT_FILE }}"
echo "Updated template saved as: ${{ env.CF_OUTPUT_FILE }}"
- name: Store CloudFormation Template in S3
run: |
# Store the current manifest as latest
aws s3 cp ${{env.CF_OUTPUT_FILE}} s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/cloudformation/plane-commercial-cloudformation-latest.yaml
date_string=$(date +%d%b%Y)
aws s3 cp ${{env.CF_OUTPUT_FILE}} s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/cloudformation/plane-commercial-cloudformation-${date_string}.yaml
- name: Upload Updated CloudFormation Template
uses: actions/upload-artifact@v7
with:
name: cloudformation-template
path: ${{ env.CF_OUTPUT_FILE }}
retention-days: 30
- name: Print Build Summary
id: print_build_summary
run: |
# Extract AMI details from manifest
AMI_STRING=$(jq -r '.builds[-1].artifact_id' ${{env.CURRENT_MANIFEST_FILE}})
AMI_NAME=$(jq -r '.builds[-1].custom_data.ami_name' ${{env.CURRENT_MANIFEST_FILE}})
BUILD_TIME=$(jq -r '.builds[-1].custom_data.build_time' ${{env.CURRENT_MANIFEST_FILE}})
# Create array of AMI information
declare -a AMI_INFO
IFS=',' read -ra AMI_ARRAY <<< "$AMI_STRING"
for ami in "${AMI_ARRAY[@]}"; do
REGION=$(echo "$ami" | cut -d ":" -f1)
AMI_ID=$(echo "$ami" | cut -d ":" -f2)
AMI_INFO+=("$REGION:$AMI_ID")
done
# Create build summary with all AMIs
{
echo "### 🌎 Regional AMI Distribution"
echo "| Region | AMI ID |"
echo "| --- | --- |"
for ami_info in "${AMI_INFO[@]}"; do
region=${ami_info%:*}
ami_id=${ami_info#*:}
echo "| \`${region}\` | \`${ami_id}\` |"
done
} >> $GITHUB_STEP_SUMMARY
date_string=$(date +%d%b%Y)
{
echo "### 📁 S3 Files"
echo "| File | Path | "
echo "| --- | --- |"
echo "| Latest CF Template | \`s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/cloudformation/plane-commercial-cloudformation-latest.yaml\` |"
echo "| CF Template | \`s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/cloudformation/plane-commercial-cloudformation-${date_string}.yaml\` |"
echo "| Current Manifest | \`s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-manifest-${{ github.sha }}.json\` |"
echo "| Latest Manifest | \`s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-manifest-latest.json\` |"
} >> $GITHUB_STEP_SUMMARY
# Console output for logs
echo "✅ AMI built successfully!"
echo "🔹 AMI Information:"
for ami_info in "${AMI_INFO[@]}"; do
region=${ami_info%:*}
ami_id=${ami_info#*:}
echo " • Region: ${region}, AMI ID: ${ami_id}"
done
@@ -1,180 +0,0 @@
name: AMI Build - DigitalOcean
on:
push:
branches:
- appliance-digitalocean
workflow_dispatch:
inputs:
ami_prefix:
description: 'AMI Prefix'
required: true
default: 'plane-commercial'
prime_host:
description: 'Prime Host'
required: true
default: 'https://prime.plane.so'
mark_manifest_latest:
description: 'Mark manifest as latest'
type: boolean
required: false
default: false
env:
# Inputs
AMI_PREFIX: ${{ inputs.ami_prefix || 'plane-commercial' }}
PRIME_HOST: ${{ inputs.prime_host || 'https://prime.plane.so' }}
# Inputs by Devops
AWS_MANIFEST_BUCKET: 'plane-terraform-marketplace'
AWS_VPC_REGION: 'us-east-1'
# Secrets
AWS_ACCESS_KEY: ${{ secrets.MARKETPLACE_AWS_ACCESS_KEY_ID }}
AWS_SECRET_KEY: ${{ secrets.MARKETPLACE_AWS_SECRET_ACCESS_KEY }}
# Constants
CURRENT_MANIFEST_FILE: 'ee-docker-digital-ocean-manifest.json'
EE_PACKER_FILE: 'ee-docker-digital-ocean.pkr.hcl'
MARK_MANIFEST_LATEST: ${{ inputs.mark_manifest_latest || false }}
jobs:
build_ami:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ env.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ env.AWS_SECRET_KEY }}
aws-region: ${{ env.AWS_VPC_REGION }}
- name: Setup `packer`
uses: hashicorp/setup-packer@v3
id: setup
with:
version: latest
- name: Copy Upload Assets
run: |
mkdir -p plane-dist
cp deployments/ami/commercial/cloudinit-ee/* plane-dist/
- name: Run `packer init`
id: init
run: "packer init ./deployments/ami/commercial/${{ env.EE_PACKER_FILE }}"
- name: Run `packer validate`
id: validate
run: "packer validate ./deployments/ami/commercial/${{ env.EE_PACKER_FILE }}"
- name: Make Variables File
id: make_variables_file
run: |
touch variables.pkrvars.hcl
echo "ami_name_prefix = \"${AMI_PREFIX}\"" >> variables.pkrvars.hcl
echo "prime_host = \"${PRIME_HOST}\"" >> variables.pkrvars.hcl
echo "instance_type = \"s-2vcpu-4gb\"" >> variables.pkrvars.hcl
echo "manifest_file_name = \"${{ env.CURRENT_MANIFEST_FILE }}\"" >> variables.pkrvars.hcl
cat variables.pkrvars.hcl
- name: Run `packer build`
id: build
run: |
packer build \
-var "api_token=${{ secrets.MARKETPLACE_DIGITAL_OCEAN_API_TOKEN }}" \
-var-file=variables.pkrvars.hcl \
./deployments/ami/commercial/${{ env.EE_PACKER_FILE }}
- name: Extract AMI Information and Create Summary
id: ami_info
run: |
# Extract AMI details from manifest
AMI_STRING=$(jq -r '.builds[-1].artifact_id' ${{env.CURRENT_MANIFEST_FILE}})
AMI_NAME=$(jq -r '.builds[-1].custom_data.ami_name' ${{env.CURRENT_MANIFEST_FILE}})
BUILD_TIME=$(jq -r '.builds[-1].custom_data.build_time' ${{env.CURRENT_MANIFEST_FILE}})
# Create array of AMI information
declare -a AMI_INFO
IFS=',' read -ra AMI_ARRAY <<< "$AMI_STRING"
for ami in "${AMI_ARRAY[@]}"; do
REGION=$(echo "$ami" | cut -d ":" -f1)
AMI_ID=$(echo "$ami" | cut -d ":" -f2)
AMI_INFO+=("$REGION:$AMI_ID")
done
# Add git information to manifest
jq --arg branch "${{ github.ref_name }}" \
--arg commit "${{ github.sha }}" \
--arg build_time "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
'.builds[-1].custom_data += {git_branch: $branch, git_commit: $commit, build_timestamp: $build_time}' \
${{env.CURRENT_MANIFEST_FILE}} > temp-manifest.json
mv temp-manifest.json ${{env.CURRENT_MANIFEST_FILE}}
- name: Store Manifest in S3
run: |
# Also store a versioned copy
aws s3 cp ${{env.CURRENT_MANIFEST_FILE}} "s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-digitalocean-manifest-${{ github.sha }}.json"
- name: Store Manifest in S3 as latest
if: ${{ env.MARK_MANIFEST_LATEST == 'true' }}
run: |
# Store the current manifest as latest
aws s3 cp ${{env.CURRENT_MANIFEST_FILE}} s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-digitalocean-manifest-latest.json || true
- name: Upload Build Manifest as Artifact
uses: actions/upload-artifact@v7
with:
name: ee-docker-digital-ocean-manifest
path: ${{env.CURRENT_MANIFEST_FILE}}
retention-days: 30
- name: Print Build Summary
id: print_build_summary
run: |
# Extract AMI details from manifest
AMI_STRING=$(jq -r '.builds[-1].artifact_id' ${{env.CURRENT_MANIFEST_FILE}})
AMI_NAME=$(jq -r '.builds[-1].custom_data.ami_name' ${{env.CURRENT_MANIFEST_FILE}})
BUILD_TIME=$(jq -r '.builds[-1].custom_data.build_time' ${{env.CURRENT_MANIFEST_FILE}})
# Create array of AMI information
declare -a AMI_INFO
IFS=',' read -ra AMI_ARRAY <<< "$AMI_STRING"
for ami in "${AMI_ARRAY[@]}"; do
REGION=$(echo "$ami" | cut -d ":" -f1)
AMI_ID=$(echo "$ami" | cut -d ":" -f2)
AMI_INFO+=("$REGION:$AMI_ID")
done
# Create build summary with all AMIs
{
echo "### 🌎 Regional AMI Distribution"
echo "| Region | AMI ID |"
echo "| --- | --- |"
for ami_info in "${AMI_INFO[@]}"; do
region=${ami_info%:*}
ami_id=${ami_info#*:}
echo "| \`${region}\` | \`${ami_id}\` |"
done
} >> $GITHUB_STEP_SUMMARY
date_string=$(date +%d%b%Y)
{
echo "### 📁 S3 Files"
echo "| File | Path | "
echo "| --- | --- |"
echo "| Current Manifest | \`s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-manifest-${{ github.sha }}.json\` |"
echo "| Latest Manifest | \`s3://${{ env.AWS_MANIFEST_BUCKET }}/plane-commercial/ami/manifests/plane-commercial-manifest-latest.json\` |"
} >> $GITHUB_STEP_SUMMARY
# Console output for logs
echo "✅ AMI built successfully!"
echo "🔹 AMI Information:"
for ami_info in "${AMI_INFO[@]}"; do
region=${ami_info%:*}
ami_id=${ami_info#*:}
echo " • Region: ${region}, AMI ID: ${ami_id}"
done
-176
View File
@@ -1,176 +0,0 @@
name: API Pytest Suite
on:
workflow_dispatch:
inputs:
test_marker:
description: "pytest -m filter"
required: false
default: "unit or contract or smoke"
type: string
run_slow_tests:
description: "Include slow-marked tests"
required: false
default: false
type: boolean
branch:
description: "Branch to run tests against"
required: false
default: "preview"
type: string
workflow_call:
inputs:
test_marker:
description: "pytest -m filter"
required: false
default: "unit or contract or smoke"
type: string
run_slow_tests:
description: "Include slow-marked tests"
required: false
default: false
type: boolean
branch:
description: "Branch to run tests against"
required: false
default: "preview"
type: string
secrets:
AWS_EKS_ROLE_ARN:
required: false
AWS_SECRET_ROLE_ARN:
required: false
AWS_SECRET_NAME:
required: false
AWS_REGION:
required: false
EKS_CLUSTER_NAME:
required: false
jobs:
run-pytests:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.branch || 'preview' }}
- name: Configure AWS credentials to assume the secret role
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_SECRET_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION || 'us-east-1' }}
- name: Retrieve deploy secrets
run: |
SECRET_JSON=$(aws secretsmanager get-secret-value \
--secret-id ${{ secrets.AWS_SECRET_NAME || 'github-actions/github-actions-secrets' }} \
--query SecretString \
--output text)
REDIS_URL=$(echo "$SECRET_JSON" | jq -r '.redis_url')
echo "::add-mask::$REDIS_URL"
DELIMITER=$(openssl rand -hex 16)
{
echo "redis_url<<${DELIMITER}"
echo "$REDIS_URL"
echo "${DELIMITER}"
} >> "$GITHUB_ENV"
- name: Configure AWS credentials to assume the EKS role
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_EKS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION || 'us-east-1' }}
- name: Update kubeconfig
run: |
aws eks update-kubeconfig \
--region ${{ secrets.AWS_REGION || 'us-east-1' }} \
--name ${{ secrets.EKS_CLUSTER_NAME || 'plane-eks-dev' }}
- name: Create test database
run: |
set -e
DB_NAME="t${{ github.run_id }}"
cat <<EOF | kubectl apply -f -
---
apiVersion: db.movetokube.com/v1alpha1
kind: Postgres
metadata:
name: ${DB_NAME}
namespace: postgres-operator
spec:
database: ${DB_NAME}
dropOnDelete: true
schemas:
- public
---
apiVersion: db.movetokube.com/v1alpha1
kind: PostgresUser
metadata:
name: ${DB_NAME}-apptest
namespace: postgres-operator
spec:
database: ${DB_NAME}
role: ${DB_NAME}_apptest
privileges: OWNER
secretName: ${DB_NAME}-apptest-secret
EOF
SECRET_NAME="${DB_NAME}-apptest-secret-${DB_NAME}-apptest"
echo "Waiting for database secret ${SECRET_NAME} to be created..."
kubectl wait --for=jsonpath='{.data.POSTGRES_URL}' \
secret/${SECRET_NAME} \
-n postgres-operator \
--timeout=120s
PG_URI=$(kubectl get secret "${SECRET_NAME}" -n postgres-operator -o jsonpath='{.data.POSTGRES_URL}' | base64 -d)
echo "::add-mask::$PG_URI"
DELIMITER=$(openssl rand -hex 16)
{
echo "DATABASE_URL<<${DELIMITER}"
echo "$PG_URI"
echo "${DELIMITER}"
} >> "$GITHUB_ENV"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: apps/api/requirements/test.txt
- name: Install system dependencies
run: sudo apt-get install -y libldap2-dev libsasl2-dev
- name: Install test dependencies
working-directory: apps/api
run: pip install -r requirements/test.txt
- name: Run pytest
working-directory: apps/api
env:
DATABASE_URL: ${{ env.DATABASE_URL }}
REDIS_URL: ${{ env.redis_url }}
SECRET_KEY: ci-test-secret-key
run: |
MARKER="${{ inputs.test_marker || 'unit or contract or smoke' }}"
if [[ "${{ inputs.run_slow_tests }}" == "true" ]]; then
MARKER="${MARKER} or slow"
fi
pytest -m "${MARKER}" --timeout=60
- name: Cleanup test database
if: always()
run: |
DB_NAME="t${{ github.run_id }}"
kubectl delete postgres "${DB_NAME}" -n postgres-operator --ignore-not-found || true
kubectl delete postgresuser "${DB_NAME}-apptest" -n postgres-operator --ignore-not-found || true
-415
View File
@@ -1,415 +0,0 @@
name: Branch Build Cloud
on:
workflow_dispatch:
inputs:
build_type:
description: "Type of build to run"
required: true
type: choice
default: "Build"
options:
- "Build"
- "Release"
releaseVersion:
description: "Release Version"
type: string
default: v0.0.0-cloud
isPrerelease:
description: "Is Pre-release"
type: boolean
default: false
required: true
env:
TARGET_BRANCH: ${{ github.ref_name }}
BUILD_TYPE: ${{ github.event.inputs.build_type }}
RELEASE_VERSION: ${{ github.event.inputs.releaseVersion }}
IS_PRERELEASE: ${{ github.event.inputs.isPrerelease }}
jobs:
branch_build_setup:
name: Build Setup
runs-on: ubuntu-22.04-4core
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
dh_img_admin: ${{ steps.set_env_variables.outputs.DH_IMG_ADMIN }}
dh_img_live: ${{ steps.set_env_variables.outputs.DH_IMG_LIVE }}
dh_img_silo: ${{ steps.set_env_variables.outputs.DH_IMG_SILO }}
dh_img_backend: ${{ steps.set_env_variables.outputs.DH_IMG_BACKEND }}
dh_img_email: ${{ steps.set_env_variables.outputs.DH_IMG_EMAIL }}
dh_img_pi: ${{ steps.set_env_variables.outputs.dh_img_pi }}
dh_img_flux: ${{ steps.set_env_variables.outputs.DH_IMG_FLUX }}
dh_img_node_runner: ${{ steps.set_env_variables.outputs.DH_IMG_NODE_RUNNER }}
build_type: ${{steps.set_env_variables.outputs.BUILD_TYPE}}
build_release: ${{ steps.set_env_variables.outputs.BUILD_RELEASE }}
build_prerelease: ${{ steps.set_env_variables.outputs.BUILD_PRERELEASE }}
release_version: ${{ steps.set_env_variables.outputs.RELEASE_VERSION }}
steps:
- id: set_env_variables
name: Set Environment Variables
run: |
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
BR_NAME=$( echo "${{ env.TARGET_BRANCH }}" | sed 's/[^a-zA-Z0-9.-]//g')
echo "TARGET_BRANCH=$BR_NAME" >> $GITHUB_OUTPUT
echo "DH_IMG_WEB=web-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_SPACE=space-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_ADMIN=admin-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_LIVE=live-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_SILO=silo-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_BACKEND=backend-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_EMAIL=email-cloud" >> $GITHUB_OUTPUT
echo "dh_img_pi=plane-pi-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_FLUX=flux-cloud" >> $GITHUB_OUTPUT
echo "DH_IMG_NODE_RUNNER=node-runner-cloud" >> $GITHUB_OUTPUT
echo "BUILD_TYPE=${{env.BUILD_TYPE}}" >> $GITHUB_OUTPUT
BUILD_RELEASE=false
BUILD_PRERELEASE=false
RELVERSION="latest"
if [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
FLAT_RELEASE_VERSION=$(echo "${{ env.RELEASE_VERSION }}" | sed 's/[^a-zA-Z0-9.-]//g')
echo "FLAT_RELEASE_VERSION=${FLAT_RELEASE_VERSION}" >> $GITHUB_OUTPUT
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
if [[ ! $FLAT_RELEASE_VERSION =~ $semver_regex ]]; then
echo "Invalid Release Version Format : $FLAT_RELEASE_VERSION"
echo "Please provide a valid SemVer version"
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
echo "Exiting the build process"
exit 1 # Exit with status 1 to fail the step
fi
BUILD_RELEASE=true
RELVERSION=$FLAT_RELEASE_VERSION
if [ "${{ env.IS_PRERELEASE }}" == "true" ]; then
BUILD_PRERELEASE=true
fi
fi
echo "BUILD_RELEASE=${BUILD_RELEASE}" >> $GITHUB_OUTPUT
echo "BUILD_PRERELEASE=${BUILD_PRERELEASE}" >> $GITHUB_OUTPUT
echo "RELEASE_VERSION=${RELVERSION}" >> $GITHUB_OUTPUT
branch_build_push_admin:
name: Build-Push Admin Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Admin Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
build-context: .
dockerfile-path: ./apps/admin/Dockerfile.admin
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_web:
name: Build-Push Web Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Web Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
build-context: .
dockerfile-path: ./apps/web/Dockerfile.web
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
build-args: |
VITE_APP_VERSION=${{ needs.branch_build_setup.outputs.release_version }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
SENTRY_AUTH_TOKEN=SENTRY_AUTH_TOKEN
branch_build_push_space:
name: Build-Push Space Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Space Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
build-context: .
dockerfile-path: ./apps/space/Dockerfile.space
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
build-args: |
VITE_APP_VERSION=${{ needs.branch_build_setup.outputs.release_version }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
SENTRY_AUTH_TOKEN=SENTRY_AUTH_TOKEN
branch_build_push_live:
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Live Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_live }}
build-context: .
dockerfile-path: ./apps/live/Dockerfile.live
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_silo:
name: Build-Push Silo Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Silo Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_silo }}
build-context: .
dockerfile-path: ./apps/silo/Dockerfile.silo
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_api:
name: Build-Push API Server Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Backend Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
build-context: ./apps/api
dockerfile-path: ./apps/api/Dockerfile.api
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_email:
name: Build-Push Email Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v6
- name: Email Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_email }}
build-context: ./apps/email
dockerfile-path: ./apps/email/Dockerfile
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_plane_pi:
name: Build-Push Plane PI Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Plane PI Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_pi }}
build-context: ./apps/pi
dockerfile-path: ./apps/pi/Dockerfile.api
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_flux:
name: Build-Push Flux Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Flux Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_flux }}
build-context: .
dockerfile-path: ./apps/flux/Dockerfile.flux
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_node_runner:
name: Build-Push Node Runner Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Node Runner Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_node_runner }}
build-context: .
dockerfile-path: ./apps/runners/node-runner/Dockerfile.runner
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
runs-on: ubuntu-22.04-4core
needs:
[
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_silo,
branch_build_push_api,
branch_build_push_email,
branch_build_push_plane_pi,
branch_build_push_flux,
branch_build_push_node_runner,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ env.REL_VERSION }}
name: ${{ env.REL_VERSION }}
target_commitish: ${{ github.ref_name }}
draft: false
prerelease: ${{ env.IS_PRERELEASE }}
generate_release_notes: true
-962
View File
@@ -1,962 +0,0 @@
name: Branch Build Enterprise
on:
workflow_dispatch:
inputs:
build_type:
description: "Type of build to run"
required: true
type: choice
default: "Build"
options:
- "Build"
- "Release"
build_images_type:
description: "Images to build"
required: true
type: choice
default: "regular"
options:
- "regular"
- "fips"
releaseVersion:
description: "Release Version"
type: string
default: v0.0.0
isPrerelease:
description: "Is Pre-release"
type: boolean
default: false
required: true
arm64:
description: "Build for ARM64 architecture"
required: false
default: false
type: boolean
aio_build:
description: "Build for AIO docker image"
required: false
default: false
type: boolean
cloud_builder:
description: "Build using Cloud Builder"
required: false
default: false
type: boolean
register_on_prime:
description: "Register this release on Prime"
required: false
default: false
type: boolean
prime_is_pre_release:
description: "Mark this Version as pre-release on Prime"
required: false
default: false
type: boolean
release_date:
description: "Release date for Prime (ISO8601, e.g. 2026-04-24T10:00:00Z). Empty = now."
required: false
default: ""
type: string
env:
TARGET_BRANCH: ${{ github.ref_name }}
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
BUILD_TYPE: ${{ github.event.inputs.build_type }}
RELEASE_VERSION: ${{ github.event.inputs.releaseVersion }}
IS_PRERELEASE: ${{ github.event.inputs.isPrerelease }}
AIO_BUILD: ${{ github.event.inputs.aio_build }}
CLOUD_BUILDER: ${{ github.event.inputs.cloud_builder }}
BUILD_IMAGES_TYPE: ${{ github.event.inputs.build_images_type }}
jobs:
branch_build_setup:
name: Build Setup
runs-on: ubuntu-22.04-4core
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
artifact_upload_to_s3: ${{ steps.set_env_variables.outputs.artifact_upload_to_s3 }}
artifact_s3_suffix: ${{ steps.set_env_variables.outputs.artifact_s3_suffix }}
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
dh_img_admin: ${{ steps.set_env_variables.outputs.DH_IMG_ADMIN }}
dh_img_live: ${{ steps.set_env_variables.outputs.DH_IMG_LIVE }}
dh_img_backend: ${{ steps.set_env_variables.outputs.DH_IMG_BACKEND }}
dh_img_proxy: ${{ steps.set_env_variables.outputs.DH_IMG_PROXY }}
dh_img_monitor: ${{ steps.set_env_variables.outputs.DH_IMG_MONITOR }}
dh_img_silo: ${{ steps.set_env_variables.outputs.DH_IMG_SILO }}
dh_img_email: ${{ steps.set_env_variables.outputs.DH_IMG_EMAIL }}
dh_img_pi: ${{ steps.set_env_variables.outputs.DH_IMG_PI }}
dh_img_aio: ${{ steps.set_env_variables.outputs.DH_IMG_AIO }}
dh_img_node_runner: ${{ steps.set_env_variables.outputs.DH_IMG_NODE_RUNNER }}
dh_img_flux: ${{ steps.set_env_variables.outputs.DH_IMG_FLUX }}
build_type: ${{steps.set_env_variables.outputs.BUILD_TYPE}}
build_release: ${{ steps.set_env_variables.outputs.BUILD_RELEASE }}
build_prerelease: ${{ steps.set_env_variables.outputs.BUILD_PRERELEASE }}
release_version: ${{ steps.set_env_variables.outputs.RELEASE_VERSION }}
arm64_build: ${{ steps.set_env_variables.outputs.ARM64_BUILD }}
aio_build: ${{ steps.set_env_variables.outputs.AIO_BUILD }}
fips_build: ${{ steps.set_env_variables.outputs.FIPS_BUILD }}
steps:
- id: set_env_variables
name: Set Environment Variables
run: |
if [ "${{ env.CLOUD_BUILDER }}" == "true" ]; then
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
else
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi
if [ "${{ env.ARM64_BUILD }}" == "true" ] || ([ "${{ env.BUILD_TYPE }}" == "Release" ] && [ "${{ env.IS_PRERELEASE }}" != "true" ]); then
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "ARM64_BUILD=true" >> $GITHUB_OUTPUT
else
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "ARM64_BUILD=false" >> $GITHUB_OUTPUT
fi
BR_NAME=$( echo "${{ env.TARGET_BRANCH }}" |sed 's/[^a-zA-Z0-9.-]//g')
echo "TARGET_BRANCH=$BR_NAME" >> $GITHUB_OUTPUT
if [ "${{ env.BUILD_IMAGES_TYPE }}" == "fips" ]; then
echo "FIPS_BUILD=true" >> $GITHUB_OUTPUT
echo "DH_IMG_WEB=web-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_SPACE=space-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_ADMIN=admin-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_LIVE=live-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_BACKEND=backend-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_PROXY=proxy-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_MONITOR=monitor-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_SILO=silo-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_EMAIL=email-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_PI=plane-pi-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_AIO=plane-aio-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_NODE_RUNNER=node-runner-commercial-fips" >> $GITHUB_OUTPUT
echo "DH_IMG_FLUX=flux-commercial-fips" >> $GITHUB_OUTPUT
else
echo "FIPS_BUILD=false" >> $GITHUB_OUTPUT
echo "DH_IMG_WEB=web-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_SPACE=space-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_ADMIN=admin-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_LIVE=live-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_BACKEND=backend-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_PROXY=proxy-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_MONITOR=monitor-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_SILO=silo-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_EMAIL=email-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_PI=plane-pi-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_AIO=plane-aio-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_NODE_RUNNER=node-runner-commercial" >> $GITHUB_OUTPUT
echo "DH_IMG_FLUX=flux-commercial" >> $GITHUB_OUTPUT
fi
echo "BUILD_TYPE=${{env.BUILD_TYPE}}" >> $GITHUB_OUTPUT
BUILD_RELEASE=false
BUILD_PRERELEASE=false
RELVERSION="latest"
BUILD_AIO=${{ env.AIO_BUILD }}
if [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
FLAT_RELEASE_VERSION=$(echo "${{ env.RELEASE_VERSION }}" | sed 's/[^a-zA-Z0-9.-]//g')
echo "FLAT_RELEASE_VERSION=${FLAT_RELEASE_VERSION}" >> $GITHUB_OUTPUT
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
if [[ ! $FLAT_RELEASE_VERSION =~ $semver_regex ]]; then
echo "Invalid Release Version Format : $FLAT_RELEASE_VERSION"
echo "Please provide a valid SemVer version"
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
echo "Exiting the build process"
exit 1 # Exit with status 1 to fail the step
fi
BUILD_RELEASE=true
RELVERSION=$FLAT_RELEASE_VERSION
if [ "${{ env.IS_PRERELEASE }}" == "true" ]; then
BUILD_PRERELEASE=true
fi
fi
echo "BUILD_RELEASE=${BUILD_RELEASE}" >> $GITHUB_OUTPUT
echo "BUILD_PRERELEASE=${BUILD_PRERELEASE}" >> $GITHUB_OUTPUT
echo "RELEASE_VERSION=${RELVERSION}" >> $GITHUB_OUTPUT
if [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=${{ env.RELEASE_VERSION }}" >> $GITHUB_OUTPUT
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=$BR_NAME" >> $GITHUB_OUTPUT
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ] || [ "${{ env.TARGET_BRANCH }}" == "develop" ] || [ "${{ env.TARGET_BRANCH }}" == "uat" ]; then
echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=$BR_NAME" >> $GITHUB_OUTPUT
else
echo "artifact_upload_to_s3=false" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=$BR_NAME" >> $GITHUB_OUTPUT
fi
echo "AIO_BUILD=${BUILD_AIO}" >> $GITHUB_OUTPUT
branch_build_push_admin:
name: Build-Push Admin Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Admin Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
build-context: .
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/admin/Dockerfile.fips' || './apps/admin/Dockerfile.admin' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_flux:
name: Build-Push Flux Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Flux Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
fips-docker-file-path: ${{ (needs.branch_build_setup.outputs.fips_build == 'true' && './apps/flux/Dockerfile.fips') || '' }}
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_flux }}
build-context: .
dockerfile-path: ./apps/flux/Dockerfile.flux
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_node_runner:
name: Build-Push Node Runner Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Node Runner Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
fips-docker-file-path: ${{ (needs.branch_build_setup.outputs.fips_build == 'true' && './apps/runners/node-runner/Dockerfile.fips') || '' }}
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_node_runner }}
build-context: .
dockerfile-path: ./apps/runners/node-runner/Dockerfile.runner
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_web:
name: Build-Push Web Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Web Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
build-context: .
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/web/Dockerfile.fips' || './apps/web/Dockerfile.web' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_space:
name: Build-Push Space Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Space Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
build-context: .
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/space/Dockerfile.fips' || './apps/space/Dockerfile.space' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_live:
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Live Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_live }}
build-context: .
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/live/Dockerfile.fips' || './apps/live/Dockerfile.live' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_silo:
name: Build-Push Silo Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Silo Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_silo }}
build-context: .
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/silo/Dockerfile.fips' || './apps/silo/Dockerfile.silo' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_api:
name: Build-Push API Server Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Backend Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
build-context: ./apps/api
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/api/Dockerfile.fips' || './apps/api/Dockerfile.api' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_proxy:
name: Build-Push Proxy Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Proxy Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_proxy }}
build-context: ./apps/proxy
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/proxy/Dockerfile.fips' || './apps/proxy/Dockerfile.ee' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_monitor:
name: Build-Push Monitor Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Generate Keypair
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
openssl genrsa -out private_key.pem 2048
else
echo "${{ secrets.DEFAULT_PRIME_PRIVATE_KEY }}" > private_key.pem
fi
openssl rsa -in private_key.pem -pubout -out public_key.pem
cat public_key.pem
# Generating the private key env for the generated keys
PRIVATE_KEY=$(cat private_key.pem | base64 -w 0)
echo "PRIVATE_KEY=${PRIVATE_KEY}" >> $GITHUB_ENV
- name: Upload Private Key for Prime
if: ${{ github.event.inputs.register_on_prime == 'true' }}
uses: actions/upload-artifact@v7
with:
name: prime-private-key
path: private_key.pem
retention-days: 1
- name: Monitor Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_monitor }}
build-context: ./apps/monitor
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/monitor/Dockerfile.fips' || './apps/monitor/Dockerfile' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
build-args: |
PRIVATE_KEY=${{ env.PRIVATE_KEY }}
branch_build_push_email:
name: Build-Push Email Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v6
- name: Email Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_email }}
build-context: ./apps/email
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/email/Dockerfile.fips' || './apps/email/Dockerfile' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_plane_pi:
name: Build-Push Plane PI Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Plane PI Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_pi }}
build-context: ./apps/pi
dockerfile-path: ${{ needs.branch_build_setup.outputs.fips_build == 'true' && './apps/pi/Dockerfile.fips' || './apps/pi/Dockerfile.api' }}
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_aio:
if: ${{ needs.branch_build_setup.outputs.aio_build == 'true' }}
name: Build-Push AIO Docker Image
runs-on: ubuntu-22.04-4core
needs:
[
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_api,
branch_build_push_proxy,
branch_build_push_monitor,
branch_build_push_email,
branch_build_push_silo,
branch_build_push_plane_pi,
branch_build_push_node_runner,
branch_build_push_flux,
]
steps:
- name: Checkout Files
uses: actions/checkout@v6
- name: Prepare AIO Assets
id: prepare_aio_assets
run: |
cd deployments/aio/commercial
if [ "${{ needs.branch_build_setup.outputs.build_type }}" == "Release" ]; then
aio_version=${{ needs.branch_build_setup.outputs.release_version }}
else
aio_version=${{ needs.branch_build_setup.outputs.gh_branch_name }}
fi
bash ./build.sh --release $aio_version
echo "AIO_BUILD_VERSION=${aio_version}" >> $GITHUB_OUTPUT
- name: Upload AIO Assets
uses: actions/upload-artifact@v7
with:
path: ./deployments/aio/commercial/dist
name: aio-assets-dist
- name: AIO Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_aio }}
build-context: ./deployments/aio/commercial
dockerfile-path: ./deployments/aio/commercial/Dockerfile
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
additional-assets: aio-assets-dist
additional-assets-dir: ./deployments/aio/commercial/dist
build-args: |
PLANE_VERSION=${{ steps.prepare_aio_assets.outputs.AIO_BUILD_VERSION }}
upload_artifacts_s3:
if: ${{ needs.branch_build_setup.outputs.artifact_upload_to_s3 == 'true' }}
name: Upload artifacts to S3 Bucket
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
container:
image: docker:29.3.1-cli-alpine3.23
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
ARTIFACT_SUFFIX: ${{ needs.branch_build_setup.outputs.artifact_s3_suffix }}
AWS_ACCESS_KEY_ID: ${{ secrets.SELF_HOST_BUCKET_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SELF_HOST_BUCKET_SECRET_KEY }}
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v6
- name: Upload artifacts
run: |
apk update
apk add --no-cache aws-cli
mkdir -p ~/${{ env.ARTIFACT_SUFFIX }}
if [ "${{ needs.branch_build_setup.outputs.fips_build }}" = "true" ]; then
cp ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-fips.yml
cp ~/${{ env.ARTIFACT_SUFFIX }}/coolify-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/coolify-compose-fips.yml
cp ~/${{ env.ARTIFACT_SUFFIX }}/portainer-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/portainer-compose-fips.yml
sed -i 's/-commercial:/-commercial-fips:/g' ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-fips.yml
sed -i 's/-commercial:/-commercial-fips:/g' ~/${{ env.ARTIFACT_SUFFIX }}/coolify-compose-fips.yml
sed -i 's/-commercial:/-commercial-fips:/g' ~/${{ env.ARTIFACT_SUFFIX }}/portainer-compose-fips.yml
else
sed -i 's@APP_RELEASE_VERSION=.*@APP_RELEASE_VERSION='${{ env.ARTIFACT_SUFFIX }}'@' deployments/cli/commercial/variables.env
cp deployments/cli/commercial/variables.env ~/${{ env.ARTIFACT_SUFFIX }}/variables.env
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/cli/commercial/docker-compose.yml
cp deployments/cli/commercial/docker-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-caddy.yml
cp deployments/cli/commercial/docker-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose.yml
cp ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-airgapped.yml
sed -i 's@${IS_AIRGAPPED.*@"1"@' ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-airgapped.yml
cp ~/${{ env.ARTIFACT_SUFFIX }}/variables.env ~/${{ env.ARTIFACT_SUFFIX }}/variables-airgapped.env
sed -i 's@IS_AIRGAPPED=.*@IS_AIRGAPPED=1@' ~/${{ env.ARTIFACT_SUFFIX }}/variables-airgapped.env
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/coolify/commercial/coolify-compose.yml
cp deployments/coolify/commercial/coolify-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/coolify-compose.yml
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/portainer/commercial/portainer-compose.yml
cp deployments/portainer/commercial/portainer-compose.yml ~/${{ env.ARTIFACT_SUFFIX }}/portainer-compose.yml
fi
# required for prime-cli backward compatibility
cp apps/proxy/Caddyfile.ee ~/${{ env.ARTIFACT_SUFFIX }}/Caddyfile
# Process podman quadlets
cd deployments/podman/commercial/
rm -rf plane-podman
mkdir -p plane-podman/plane/
mkdir -p plane-podman/config/
# Create a new file with initial content
cp ./install.sh ./plane-podman/install.sh
cp ../../cli/commercial/variables.env ./plane-podman/plane/plane.env
echo "" >> ./plane-podman/plane/plane.env
# Process quadlets.env
while IFS= read -r line; do
# Replace APP_VERSION with ARTIFACT_SUFFIX
if echo "$line" | grep -q "^APP_VERSION="; then
echo "APP_VERSION=${{ env.ARTIFACT_SUFFIX }}" >> ./plane-podman/plane/plane.env
continue
fi
# If line starts with #, write it and a single empty line
if echo "$line" | grep -q '^[[:space:]]*#'; then
echo "" >> ./plane-podman/plane/plane.env
echo "$line" >> ./plane-podman/plane/plane.env
continue
fi
# Skip empty lines
if [ -z "$line" ]; then
continue
fi
# Process key-value pairs
key=$(echo "$line" | cut -d'=' -f1)
value=$(echo "$line" | cut -d'=' -f2)
if ! grep -q "^${key}=" ./plane-podman/plane/plane.env; then
echo "${key}=${value}" >> ./plane-podman/plane/plane.env
elif [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s|${key}=.*|${key}=${value}|" ./plane-podman/plane/plane.env
else
sed -i "s|${key}=.*|${key}=${value}|" ./plane-podman/plane/plane.env
fi
done < quadlets.env
# Verify files exist before copying
if [ ! -f Caddyfile ]; then
echo "Error: Caddyfile not found"
exit 1
fi
cp Caddyfile ./plane-podman/plane/Caddyfile
# Verify podman-quadlets.ini exists
if [ ! -f podman-quadlets.ini ]; then
echo "Error: podman-quadlets.ini not found"
exit 1
fi
# check APP_RELEASE_VERSION in podman-quadlets.ini and change it to ${ARTIFACT_SUFFIX}
sed -i 's@${APP_RELEASE_VERSION}.*@'${{ env.ARTIFACT_SUFFIX }}'@' podman-quadlets.ini
# Split podman-quadlets.ini into separate files in plane-podman folder
cat podman-quadlets.ini | awk '/^# [a-z_-]+\.(container|network)$/ {filename="plane-podman/config/" substr($2,1); next} /^---$/ {next} filename && !/^#/ {print > filename}'
# Create tar of podman quadlets with proper directory structure
cd plane-podman
tar -czf ~/${{ env.ARTIFACT_SUFFIX }}/podman-quadlets.tar.gz .
cd ..
# Verify the tar was created successfully
if [ ! -f ~/${{ env.ARTIFACT_SUFFIX }}/podman-quadlets.tar.gz ]; then
echo "Error: Failed to create podman-quadlets.tar.gz"
exit 1
fi
aws s3 cp ~/${{ env.ARTIFACT_SUFFIX }} s3://${{ vars.SELF_HOST_BUCKET_NAME }}/plane-enterprise/${{ env.ARTIFACT_SUFFIX }} --recursive
- name: Upload tar artifacts
uses: actions/upload-artifact@v7
with:
name: podman-quadlets.tar.gz
path: ~/${{ env.ARTIFACT_SUFFIX }}/podman-quadlets.tar.gz
- name: Upload docker-compose.yml
if: ${{ needs.branch_build_setup.outputs.fips_build == 'false' }}
uses: actions/upload-artifact@v7
with:
name: docker-compose.yml
path: ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose.yml
- name: Upload docker-compose-airgapped.yml
if: ${{ needs.branch_build_setup.outputs.fips_build == 'false' }}
uses: actions/upload-artifact@v7
with:
name: docker-compose-airgapped.yml
path: ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-airgapped.yml
- name: Upload docker-compose.yml
if: ${{ needs.branch_build_setup.outputs.fips_build == 'true' }}
uses: actions/upload-artifact@v7
with:
name: docker-compose-fips.yml
path: ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-fips.yml
- name: Upload coolify-compose.yml
if: ${{ needs.branch_build_setup.outputs.fips_build == 'false' }}
uses: actions/upload-artifact@v7
with:
name: coolify-compose.yml
path: ~/${{ env.ARTIFACT_SUFFIX }}/coolify-compose.yml
- name: Upload coolify-compose.yml
if: ${{ needs.branch_build_setup.outputs.fips_build == 'true' }}
uses: actions/upload-artifact@v7
with:
name: coolify-compose-fips.yml
path: ~/${{ env.ARTIFACT_SUFFIX }}/coolify-compose-fips.yml
- name: Upload portainer-compose.yml
if: ${{ needs.branch_build_setup.outputs.fips_build == 'false' }}
uses: actions/upload-artifact@v7
with:
name: portainer-compose.yml
path: ~/${{ env.ARTIFACT_SUFFIX }}/portainer-compose.yml
- name: Upload portainer-compose.yml
if: ${{ needs.branch_build_setup.outputs.fips_build == 'true' }}
uses: actions/upload-artifact@v7
with:
name: portainer-compose-fips.yml
path: ~/${{ env.ARTIFACT_SUFFIX }}/portainer-compose-fips.yml
- name: Upload variables.env
uses: actions/upload-artifact@v7
with:
name: variables.env
path: ~/${{ env.ARTIFACT_SUFFIX }}/variables.env
- name: Upload variables-airgapped.env
if: ${{ needs.branch_build_setup.outputs.fips_build == 'false' }}
uses: actions/upload-artifact@v7
with:
name: variables-airgapped.env
path: ~/${{ env.ARTIFACT_SUFFIX }}/variables-airgapped.env
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
runs-on: ubuntu-22.04-4core
needs:
[
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_api,
branch_build_push_proxy,
branch_build_push_monitor,
branch_build_push_email,
branch_build_push_silo,
branch_build_push_plane_pi,
branch_build_push_node_runner,
branch_build_push_flux,
upload_artifacts_s3,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
ARTIFACT_SUFFIX: ${{ needs.branch_build_setup.outputs.artifact_s3_suffix }}
AIRGAPPED_ARM64_BUILD: ${{ needs.branch_build_setup.outputs.arm64_build == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download podman quadlets
uses: actions/download-artifact@v8
with:
name: podman-quadlets.tar.gz
path: deployments/podman/commercial/
- name: Update docker-compose
run: |
if [ "${{ needs.branch_build_setup.outputs.fips_build }}" = "true" ]; then
sed -i 's@APP_RELEASE_VERSION=.*@APP_RELEASE_VERSION='${{ env.ARTIFACT_SUFFIX }}'@' deployments/cli/commercial/variables.env
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/cli/commercial/docker-compose.yml
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/coolify/commercial/coolify-compose.yml
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/portainer/commercial/portainer-compose.yml
else
sed -i 's@APP_RELEASE_VERSION=.*@APP_RELEASE_VERSION='${{ env.ARTIFACT_SUFFIX }}'@' deployments/cli/commercial/variables.env
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/cli/commercial/docker-compose.yml
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/coolify/commercial/coolify-compose.yml
sed -i 's@${APP_RELEASE_VERSION.*@'${{ env.ARTIFACT_SUFFIX }}'@' deployments/portainer/commercial/portainer-compose.yml
cp deployments/cli/commercial/docker-compose.yml deployments/cli/commercial/docker-compose-caddy.yml
cp deployments/portainer/commercial/portainer-compose.yml deployments/portainer/commercial/swarm-compose.yml
cp deployments/cli/commercial/docker-compose.yml deployments/cli/commercial/docker-compose-airgapped.yml
sed -i 's@${IS_AIRGAPPED.*@"1"@' deployments/cli/commercial/docker-compose-airgapped.yml
cp deployments/cli/commercial/variables.env deployments/cli/commercial/variables-airgapped.env
sed -i 's@IS_AIRGAPPED=.*@IS_AIRGAPPED=1@' deployments/cli/commercial/variables-airgapped.env
fi
- name: Create Release
if: ${{ needs.branch_build_setup.outputs.fips_build == 'false' }}
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ env.REL_VERSION }}
name: ${{ env.REL_VERSION }}
target_commitish: ${{ github.ref_name }}
draft: false
prerelease: ${{ env.IS_PRERELEASE }}
generate_release_notes: true
files: |
${{ github.workspace }}/deployments/cli/commercial/variables.env
${{ github.workspace }}/deployments/cli/commercial/variables-airgapped.env
${{ github.workspace }}/deployments/cli/commercial/docker-compose.yml
${{ github.workspace }}/deployments/cli/commercial/docker-compose-airgapped.yml
${{ github.workspace }}/deployments/coolify/commercial/coolify-compose.yml
${{ github.workspace }}/deployments/portainer/commercial/portainer-compose.yml
${{ github.workspace }}/deployments/portainer/commercial/swarm-compose.yml
${{ github.workspace }}/deployments/podman/commercial/podman-quadlets.tar.gz
${{ (env.FIPS_BUILD == 'true' && format('{0}/deployments/cli/commercial/docker-compose-fips.yml', github.workspace)) || '' }}
${{ (env.FIPS_BUILD == 'true' && format('{0}/deployments/coolify/commercial/coolify-compose-fips.yml', github.workspace)) || '' }}
${{ (env.FIPS_BUILD == 'true' && format('{0}/deployments/portainer/commercial/portainer-compose-fips.yml', github.workspace)) || '' }}
- name: Create Release FIPS
if: ${{ needs.branch_build_setup.outputs.fips_build == 'true' }}
id: create_release_fips
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ env.REL_VERSION }}
name: ${{ env.REL_VERSION }}
target_commitish: ${{ github.ref_name }}
draft: false
prerelease: ${{ env.IS_PRERELEASE }}
generate_release_notes: true
files: |
${{ github.workspace }}/deployments/cli/commercial/variables.env
${{ github.workspace }}/deployments/cli/commercial/docker-compose-fips.yml
${{ github.workspace }}/deployments/coolify/commercial/coolify-compose-fips.yml
${{ github.workspace }}/deployments/portainer/commercial/portainer-compose-fips.yml
register_on_prime_version:
if: ${{ github.event.inputs.register_on_prime == 'true' }}
name: Register Release on Prime
runs-on: ubuntu-22.04-4core
needs:
[
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_api,
branch_build_push_proxy,
branch_build_push_monitor,
branch_build_push_email,
branch_build_push_silo,
branch_build_push_plane_pi,
branch_build_push_node_runner,
branch_build_push_flux,
]
steps:
- name: Download Private Key
uses: actions/download-artifact@v8
with:
name: prime-private-key
- name: Register on Prime
env:
PRIME_URL: ${{ secrets.PRIME_VERSION_WEBHOOK_URL }}
PRIME_SECRET: ${{ secrets.PRIME_VERSION_REGISTRATION_SECRET }}
TAG: ${{ needs.branch_build_setup.outputs.release_version }}
PRIME_IS_PRERELEASE: ${{ github.event.inputs.prime_is_pre_release }}
RELEASE_DATE: ${{ github.event.inputs.release_date }}
run: |
# jq --rawfile reads private_key.pem verbatim (including newlines)
# and JSON-escapes it. Prime receives the raw PEM text, not base64.
curl --fail-with-body -sS -X POST "$PRIME_URL" \
-H "Content-Type: application/json" \
-H "X-Prime-Registration-Secret: $PRIME_SECRET" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg pre "$PRIME_IS_PRERELEASE" \
--arg rd "$RELEASE_DATE" \
--rawfile key private_key.pem \
'{tag:$tag, is_pre_release:($pre == "true"), release_date:$rd, encryption_key:$key}')"
- name: Delete Private Key Artifact
if: always()
uses: geekyeggo/delete-artifact@v5
with:
name: prime-private-key
failOnError: false
-434
View File
@@ -1,434 +0,0 @@
name: Branch Build CE
on:
workflow_dispatch:
inputs:
build_type:
description: "Type of build to run"
required: true
type: choice
default: "Build"
options:
- "Build"
- "Release"
releaseVersion:
description: "Release Version"
type: string
default: v0.0.0
isPrerelease:
description: "Is Pre-release"
type: boolean
default: false
required: true
arm64:
description: "Build for ARM64 architecture"
required: false
default: false
type: boolean
aio_build:
description: "Build for AIO docker image"
required: false
default: false
type: boolean
push:
branches:
- preview
- canary
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
TARGET_BRANCH: ${{ github.ref_name }}
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
BUILD_TYPE: ${{ github.event.inputs.build_type }}
RELEASE_VERSION: ${{ github.event.inputs.releaseVersion }}
IS_PRERELEASE: ${{ github.event.inputs.isPrerelease }}
AIO_BUILD: ${{ github.event.inputs.aio_build }}
jobs:
branch_build_setup:
name: Build Setup
runs-on: ubuntu-22.04-4core
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
dh_img_admin: ${{ steps.set_env_variables.outputs.DH_IMG_ADMIN }}
dh_img_live: ${{ steps.set_env_variables.outputs.DH_IMG_LIVE }}
dh_img_backend: ${{ steps.set_env_variables.outputs.DH_IMG_BACKEND }}
dh_img_proxy: ${{ steps.set_env_variables.outputs.DH_IMG_PROXY }}
dh_img_aio: ${{ steps.set_env_variables.outputs.DH_IMG_AIO }}
build_type: ${{steps.set_env_variables.outputs.BUILD_TYPE}}
build_release: ${{ steps.set_env_variables.outputs.BUILD_RELEASE }}
build_prerelease: ${{ steps.set_env_variables.outputs.BUILD_PRERELEASE }}
release_version: ${{ steps.set_env_variables.outputs.RELEASE_VERSION }}
aio_build: ${{ steps.set_env_variables.outputs.AIO_BUILD }}
steps:
- id: set_env_variables
name: Set Environment Variables
run: |
if [ "${{ env.ARM64_BUILD }}" == "true" ] || ([ "${{ env.BUILD_TYPE }}" == "Release" ] && [ "${{ env.IS_PRERELEASE }}" != "true" ]); then
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
else
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi
BR_NAME=$( echo "${{ env.TARGET_BRANCH }}" |sed 's/[^a-zA-Z0-9.-]//g')
echo "TARGET_BRANCH=$BR_NAME" >> $GITHUB_OUTPUT
echo "DH_IMG_WEB=plane-frontend" >> $GITHUB_OUTPUT
echo "DH_IMG_SPACE=plane-space" >> $GITHUB_OUTPUT
echo "DH_IMG_ADMIN=plane-admin" >> $GITHUB_OUTPUT
echo "DH_IMG_LIVE=plane-live" >> $GITHUB_OUTPUT
echo "DH_IMG_BACKEND=plane-backend" >> $GITHUB_OUTPUT
echo "DH_IMG_PROXY=plane-proxy" >> $GITHUB_OUTPUT
echo "DH_IMG_AIO=plane-aio-community" >> $GITHUB_OUTPUT
echo "BUILD_TYPE=${{env.BUILD_TYPE}}" >> $GITHUB_OUTPUT
BUILD_RELEASE=false
BUILD_PRERELEASE=false
RELVERSION="latest"
BUILD_AIO=${{ env.AIO_BUILD }}
if [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
FLAT_RELEASE_VERSION=$(echo "${{ env.RELEASE_VERSION }}" | sed 's/[^a-zA-Z0-9.-]//g')
echo "FLAT_RELEASE_VERSION=${FLAT_RELEASE_VERSION}" >> $GITHUB_OUTPUT
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
if [[ ! $FLAT_RELEASE_VERSION =~ $semver_regex ]]; then
echo "Invalid Release Version Format : $FLAT_RELEASE_VERSION"
echo "Please provide a valid SemVer version"
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
echo "Exiting the build process"
exit 1 # Exit with status 1 to fail the step
fi
BUILD_RELEASE=true
RELVERSION=$FLAT_RELEASE_VERSION
if [ "${{ env.IS_PRERELEASE }}" == "true" ]; then
BUILD_PRERELEASE=true
fi
BUILD_AIO=true
fi
echo "BUILD_RELEASE=${BUILD_RELEASE}" >> $GITHUB_OUTPUT
echo "BUILD_PRERELEASE=${BUILD_PRERELEASE}" >> $GITHUB_OUTPUT
echo "RELEASE_VERSION=${RELVERSION}" >> $GITHUB_OUTPUT
echo "AIO_BUILD=${BUILD_AIO}" >> $GITHUB_OUTPUT
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v6
branch_build_push_admin:
name: Build-Push Admin Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Admin Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
build-context: .
dockerfile-path: ./apps/admin/Dockerfile.admin
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_web:
name: Build-Push Web Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Web Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
build-context: .
dockerfile-path: ./apps/web/Dockerfile.web
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_space:
name: Build-Push Space Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Space Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
build-context: .
dockerfile-path: ./apps/space/Dockerfile.space
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_live:
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Live Build and Push
uses: makeplane/actions/build-push@v1.5.1
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_live }}
build-context: .
dockerfile-path: ./apps/live/Dockerfile.live
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
secret-envs: |
TURBO_TOKEN=TURBO_TOKEN
TURBO_REMOTE_CACHE_SIGNATURE_KEY=TURBO_REMOTE_CACHE_SIGNATURE_KEY
branch_build_push_api:
name: Build-Push API Server Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Backend Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
build-context: ./apps/api
dockerfile-path: ./apps/api/Dockerfile.api
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_proxy:
name: Build-Push Proxy Docker Image
runs-on: ubuntu-22.04-4core
needs: [branch_build_setup]
steps:
- name: Proxy Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_proxy }}
build-context: ./apps/proxy
dockerfile-path: ./apps/proxy/Dockerfile.ce
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_aio:
if: ${{ needs.branch_build_setup.outputs.aio_build == 'true' }}
name: Build-Push AIO Docker Image
runs-on: ubuntu-22.04-4core
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v6
- name: Prepare AIO Assets
id: prepare_aio_assets
run: |
cd deployments/aio/community
if [ "${{ needs.branch_build_setup.outputs.build_type }}" == "Release" ]; then
aio_version=${{ needs.branch_build_setup.outputs.release_version }}
else
aio_version=${{ needs.branch_build_setup.outputs.gh_branch_name }}
fi
bash ./build.sh --release $aio_version
echo "AIO_BUILD_VERSION=${aio_version}" >> $GITHUB_OUTPUT
- name: Upload AIO Assets
uses: actions/upload-artifact@v7
with:
path: ./deployments/aio/community/dist
name: aio-assets-dist
- name: AIO Build and Push
uses: makeplane/actions/build-push@v1.5.1
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_aio }}
build-context: ./deployments/aio/community
dockerfile-path: ./deployments/aio/community/Dockerfile
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
additional-assets: aio-assets-dist
additional-assets-dir: ./deployments/aio/community/dist
build-args: |
PLANE_VERSION=${{ steps.prepare_aio_assets.outputs.AIO_BUILD_VERSION }}
upload_build_assets:
name: Upload Build Assets
runs-on: ubuntu-22.04-4core
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v6
- name: Update Assets
run: |
if [ "${{ needs.branch_build_setup.outputs.build_type }}" == "Release" ]; then
REL_VERSION=${{ needs.branch_build_setup.outputs.release_version }}
else
REL_VERSION=${{ needs.branch_build_setup.outputs.gh_branch_name }}
fi
cp ./deployments/cli/community/install.sh deployments/cli/community/setup.sh
sed -i 's/${APP_RELEASE:-stable}/${APP_RELEASE:-'${REL_VERSION}'}/g' deployments/cli/community/docker-compose.yml
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deployments/cli/community/variables.env
- name: Upload Assets
uses: actions/upload-artifact@v7
with:
name: community-assets
path: |
./deployments/cli/community/setup.sh
./deployments/cli/community/restore.sh
./deployments/cli/community/restore-airgapped.sh
./deployments/cli/community/docker-compose.yml
./deployments/cli/community/variables.env
./deployments/swarm/community/swarm.sh
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
runs-on: ubuntu-22.04-4core
needs:
[
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_api,
branch_build_push_proxy,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Update Assets
run: |
cp ./deployments/cli/community/install.sh deployments/cli/community/setup.sh
sed -i 's/${APP_RELEASE:-stable}/${APP_RELEASE:-'${REL_VERSION}'}/g' deployments/cli/community/docker-compose.yml
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deployments/cli/community/variables.env
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ env.REL_VERSION }}
name: ${{ env.REL_VERSION }}
target_commitish: ${{ github.ref_name }}
draft: false
prerelease: ${{ env.IS_PRERELEASE }}
generate_release_notes: true
files: |
${{ github.workspace }}/deployments/cli/community/setup.sh
${{ github.workspace }}/deployments/cli/community/restore.sh
${{ github.workspace }}/deployments/cli/community/restore-airgapped.sh
${{ github.workspace }}/deployments/cli/community/docker-compose.yml
${{ github.workspace }}/deployments/cli/community/variables.env
${{ github.workspace }}/deployments/swarm/community/swarm.sh
-43
View File
@@ -1,43 +0,0 @@
name: Version Change Before Release
on:
pull_request:
branches:
- master
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
- name: Get PR Branch version
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Fetch base branch
run: git fetch origin master:master
- name: Get Master Branch version
run: |
git checkout master
echo "MASTER_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Get master branch version and compare
run: |
echo "Comparing versions: PR version is $PR_VERSION, Master version is $MASTER_VERSION"
if [ "$PR_VERSION" == "$MASTER_VERSION" ]; then
echo "Version in PR branch is the same as in master. Failing the CI."
exit 1
else
echo "Version check passed. Versions are different."
fi
env:
PR_VERSION: ${{ env.PR_VERSION }}
MASTER_VERSION: ${{ env.MASTER_VERSION }}
-112
View File
@@ -1,112 +0,0 @@
name: Cleanup Closed PRs
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
workflow_call:
inputs:
pr_number:
description: 'PR number whose deployment to tear down'
required: true
type: string
deploy_type:
description: 'Deploy type: enterprise or cloud'
required: true
type: string
jobs:
execute:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
id-token: write
contents: read
pull-requests: read
steps:
# Checkout the code
- uses: actions/checkout@v6
- name: Configure AWS credentials to assume the EKS role
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_EKS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION || 'us-east-1' }}
- name: Update kubeconfig
run: |
aws eks update-kubeconfig \
--region ${{ secrets.AWS_REGION || 'us-east-1' }} \
--name ${{ secrets.EKS_CLUSTER_NAME || 'plane-eks-dev' }}
- name: Verify access
run: |
kubectl get nodes
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: v3.14.4
# ---------------- CLEANUP ----------------
- name: Cleanup merged or closed PR deployments
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
set -e
if [ -n "${{ inputs.pr_number }}" ]; then
# Single-PR recreate: target only the specified namespace
PR="${{ inputs.pr_number }}"
DEPLOY_TYPE="${{ inputs.deploy_type }}"
if [ "${DEPLOY_TYPE}" == "enterprise" ]; then
deployed_charts="ee-${PR}-ent"
else
deployed_charts="ee-${PR}-cloud"
fi
else
# Scheduled/manual: match all deployed PR releases
deployed_charts="$(helm ls -A --filter 'ee-[0-9]+-(ent|cloud)' --output json | jq -r '.[].name')"
fi
for deployed_chart in $deployed_charts; do
namespace="$deployed_chart"
pr=$(echo "$deployed_chart" | sed -n 's/^ee-\([0-9]*\)-.*/\1/p')
# For scheduled/manual runs only: skip PRs that are still open
if [ -z "${{ inputs.pr_number }}" ]; then
pr_state="$(gh pr view "$pr" --json state --jq .state)"
if [[ "$pr_state" != "MERGED" && "$pr_state" != "CLOSED" ]]; then
continue
fi
fi
echo "Cleaning up namespace $namespace for PR $pr"
# K8s cleanup - failures must not prevent DB drop (wrapped so set -e won't exit)
{
helm uninstall "$namespace" --namespace "$namespace" || true
kubectl delete namespace "$namespace" --grace-period=0 --force || true
pv_json=$(kubectl get pv -o json 2>/dev/null || echo '{}')
pv_list=$(echo "$pv_json" | jq -r --arg ns "$namespace" '.items[]? | select(.spec.claimRef.namespace == $ns) | .metadata.name' 2>/dev/null) || pv_list=""
for pv in $pv_list; do
echo "Deleting PV $pv"
kubectl patch pv "$pv" --type=json \
-p='[{"op": "remove", "path": "/metadata/finalizers"}]' || true
kubectl delete pv "$pv" --grace-period=0 --force || true
done
} || true
DB_ENT="ee${pr}ent"
DB_CLOUD="ee${pr}cloud"
# Delete postgres-operator CRDs (operator will drop databases via dropOnDelete)
kubectl delete postgres "${DB_ENT}" -n postgres-operator --ignore-not-found || true
kubectl delete postgresuser "${DB_ENT}-appuser" -n postgres-operator --ignore-not-found || true
kubectl delete postgres "${DB_CLOUD}" -n postgres-operator --ignore-not-found || true
kubectl delete postgresuser "${DB_CLOUD}-appuser" -n postgres-operator --ignore-not-found || true
done
echo "Cleanup complete."
-62
View File
@@ -1,62 +0,0 @@
name: "CodeQL"
on:
workflow_dispatch:
push:
branches: ["preview", "canary", "master"]
pull_request:
branches: ["preview", "canary", "master"]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["python", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
-57
View File
@@ -1,57 +0,0 @@
name: Copy Right Check
on:
workflow_dispatch:
pull_request:
branches:
- "preview"
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "reopened"
paths-ignore:
- ".github/**"
- "**/*.md"
- "docs/**"
- ".gitignore"
- ".editorconfig"
- "LICENSE"
- "**/Dockerfile*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
license-check:
name: Licence Check
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.22"
- name: Install addlicense
run: |
go install github.com/google/addlicense@latest
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Check Copyright For Python Files
run: |
set -e
echo "Running copyright check..."
addlicense -check -f COPYRIGHT.txt -ignore "**/migrations/**" $(git ls-files '*.py')
echo "Copyright check passed."
- name: Check Copyright For TypeScript Files
run: |
set -e
echo "Running copyright check..."
addlicense -check -f COPYRIGHT.txt -ignore "**/*.config.ts" -ignore "**/*.d.ts" $(git ls-files '*.ts' '*.tsx')
echo "Copyright check passed."
File diff suppressed because it is too large Load Diff
@@ -1,59 +0,0 @@
name: Deploy Storybook (Blocks)
on:
workflow_dispatch:
push:
branches:
- preview
paths:
- "packages/blocks/**"
- "packages/propel/**"
- "packages/ui/**"
- "packages/types/**"
- "packages/constants/**"
- "packages/utils/**"
- "packages/hooks/**"
- "packages/tailwindcss/**"
- "pnpm-lock.yaml"
- "package.json"
- "pnpm-workspace.yaml"
- "turbo.json"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-storybook:
name: Build & Deploy Storybook
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
cache: "pnpm"
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Storybook
run: pnpm turbo run build-storybook --filter=@plane/blocks
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.STORYBOOK_CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
packageManager: npx
command: pages deploy packages/blocks/storybook-static --project-name=ui-blocks --branch=preview
-58
View File
@@ -1,58 +0,0 @@
name: Deploy Storybook (Propel)
on:
workflow_dispatch:
push:
branches:
- preview
paths:
- "packages/propel/**"
- "packages/ui/**"
- "packages/types/**"
- "packages/constants/**"
- "packages/utils/**"
- "packages/hooks/**"
- "packages/tailwindcss/**"
- "pnpm-lock.yaml"
- "package.json"
- "pnpm-workspace.yaml"
- "turbo.json"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-storybook:
name: Build & Deploy Storybook
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
cache: "pnpm"
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Storybook
run: pnpm turbo run build-storybook --filter=@plane/propel
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.STORYBOOK_CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
packageManager: npx
command: pages deploy packages/propel/storybook-static --project-name=ui-kit --branch=preview
-168
View File
@@ -1,168 +0,0 @@
name: Feature Preview
on:
workflow_dispatch:
inputs:
base_tag_name:
description: 'Base Tag Name'
required: false
default: 'preview'
env:
TARGET_BRANCH: ${{ github.ref_name }}
jobs:
branch_build_setup:
name: Build Setup
runs-on: ubuntu-latest
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
aio_base_tag: ${{ steps.set_env_variables.outputs.AIO_BASE_TAG }}
do_full_build: ${{ steps.set_env_variables.outputs.DO_FULL_BUILD }}
do_slim_build: ${{ steps.set_env_variables.outputs.DO_SLIM_BUILD }}
steps:
- id: set_env_variables
name: Set Environment Variables
run: |
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
else
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
fi
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
FLAT_BRANCH_NAME=$(echo "${{ env.TARGET_BRANCH }}" | sed 's/[^a-zA-Z0-9]/-/g')
echo "FLAT_BRANCH_NAME=$FLAT_BRANCH_NAME" >> $GITHUB_OUTPUT
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v6
full_build_push:
runs-on: ubuntu-22.04
needs: [branch_build_setup]
env:
BUILD_TYPE: full
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
AIO_IMAGE_TAGS: makeplane/plane-aio-feature:${{ needs.branch_build_setup.outputs.flat_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver: ${{ env.BUILDX_DRIVER }}
version: ${{ env.BUILDX_VERSION }}
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Check out the repo
uses: actions/checkout@v6
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v7
with:
context: .
file: ./aio/Dockerfile-app
platforms: ${{ env.BUILDX_PLATFORMS }}
tags: ${{ env.AIO_IMAGE_TAGS }}
push: true
build-args:
BUILD_TAG=${{ env.AIO_BASE_TAG }}
BUILD_TYPE=${{env.BUILD_TYPE}}
# cache-from: type=gha
# cache-to: type=gha,mode=max
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
outputs:
AIO_IMAGE_TAGS: ${{ env.AIO_IMAGE_TAGS }}
feature-deploy:
needs: [branch_build_setup, full_build_push]
name: Feature Deploy
runs-on: ubuntu-latest
env:
KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }}
DEPLOYMENT_NAME: ${{ needs.branch_build_setup.outputs.flat_branch_name }}
steps:
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
tags: tag:ci
- name: Kubectl Setup
run: |
curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_KUBE_VERSION }}/bin/linux/amd64/kubectl"
chmod +x kubectl
mkdir -p ~/.kube
echo "$KUBE_CONFIG_FILE" > ~/.kube/config
chmod 600 ~/.kube/config
- name: HELM Setup
run: |
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
- name: App Deploy
run: |
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }}
APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}"
helm --kube-insecure-skip-tls-verify uninstall \
${{ env.DEPLOYMENT_NAME }} \
--namespace $APP_NAMESPACE \
--timeout 10m0s \
--wait \
--ignore-not-found
METADATA=$(helm --kube-insecure-skip-tls-verify upgrade \
--install=true \
--namespace $APP_NAMESPACE \
--set dockerhub.loginid=${{ secrets.DOCKERHUB_USERNAME }} \
--set dockerhub.password=${{ secrets.DOCKERHUB_TOKEN_RO}} \
--set config.feature_branch=${{ env.DEPLOYMENT_NAME }} \
--set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \
--set ingress.tls_secret=${{vars.FEATURE_PREVIEW_INGRESS_TLS_SECRET || '' }} \
--output json \
--timeout 10m0s \
--wait \
${{ env.DEPLOYMENT_NAME }} feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} )
APP_NAME=$(echo $METADATA | jq -r '.name')
INGRESS_HOSTNAME=$(kubectl get ingress -n $APP_NAMESPACE --insecure-skip-tls-verify \
-o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \
jq -r '.spec.rules[0].host')
echo "****************************************"
echo "APP NAME ::: $APP_NAME"
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
echo "****************************************"
-56
View File
@@ -1,56 +0,0 @@
name: i18n sync check
on:
workflow_dispatch:
pull_request:
branches:
- "preview"
- "phoenix-releases"
- "master"
- "release/**"
types:
- "opened"
- "synchronize"
- "reopened"
- "ready_for_review"
paths:
- "packages/i18n/**"
- ".github/workflows/i18n-sync-check.yml"
push:
branches:
- "preview"
- "phoenix-releases"
- "master"
paths:
- "packages/i18n/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
sync-check:
name: check:sync
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.event_name == 'push' || github.event.pull_request.draft == false
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Run sync check
run: pnpm dlx tsx packages/i18n/scripts/sync-check.ts --ci
@@ -1,50 +0,0 @@
name: Build and lint API and PI
on:
workflow_dispatch:
pull_request:
branches:
- "preview"
- "phoenix-releases"
- "master"
- "release/**"
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "reopened"
paths:
- "apps/api/**"
- "apps/pi/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-api:
name: Lint API
runs-on: ubuntu-latest
timeout-minutes: 25
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12.x"
cache: "pip"
cache-dependency-path: "apps/api/requirements.txt"
# - name: Install OpenLDAP dependencies
# run: |
# sudo apt-get update
# sudo apt-get install -y libldap2-dev libsasl2-dev
- name: Install Pylint
run: python -m pip install ruff
# - name: Install API Dependencies
# run: cd apps/api && pip install -r requirements.txt
- name: Lint apps/api
run: ruff check apps/api
- name: Lint apps/pi
run: ruff check apps/pi
@@ -1,238 +0,0 @@
name: Build and lint web apps
on:
workflow_dispatch:
pull_request:
branches:
- "preview"
- "phoenix-releases"
- "master"
- "release/**"
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "reopened"
paths:
- "apps/web/**"
- "apps/admin/**"
- "apps/space/**"
- "apps/live/**"
- "apps/silo/**"
- "apps/flux/**"
- "packages/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- "tsconfig.json"
- ".oxlintrc.json"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
jobs:
# Format check has no build dependencies - run immediately in parallel
check-format:
name: check:format
runs-on: ubuntu-22.04-4core
timeout-minutes: 10
if: github.event.pull_request.draft == false
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check formatting
run: pnpm turbo run check:format --affected
# Build packages - required for lint and type checks
build:
name: Build packages
runs-on: ubuntu-22.04-4core
timeout-minutes: 15
if: github.event.pull_request.draft == false
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
NODE_OPTIONS: "--max-old-space-size=6144"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Restore Turbo cache
uses: actions/cache/restore@v5
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.event.pull_request.base.sha }}-
turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm turbo run build --affected
- name: Save Turbo cache
uses: actions/cache/save@v5
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
# Lint check depends on build artifacts (type checking is covered by the build step)
check-lint:
name: check:lint
runs-on: ubuntu-22.04-4core
needs: build
timeout-minutes: 15
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
NODE_OPTIONS: "--max-old-space-size=4096"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Restore Turbo cache
uses: actions/cache/restore@v5
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run check:lint
run: pnpm turbo run check:lint --affected
test:
name: test
runs-on: ubuntu-22.04-4core
timeout-minutes: 30
if: github.event.pull_request.draft == false
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
NODE_OPTIONS: "--max-old-space-size=6144"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22.18.0"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
- name: Restore Turbo cache
uses: actions/cache/restore@v5
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.event.pull_request.base.sha }}-
turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test --affected
+54
View File
@@ -0,0 +1,54 @@
name: Build Api Server Docker Image
on:
push:
branches:
- 'develop'
- 'master'
tags:
- '*'
jobs:
build_push_backend:
name: Build Api Server Docker Image
runs-on: ubuntu-20.04
permissions:
contents: read
packages: write
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Github Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-backend
- name: Build Api Server
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+54
View File
@@ -0,0 +1,54 @@
name: Build Frontend Docker Image
on:
push:
branches:
- 'develop'
- 'master'
tags:
- '*'
jobs:
build_push_frontend:
name: Build Frontend Docker Image
runs-on: ubuntu-20.04
permissions:
contents: read
packages: write
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Github Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-frontend
- name: Build Frontend Server
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/app/Dockerfile.web
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-140
View File
@@ -1,140 +0,0 @@
name: React Doctor
on:
pull_request:
branches:
- "preview"
- "master"
- "release/**"
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "reopened"
paths:
- "apps/web/**"
- "apps/admin/**"
- "apps/space/**"
- "packages/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
react-doctor:
name: React Doctor
runs-on: ubuntu-22.04-4core
timeout-minutes: 10
if: github.event.pull_request.draft == false
# Non-blocking: this job can fail without preventing merge
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Run React Doctor
run: |
npx -y react-doctor@latest . --diff ${{ github.event.pull_request.base.ref }} 2>&1 | tee /tmp/react-doctor-output.txt || true
- name: Generate summary comment
if: always()
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const raw = fs.readFileSync('/tmp/react-doctor-output.txt', 'utf8');
// Strip ANSI escape codes
const clean = raw.replace(/\x1b\[[0-9;]*m/g, '');
// Parse each project scan
const lines = clean.split('\n');
const projects = [];
let currentProject = null;
for (const line of lines) {
const scanMatch = line.match(/Scanning .*\/(apps|packages)\/([^.]+)\.\.\./);
if (scanMatch) {
currentProject = { name: `${scanMatch[1]}/${scanMatch[2]}` };
projects.push(currentProject);
continue;
}
if (!currentProject) continue;
const scoreMatch = line.match(/(\d+)\s*\/\s*100\s+(Great|Needs work|Critical)/);
if (scoreMatch) {
currentProject.score = parseInt(scoreMatch[1]);
currentProject.label = scoreMatch[2];
}
const statsMatch = line.match(/(?:✗\s*(\d+)\s*errors?\s*)?(?:⚠\s*(\d+)\s*warnings?)?\s*across\s*(\d+)\/(\d+)\s*files/);
if (statsMatch) {
currentProject.errors = parseInt(statsMatch[1] || '0');
currentProject.warnings = parseInt(statsMatch[2] || '0');
currentProject.filesAffected = parseInt(statsMatch[3]);
currentProject.filesTotal = parseInt(statsMatch[4]);
}
if (line.includes('No issues found')) {
currentProject.score = currentProject.score || 100;
currentProject.label = currentProject.label || 'Great';
currentProject.errors = 0;
currentProject.warnings = 0;
}
}
if (projects.length === 0) {
console.log('No project scores found in output');
return;
}
const emoji = (score) => {
if (score >= 90) return '🟢';
if (score >= 75) return '🟡';
return '🔴';
};
let body = `<!-- react-doctor -->\n## 🩺 React Doctor\n\n`;
body += `| Project | Score | Status | Errors | Warnings |\n`;
body += `|---------|-------|--------|--------|----------|\n`;
for (const p of projects) {
if (p.score === undefined) continue;
body += `| \`${p.name}\` | ${emoji(p.score)} **${p.score}**/100 | ${p.label} | ${p.errors || 0} | ${p.warnings || 0} |\n`;
}
body += `\n> Non-blocking — this check is informational only.`;
// Find and update or create comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes('<!-- react-doctor -->'));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
-98
View File
@@ -1,98 +0,0 @@
name: Generate SBOM
on:
workflow_dispatch:
inputs:
tag_name:
description: "Docker image tag to generate SBOMs for (e.g. preview, v1.14.0)"
required: true
type: string
image_variant:
description: "Image variant to scan"
required: true
type: choice
default: "commercial"
options:
- "commercial"
- "community"
sbom_format:
description: "SBOM output format"
required: true
type: choice
default: "spdx-json"
options:
- "spdx-json"
- "cyclonedx-json"
env:
TAG_NAME: ${{ github.event.inputs.tag_name }}
IMAGE_VARIANT: ${{ github.event.inputs.image_variant }}
SBOM_FORMAT: ${{ github.event.inputs.sbom_format }}
jobs:
setup:
name: Setup Image Matrix
runs-on: ubuntu-22.04
outputs:
images: ${{ steps.set_images.outputs.images }}
steps:
- id: set_images
name: Set Image List
run: |
if [ "${{ env.IMAGE_VARIANT }}" == "commercial" ]; then
echo 'images=["web-commercial","space-commercial","admin-commercial","live-commercial","backend-commercial","proxy-commercial","monitor-commercial","silo-commercial","email-commercial","plane-pi-commercial","plane-aio-commercial"]' >> $GITHUB_OUTPUT
elif [ "${{ env.IMAGE_VARIANT }}" == "community" ]; then
echo 'images=["plane-frontend","plane-space","plane-admin","plane-live","plane-backend","plane-proxy","plane-aio-community"]' >> $GITHUB_OUTPUT
fi
generate-sbom:
name: SBOM - ${{ matrix.image }}
needs: setup
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
image: ${{ fromJson(needs.setup.outputs.images) }}
steps:
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull Docker Image
run: docker pull makeplane/${{ matrix.image }}:${{ env.TAG_NAME }}
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0
- name: Generate SBOM
run: |
syft makeplane/${{ matrix.image }}:${{ env.TAG_NAME }} \
--output ${{ env.SBOM_FORMAT }}=sbom-${{ matrix.image }}-${{ env.TAG_NAME }}.${{ env.SBOM_FORMAT == 'spdx-json' && 'spdx.json' || 'cdx.json' }}
- name: Upload SBOM Artifact
uses: actions/upload-artifact@v7
with:
name: sbom-${{ matrix.image }}-${{ env.TAG_NAME }}
path: sbom-${{ matrix.image }}-*
retention-days: 90
merge-sboms:
name: Merge All SBOMs
needs: [setup, generate-sbom]
runs-on: ubuntu-22.04
steps:
- name: Download All SBOMs
uses: actions/download-artifact@v8
with:
pattern: sbom-*
merge-multiple: true
path: sboms/
- name: Upload Merged SBOM Bundle
uses: actions/upload-artifact@v7
with:
name: sbom-bundle-${{ env.IMAGE_VARIANT }}-${{ env.TAG_NAME }}
path: sboms/
retention-days: 90
@@ -1,24 +0,0 @@
name: Slash Command Dispatch
on:
issue_comment:
types: [created]
jobs:
slashCommandDispatch:
runs-on: ubuntu-latest
# Only run if comment is on a PR and contains the slash command
if: |
github.event.issue.pull_request != null &&
(startsWith(github.event.comment.body, '/deploy'))
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
permission: write
issue-type: pull-request
commands: |
deploy
-56
View File
@@ -1,56 +0,0 @@
name: Create PR on Sync
on:
workflow_dispatch:
push:
branches:
- "sync/**"
env:
CURRENT_BRANCH: ${{ github.ref_name }}
TARGET_BRANCH: "preview" # The target branch that you would like to merge changes like develop
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
jobs:
create_pull_request:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Setup Git
run: |
git config user.name "$ACCOUNT_USER_NAME"
git config user.email "$ACCOUNT_USER_EMAIL"
- name: Setup GH CLI and Git Config
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Create PR to Target Branch
run: |
# Determine target branch based on current branch prefix
TARGET_BRANCH="preview"
PR_TITLE="${{vars.SYNC_PR_TITLE}}"
# get all pull requests and check if there is already a PR
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $CURRENT_BRANCH --state open --json number | jq '.[] | .number')
if [ -n "$PR_EXISTS" ]; then
echo "Pull Request already exists: $PR_EXISTS"
else
echo "Creating new pull request"
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "$PR_TITLE" --body "")
echo "Pull Request created: $PR_URL"
fi
-44
View File
@@ -1,44 +0,0 @@
name: Sync Repositories
on:
workflow_dispatch:
push:
branches:
- preview
env:
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
jobs:
sync_changes:
runs-on: ubuntu-22.04
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Setup GH CLI
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Push Changes to Target Repo
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin-a $SOURCE_BRANCH:$TARGET_BRANCH
+4 -58
View File
@@ -1,6 +1,5 @@
node_modules
.next
.yarn
### NextJS ###
# Dependencies
@@ -9,29 +8,24 @@ node_modules
.pnp.js
# Testing
coverage/
/coverage
# Next.js
/.next/
/out/
# Production
dist/
out/
build/
.react-router/
/build
# Misc
.DS_Store
*.pem
.history
tsconfig.tsbuildinfo
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.pnpm-debug.log*
# Local env files
@@ -49,21 +43,16 @@ pnpm-debug.log*
## Django ##
venv
.venv
*.pyc
staticfiles
mediafiles
.env
.DS_Store
logs/
htmlcov/
.coverage
node_modules/
assets/dist/
npm-debug.log
yarn-error.log
pnpm-debug.log
# Editor directories and files
.idea
@@ -73,54 +62,11 @@ pnpm-debug.log
*.sln
package-lock.json
.vscode
.zed
# Sentry
.sentryclirc
# lock files
package-lock.json
.secrets
tmp/
## packages
dist
.flatfile
.temp/
deploy/selfhost/plane-app/
## Storybook
*storybook.log
output.css
dev-editor
# Redis
*.rdb
*.rdb.gz
storybook-static
# Monitor
monitor/prime.key
monitor/prime.key.pub
monitor.db
.cursor
build/
.react-router/
temp/
# Auto-generated i18n translation key types (regenerated on build)
packages/i18n/src/types/keys.generated.ts
# Ignore any test results JSON files
*test_results*.json
apps/pi/tests/test_results.json
scripts/
!packages/i18n/scripts/
.agents
pnpm-lock.yaml
pnpm-workspace.yaml
-1
View File
@@ -1 +0,0 @@
pnpm lint-staged
-6
View File
@@ -1,6 +0,0 @@
[env]
_.file = ".env"
[tools]
node = { version = "22", postinstall = "corepack enable" }
actionlint = "1.7.12"
-49
View File
@@ -1,49 +0,0 @@
# ------------------------------
# Core Workspace Behavior
# ------------------------------
# Use isolated node_modules (no symlinks) for Docker compatibility
# Required for `turbo prune --docker` to work correctly
node-linker = isolated
# Always prefer using local workspace packages when available
prefer-workspace-packages = true
# Symlink workspace packages instead of duplicating them
link-workspace-packages = true
# Use a single lockfile across the whole monorepo
shared-workspace-lockfile = true
# Ensure packages added from workspace save using workspace: protocol
save-workspace-protocol = rolling
# ------------------------------
# Dependency Resolution
# ------------------------------
# Choose the highest compatible version across the workspace
# → reduces fragmentation & node_modules bloat
resolution-mode = highest
# Automatically install peer dependencies instead of forcing every package to declare them
auto-install-peers = true
# Don't break the install if peers are missing
strict-peer-dependencies = false
# ------------------------------
# Performance Optimizations
# ------------------------------
# Use cached artifacts for native modules (sharp, esbuild, etc.)
side-effects-cache = true
# Prefer local cached packages rather than hitting network
prefer-offline = true
# In CI, refuse to modify lockfile (prevents drift)
prefer-frozen-lockfile = true
-13
View File
@@ -1,13 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"overrides": [
{
"files": ["packages/codemods/**/*"],
"options": {
"printWidth": 80
}
}
]
}
-354
View File
@@ -1,354 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["promise", "react", "jsx-a11y", "typescript", "unicorn"],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true
},
"ignorePatterns": [
"**/.cache/**",
"**/.env.*",
"**/.env",
"**/.next/**",
"**/.react-router/**",
"**/.storybook/**",
"**/.turbo/**",
"**/.vite/**",
"**/*.config.{js,mjs,cjs,ts}",
"**/build/**",
"**/coverage/**",
"**/dist/**",
"**/node_modules/**",
"**/public/**",
"**/storybook-static/**"
],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "warn",
"no-const-assign": "error",
"no-constant-binary-expression": "warn",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "warn",
"no-empty-character-class": "error",
"no-empty-pattern": "warn",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "warn",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "warn",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "warn",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": [
"warn",
{
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"no-useless-backreference": "error",
"no-useless-catch": "warn",
"no-useless-escape": "warn",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "warn",
"promise/always-return": "warn",
"promise/no-return-wrap": "error",
"promise/param-names": "warn",
"promise/catch-or-return": "warn",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/no-new-statics": "error",
"promise/valid-params": "warn",
"react/display-name": "warn",
"react/jsx-key": "error",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "warn",
"react/jsx-no-undef": "error",
"react/no-children-prop": "error",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-find-dom-node": "error",
"react/no-is-mounted": "error",
"react/no-render-return-value": "error",
"react/no-string-refs": "error",
"react/no-unescaped-entities": "error",
"react/no-unknown-property": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/anchor-is-valid": "warn",
"jsx-a11y/aria-activedescendant-has-tabindex": "error",
"jsx-a11y/aria-props": "error",
"jsx-a11y/aria-proptypes": "error",
"jsx-a11y/aria-role": "error",
"jsx-a11y/aria-unsupported-elements": "error",
"jsx-a11y/autocomplete-valid": "error",
"jsx-a11y/click-events-have-key-events": "warn",
"jsx-a11y/heading-has-content": "error",
"jsx-a11y/html-has-lang": "error",
"jsx-a11y/iframe-has-title": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"jsx-a11y/label-has-associated-control": "warn",
"jsx-a11y/media-has-caption": "error",
"jsx-a11y/mouse-events-have-key-events": "warn",
"jsx-a11y/no-access-key": "error",
"jsx-a11y/no-autofocus": "warn",
"jsx-a11y/no-distracting-elements": "error",
"jsx-a11y/no-noninteractive-tabindex": [
"warn",
{
"tags": [],
"roles": ["tabpanel"],
"allowExpressionValues": true
}
],
"jsx-a11y/no-redundant-roles": "warn",
"jsx-a11y/no-static-element-interactions": [
"warn",
{
"allowExpressionValues": true,
"handlers": ["onClick", "onMouseDown", "onMouseUp", "onKeyPress", "onKeyDown", "onKeyUp"]
}
],
"jsx-a11y/role-has-required-aria-props": "error",
"jsx-a11y/role-supports-aria-props": "error",
"jsx-a11y/scope": "error",
"jsx-a11y/tabindex-no-positive": "warn",
"turbo/no-undeclared-env-vars": "error",
"@typescript-eslint/await-thenable": "warn",
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-array-delete": "error",
"@typescript-eslint/no-base-to-string": "warn",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-duplicate-type-constituents": "warn",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-for-in-array": "warn",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-redundant-type-constituents": "warn",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-enum-comparison": "warn",
"@typescript-eslint/no-unsafe-function-type": "error",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-unary-minus": "error",
"no-unused-expressions": "warn",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/only-throw-error": "warn",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/prefer-promise-reject-errors": "warn",
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unbound-method": "warn",
"@typescript-eslint/no-import-type-side-effects": "error",
"jsx-a11y-js/interactive-supports-focus": "warn",
"jsx-a11y-js/no-noninteractive-element-to-interactive-role": "warn",
"react-hooks-js/immutability": "warn",
"react-hooks-js/preserve-manual-memoization": "warn",
"react-hooks-js/purity": "warn",
"react-hooks-js/refs": "warn",
"react-hooks-js/set-state-in-effect": "warn",
"react-hooks-js/static-components": "warn",
"react/only-export-components": [
"warn",
{
"allowExportNames": [
"action",
"clientAction",
"clientLoader",
"clientMiddleware",
"ErrorBoundary",
"handle",
"headers",
"HydrateFallback",
"links",
"loader",
"meta",
"middleware",
"shouldRevalidate"
],
"customHOCs": ["observer"]
}
]
},
"jsPlugins": [
"eslint-plugin-turbo",
{ "name": "jsx-a11y-js", "specifier": "eslint-plugin-jsx-a11y" },
{ "name": "react-hooks-js", "specifier": "eslint-plugin-react-hooks" }
],
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
"rules": {
"constructor-super": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": ["**/*.{ts,tsx}"],
"rules": {
"import/namespace": "error",
"import/default": "error",
"import/no-named-as-default": "warn",
"import/no-named-as-default-member": "warn",
"import/no-duplicates": "warn",
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@headlessui/react",
"importNames": ["Disclosure", "Switch", "Tabs"],
"message": "Use @plane/propel components instead of @headlessui/react"
},
{
"name": "@plane/ui",
"importNames": ["Collapsible", "CollapsibleButton", "Switch", "Tabs"],
"message": "Use @plane/propel components instead of @plane/ui"
}
]
}
]
},
"env": {
"es2018": true
},
"plugins": ["import"]
},
{
"files": ["**/*.{js,mjs,cjs,jsx}"],
"rules": {
"@typescript-eslint/await-thenable": "off",
"@typescript-eslint/no-array-delete": "off",
"@typescript-eslint/no-base-to-string": "off",
"@typescript-eslint/no-confusing-void-expression": "off",
"@typescript-eslint/no-deprecated": "off",
"@typescript-eslint/no-duplicate-type-constituents": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-for-in-array": "off",
"@typescript-eslint/no-implied-eval": "off",
"@typescript-eslint/no-meaningless-void-operator": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-misused-spread": "off",
"@typescript-eslint/no-mixed-enums": "off",
"@typescript-eslint/no-redundant-type-constituents": "off",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "off",
"@typescript-eslint/no-unnecessary-template-expression": "off",
"@typescript-eslint/no-unnecessary-type-arguments": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-type-assertion": "off",
"@typescript-eslint/no-unsafe-unary-minus": "off",
"@typescript-eslint/non-nullable-type-assertion-style": "off",
"@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/prefer-includes": "off",
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/prefer-promise-reject-errors": "off",
"@typescript-eslint/prefer-reduce-type-parameter": "off",
"@typescript-eslint/prefer-return-this-type": "off",
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/related-getter-setter-pairs": "off",
"@typescript-eslint/require-array-sort-compare": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/return-await": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/switch-exhaustiveness-check": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/use-unknown-in-catch-callback-variable": "off"
}
},
{
"files": ["**/*.{mjs,cjs}"],
"rules": {
"@typescript-eslint/no-require-imports": "off"
},
"env": {
"node": true
}
}
]
}
-10
View File
@@ -1,10 +0,0 @@
.next/
.react-router/
.turbo/
.vite/
build/
dist/
node_modules/
out/
pnpm-lock.yaml
storybook-static/
-186
View File
@@ -1,186 +0,0 @@
# AGENTS.md
This file provides guidance to AI coding agents when working with code in this repository.
## Project Overview
Plane is an open-source project management tool (similar to Jira/Linear). This is the Enterprise Edition repository containing both open-source and commercial features.
## Tech Stack
- **Frontend**: React 18 with React Router 7, TypeScript, MobX, Tailwind CSS
- **Backend**: Django with Django REST Framework, Celery for background tasks
- **Real-time**: Node.js with Socket.IO and Hocuspocus (collaborative editing)
- **Database**: PostgreSQL 15, Redis (Valkey), RabbitMQ
- **Build**: pnpm 10.24.0, Turbo, Vite
- **Node**: 22.18.0+
- **Python**: 3.8+
## Monorepo Structure
```
apps/
web/ # Main UI (React Router, port 3000)
admin/ # Admin panel (port 3001)
api/ # Django REST backend (port 8000)
live/ # Real-time collaboration server (Socket.IO + Hocuspocus)
space/ # Public project space portal
silo/ # Integration system (Slack, GitHub, Jira/Linear imports)
flux/ # Request routing proxy
pi/ # Plane Intelligence (AI features)
monitor/ # Go-based health check service
email/ # Email processing service
packages/
propel/ # New Storybook component library (@plane/propel) - actively developed
ui/ # Legacy component library (@plane/ui) - being replaced by propel
types/ # Shared TypeScript types (@plane/types)
shared-state/ # MobX stores (@plane/shared-state)
services/ # API client services (@plane/services)
hooks/ # React hooks (@plane/hooks)
editor/ # Rich text editor (Tiptap/ProseMirror)
i18n/ # Internationalization
constants/ # Shared constants
utils/ # Utility functions
```
## Common Commands
### Monorepo (from root)
```bash
pnpm dev # Start all dev servers
pnpm build # Build all packages and apps
pnpm check # Run format, lint, and type checks
pnpm check:lint # ESLint only
pnpm check:types # TypeScript only
pnpm fix # Auto-fix format and lint issues
pnpm fix:lint # Fix lint issues only
pnpm fix:format # Fix formatting only
pnpm clean # Remove node_modules, dist, build folders
```
### Target Specific Package
```bash
pnpm turbo run <command> --filter=<package>
pnpm --filter=@plane/propel storybook # Start Storybook on port 6006
pnpm --filter=web dev # Run only web app
```
### Django API (from apps/api)
```bash
# Run with Docker (recommended for local dev)
# Start all services
docker compose -f docker-compose-local.yml --profile all up
# External services only (postgres, redis, rabbitmq, minio)
docker compose -f docker-compose-local.yml --profile services up
# External services + api, worker, beat-worker
docker compose -f docker-compose-local.yml --profile api up
# Run tests
pytest # All tests
pytest -m unit # Unit tests only
pytest -m contract # Contract tests only
pytest plane/tests/unit/models/test_*.py # Specific test file
pytest -k "test_function_name" # Specific test by name
# Django commands (inside container or with venv)
python manage.py migrate
python manage.py runserver
```
### Test Markers (pytest)
- `unit` - Unit tests for models, serializers, utilities
- `contract` - Contract tests for API endpoints
- `smoke` - Smoke tests for critical functionality
- `slow` - Tests that may be skipped in CI
## Local Development Setup
1. Clone the repo and run `./setup.sh`
2. Start backend services: `docker compose -f docker-compose-local.yml --profile all up`
- Use `--profile services` for only external services (postgres, redis, rabbitmq, minio)
- Use `--profile api` for external services + api, worker, beat-worker, migrator
3. Start frontend: `pnpm dev`
4. Admin setup: http://localhost:3001/god-mode/
5. Main app: http://localhost:3000
**Requirements**: Docker, Node.js 22+, Python 3.8+, 12GB+ RAM recommended
## Copyright Headers
Every source file in this repository contains a copyright/license header. When reading files, **ignore these headers** — they are boilerplate and not relevant to understanding the code logic. Do **not** remove, modify, or omit them when editing existing files. When creating **new** files, include the appropriate header.
**TypeScript / JavaScript / TSX / JSX:**
```ts
/**
* SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
* SPDX-License-Identifier: LicenseRef-Plane-Commercial
*
* Licensed under the Plane Commercial License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://plane.so/legals/eula
*
* DO NOT remove or modify this notice.
* NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
*/
```
**Python:**
```python
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
#
# Licensed under the Plane Commercial License (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# https://plane.so/legals/eula
#
# DO NOT remove or modify this notice.
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
```
## Code Style
- **TypeScript**: Strict mode, use `workspace:*` for internal packages, `catalog:` for external deps
- **Formatting**: oxfmt with built-in Tailwind class sorting (runs on commit via Husky)
- **Linting**: ESLint 9 with typed linting from root config
- **Naming**: camelCase for variables/functions, PascalCase for components/types
- **State**: MobX stores in `@plane/shared-state`
- **Python**: Ruff for linting/formatting, line length 120
## Architecture Notes
### Frontend Data Flow
Components use MobX stores from `@plane/shared-state`. API calls go through services in `@plane/services` which wrap axios. Real-time updates come via Socket.IO from the `live` server.
### Backend Structure (apps/api/plane)
- `api/` - REST API endpoints (DRF ViewSets)
- `app/` - Core application logic
- `bgtasks/` - Celery background tasks
- `authentication/` - Auth providers (OAuth, SAML, LDAP, OIDC)
- `automations/` - Workflow automation engine
- `ee/` - Enterprise Edition features
- `event_stream/` - Event publishing for real-time
- `graphql/` - GraphQL API layer
### Real-time Server (apps/live)
- `socket-io/` - Socket.IO for workspace events
- `hocuspocus.ts` - Collaborative document editing (Yjs)
## Important Files
- `turbo.json` - Turbo build configuration
- `pnpm-workspace.yaml` - Workspace and catalog definitions
- `eslint.config.mjs` - Root ESLint configuration
- `apps/api/pytest.ini` - Python test configuration
- `apps/api/plane/settings/` - Django settings by environment
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-6
View File
@@ -1,6 +0,0 @@
eslint.config.mjs @sriramveeraghanta @lifeiscontent
apps/silo/ @Prashant-Surya
apps/api/ @dheeru0198 @pablohashescobar
apps/pi/ @sunder-ch
apps/flux/ @sriramveeraghanta
deployments/ @mguptahub @pratapalakshmi
+13 -13
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
squawk@plane.so.
hello@plane.so.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
https://www.contributor-covenant.org/translations.
+9 -207
View File
@@ -4,7 +4,7 @@ Thank you for showing an interest in contributing to Plane! All kinds of contrib
## Submitting an issue
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new information.
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new informplaneation.
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
@@ -15,81 +15,21 @@ Without said minimal reproduction, we won't be able to investigate all [issues](
You can open a new issue with this [issue form](https://github.com/makeplane/plane/issues/new).
### Naming conventions for issues
When opening a new issue, please use a clear and concise title that follows this format:
- For bugs: `🐛 Bug: [short description]`
- For features: `🚀 Feature: [short description]`
- For improvements: `🛠️ Improvement: [short description]`
- For documentation: `📘 Docs: [short description]`
**Examples:**
- `🐛 Bug: API token expiry time not saving correctly`
- `📘 Docs: Clarify RAM requirement for local setup`
- `🚀 Feature: Allow custom time selection for token expiration`
This helps us triage and manage issues more efficiently.
## Projects setup and Architecture
### Requirements
- Docker Engine installed and running
- Node.js version 20+ [LTS version](https://nodejs.org/en/about/previous-releases)
- Node.js version v16.18.0
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
- **Memory**: Minimum **12 GB RAM** recommended
> ⚠️ Running the project on a system with only 8 GB RAM may lead to setup failures or memory crashes (especially during Docker container build/start or dependency install). Use cloud environments like GitHub Codespaces or upgrade local RAM if possible.
- pnpm version 7.22.0
### Setup the project
The project is a monorepo, with backend api and frontend in a single repo.
The backend is a django project which is kept inside apps/api
1. Clone the repo
```bash
git clone https://github.com/makeplane/plane.git [folder-name]
cd [folder-name]
chmod +x setup.sh
```
2. Run setup.sh
```bash
./setup.sh
```
3. Start the containers
```bash
# Start all services (recommended for first-time setup)
docker compose -f docker-compose-local.yml --profile all up
# Or start only external services (postgres, redis, rabbitmq, minio)
# if you want to run api/workers outside Docker
docker compose -f docker-compose-local.yml --profile services up
# Or start external services + api, worker, beat-worker, and migrator
docker compose -f docker-compose-local.yml --profile api up
```
> **Tip:** To avoid passing `--profile` every time, add `COMPOSE_PROFILES=all` to your `.env` file. Then you can simply run `docker compose -f docker-compose-local.yml up`.
4. Start web apps:
```bash
pnpm dev
```
5. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
6. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont auto-reload. Happy contributing! 🎉
The backend is a django project which is kept inside apiserver
## Missing a Feature?
@@ -101,157 +41,19 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We lint with [ESLint 9](https://eslint.org/docs/latest/) using the shared `eslint.config.mjs` (type-aware via `typescript-eslint`) and format with [oxfmt](https://oxc.rs/docs/guide/usage/formatter) using `.oxfmtrc.json`.
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).
## Ways to contribute
- Try Plane Cloud and the self hosting platform and give feedback
- Add new integrations
- Add or update translations
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
- Share your thoughts and suggestions with us
- Help create tutorials and blog posts
- Request a feature by submitting a proposal
- Report a bug
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
## Contributing to language support
This guide is designed to help contributors understand how to add or update translations in the application.
### Understanding translation structure
#### File organization
Translations are organized by language in the locales directory. Each language has its own folder containing JSON files for translations. Here's how it looks:
```
packages/i18n/src/locales/
├── en/
│ ├── core.json # Critical translations
│ └── translations.json
├── fr/
│ └── translations.json
└── [language]/
└── translations.json
```
#### Nested structure
To keep translations organized, we use a nested structure for keys. This makes it easier to manage and locate specific translations. For example:
```json
{
"issue": {
"label": "Work item",
"title": {
"label": "Work item title"
}
}
}
```
### Translation formatting guide
We use [IntlMessageFormat](https://formatjs.github.io/docs/intl-messageformat/) to handle dynamic content, such as variables and pluralization. Here's how to format your translations:
#### Examples
- **Simple variables**
```json
{
"greeting": "Hello, {name}!"
}
```
- **Pluralization**
```json
{
"items": "{count, plural, one {Work item} other {Work items}}"
}
```
### Contributing guidelines
#### Updating existing translations
1. Locate the key in `locales/<language>/translations.json`.
2. Update the value while ensuring the key structure remains intact.
3. Preserve any existing ICU formats (e.g., variables, pluralization).
#### Adding new translation keys
1. When introducing a new key, ensure it is added to **all** language files, even if translations are not immediately available. Use English as a placeholder if needed.
2. Keep the nesting structure consistent across all languages.
3. If the new key requires dynamic content (e.g., variables or pluralization), ensure the ICU format is applied uniformly across all languages.
### Adding new languages
Adding a new language involves several steps to ensure it integrates seamlessly with the project. Follow these instructions carefully:
1. **Update type definitions**
Add the new language to the TLanguage type in the language definitions file:
```ts
// packages/i18n/src/types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
```
1. **Add language configuration**
Include the new language in the list of supported languages:
```ts
// packages/i18n/src/constants/language.ts
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Your Language", value: "your-lang" },
];
```
2. **Create translation files**
1. Create a new folder for your language under locales (e.g., `locales/your-lang/`).
2. Add a `translations.json` file inside the folder.
3. Copy the structure from an existing translation file and translate all keys.
3. **Update import logic**
Modify the language import logic to include your new language:
```ts
private importLanguageFile(language: TLanguage): Promise<any> {
switch (language) {
case "your-lang":
return import("../locales/your-lang/translations.json");
// ...
}
}
```
### Quality checklist
Before submitting your contribution, please ensure the following:
- All translation keys exist in every language file.
- Nested structures match across all language files.
- ICU message formats are correctly implemented.
- All languages load without errors in the application.
- Dynamic values and pluralization work as expected.
- There are no missing or untranslated keys.
#### Pro tips
- When in doubt, refer to the English translations for context.
- Verify pluralization works with different numbers.
- Ensure dynamic values (e.g., `{name}`) are correctly interpolated.
- Double-check that nested key access paths are accurate.
Happy translating! 🌍✨
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Forum](https://forum.plane.so).
-10
View File
@@ -1,10 +0,0 @@
SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
SPDX-License-Identifier: LicenseRef-Plane-Commercial
Licensed under the Plane Commercial License (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://plane.so/legals/eula
DO NOT remove or modify this notice.
NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
-34
View File
@@ -1,34 +0,0 @@
## Copyright check
To verify that all tracked Python files contain the correct copyright header for **Plane Software Inc.** for the year **2023**, run this command from the repository root:
```bash
addlicense --check -f COPYRIGHT.txt -ignore "**/migrations/**" $(git ls-files '*.py')
```
#### To Apply Changes
python files
```bash
addlicense -v -f COPYRIGHT.txt -ignore "**/migrations/**" $(git ls-files '*.py')
```
ts and tsx files in a specific app
```bash
addlicense -v -f COPYRIGHT.txt \
-ignore "**/*.config.ts" \
-ignore "**/*.d.ts" \
$(git ls-files 'packages/*.ts')
```
Note: Please make sure ts command is running on specific folder, running it for the whole mono repo is crashing os processes.
#### Other Options
- **`addlicense -check`**: runs in check-only mode and fails if any file is missing or has an incorrect header.
- **`-c "Plane Software Inc."`**: sets the copyright holder.
- **`-f LICENSE.txt`**: uses the contents and format defined in `LICENSE.txt` as the header template.
- **`-y 2023`**: sets the year in the header.
- **`$(git ls-files '*.py')`**: restricts the check to Python files tracked in git.
+116
View File
@@ -0,0 +1,116 @@
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN yarn turbo run build --filter=app
FROM python:3.11.1-alpine3.17 AS backend
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /code
RUN apk --update --no-cache add \
"libpq~=15" \
"libxslt~=1.1" \
"nodejs-current~=19" \
"xmlsec~=1.2" \
"nginx" \
"nodejs" \
"npm" \
"supervisor"
COPY apiserver/requirements.txt ./
COPY apiserver/requirements ./requirements
RUN apk add libffi-dev
RUN apk --update --no-cache --virtual .build-deps add \
"bash~=5.2" \
"g++~=12.2" \
"gcc~=12.2" \
"cargo~=1.64" \
"git~=2" \
"make~=4.3" \
"postgresql13-dev~=13" \
"libc-dev" \
"linux-headers" \
&& \
pip install -r requirements.txt --compile --no-cache-dir \
&& \
apk del .build-deps
# Add in Django deps and generate Django's static files
COPY apiserver/manage.py manage.py
COPY apiserver/plane plane/
COPY apiserver/templates templates/
COPY apiserver/gunicorn.config.py ./
RUN apk --update --no-cache add "bash~=5.2"
COPY apiserver/bin ./bin/
RUN chmod +x ./bin/takeoff ./bin/worker
RUN chmod -R 777 /code
# Expose container port and run entry point script
EXPOSE 8000
EXPOSE 3000
EXPOSE 80
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 plane
RUN adduser --system --uid 1001 captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf
#######################################################################
COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
#######################################################################
COPY nginx/supervisor.conf /code/supervisor.conf
CMD ["supervisord","-c","/code/supervisor.conf"]
-265
View File
@@ -1,265 +0,0 @@
# FIPS Compliance Changes Required
This document lists all locations that need to be modified to achieve FIPS compliance.
## Critical Changes (Must Fix)
### 1. Remove `pycryptodome` Dependency
**Status:****COMPLETED** - Replaced with FIPS-compliant `cryptography` library
#### Files Updated:
1. **`apps/api/requirements/base.txt`** ✅ **COMPLETED**
- ✅ Removed: `pycryptodome==3.22.0`
- ✅ Now using: `cryptography==44.0.1` (FIPS-compliant when built with FIPS-enabled OpenSSL)
2. **`apps/pi/requirements.txt`** ✅ **COMPLETED**
- ✅ Removed: `pycryptodome==3.23.0`
- ✅ Now using: `cryptography==46.0.2` (FIPS-compliant when built with FIPS-enabled OpenSSL)
#### Code Files Updated:
3. **`apps/api/plane/utils/encryption.py`** ✅ **COMPLETED**
```python
# IMPLEMENTED (FIPS-COMPLIANT):
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import secrets # For FIPS-compliant random bytes generation
```
- ✅ Functions updated:
- `encrypt()` - Now uses `AESGCM(key).encrypt()` and `secrets.token_bytes()`
- `decrypt()` - Now uses `AESGCM(key).decrypt()`
4. **`apps/pi/pi/app/utils/encryption.py`** ✅ **COMPLETED**
```python
# IMPLEMENTED (FIPS-COMPLIANT):
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
```
- ✅ Functions updated:
- `decrypt()` - Now uses `AESGCM(key).decrypt()`
- `decrypt_from_string()` - Uses updated `decrypt()` function
### 2. Configure `cryptography` Library for FIPS
**Status:** ⚠️ **CONDITIONAL** - Can be FIPS-compliant if properly configured
#### Files Using `cryptography`:
5. **`apps/api/plane/license/utils/encryption.py`** (Line 15)
- Uses: `from cryptography.fernet import Fernet`
- **Action Required:** Ensure OpenSSL is FIPS-enabled and verify FIPS mode enforcement
- Functions: `encrypt_data()`, `decrypt_data()`
6. **`apps/api/plane/utils/integrations/github.py`** (Lines 17-18)
- Uses: `from cryptography.hazmat.primitives.serialization import load_pem_private_key`
- Uses: `from cryptography.hazmat.backends import default_backend`
- **Action Required:** Verify FIPS mode is enforced
7. **`apps/pi/pi/services/actions/oauth_url_encoder.py`** (Line 21)
- Uses: `from cryptography.fernet import Fernet`
- **Action Required:** Ensure FIPS mode enforcement
#### Dependency Files:
8. **`apps/api/requirements/base.txt`** (Line 53)
- Current: `cryptography==44.0.1`
- **Action Required:**
- Ensure version is compatible with FIPS-enabled OpenSSL
- Verify build against FIPS-validated OpenSSL in Dockerfile
9. **`apps/pi/requirements.txt`** (Line 12)
- Current: `cryptography==46.0.2`
- **Action Required:** Same as above
### 3. Dockerfile Configuration for FIPS
#### Python Applications:
10. **`apps/api/Dockerfile.fips`**
- **Current:** Uses `registry.access.redhat.com/ubi10/python-312-minimal` ✅ (Good - UBI has FIPS support)
- **Action Required:**
- Ensure OpenSSL is FIPS-enabled in the container
- Add environment variable to enable FIPS mode: `OPENSSL_CONF=/path/to/openssl-fips.cnf`
- Verify `cryptography` library is built against FIPS-enabled OpenSSL
- Consider adding: `RUN pip install --no-binary cryptography cryptography` to ensure proper linking
11. **`apps/pi/Dockerfile.fips`**
- **Current:** Multi-stage build with `python:3.12-slim` builder and `registry.access.redhat.com/ubi10/python-312-minimal` runtime
- **Action Required:**
- Ensure builder stage also has FIPS-enabled OpenSSL if building cryptography
- Verify runtime stage has FIPS mode enabled
- Add FIPS configuration
#### Node.js Applications:
12. **`apps/silo/Dockerfile.fips`**
- **Current:** Uses `registry.access.redhat.com/ubi10/nodejs-22` ✅ (Good)
- **Action Required:**
- Verify Node.js is built with FIPS support
- Ensure OpenSSL FIPS mode is enabled
- The code uses Node.js built-in `crypto` module which should be FIPS-compliant when Node.js is FIPS-enabled
13. **`apps/web/Dockerfile.fips`**
- **Current:** Uses `registry.access.redhat.com/ubi10/nginx-126` ✅ (Good)
- **Status:** Already fixed (USER root added)
14. **`apps/admin/Dockerfile.fips`**
- **Current:** Uses `registry.access.redhat.com/ubi10/nginx-126` ✅ (Good)
- **Status:** Already fixed (USER root added)
15. **`apps/space/Dockerfile.fips`**
- **Action Required:** Verify FIPS configuration
16. **`apps/live/Dockerfile.fips`**
- **Action Required:** Verify FIPS configuration
#### Go Applications:
17. **`apps/monitor/Dockerfile.fips`**
- **Current:** Uses `registry.access.redhat.com/ubi10/ubi-minimal` ✅ (Good)
- **Status:** Go standard library crypto should be FIPS-compliant when using FIPS-validated system libraries
18. **`apps/email/Dockerfile.fips`**
- **Current:** Uses `registry.access.redhat.com/ubi10/ubi-minimal` ✅ (Good)
- **Status:** Go standard library crypto should be FIPS-compliant
### 4. Node.js Package Overrides
19. **`package.json`** (Line 80)
- Current: `"pbkdf2": "3.1.3"` in pnpm overrides
- **Action Required:**
- Verify if this package is actually used
- If used, replace with Node.js built-in `crypto.pbkdf2Sync()` (which is FIPS-compliant)
- The codebase already uses `crypto.pbkdf2Sync()` in `apps/silo/src/helpers/decrypt.ts`, so this override may be unnecessary
## Implementation Status
### Phase 1: Critical (Blocking FIPS Compliance) - ✅ COMPLETED
1. ✅ **COMPLETED** - Removed `pycryptodome` from requirements files
- `apps/api/requirements/base.txt` - Removed `pycryptodome==3.22.0`
- `apps/pi/requirements.txt` - Removed `pycryptodome==3.23.0`
2. ✅ **COMPLETED** - Replaced `Crypto.Cipher.AES` usage with `cryptography.hazmat.primitives.ciphers.aead.AESGCM`
- `apps/api/plane/utils/encryption.py` - Updated imports and functions
- `apps/pi/pi/app/utils/encryption.py` - Updated imports and functions
3. ✅ **COMPLETED** - Replaced `Crypto.Random.get_random_bytes()` with `secrets.token_bytes()`
- `apps/api/plane/utils/encryption.py` - Uses `secrets.token_bytes(12)` for nonce generation
4. ✅ **COMPLETED** - Updated encryption/decryption functions:
- `apps/api/plane/utils/encryption.py` - Both `encrypt()` and `decrypt()` functions updated
- `apps/pi/pi/app/utils/encryption.py` - `decrypt()` and `decrypt_from_string()` functions updated
### Phase 2: Configuration (Ensure FIPS Mode) - ⚠️ PENDING
5. ⚠️ **PENDING** - Configure Dockerfiles to enable FIPS mode
- `apps/api/Dockerfile.fips` - Needs FIPS mode configuration
- `apps/pi/Dockerfile.fips` - Needs FIPS mode configuration
6. ⚠️ **PENDING** - Verify `cryptography` library is built against FIPS-enabled OpenSSL
- Ensure Dockerfiles build cryptography with FIPS-enabled OpenSSL
7. ⚠️ **PENDING** - Add FIPS mode verification in application startup
- Add runtime checks to verify FIPS mode is enabled
8. ⚠️ **PENDING** - Test encryption/decryption with FIPS mode enabled
- Verify functionality in FIPS-enabled environment
### Phase 3: Verification (Testing & Validation) - ⚠️ PENDING
9. ⚠️ **PENDING** - Add FIPS compliance tests
10. ⚠️ **PENDING** - Verify all cryptographic operations use FIPS-validated algorithms
11. ⚠️ **PENDING** - Document FIPS configuration requirements
## Code Migration Example
### Before (Non-FIPS Compliant):
```python
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
def encrypt(plain_text: str):
key = derive_key()
iv = get_random_bytes(12)
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode())
return {
"iv": base64.b64encode(iv).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
"tag": base64.b64encode(tag).decode(),
}
```
### After (FIPS Compliant) - ✅ IMPLEMENTED:
```python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import secrets
import base64
def encrypt(plain_text: str):
key = derive_key()
aesgcm = AESGCM(key)
nonce = secrets.token_bytes(12) # 12 bytes for GCM (FIPS-compliant random generation)
# AESGCM.encrypt returns ciphertext + tag (16 bytes) concatenated
encrypted = aesgcm.encrypt(nonce, plain_text.encode(), None)
# Split ciphertext and tag (last 16 bytes are the tag)
ciphertext = encrypted[:-16]
tag = encrypted[-16:]
return {
"iv": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
"tag": base64.b64encode(tag).decode(),
}
def decrypt(encrypted_data: dict):
key = derive_key()
aesgcm = AESGCM(key)
nonce = base64.b64decode(encrypted_data["iv"])
ciphertext = base64.b64decode(encrypted_data["ciphertext"])
tag = base64.b64decode(encrypted_data["tag"])
# Reconstruct: ciphertext + tag for AESGCM.decrypt()
encrypted = ciphertext + tag
return aesgcm.decrypt(nonce, encrypted, None).decode()
```
## Testing Requirements
### Completed Verification:
1. ✅ **VERIFIED** - Encryption/decryption works correctly with new implementation
- Code tested and confirmed working with `cryptography` library
- Maintains same data format (iv, ciphertext, tag) for backward compatibility
### Pending Verification:
2. ⚠️ **PENDING** - Backward compatibility with existing encrypted data
- Need to verify that data encrypted with old `pycryptodome` can be decrypted with new implementation
- Note: This may require migration if data format differs
3. ⚠️ **PENDING** - FIPS mode is actually enabled and enforced
- Verify in FIPS-enabled environment
4. ⚠️ **PENDING** - No fallback to non-FIPS algorithms
- Verify cryptography library uses FIPS-validated algorithms only
5. ⚠️ **PENDING** - All tests pass with FIPS mode enabled
- Run full test suite in FIPS-enabled environment
## Summary
### ✅ Completed Changes:
- Removed all `pycryptodome` dependencies
- Replaced with FIPS-compliant `cryptography` library using `AESGCM`
- Updated all encryption/decryption functions in:
- `apps/api/plane/utils/encryption.py`
- `apps/pi/pi/app/utils/encryption.py`
- Code tested and verified working
### ⚠️ Remaining Tasks:
- Configure Dockerfiles for FIPS mode
- Verify FIPS mode enforcement at runtime
- Test backward compatibility with existing encrypted data
- Add FIPS compliance tests
## Notes
- **Node.js `crypto` module:** Already FIPS-compliant when Node.js is built with FIPS support. No changes needed for TypeScript/JavaScript code using built-in `crypto`.
- **Python `hashlib`:** Uses OpenSSL when available, so FIPS-compliant if OpenSSL is FIPS-enabled. No changes needed.
- **Go standard library:** FIPS-compliant when using FIPS-validated system libraries. No changes needed.
- **`cryptography` library:** The code now uses `cryptography.hazmat.primitives.ciphers.aead.AESGCM` which is FIPS-compliant when the library is built against FIPS-enabled OpenSSL (as provided in Red Hat UBI images).
+193 -36
View File
@@ -1,44 +1,201 @@
The Plane Commercial License (the "Commercial License")
Copyright (c) 2023-present Plane Software, Inc. All rights reserved.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
With regard to the Plane Software:
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
This software and associated documentation files (the "Software") may only be
used in production if you (and any entity that you represent) have agreed to,
and are in compliance with, the Plane End User License Agreement available at
https://plane.so/legals/eula (the "EULA"), or other agreements governing the
use of the Software, as mutually agreed by you and Plane Software, Inc.
("Plane"), and otherwise have a valid Plane Enterprise subscription or other
commercial entitlement for the correct number of seats (the "Commercial Terms").
1. Definitions.
Subject to the foregoing paragraph, you are free to modify this Software and
publish patches to the Software. You agree that Plane and/or its licensors (as
applicable) retain all right, title and interest in and to all such
modifications and/or patches, and all such modifications and/or patches may
only be used, copied, modified, displayed, distributed, or otherwise exploited
in accordance with the EULA and the applicable Commercial Terms.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
Notwithstanding the foregoing, you may copy and modify the Software for
development and testing purposes without requiring a subscription, provided
that such use is non-production and internal. You agree that Plane and/or its
licensors (as applicable) retain all right, title and interest in and to all
such modifications.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
You are not granted any other rights beyond what is expressly stated herein.
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute,
sublicense, and/or sell the Software.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
The full text of this Commercial License shall be included in all copies or
substantial portions of the Software.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
For all third party components incorporated into the Plane Software, those
components are licensed under the original license provided by the owner of the
applicable component.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Plane Software Labs Private Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+72 -100
View File
@@ -2,164 +2,136 @@
<p align="center">
<a href="https://plane.so">
<img src="https://media.docs.plane.so/logo/plane_github_readme.png" alt="Plane Logo" width="400">
<img src="https://res.cloudinary.com/toolspacedev/image/upload/v1680596414/Plane/Plane_Icon_Blue_on_White_150x150_muysa3.jpg" alt="Plane Logo" width="70">
</a>
</p>
<p align="center"><b>Modern project management for all teams</b></p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source, self-hosted project planning tool</b></p>
<p align="center">
<a href="https://plane.so/"><b>Website</b></a> •
<a href="https://forum.plane.so"><b>Forum</b></a> •
<a href="https://twitter.com/planepowers"><b>Twitter</b></a>
<a href="https://docs.plane.so/"><b>Documentation</b></a>
<a href="https://discord.com/invite/A92xrEGCge">
<img alt="Discord" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
</a>
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
</p>
<br />
<p>
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
<a href="https://app.plane.so/" target="_blank">
<img
src="https://media.docs.plane.so/GitHub-readme/github-top.webp"
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680599798/Plane/plane_1_1_tnb32j.png"
alt="Plane Screens"
width="100%"
/>
</a>
</p>
Meet [Plane](https://plane.so/), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘‍♀️
Meet Plane. An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️.
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
## 🚀 Installation
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
Getting started with Plane is simple. Choose the setup that works best for you:
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
- **Plane Cloud**
Sign up for a free account on [Plane Cloud](https://app.plane.so)—it's the fastest way to get up and running without worrying about infrastructure.
- **Self-host Plane**
Prefer full control over your data and infrastructure? Install and run Plane on your own servers. Follow our detailed [deployment guides](https://developers.plane.so/self-hosting/overview) to get started.
## ⚡️ Quick start with Docker Compose
| Installation methods | Docs link |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://developers.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://developers.plane.so/self-hosting/methods/kubernetes) |
### Docker Compose Setup
`Instance admins` can configure instance settings with [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
- Clone the Repository
## 🌟 Features
```bash
git clone https://github.com/makeplane/plane
```
- **Work Items**
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
- Change Directory
- **Cycles**
Maintain your teams momentum with Cycles. Track progress effortlessly using burn-down charts and other insightful tools.
```bash
cd plane
```
- **Modules**
Simplify complex projects by dividing them into smaller, manageable modules.
- Run setup.sh
- **Views**
Customize your workflow by creating filters to display only the most relevant issues. Save and share these views with ease.
```bash
./setup.sh localhost
```
- **Pages**
Capture and organize ideas using Plane Pages, complete with AI capabilities and a rich text editor. Format text, insert images, add hyperlinks, or convert your notes into actionable items.
> If running in a cloud env replace localhost with public facing IP address of the VM
- **Analytics**
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
## 🛠️ Local development
- Run Docker compose up
See [CONTRIBUTING](./CONTRIBUTING.md)
```bash
docker-compose up
```
## ⚙️ Built with
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
[![React Router](https://img.shields.io/badge/-React%20Router-CA4245?logo=react-router&style=for-the-badge&logoColor=white)](https://reactrouter.com/)
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)
[![Node JS](https://img.shields.io/badge/node.js-339933?style=for-the-badge&logo=Node.js&logoColor=white)](https://nodejs.org/en)
## 🚀 Features
* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
## 📸 Screenshots
<p>
<a href="https://plane.so" target="_blank">
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://media.docs.plane.so/GitHub-readme/github-work-items.webp"
alt="Plane Views"
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601719/Plane/plane_2_iqao52.png"
alt="Plane Issue Details"
width="100%"
/>
</a>
</p>
<p>
<a href="https://plane.so" target="_blank">
</p>
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://media.docs.plane.so/GitHub-readme/github-cycles.webp"
width="100%"
/>
</a>
</p>
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://media.docs.plane.so/GitHub-readme/github-modules.webp"
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680604273/Plane/plane_5_1_nwsl3a.png"
alt="Plane Cycles and Modules"
width="100%"
/>
</a>
</p>
<p>
<a href="https://plane.so" target="_blank">
</p>
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://media.docs.plane.so/GitHub-readme/github-views.webp"
alt="Plane Analytics"
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601713/Plane/plane_4_cqm0g8.png"
alt="Plane Quick Lists"
width="100%"
/>
</a>
</p>
<p>
<a href="https://plane.so" target="_blank">
</p>
<p>
<a href="https://app.plane.so/" target="_blank">
<img
src="https://media.docs.plane.so/GitHub-readme/github-analytics.webp"
alt="Plane Pages"
src="https://res.cloudinary.com/toolspacedev/image/upload/v1680601712/Plane/plane_3_1_cu4fsc.png"
alt="Plane Command K"
width="100%"
/>
</a>
</p>
</p>
## 📝 Documentation
## 📚Documentation
Explore Plane's [product documentation](https://docs.plane.so/) and [developer documentation](https://developers.plane.so/) to learn about features, setup, and usage.
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md).
## ❤️ Community
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Discord server](https://discord.com/invite/A92xrEGCge). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. Wed love to hear from you!
To chat with other community members you can join the [Plane Discord](https://discord.com/invite/q9HKAdau).
## 🛡️ Security
Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels.
If you discover a security vulnerability in Plane, please report it responsibly instead of opening a public issue. We take all legitimate reports seriously and will investigate them promptly. See [Security policy](https://github.com/makeplane/plane/blob/master/SECURITY.md) for more info.
## ⛓️ Security
To disclose any security issues, please email us at security@plane.so.
## 🤝 Contributing
There are many ways you can contribute to Plane:
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
- Talk or write about Plane or any other ecosystem integration and [let us know](https://discord.com/invite/A92xrEGCge)!
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
### Repo activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
### We couldn't have done this without you.
<a href="https://github.com/makeplane/plane/graphs/contributors">
<img src="https://contrib.rocks/image?repo=makeplane/plane" />
</a>
## License
This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt).
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. Email security@plane.so to disclose any security vulnerabilities.
-39
View File
@@ -1,39 +0,0 @@
# Security policy
This document outlines the security protocols and vulnerability reporting guidelines for the Plane project. Ensuring the security of our systems is a top priority, and while we work diligently to maintain robust protection, vulnerabilities may still occur. We highly value the communitys role in identifying and reporting security concerns to uphold the integrity of our systems and safeguard our users.
## Reporting a vulnerability
If you have identified a security vulnerability, submit your findings to [security@plane.so](mailto:security@plane.so).
Ensure your report includes all relevant information needed for us to reproduce and assess the issue. Include the IP address or URL of the affected system.
To ensure a responsible and effective disclosure process, please adhere to the following:
- Maintain confidentiality and refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address the issue.
- Refrain from running automated vulnerability scans on our infrastructure or dashboard without prior consent. Contact us to set up a sandbox environment if necessary.
- Do not exploit any discovered vulnerabilities for malicious purposes, such as accessing or altering user data.
- Do not engage in physical security attacks, social engineering, distributed denial of service (DDoS) attacks, spam campaigns, or attacks on third-party applications as part of your vulnerability testing.
## Out of scope
While we appreciate all efforts to assist in improving our security, please note that the following types of vulnerabilities are considered out of scope:
- Vulnerabilities requiring man-in-the-middle (MITM) attacks or physical access to a users device.
- Content spoofing or text injection issues without a clear attack vector or the ability to modify HTML/CSS.
- Issues related to email spoofing.
- Missing DNSSEC, CAA, or CSP headers.
- Absence of secure or HTTP-only flags on non-sensitive cookies.
## Our commitment
At Plane, we are committed to maintaining transparent and collaborative communication throughout the vulnerability resolution process. Here's what you can expect from us:
- **Response Time** <br/>
We will acknowledge receipt of your vulnerability report within three business days and provide an estimated timeline for resolution.
- **Legal Protection** <br/>
We will not initiate legal action against you for reporting vulnerabilities, provided you adhere to the reporting guidelines.
- **Confidentiality** <br/>
Your report will be treated with confidentiality. We will not disclose your personal information to third parties without your consent.
- **Recognition** <br/>
With your permission, we are happy to publicly acknowledge your contribution to improving our security once the issue is resolved.
- **Timely Resolution** <br/>
We are committed to working closely with you throughout the resolution process, providing timely updates as necessary. Our goal is to address all reported vulnerabilities swiftly, and we will actively engage with you to coordinate a responsible disclosure once the issue is fully resolved.
We appreciate your help in ensuring the security of our platform. Your contributions are crucial to protecting our users and maintaining a secure environment. Thank you for working with us to keep Plane safe.
+28
View File
@@ -0,0 +1,28 @@
DJANGO_SETTINGS_MODULE="plane.settings.production"
# Database
DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane
# Cache
REDIS_URL=redis://redis:6379/
# SMTP
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT="587"
EMAIL_USE_TLS="1"
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
# AWS
AWS_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_S3_BUCKET_NAME=""
AWS_S3_ENDPOINT_URL=""
# FE
WEB_URL="localhost/"
# OAUTH
GITHUB_CLIENT_SECRET=""
# Flags
DISABLE_COLLECTSTATIC=1
DOCKERIZED=1
# GPT Envs
OPENAI_API_KEY=0
GPT_ENGINE=0
+61
View File
@@ -0,0 +1,61 @@
FROM python:3.11.1-alpine3.17 AS backend
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /code
RUN apk --update --no-cache add \
"libpq~=15" \
"libxslt~=1.1" \
"nodejs-current~=19" \
"xmlsec~=1.2"
COPY requirements.txt ./
COPY requirements ./requirements
RUN apk add libffi-dev
RUN apk --update --no-cache --virtual .build-deps add \
"bash~=5.2" \
"g++~=12.2" \
"gcc~=12.2" \
"cargo~=1.64" \
"git~=2" \
"make~=4.3" \
"postgresql13-dev~=13" \
"libc-dev" \
"linux-headers" \
&& \
pip install -r requirements.txt --compile --no-cache-dir \
&& \
apk del .build-deps
RUN addgroup -S plane && \
adduser -S captain -G plane
RUN chown captain.plane /code
USER captain
# Add in Django deps and generate Django's static files
COPY manage.py manage.py
COPY plane plane/
COPY templates templates/
COPY gunicorn.config.py ./
USER root
RUN apk --update --no-cache add "bash~=5.2"
COPY ./bin ./bin/
RUN chmod +x ./bin/takeoff ./bin/worker
RUN chmod -R 777 /code
USER captain
# Expose container port and run entry point script
EXPOSE 8000
# CMD [ "./bin/takeoff" ]
+2
View File
@@ -0,0 +1,2 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info
+176
View File
@@ -0,0 +1,176 @@
# All the python scripts that are used for back migrations
import uuid
import random
from django.contrib.auth.hashers import make_password
from plane.db.models import ProjectIdentifier
from plane.db.models import Issue, IssueComment, User, Project, ProjectMember, Label
# Update description and description html values for old descriptions
def update_description():
try:
issues = Issue.objects.all()
updated_issues = []
for issue in issues:
issue.description_html = f"<p>{issue.description}</p>"
issue.description_stripped = issue.description
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["description_html", "description_stripped"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_comments():
try:
issue_comments = IssueComment.objects.all()
updated_issue_comments = []
for issue_comment in issue_comments:
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
updated_issue_comments.append(issue_comment)
IssueComment.objects.bulk_update(
updated_issue_comments, ["comment_html"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_project_identifiers():
try:
project_identifiers = ProjectIdentifier.objects.filter(
workspace_id=None
).select_related("project", "project__workspace")
updated_identifiers = []
for identifier in project_identifiers:
identifier.workspace_id = identifier.project.workspace_id
updated_identifiers.append(identifier)
ProjectIdentifier.objects.bulk_update(
updated_identifiers, ["workspace_id"], batch_size=50
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_user_empty_password():
try:
users = User.objects.filter(password="")
updated_users = []
for user in users:
user.password = make_password(uuid.uuid4().hex)
user.is_password_autoset = True
updated_users.append(user)
User.objects.bulk_update(updated_users, ["password"], batch_size=50)
print("Success")
except Exception as e:
print(e)
print("Failed")
def updated_issue_sort_order():
try:
issues = Issue.objects.all()
updated_issues = []
for issue in issues:
issue.sort_order = issue.sequence_id * random.randint(100, 500)
updated_issues.append(issue)
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_project_cover_images():
try:
project_cover_images = [
"https://images.unsplash.com/photo-1677432658720-3d84f9d657b4?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1661107564401-57497d8fe86f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1677352241429-dc90cfc7a623?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1677196728306-eeafea692454?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1331&q=80",
"https://images.unsplash.com/photo-1660902179734-c94c944f7830?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1255&q=80",
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1677040628614-53936ff66632?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1676920410907-8d5f8dd4b5ba?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1676846328604-ce831c481346?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1155&q=80",
"https://images.unsplash.com/photo-1676744843212-09b7e64c3a05?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1676798531090-1608bedeac7b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1597088758740-56fd7ec8a3f0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1169&q=80",
"https://images.unsplash.com/photo-1676638392418-80aad7c87b96?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
"https://images.unsplash.com/photo-1649639194967-2fec0b4ea7bc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675883086902-b453b3f8146e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
"https://images.unsplash.com/photo-1675887057159-40fca28fdc5d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1173&q=80",
"https://images.unsplash.com/photo-1675373980203-f84c5a672aa5?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675191475318-d2bf6bad1200?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1675456230532-2194d0c4bcc0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675371788315-60fa0ef48267?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
]
projects = Project.objects.all()
updated_projects = []
for project in projects:
project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project)
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_user_view_property():
try:
project_members = ProjectMember.objects.all()
updated_project_members = []
for project_member in project_members:
project_member.default_props = {
"filters": {"type": None},
"orderBy": "-created_at",
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": None,
"showEmptyGroups": True,
}
updated_project_members.append(project_member)
ProjectMember.objects.bulk_update(
updated_project_members, ["default_props"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_label_color():
try:
labels = Label.objects.filter(color="")
updated_labels = []
for label in labels:
label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF)
updated_labels.append(label)
Label.objects.bulk_update(updated_labels, ["color"], batch_size=100)
print("Success")
except Exception as e:
print(e)
print("Failed")
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
python manage.py wait_for_db
python manage.py migrate
# Create a Default User
python bin/user_script.py
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --config gunicorn.config.py --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
+28
View File
@@ -0,0 +1,28 @@
import os, sys
import uuid
sys.path.append("/code")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
import django
django.setup()
from plane.db.models import User
def populate():
default_email = os.environ.get("DEFAULT_EMAIL", "captain@plane.so")
default_password = os.environ.get("DEFAULT_PASSWORD", "password123")
if not User.objects.filter(email=default_email).exists():
user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
user.set_password(default_password)
user.save()
print("User created")
print("Success")
if __name__ == "__main__":
populate()
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -e
python manage.py wait_for_db
celery -A plane worker -l info
+6
View File
@@ -0,0 +1,6 @@
from psycogreen.gevent import patch_psycopg
def post_fork(server, worker):
patch_psycopg()
worker.log.info("Made Psycopg2 Green")
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env python
import os
import sys
if __name__ == '__main__':
os.environ.setdefault(
'DJANGO_SETTINGS_MODULE',
'plane.settings.production')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
+3
View File
@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AnalyticsConfig(AppConfig):
name = 'plane.analytics'
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = "plane.api"
@@ -0,0 +1,2 @@
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission
@@ -0,0 +1,91 @@
# Third Party imports
from rest_framework.permissions import BasePermission, SAFE_METHODS
# Module import
from plane.db.models import WorkspaceMember, ProjectMember
# Permission Mappings
Admin = 20
Member = 15
Viewer = 10
Guest = 5
class ProjectBasePermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
).exists()
## Only Project Admins can update project attributes
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role=Admin,
project_id=view.project_id,
).exists()
class ProjectMemberPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
).exists()
## Only Project Admins can update project attributes
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
project_id=view.project_id,
).exists()
class ProjectEntityPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
project_id=view.project_id,
).exists()
## Only project members or admins can create and edit the project attributes
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
project_id=view.project_id,
).exists()
@@ -0,0 +1,55 @@
# Third Party imports
from rest_framework.permissions import BasePermission, SAFE_METHODS
# Module imports
from plane.db.models import WorkspaceMember
# Permission Mappings
Owner = 20
Admin = 15
Member = 10
Guest = 5
# TODO: Move the below logic to python match - python v3.10
class WorkSpaceBasePermission(BasePermission):
def has_permission(self, request, view):
# allow anyone to create a workspace
if request.user.is_anonymous:
return False
if request.method == "POST":
return True
## Safe Methods
if request.method in SAFE_METHODS:
return True
# allow only admins and owners to update the workspace settings
if request.method in ["PUT", "PATCH"]:
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
).exists()
# allow only owner to delete the workspace
if request.method == "DELETE":
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, role=Owner
).exists()
class WorkSpaceAdminPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
).exists()
@@ -0,0 +1,71 @@
from .base import BaseSerializer
from .people import (
ChangePasswordSerializer,
ResetPasswordSerializer,
TokenSerializer,
)
from .user import UserSerializer, UserLiteSerializer
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
)
from .project import (
ProjectSerializer,
ProjectDetailSerializer,
ProjectMemberSerializer,
ProjectMemberInviteSerializer,
ProjectIdentifierSerializer,
ProjectFavoriteSerializer,
ProjectLiteSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .shortcut import ShortCutSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer
from .asset import FileAssetSerializer
from .issue import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
TimeLineIssueSerializer,
IssuePropertySerializer,
BlockerIssueSerializer,
BlockedIssueSerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
IssueFlatSerializer,
IssueStateSerializer,
IssueLinkSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
)
from .module import (
ModuleWriteSerializer,
ModuleSerializer,
ModuleIssueSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
)
from .api_token import APITokenSerializer
from .integration import (
IntegrationSerializer,
WorkspaceIntegrationSerializer,
GithubIssueSyncSerializer,
GithubRepositorySerializer,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
)
from .importer import ImporterSerializer
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
from .estimate import EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer
@@ -0,0 +1,14 @@
from .base import BaseSerializer
from plane.db.models import APIToken
class APITokenSerializer(BaseSerializer):
class Meta:
model = APIToken
fields = [
"label",
"user",
"user_type",
"workspace",
"created_at",
]
+14
View File
@@ -0,0 +1,14 @@
from .base import BaseSerializer
from plane.db.models import FileAsset
class FileAssetSerializer(BaseSerializer):
class Meta:
model = FileAsset
fields = "__all__"
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
]
+5
View File
@@ -0,0 +1,5 @@
from rest_framework import serializers
class BaseSerializer(serializers.ModelSerializer):
id = serializers.PrimaryKeyRelatedField(read_only=True)
+60
View File
@@ -0,0 +1,60 @@
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .issue import IssueStateSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
completed_issues = serializers.IntegerField(read_only=True)
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Cycle
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"owned_by",
]
class CycleIssueSerializer(BaseSerializer):
issue_detail = IssueStateSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle",
]
class CycleFavoriteSerializer(BaseSerializer):
cycle_detail = CycleSerializer(source="cycle", read_only=True)
class Meta:
model = CycleFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
@@ -0,0 +1,38 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
class EstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
class EstimatePointSerializer(BaseSerializer):
class Meta:
model = EstimatePoint
fields = "__all__"
read_only_fields = [
"estimate",
"workspace",
"project",
]
class EstimateReadSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
@@ -0,0 +1,14 @@
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Importer
fields = "__all__"
@@ -0,0 +1,7 @@
from .base import IntegrationSerializer, WorkspaceIntegrationSerializer
from .github import (
GithubRepositorySerializer,
GithubRepositorySyncSerializer,
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
@@ -0,0 +1,20 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.db.models import Integration, WorkspaceIntegration
class IntegrationSerializer(BaseSerializer):
class Meta:
model = Integration
fields = "__all__"
read_only_fields = [
"verified",
]
class WorkspaceIntegrationSerializer(BaseSerializer):
integration_detail = IntegrationSerializer(read_only=True, source="integration")
class Meta:
model = WorkspaceIntegration
fields = "__all__"
@@ -0,0 +1,45 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.db.models import (
GithubIssueSync,
GithubRepository,
GithubRepositorySync,
GithubCommentSync,
)
class GithubRepositorySerializer(BaseSerializer):
class Meta:
model = GithubRepository
fields = "__all__"
class GithubRepositorySyncSerializer(BaseSerializer):
repo_detail = GithubRepositorySerializer(source="repository")
class Meta:
model = GithubRepositorySync
fields = "__all__"
class GithubIssueSyncSerializer(BaseSerializer):
class Meta:
model = GithubIssueSync
fields = "__all__"
read_only_fields = [
"project",
"workspace",
"repository_sync",
]
class GithubCommentSyncSerializer(BaseSerializer):
class Meta:
model = GithubCommentSync
fields = "__all__"
read_only_fields = [
"project",
"workspace",
"repository_sync",
"issue_sync",
]
+541
View File
@@ -0,0 +1,541 @@
# Third Party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .state import StateSerializer, StateLiteSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import (
User,
Issue,
IssueActivity,
IssueComment,
TimelineIssue,
IssueProperty,
IssueBlocker,
IssueAssignee,
IssueLabel,
Label,
IssueBlocker,
CycleIssue,
Cycle,
Module,
ModuleIssue,
IssueLink,
IssueAttachment,
)
class IssueFlatSerializer(BaseSerializer):
## Contain only flat fields
class Meta:
model = Issue
fields = [
"id",
"name",
"description",
"priority",
"start_date",
"target_date",
"sequence_id",
"sort_order",
]
##TODO: Find a better way to write this serializer
## Find a better approach to save manytomany?
class IssueCreateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
assignees_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
# List of issues that are blocking this issue
blockers_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
labels_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
# List of issues that are blocked by this issue
blocks_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Issue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
def create(self, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
project = self.context["project"]
issue = Issue.objects.create(**validated_data, project=project)
if blockers is not None and len(blockers):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=issue,
blocked_by=blocker,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
for blocker in blockers
],
batch_size=10,
)
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
for user in assignees
],
batch_size=10,
)
else:
# Then assign it to default assignee
if project.default_assignee is not None:
IssueAssignee.objects.create(
assignee=project.default_assignee,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
if labels is not None and len(labels):
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
for label in labels
],
batch_size=10,
)
if blocks is not None and len(blocks):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
for block in blocks
],
batch_size=10,
)
return issue
def update(self, instance, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
if blockers is not None:
IssueBlocker.objects.filter(block=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=instance,
blocked_by=blocker,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
)
for blocker in blockers
],
batch_size=10,
)
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
issue=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
)
for user in assignees
],
batch_size=10,
)
if labels is not None:
IssueLabel.objects.filter(issue=instance).delete()
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
)
for label in labels
],
batch_size=10,
)
if blocks is not None:
IssueBlocker.objects.filter(blocked_by=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
)
for block in blocks
],
batch_size=10,
)
return super().update(instance, validated_data)
class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueActivity
fields = "__all__"
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class TimeLineIssueSerializer(BaseSerializer):
class Meta:
model = TimelineIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssuePropertySerializer(BaseSerializer):
class Meta:
model = IssueProperty
fields = "__all__"
read_only_fields = [
"user",
"workspace",
"project",
]
class LabelSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Label
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = [
"id",
"name",
"color",
]
class IssueLabelSerializer(BaseSerializer):
# label_details = LabelSerializer(read_only=True, source="label")
class Meta:
model = IssueLabel
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
class BlockedIssueSerializer(BaseSerializer):
blocked_issue_detail = IssueFlatSerializer(source="block", read_only=True)
class Meta:
model = IssueBlocker
fields = "__all__"
class BlockerIssueSerializer(BaseSerializer):
blocker_issue_detail = IssueFlatSerializer(source="blocked_by", read_only=True)
class Meta:
model = IssueBlocker
fields = "__all__"
class IssueAssigneeSerializer(BaseSerializer):
assignee_details = UserLiteSerializer(read_only=True, source="assignee")
class Meta:
model = IssueAssignee
fields = "__all__"
class CycleBaseSerializer(BaseSerializer):
class Meta:
model = Cycle
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssueCycleDetailSerializer(BaseSerializer):
cycle_detail = CycleBaseSerializer(read_only=True, source="cycle")
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class ModuleBaseSerializer(BaseSerializer):
class Meta:
model = Module
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssueModuleDetailSerializer(BaseSerializer):
module_detail = ModuleBaseSerializer(read_only=True, source="module")
class Meta:
model = ModuleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssueLinkSerializer(BaseSerializer):
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
class Meta:
model = IssueLink
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
"issue",
]
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return IssueLink.objects.create(**validated_data)
class IssueAttachmentSerializer(BaseSerializer):
class Meta:
model = IssueAttachment
fields = "__all__"
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
"workspace",
"project",
"issue",
]
# Issue Serializer with state details
class IssueStateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
project_detail = ProjectSerializer(read_only=True, source="project")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
class Meta:
model = Issue
fields = "__all__"
class IssueSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project")
state_detail = StateSerializer(read_only=True, source="state")
parent_detail = IssueFlatSerializer(read_only=True, source="parent")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
# List of issues blocked by this issue
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
# List of issues that block this issue
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
model = Issue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssueLiteSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
cycle_id = serializers.UUIDField(read_only=True)
module_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
class Meta:
model = Issue
fields = "__all__"
read_only_fields = [
"start_date",
"target_date",
"completed_at",
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
+189
View File
@@ -0,0 +1,189 @@
# Third Party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import (
User,
Module,
ModuleMember,
ModuleIssue,
ModuleLink,
ModuleFavorite,
)
class ModuleWriteSerializer(BaseSerializer):
members_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Module
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
def create(self, validated_data):
members = validated_data.pop("members_list", None)
project = self.context["project"]
module = Module.objects.create(**validated_data, project=project)
if members is not None:
ModuleMember.objects.bulk_create(
[
ModuleMember(
module=module,
member=member,
project=project,
workspace=project.workspace,
created_by=module.created_by,
updated_by=module.updated_by,
)
for member in members
],
batch_size=10,
ignore_conflicts=True,
)
return module
def update(self, instance, validated_data):
members = validated_data.pop("members_list", None)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
ModuleMember.objects.bulk_create(
[
ModuleMember(
module=instance,
member=member,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
)
for member in members
],
batch_size=10,
ignore_conflicts=True,
)
return super().update(instance, validated_data)
class ModuleFlatSerializer(BaseSerializer):
class Meta:
model = Module
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class ModuleIssueSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(read_only=True, source="module")
issue_detail = IssueStateSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
model = ModuleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
"module",
]
class ModuleLinkSerializer(BaseSerializer):
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
class Meta:
model = ModuleLink
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
"module",
]
# Validation if url already exists
def create(self, validated_data):
if ModuleLink.objects.filter(
url=validated_data.get("url"), module_id=validated_data.get("module_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
link_module = ModuleLinkSerializer(read_only=True, many=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
completed_issues = serializers.IntegerField(read_only=True)
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
class Meta:
model = Module
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class ModuleFavoriteSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(source="module", read_only=True)
class Meta:
model = ModuleFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
+105
View File
@@ -0,0 +1,105 @@
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label
class PageBlockSerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = PageBlock
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"page",
]
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelSerializer(read_only=True, source="labels", many=True)
labels_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
blocks = PageBlockSerializer(read_only=True, many=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Page
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"owned_by",
]
def create(self, validated_data):
labels = validated_data.pop("labels_list", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
page = Page.objects.create(
**validated_data, project_id=project_id, owned_by_id=owned_by_id
)
if labels is not None:
PageLabel.objects.bulk_create(
[
PageLabel(
label=label,
page=page,
project_id=project_id,
workspace_id=page.workspace_id,
created_by_id=page.created_by_id,
updated_by_id=page.updated_by_id,
)
for label in labels
],
batch_size=10,
)
return page
def update(self, instance, validated_data):
labels = validated_data.pop("labels_list", None)
if labels is not None:
PageLabel.objects.filter(page=instance).delete()
PageLabel.objects.bulk_create(
[
PageLabel(
label=label,
page=instance,
project_id=instance.project_id,
workspace_id=instance.workspace_id,
created_by_id=instance.created_by_id,
updated_by_id=instance.updated_by_id,
)
for label in labels
],
batch_size=10,
)
return super().update(instance, validated_data)
class PageFavoriteSerializer(BaseSerializer):
page_detail = PageSerializer(source="page", read_only=True)
class Meta:
model = PageFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
+57
View File
@@ -0,0 +1,57 @@
from rest_framework.serializers import (
ModelSerializer,
Serializer,
CharField,
SerializerMethodField,
)
from rest_framework.authtoken.models import Token
from rest_framework_simplejwt.tokens import RefreshToken
from plane.db.models import User
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = "__all__"
extra_kwargs = {"password": {"write_only": True}}
class ChangePasswordSerializer(Serializer):
model = User
"""
Serializer for password change endpoint.
"""
old_password = CharField(required=True)
new_password = CharField(required=True)
class ResetPasswordSerializer(Serializer):
model = User
"""
Serializer for password change endpoint.
"""
new_password = CharField(required=True)
confirm_password = CharField(required=True)
class TokenSerializer(ModelSerializer):
user = UserSerializer()
access_token = SerializerMethodField()
refresh_token = SerializerMethodField()
def get_access_token(self, obj):
refresh_token = RefreshToken.for_user(obj.user)
return str(refresh_token.access_token)
def get_refresh_token(self, obj):
refresh_token = RefreshToken.for_user(obj.user)
return str(refresh_token)
class Meta:
model = Token
fields = "__all__"
+132
View File
@@ -0,0 +1,132 @@
# Django imports
from django.db import IntegrityError
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer
from plane.db.models import (
Project,
ProjectMember,
ProjectMemberInvite,
ProjectIdentifier,
ProjectFavorite,
)
class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Project
fields = "__all__"
read_only_fields = [
"workspace",
]
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(detail="Project Identifier is required")
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken")
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
)
_ = ProjectIdentifier.objects.create(
name=project.identifier,
project=project,
workspace_id=self.context["workspace_id"],
)
return project
def update(self, instance, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
# If identifier is not passed update the project and return
if identifier == "":
project = super().update(instance, validated_data)
return project
# If no Project Identifier is found create it
project_identifier = ProjectIdentifier.objects.filter(
name=identifier, workspace_id=instance.workspace_id
).first()
if project_identifier is None:
project = super().update(instance, validated_data)
project_identifier = ProjectIdentifier.objects.filter(
project=project
).first()
if project_identifier is not None:
project_identifier.name = identifier
project_identifier.save()
return project
# If found check if the project_id to be updated and identifier project id is same
if project_identifier.project_id == instance.id:
# If same pass update
project = super().update(instance, validated_data)
return project
# If not same fail update
raise serializers.ValidationError(detail="Project Identifier is already taken")
class ProjectDetailSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
class Meta:
model = Project
fields = "__all__"
class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
project = ProjectSerializer(read_only=True)
member = UserLiteSerializer(read_only=True)
class Meta:
model = ProjectMember
fields = "__all__"
class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True)
class Meta:
model = ProjectMemberInvite
fields = "__all__"
class ProjectIdentifierSerializer(BaseSerializer):
class Meta:
model = ProjectIdentifier
fields = "__all__"
class ProjectFavoriteSerializer(BaseSerializer):
project_detail = ProjectSerializer(source="project", read_only=True)
class Meta:
model = ProjectFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"user",
]
class ProjectLiteSerializer(BaseSerializer):
class Meta:
model = Project
fields = ["id", "identifier", "name"]
read_only_fields = fields
@@ -0,0 +1,14 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import Shortcut
class ShortCutSerializer(BaseSerializer):
class Meta:
model = Shortcut
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
+31
View File
@@ -0,0 +1,31 @@
# Module imports
from .base import BaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import State
class StateSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = State
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = [
"id",
"name",
"color",
"group",
]
read_only_fields = fields
+43
View File
@@ -0,0 +1,43 @@
# Module import
from .base import BaseSerializer
from plane.db.models import User
class UserSerializer(BaseSerializer):
class Meta:
model = User
fields = "__all__"
read_only_fields = [
"id",
"created_at",
"updated_at",
"is_superuser",
"is_staff",
"last_active",
"last_login_time",
"last_logout_time",
"last_login_ip",
"last_logout_ip",
"last_login_uagent",
"token_updated_at",
"is_onboarded",
"is_bot",
]
extra_kwargs = {"password": {"write_only": True}}
class UserLiteSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"first_name",
"last_name",
"email",
"avatar",
"is_bot",
]
read_only_fields = [
"id",
"is_bot",
]
+54
View File
@@ -0,0 +1,54 @@
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import IssueView, IssueViewFavorite
from plane.utils.issue_filters import issue_filters
class IssueViewSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = IssueView
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
class IssueViewFavoriteSerializer(BaseSerializer):
view_detail = IssueViewSerializer(source="issue_view", read_only=True)
class Meta:
model = IssueViewFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
@@ -0,0 +1,119 @@
# Third party imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import (
User,
Workspace,
WorkspaceMember,
Team,
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
)
class WorkSpaceSerializer(BaseSerializer):
owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
class Meta:
model = Workspace
fields = "__all__"
read_only_fields = [
"id",
"created_by",
"updated_by",
"created_at",
"updated_at",
"owner",
]
class WorkSpaceMemberSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
class WorkSpaceMemberInviteSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
class Meta:
model = WorkspaceMemberInvite
fields = "__all__"
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Team
fields = "__all__"
read_only_fields = [
"workspace",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
def create(self, validated_data, **kwargs):
if "members" in validated_data:
members = validated_data.pop("members")
workspace = self.context["workspace"]
team = Team.objects.create(**validated_data, workspace=workspace)
team_members = [
TeamMember(member=member, team=team, workspace=workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
else:
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(member=member, team=instance, workspace=instance.workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
else:
return super().update(instance, validated_data)
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
fields = "__all__"
read_only_fields = [
"workspace",
"actor",
]
File diff suppressed because it is too large Load Diff
+141
View File
@@ -0,0 +1,141 @@
from .project import (
ProjectViewSet,
ProjectMemberViewSet,
UserProjectInvitationsViewset,
InviteProjectEndpoint,
AddTeamToProjectEndpoint,
ProjectMemberInvitationsViewset,
ProjectMemberInviteDetailViewSet,
ProjectIdentifierEndpoint,
AddMemberToProjectEndpoint,
ProjectJoinEndpoint,
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
)
from .people import (
UserEndpoint,
UpdateUserOnBoardedEndpoint,
UserActivityEndpoint,
)
from .oauth import OauthEndpoint
from .base import BaseAPIView, BaseViewSet
from .workspace import (
WorkSpaceViewSet,
UserWorkSpacesEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint,
WorkSpaceMemberViewSet,
TeamMemberViewSet,
WorkspaceInvitationsViewset,
UserWorkspaceInvitationsEndpoint,
UserWorkspaceInvitationEndpoint,
UserLastProjectWithWorkspaceEndpoint,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
)
from .state import StateViewSet, StateDeleteIssueCheckEndpoint
from .shortcut import ShortCutViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import (
CycleViewSet,
CycleIssueViewSet,
CycleDateCheckEndpoint,
CurrentUpcomingCyclesEndpoint,
CompletedCyclesEndpoint,
CycleFavoriteViewSet,
DraftCyclesEndpoint,
TransferCycleIssueEndpoint,
InCompleteCyclesEndpoint,
)
from .asset import FileAssetEndpoint, UserAssetsEndpoint
from .issue import (
IssueViewSet,
WorkSpaceIssuesEndpoint,
IssueActivityEndpoint,
IssueCommentViewSet,
TimeLineIssueViewSet,
IssuePropertyViewSet,
LabelViewSet,
BulkDeleteIssuesEndpoint,
UserWorkSpaceIssues,
SubIssuesEndpoint,
IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
)
from .auth_extended import (
VerifyEmailEndpoint,
RequestEmailVerificationEndpoint,
ForgotPasswordEndpoint,
ResetPasswordEndpoint,
ChangePasswordEndpoint,
)
from .authentication import (
SignInEndpoint,
SignOutEndpoint,
MagicSignInEndpoint,
MagicSignInGenerateEndpoint,
)
from .module import (
ModuleViewSet,
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
)
from .api_token import ApiTokenEndpoint
from .integration import (
WorkspaceIntegrationViewSet,
IntegrationViewSet,
GithubIssueSyncViewSet,
GithubRepositorySyncViewSet,
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
BulkCreateGithubIssueSyncEndpoint,
)
from .importer import (
ServiceIssueImportSummaryEndpoint,
ImportServiceEndpoint,
UpdateServiceImportStatusEndpoint,
BulkImportIssuesEndpoint,
BulkImportModulesEndpoint,
)
from .page import (
PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint,
RecentPagesEndpoint,
FavoritePagesEndpoint,
MyPagesEndpoint,
CreatedbyOtherPagesEndpoint,
)
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint
from .estimate import (
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
)
from .release import ReleaseNotesEndpoint
+70
View File
@@ -0,0 +1,70 @@
# Python import
from uuid import uuid4
# Third party
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module import
from .base import BaseAPIView
from plane.db.models import APIToken
from plane.api.serializers import APITokenSerializer
class ApiTokenEndpoint(BaseAPIView):
def post(self, request):
try:
label = request.data.get("label", str(uuid4().hex))
workspace = request.data.get("workspace", False)
if not workspace:
return Response(
{"error": "Workspace is required"}, status=status.HTTP_200_OK
)
api_token = APIToken.objects.create(
label=label, user=request.user, workspace_id=workspace
)
serializer = APITokenSerializer(api_token)
# Token will be only vissible while creating
return Response(
{"api_token": serializer.data, "token": api_token.token},
status=status.HTTP_201_CREATED,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def get(self, request):
try:
api_tokens = APIToken.objects.filter(user=request.user)
serializer = APITokenSerializer(api_tokens, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, pk):
try:
api_token = APIToken.objects.get(pk=pk)
api_token.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except APIToken.DoesNotExist:
return Response(
{"error": "Token does not exists"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+111
View File
@@ -0,0 +1,111 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
from plane.db.models import FileAsset
from plane.api.serializers import FileAssetSerializer
class FileAssetEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
"""
A viewset for viewing and editing task instances.
"""
def get(self, request, workspace_id, asset_key):
asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key)
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
return Response(serializer.data)
def post(self, request, slug):
try:
serializer = FileAssetSerializer(data=request.data)
if serializer.is_valid():
if request.user.last_workspace_id is None:
return Response(
{"error": "Workspace id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer.save(workspace_id=request.user.last_workspace_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, workspace_id, asset_key):
try:
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
# Delete the file from storage
file_asset.asset.delete(save=False)
# Delete the file object
file_asset.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key):
try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
serializer = FileAssetSerializer(files, context={"request": request})
return Response(serializer.data)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND
)
def post(self, request):
try:
serializer = FileAssetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, asset_key):
try:
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
# Delete the file from storage
file_asset.asset.delete(save=False)
# Delete the file object
file_asset.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+159
View File
@@ -0,0 +1,159 @@
## Python imports
import jwt
## Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils.encoding import (
smart_str,
smart_bytes,
DjangoUnicodeDecodeError,
)
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.contrib.sites.shortcuts import get_current_site
from django.conf import settings
## Third Party Imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from rest_framework_simplejwt.tokens import RefreshToken
from sentry_sdk import capture_exception
## Module imports
from . import BaseAPIView
from plane.api.serializers.people import (
ChangePasswordSerializer,
ResetPasswordSerializer,
)
from plane.db.models import User
from plane.bgtasks.email_verification_task import email_verification
from plane.bgtasks.forgot_password_task import forgot_password
class RequestEmailVerificationEndpoint(BaseAPIView):
def get(self, request):
token = RefreshToken.for_user(request.user).access_token
current_site = settings.WEB_URL
email_verification.delay(
request.user.first_name, request.user.email, token, current_site
)
return Response(
{"message": "Email sent successfully"}, status=status.HTTP_200_OK
)
class VerifyEmailEndpoint(BaseAPIView):
def get(self, request):
token = request.GET.get("token")
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
user = User.objects.get(id=payload["user_id"])
if not user.is_email_verified:
user.is_email_verified = True
user.save()
return Response(
{"email": "Successfully activated"}, status=status.HTTP_200_OK
)
except jwt.ExpiredSignatureError as indentifier:
return Response(
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
)
except jwt.exceptions.DecodeError as indentifier:
return Response(
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
)
class ForgotPasswordEndpoint(BaseAPIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
email = request.data.get("email")
if User.objects.filter(email=email).exists():
user = User.objects.get(email=email)
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
current_site = settings.WEB_URL
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
)
return Response(
{"message": "Check your email to reset your password"},
status=status.HTTP_200_OK,
)
return Response(
{"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
)
class ResetPasswordEndpoint(BaseAPIView):
permission_classes = [permissions.AllowAny]
def post(self, request, uidb64, token):
try:
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
if not PasswordResetTokenGenerator().check_token(user, token):
return Response(
{"error": "token is not valid, please check the new one"},
status=status.HTTP_401_UNAUTHORIZED,
)
serializer = ResetPasswordSerializer(data=request.data)
if serializer.is_valid():
# set_password also hashes the password that the user will get
user.set_password(serializer.data.get("new_password"))
user.save()
response = {
"status": "success",
"code": status.HTTP_200_OK,
"message": "Password updated successfully",
}
return Response(response)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except DjangoUnicodeDecodeError as indentifier:
return Response(
{"error": "token is not valid, please check the new one"},
status=status.HTTP_401_UNAUTHORIZED,
)
class ChangePasswordEndpoint(BaseAPIView):
def post(self, request):
try:
serializer = ChangePasswordSerializer(data=request.data)
user = User.objects.get(pk=request.user.id)
if serializer.is_valid():
# Check old password
if not user.object.check_password(serializer.data.get("old_password")):
return Response(
{"old_password": ["Wrong password."]},
status=status.HTTP_400_BAD_REQUEST,
)
# set_password also hashes the password that the user will get
self.object.set_password(serializer.data.get("new_password"))
self.object.save()
response = {
"status": "success",
"code": status.HTTP_200_OK,
"message": "Password updated successfully",
}
return Response(response)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

Some files were not shown because too many files have changed in this diff Show More