diff --git a/.env.example b/.env.example index 71a9074a63..0649839a45 100644 --- a/.env.example +++ b/.env.example @@ -31,3 +31,5 @@ USE_MINIO=1 # Nginx Configuration NGINX_PORT=80 + +MONGO_DB_URL="mongodb://plane-mongodb:27017/" \ No newline at end of file diff --git a/.github/workflows/build-branch-ee.yml b/.github/workflows/build-branch-ee.yml new file mode 100644 index 0000000000..06a6a244a9 --- /dev/null +++ b/.github/workflows/build-branch-ee.yml @@ -0,0 +1,467 @@ +name: Branch Build Enterprise + +on: + workflow_dispatch: + inputs: + arm64: + description: "Build for ARM64 architecture" + required: false + default: false + type: boolean + push: + branches: + - master + - preview + - develop + release: + types: [released, prereleased] + +env: + TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }} + ARM64_BUILD: ${{ github.event.inputs.arm64 }} + +jobs: + branch_build_setup: + name: Build Setup + runs-on: ${{vars.ACTION_RUNS_ON}} + 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 }} + build_web: ${{ steps.changed_files.outputs.web_any_changed }} + build_admin: ${{ steps.changed_files.outputs.admin_any_changed }} + build_space: ${{ steps.changed_files.outputs.space_any_changed }} + build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }} + build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }} + artifact_upload_to_s3: ${{ steps.set_env_variables.outputs.artifact_upload_to_s3 }} + artifact_s3_suffix: ${{ steps.set_env_variables.outputs.artifact_s3_suffix }} + + steps: + - id: set_env_variables + name: Set Environment Variables + run: | + if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ github.event_name }}" == "release" ] || [ "${{ env.ARM64_BUILD }}" == "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 }}" | tr / -) + echo "TARGET_BRANCH=$BR_NAME" >> $GITHUB_OUTPUT + + if [ "${{ github.event_name }}" == "release" ]; then + echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT + echo "artifact_s3_suffix=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then + echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT + echo "artifact_s3_suffix=latest" >> $GITHUB_OUTPUT + elif [ "${{ env.TARGET_BRANCH }}" == "preview" ] || [ "${{ env.TARGET_BRANCH }}" == "develop" ]; then + echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT + echo "artifact_s3_suffix=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT + else + echo "artifact_upload_to_s3=false" >> $GITHUB_OUTPUT + echo "artifact_s3_suffix=$BR_NAME" >> $GITHUB_OUTPUT + fi + + - id: checkout_files + name: Checkout Files + uses: actions/checkout@v4 + + - name: Get changed files + id: changed_files + uses: tj-actions/changed-files@v42 + with: + files_yaml: | + apiserver: + - apiserver/** + proxy: + - nginx/** + admin: + - admin/** + - packages/** + - "package.json" + - "yarn.lock" + - "tsconfig.json" + - "turbo.json" + space: + - space/** + - packages/** + - "package.json" + - "yarn.lock" + - "tsconfig.json" + - "turbo.json" + web: + - web/** + - packages/** + - "package.json" + - "yarn.lock" + - "tsconfig.json" + - "turbo.json" + + branch_build_push_admin: + if: ${{ needs.branch_build_setup.outputs.build_admin== 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + name: Build-Push Admin Docker Image + runs-on: ${{vars.ACTION_RUNS_ON}} + needs: [branch_build_setup] + env: + ADMIN_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/admin-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_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: Set Admin Docker Tag + run: | + if [ "${{ github.event_name }}" == "release" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/admin-enterprise:stable + TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/admin-enterprise:${{ github.event.release.tag_name }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/admin-enterprise:stable + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/admin-enterprise:${{ github.event.release.tag_name }} + elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/admin-enterprise:latest + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/admin-enterprise:latest + else + TAG=${{ env.ADMIN_TAG }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/admin-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + fi + echo "ADMIN_TAG=${TAG}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to Harbor + uses: docker/login-action@v3 + with: + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_TOKEN }} + registry: ${{ vars.HARBOR_REGISTRY }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: ${{ env.BUILDX_DRIVER }} + version: ${{ env.BUILDX_VERSION }} + endpoint: ${{ env.BUILDX_ENDPOINT }} + + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Build and Push Frontend to Docker Container Registry + uses: docker/build-push-action@v5.1.0 + with: + context: . + file: ./admin/Dockerfile.admin + platforms: ${{ env.BUILDX_PLATFORMS }} + tags: ${{ env.ADMIN_TAG }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_web: + if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + name: Build-Push Web Docker Image + runs-on: ${{vars.ACTION_RUNS_ON}} + needs: [branch_build_setup] + env: + WEB_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_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: Set Web Docker Tag + run: | + if [ "${{ github.event_name }}" == "release" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:stable + TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:${{ github.event.release.tag_name }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:stable + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:${{ github.event.release.tag_name }} + elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:latest + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:latest + else + TAG=${{ env.WEB_TAG }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + fi + echo "WEB_TAG=${TAG}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to Harbor + uses: docker/login-action@v3 + with: + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_TOKEN }} + registry: ${{ vars.HARBOR_REGISTRY }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: ${{ env.BUILDX_DRIVER }} + version: ${{ env.BUILDX_VERSION }} + endpoint: ${{ env.BUILDX_ENDPOINT }} + + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Build and Push Web to Docker Container Registry + uses: docker/build-push-action@v5.1.0 + with: + context: . + file: ./web/Dockerfile.web + platforms: ${{ env.BUILDX_PLATFORMS }} + tags: ${{ env.WEB_TAG }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_space: + if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + name: Build-Push Space Docker Image + runs-on: ${{vars.ACTION_RUNS_ON}} + needs: [branch_build_setup] + env: + SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_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: Set Space Docker Tag + run: | + if [ "${{ github.event_name }}" == "release" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:stable + TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:${{ github.event.release.tag_name }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:stable + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:${{ github.event.release.tag_name }} + elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:latest + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:latest + else + TAG=${{ env.SPACE_TAG }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + fi + echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to Harbor + uses: docker/login-action@v3 + with: + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_TOKEN }} + registry: ${{ vars.HARBOR_REGISTRY }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: ${{ env.BUILDX_DRIVER }} + version: ${{ env.BUILDX_VERSION }} + endpoint: ${{ env.BUILDX_ENDPOINT }} + + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Build and Push Space to Docker Hub + uses: docker/build-push-action@v5.1.0 + with: + context: . + file: ./space/Dockerfile.space + platforms: ${{ env.BUILDX_PLATFORMS }} + tags: ${{ env.SPACE_TAG }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_apiserver: + if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + name: Build-Push API Server Docker Image + runs-on: ${{vars.ACTION_RUNS_ON}} + needs: [branch_build_setup] + env: + BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_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: Set Backend Docker Tag + run: | + if [ "${{ github.event_name }}" == "release" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:stable + TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:${{ github.event.release.tag_name }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:stable + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:${{ github.event.release.tag_name }} + elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:latest + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:latest + else + TAG=${{ env.BACKEND_TAG }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + fi + echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to Harbor + uses: docker/login-action@v3 + with: + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_TOKEN }} + registry: ${{ vars.HARBOR_REGISTRY }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: ${{ env.BUILDX_DRIVER }} + version: ${{ env.BUILDX_VERSION }} + endpoint: ${{ env.BUILDX_ENDPOINT }} + + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Build and Push Backend to Docker Hub + uses: docker/build-push-action@v5.1.0 + with: + context: ./apiserver + file: ./apiserver/Dockerfile.api + platforms: ${{ env.BUILDX_PLATFORMS }} + push: true + tags: ${{ env.BACKEND_TAG }} + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_proxy: + if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + name: Build-Push Proxy Docker Image + runs-on: ${{vars.ACTION_RUNS_ON}} + needs: [branch_build_setup] + env: + PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_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: Set Proxy Docker Tag + run: | + if [ "${{ github.event_name }}" == "release" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:stable + TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:${{ github.event.release.tag_name }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:stable + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:${{ github.event.release.tag_name }} + elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then + TAG=${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:latest + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:latest + else + TAG=${{ env.PROXY_TAG }} + TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }} + fi + echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to Harbor + uses: docker/login-action@v3 + with: + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_TOKEN }} + registry: ${{ vars.HARBOR_REGISTRY }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: ${{ env.BUILDX_DRIVER }} + version: ${{ env.BUILDX_VERSION }} + endpoint: ${{ env.BUILDX_ENDPOINT }} + + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Build and Push Plane-Proxy to Docker Hub + uses: docker/build-push-action@v5.1.0 + with: + context: ./nginx + file: ./nginx/Dockerfile + platforms: ${{ env.BUILDX_PLATFORMS }} + tags: ${{ env.PROXY_TAG }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + upload_artifacts_s3: + if: ${{ needs.branch_build_setup.outputs.artifact_upload_to_s3 == 'true' }} + name: Upload artifacts to S3 Bucket + runs-on: ${{vars.ACTION_RUNS_ON}} + needs: [branch_build_setup] + container: + image: docker:20.10.7 + 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 }} + TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }} + steps: + - id: checkout_files + name: Checkout Files + uses: actions/checkout@v4 + + - name: Upload artifacts + run: | + apk update + apk add --no-cache aws-cli + + mkdir -p ~/${{ env.ARTIFACT_SUFFIX }} + + cp deploy/cli-install/variables.env ~/${{ env.ARTIFACT_SUFFIX }}/variables.env + cp deploy/cli-install/Caddyfile ~/${{ env.ARTIFACT_SUFFIX }}/Caddyfile + sed -e 's@${APP_RELEASE_VERSION}@'${{ env.ARTIFACT_SUFFIX }}'@' deploy/cli-install/docker-compose.yml > ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose.yml + sed -e 's@${APP_RELEASE_VERSION}@'${{ env.ARTIFACT_SUFFIX }}'@' deploy/cli-install/docker-compose-caddy.yml > ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-caddy.yml + + aws s3 cp ~/${{ env.ARTIFACT_SUFFIX }} s3://${{ vars.SELF_HOST_BUCKET_NAME }}/plane-enterprise/${{ env.ARTIFACT_SUFFIX }} --recursive + + rm -rf ~/${{ env.ARTIFACT_SUFFIX }} diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000000..3c6f5a0056 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,70 @@ +name: Manual Release Workflow + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release Tag (e.g., v0.16-cannary-1)' + required: true + prerelease: + description: 'Pre-Release' + required: true + default: true + type: boolean + draft: + description: 'Draft' + required: true + default: true + type: boolean + +permissions: + contents: write + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 # Necessary to fetch all history for tags + + - name: Set up Git + run: | + git config user.name "github-actions" + git config user.email "github-actions@github.com" + + - name: Check for the Prerelease + run: | + echo ${{ github.event.release.prerelease }} + + - name: Generate Release Notes + id: generate_notes + run: | + bash ./generate_release_notes.sh + # Directly use the content of RELEASE_NOTES.md for the release body + RELEASE_NOTES=$(cat RELEASE_NOTES.md) + echo "RELEASE_NOTES<> $GITHUB_ENV + echo "$RELEASE_NOTES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Create Tag + run: | + git tag ${{ github.event.inputs.release_tag }} + git push origin ${{ github.event.inputs.release_tag }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.event.inputs.release_tag }} + body_path: RELEASE_NOTES.md + draft: ${{ github.event.inputs.draft }} + prerelease: ${{ github.event.inputs.prerelease }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + + + + + diff --git a/admin/app/authentication/components/index.ts b/admin/app/authentication/components/index.ts index d76d61f57b..3c3507d440 100644 --- a/admin/app/authentication/components/index.ts +++ b/admin/app/authentication/components/index.ts @@ -3,3 +3,7 @@ export * from "./password-config-switch"; export * from "./authentication-method-card"; export * from "./github-config"; export * from "./google-config"; + +// enterprise +export * from "./oidc-config"; +export * from "./saml-config"; diff --git a/admin/app/authentication/components/oidc-config.tsx b/admin/app/authentication/components/oidc-config.tsx new file mode 100644 index 0000000000..a1b373b08d --- /dev/null +++ b/admin/app/authentication/components/oidc-config.tsx @@ -0,0 +1,72 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +// icons +import { Settings2 } from "lucide-react"; +// types +import { TInstanceEnterpriseAuthenticationMethodKeys } from "@plane/types"; +// ui +import { ToggleSwitch, getButtonStyling } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useInstance } from "@/hooks/store"; + +type Props = { + disabled: boolean; + updateConfig: ( + key: TInstanceEnterpriseAuthenticationMethodKeys, + value: string + ) => void; +}; + +export const OIDCConfiguration: React.FC = observer((props) => { + const { disabled, updateConfig } = props; + // store + const { formattedConfig } = useInstance(); + // derived values + const enableOIDCConfig = formattedConfig?.IS_OIDC_ENABLED ?? ""; + const isOIDCConfigured = + !!formattedConfig?.OIDC_CLIENT_ID && !!formattedConfig?.OIDC_CLIENT_SECRET; + + return ( + <> + {isOIDCConfigured ? ( +
+ + Edit + + { + Boolean(parseInt(enableOIDCConfig)) === true + ? updateConfig("IS_OIDC_ENABLED", "0") + : updateConfig("IS_OIDC_ENABLED", "1"); + }} + size="sm" + disabled={disabled} + /> +
+ ) : ( + + + Configure + + )} + + ); +}); diff --git a/admin/app/authentication/components/saml-config.tsx b/admin/app/authentication/components/saml-config.tsx new file mode 100644 index 0000000000..c71a5a59d9 --- /dev/null +++ b/admin/app/authentication/components/saml-config.tsx @@ -0,0 +1,72 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +// icons +import { Settings2 } from "lucide-react"; +// types +import { TInstanceEnterpriseAuthenticationMethodKeys } from "@plane/types"; +// ui +import { ToggleSwitch, getButtonStyling } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useInstance } from "@/hooks/store"; + +type Props = { + disabled: boolean; + updateConfig: ( + key: TInstanceEnterpriseAuthenticationMethodKeys, + value: string + ) => void; +}; + +export const SAMLConfiguration: React.FC = observer((props) => { + const { disabled, updateConfig } = props; + // store + const { formattedConfig } = useInstance(); + // derived values + const enableSAMLConfig = formattedConfig?.IS_SAML_ENABLED ?? ""; + const isSAMLConfigured = + !!formattedConfig?.SAML_ENTITY_ID && !!formattedConfig?.SAML_CERTIFICATE; + + return ( + <> + {isSAMLConfigured ? ( +
+ + Edit + + { + Boolean(parseInt(enableSAMLConfig)) === true + ? updateConfig("IS_SAML_ENABLED", "0") + : updateConfig("IS_SAML_ENABLED", "1"); + }} + size="sm" + disabled={disabled} + /> +
+ ) : ( + + + Configure + + )} + + ); +}); diff --git a/admin/app/authentication/oidc/form.tsx b/admin/app/authentication/oidc/form.tsx new file mode 100644 index 0000000000..23a6b26cad --- /dev/null +++ b/admin/app/authentication/oidc/form.tsx @@ -0,0 +1,222 @@ +import { FC, useState } from "react"; +import Link from "next/link"; +import { useForm } from "react-hook-form"; +// types +import { IFormattedInstanceConfiguration, TInstanceOIDCAuthenticationConfigurationKeys } from "@plane/types"; +// ui +import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; +// components +import { + ConfirmDiscardModal, + ControllerInput, + TControllerInputFormField, + CopyField, + TCopyField, +} from "@/components/common"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useInstance } from "@/hooks/store"; + +type Props = { + config: IFormattedInstanceConfiguration; +}; + +type OIDCConfigFormValues = Record; + +export const InstanceOIDCConfigForm: FC = (props) => { + const { config } = props; + // states + const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false); + // store hooks + const { updateInstanceConfigurations } = useInstance(); + // form data + const { + handleSubmit, + control, + reset, + formState: { errors, isDirty, isSubmitting }, + } = useForm({ + defaultValues: { + OIDC_CLIENT_ID: config["OIDC_CLIENT_ID"], + OIDC_CLIENT_SECRET: config["OIDC_CLIENT_SECRET"], + OIDC_TOKEN_URL: config["OIDC_TOKEN_URL"], + OIDC_USERINFO_URL: config["OIDC_USERINFO_URL"], + OIDC_AUTHORIZE_URL: config["OIDC_AUTHORIZE_URL"], + OIDC_LOGOUT_URL: config["OIDC_LOGOUT_URL"], + OIDC_PROVIDER_NAME: config["OIDC_PROVIDER_NAME"], + }, + }); + + const originURL = typeof window !== "undefined" ? window.location.origin : ""; + + const OIDC_FORM_FIELDS: TControllerInputFormField[] = [ + { + key: "OIDC_CLIENT_ID", + type: "text", + label: "Client ID", + description: "Your authentication provider's public identifier for the client.", + placeholder: "abc123xyz789", + error: Boolean(errors.OIDC_CLIENT_ID), + required: true, + }, + { + key: "OIDC_CLIENT_SECRET", + type: "password", + label: "Client secret", + description: "Secret key provided by your authentication provider for the client.", + placeholder: "s3cr3tK3y123!", + error: Boolean(errors.OIDC_CLIENT_SECRET), + required: true, + }, + { + key: "OIDC_AUTHORIZE_URL", + type: "text", + label: "Authorize URL", + description: "The URL for interacting with the resource owner to obtain an authorization grant.", + placeholder: "https://example.com/", + error: Boolean(errors.OIDC_AUTHORIZE_URL), + required: true, + }, + { + key: "OIDC_TOKEN_URL", + type: "text", + label: "Token URL", + description: "URL to fetch the access token from a grant or refresh token.", + placeholder: "https://example.com/oauth/token", + error: Boolean(errors.OIDC_TOKEN_URL), + required: true, + }, + { + key: "OIDC_USERINFO_URL", + type: "text", + label: "UserInfo URL", + description: "The URL to fetch user claims and information.", + placeholder: "https://example.com/userinfo", + error: Boolean(errors.OIDC_USERINFO_URL), + required: true, + }, + { + key: "OIDC_LOGOUT_URL", + type: "text", + label: "Logout URL", + description: "Add your OIDC logout URL here for seamless session management.", + placeholder: "https://example.com/logout", + error: Boolean(errors.OIDC_LOGOUT_URL), + required: false, + }, + { + key: "OIDC_PROVIDER_NAME", + type: "text", + label: "Identity provider name", + description: "This name will be shown on sign in and create account CTA buttons.", + placeholder: "Okta", + error: Boolean(errors.OIDC_PROVIDER_NAME), + required: false, + }, + ]; + + const OIDC_SERVICE_DETAILS: TCopyField[] = [ + { + key: "Origin_URI", + label: "Origin URI", + url: `${originURL}/auth/oidc/`, + description: "We will auto-generate this. Add this as a trusted origin in your identity provider.", + }, + { + key: "Callback_URI", + label: "Callback URI", + url: `${originURL}/auth/oidc/callback/`, + description: + "We will auto generate this. Paste this in the sign-in redirect URI section in your identity provider.", + }, + { + key: "Logout_URI", + label: "Logout URI", + url: `${originURL}/auth/oidc/logout/`, + description: "We will auto-generate this. Paste this in sign-out redirect URI in your identity provider", + }, + ]; + + const onSubmit = async (formData: OIDCConfigFormValues) => { + const payload: Partial = { ...formData }; + + await updateInstanceConfigurations(payload) + .then((response = []) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success", + message: "OIDC Configuration Settings updated successfully", + }); + reset({ + OIDC_CLIENT_ID: response.find((item) => item.key === "OIDC_CLIENT_ID")?.value, + OIDC_CLIENT_SECRET: response.find((item) => item.key === "OIDC_CLIENT_SECRET")?.value, + OIDC_AUTHORIZE_URL: response.find((item) => item.key === "OIDC_AUTHORIZE_URL")?.value, + OIDC_TOKEN_URL: response.find((item) => item.key === "OIDC_TOKEN_URL")?.value, + OIDC_USERINFO_URL: response.find((item) => item.key === "OIDC_USERINFO_URL")?.value, + OIDC_LOGOUT_URL: response.find((item) => item.key === "OIDC_LOGOUT_URL")?.value, + OIDC_PROVIDER_NAME: response.find((item) => item.key === "OIDC_PROVIDER_NAME")?.value, + }); + }) + .catch((err) => console.error(err)); + }; + + const handleGoBack = (e: React.MouseEvent) => { + if (isDirty) { + e.preventDefault(); + setIsDiscardChangesModalOpen(true); + } + }; + + return ( + <> + setIsDiscardChangesModalOpen(false)} + /> +
+
+
+
Configuration
+ {OIDC_FORM_FIELDS.map((field) => ( + + ))} +
+
+ + + Go back + +
+
+
+
+
+
Service provider details
+ {OIDC_SERVICE_DETAILS.map((field) => ( + + ))} +
+
+
+
+ + ); +}; diff --git a/admin/app/authentication/oidc/page.tsx b/admin/app/authentication/oidc/page.tsx new file mode 100644 index 0000000000..e75fee28c8 --- /dev/null +++ b/admin/app/authentication/oidc/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +// hooks +import { useInstance } from "@/hooks/store"; +// ui +import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui"; +// components +import { PageHeader } from "@/components/core"; +import { AuthenticationMethodCard } from "../components"; +import { InstanceOIDCConfigForm } from "./form"; +// icons +import OIDCLogo from "/public/logos/oidc-logo.png"; + +const InstanceOIDCAuthenticationPage = observer(() => { + // store + const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance(); + // state + const [isSubmitting, setIsSubmitting] = useState(false); + // config + const enableOIDCConfig = formattedConfig?.IS_OIDC_ENABLED ?? ""; + + useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); + + const updateConfig = async (key: "IS_OIDC_ENABLED", value: string) => { + setIsSubmitting(true); + + const payload = { + [key]: value, + }; + + const updateConfigPromise = updateInstanceConfigurations(payload); + + setPromiseToast(updateConfigPromise, { + loading: "Saving Configuration...", + success: { + title: "Configuration saved", + message: () => `OIDC authentication is now ${value ? "active" : "disabled"}.`, + }, + error: { + title: "Error", + message: () => "Failed to save configuration", + }, + }); + + await updateConfigPromise + .then(() => { + setIsSubmitting(false); + }) + .catch((err) => { + console.error(err); + setIsSubmitting(false); + }); + }; + return ( + <> + +
+
+ } + config={ + { + Boolean(parseInt(enableOIDCConfig)) === true + ? updateConfig("IS_OIDC_ENABLED", "0") + : updateConfig("IS_OIDC_ENABLED", "1"); + }} + size="sm" + disabled={isSubmitting || !formattedConfig} + /> + } + disabled={isSubmitting || !formattedConfig} + withBorder={false} + /> +
+
+ {formattedConfig ? ( + + ) : ( + + + + + + + + + )} +
+
+ + ); +}); + +export default InstanceOIDCAuthenticationPage; diff --git a/admin/app/authentication/page.tsx b/admin/app/authentication/page.tsx index d1e6fb0ba1..80ad364ad8 100644 --- a/admin/app/authentication/page.tsx +++ b/admin/app/authentication/page.tsx @@ -10,14 +10,17 @@ import { TInstanceConfigurationKeys } from "@plane/types"; import { Loader, setPromiseToast } from "@plane/ui"; // components import { PageHeader } from "@/components/core"; -// hooks // helpers import { resolveGeneralTheme } from "@/helpers/common.helper"; +// hooks import { useInstance } from "@/hooks/store"; // images import githubLightModeImage from "@/public/logos/github-black.png"; import githubDarkModeImage from "@/public/logos/github-white.png"; import GoogleLogo from "@/public/logos/google-logo.svg"; +// images - enterprise +import OIDCLogo from "@/public/logos/oidc-logo.png"; +import SAMLLogo from "@/public/logos/saml-logo.svg"; // local components import { AuthenticationMethodCard, @@ -25,6 +28,9 @@ import { PasswordLoginConfiguration, GithubConfiguration, GoogleConfiguration, + // enterprise + OIDCConfiguration, + SAMLConfiguration, } from "./components"; type TInstanceAuthenticationMethodCard = { @@ -116,6 +122,24 @@ const InstanceAuthenticationPage = observer(() => { }, ]; + // Enterprise authentication methods + authenticationMethodsCard.push( + { + key: "oidc", + name: "OIDC", + description: "Authenticate your users via the OpenID connect protocol.", + icon: OIDC Logo, + config: , + }, + { + key: "saml", + name: "SAML", + description: "Authenticate your users via Security Assertion Markup Language protocol.", + icon: SAML Logo, + config: , + } + ); + return ( <> diff --git a/admin/app/authentication/saml/form.tsx b/admin/app/authentication/saml/form.tsx new file mode 100644 index 0000000000..0901d6b34a --- /dev/null +++ b/admin/app/authentication/saml/form.tsx @@ -0,0 +1,226 @@ +import { FC, useState } from "react"; +import Link from "next/link"; +import { Controller, useForm } from "react-hook-form"; +// types +import { IFormattedInstanceConfiguration, TInstanceSAMLAuthenticationConfigurationKeys } from "@plane/types"; +// ui +import { Button, TOAST_TYPE, TextArea, getButtonStyling, setToast } from "@plane/ui"; +// components +import { + ConfirmDiscardModal, + ControllerInput, + TControllerInputFormField, + CopyField, + TCopyField, +} from "@/components/common"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useInstance } from "@/hooks/store"; + +type Props = { + config: IFormattedInstanceConfiguration; +}; + +type SAMLConfigFormValues = Record; + +export const InstanceSAMLConfigForm: FC = (props) => { + const { config } = props; + // states + const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false); + // store hooks + const { updateInstanceConfigurations } = useInstance(); + // form data + const { + handleSubmit, + control, + reset, + formState: { errors, isDirty, isSubmitting }, + } = useForm({ + defaultValues: { + SAML_ENTITY_ID: config["SAML_ENTITY_ID"], + SAML_SSO_URL: config["SAML_SSO_URL"], + SAML_LOGOUT_URL: config["SAML_LOGOUT_URL"], + SAML_CERTIFICATE: config["SAML_CERTIFICATE"], + SAML_PROVIDER_NAME: config["SAML_PROVIDER_NAME"], + }, + }); + + const originURL = typeof window !== "undefined" ? window.location.origin : ""; + + const SAML_FORM_FIELDS: TControllerInputFormField[] = [ + { + key: "SAML_ENTITY_ID", + type: "text", + label: "Entity ID", + description: "Unique identifier for your Identity Provider (IdP) entity.", + placeholder: "70a44354520df8bd9bcd", + error: Boolean(errors.SAML_ENTITY_ID), + required: true, + }, + { + key: "SAML_SSO_URL", + type: "text", + label: "SSO URL", + description: "URL used for Single Sign-On (SSO) with your Identity Provider (IdP).", + placeholder: "https://example.com/sso", + error: Boolean(errors.SAML_SSO_URL), + required: true, + }, + { + key: "SAML_LOGOUT_URL", + type: "text", + label: "Logout URL", + description: "Add your SAML logout URL here for seamless session management.", + placeholder: "https://example.com/logout", + error: Boolean(errors.SAML_LOGOUT_URL), + required: false, + }, + { + key: "SAML_PROVIDER_NAME", + type: "text", + label: "Identity provider name", + description: "This name will be shown on sign in and create account CTA buttons.", + placeholder: "Okta", + error: Boolean(errors.SAML_PROVIDER_NAME), + required: false, + }, + ]; + + const SAML_SERVICE_DETAILS: TCopyField[] = [ + { + key: "Metadata_Information", + label: "Entity ID / Audience / Metadata Information", + url: `${originURL}/auth/saml/metadata/`, + description: + "This contains the link to the metadata information. We will auto-generate this.", + }, + { + key: "Callback_URI", + label: "Callback URI", + url: `${originURL}/auth/saml/callback/`, + description: + "This url is a http-post request. Paste this in the single sign-on callback url section of your identity.", + }, + { + key: "Logout_URI", + label: "Logout URI", + url: `${originURL}/auth/saml/logout/`, + description: "This url is a http-redirect request. Add this to your logout URI.", + }, + ]; + + const onSubmit = async (formData: SAMLConfigFormValues) => { + const payload: Partial = { ...formData }; + + await updateInstanceConfigurations(payload) + .then((response = []) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success", + message: "SAML Configuration Settings updated successfully", + }); + reset({ + SAML_ENTITY_ID: response.find((item) => item.key === "SAML_ENTITY_ID")?.value, + SAML_SSO_URL: response.find((item) => item.key === "SAML_SSO_URL")?.value, + SAML_LOGOUT_URL: response.find((item) => item.key === "SAML_LOGOUT_URL")?.value, + SAML_CERTIFICATE: response.find((item) => item.key === "SAML_CERTIFICATE")?.value, + SAML_PROVIDER_NAME: response.find((item) => item.key === "SAML_PROVIDER_NAME")?.value, + }); + }) + .catch((err) => console.error(err)); + }; + + const handleGoBack = (e: React.MouseEvent) => { + if (isDirty) { + e.preventDefault(); + setIsDiscardChangesModalOpen(true); + } + }; + + return ( + <> + setIsDiscardChangesModalOpen(false)} + /> +
+
+
+
Configuration
+ {SAML_FORM_FIELDS.map((field) => ( + + ))} +
+

Certificate

+ ( +