diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000000..cb7b9d12e4
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,244 @@
+name: Deploy to Render
+
+on:
+ push:
+ branches: [dev, main]
+ pull_request:
+ branches: [dev, main]
+
+# Prevent multiple concurrent deployments
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build Documentation
+ runs-on: ubuntu-latest
+ timeout-minutes: 90
+ outputs:
+ environment: ${{ steps.set-env.outputs.environment }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Determine environment
+ id: set-env
+ run: |
+ if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" == "main" ]]; then
+ echo "environment=production" >> $GITHUB_OUTPUT
+ echo "SITE_URL=${{ secrets.PROD_URL }}" >> $GITHUB_ENV
+ else
+ echo "environment=development" >> $GITHUB_OUTPUT
+ echo "SITE_URL=${{ secrets.DEV_URL }}" >> $GITHUB_ENV
+ fi
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+
+ - name: Cache Docusaurus build
+ uses: actions/cache@v4
+ with:
+ path: |
+ .docusaurus
+ .cache
+ build/.cache
+ key: ${{ runner.os }}-docusaurus-build-${{ steps.set-env.outputs.environment }}-${{ hashFiles('**/package-lock.json', '**/docusaurus.config.js', 'docs/**/*.md', 'docs/**/*.mdx') }}
+ restore-keys: |
+ ${{ runner.os }}-docusaurus-build-${{ steps.set-env.outputs.environment }}-
+ ${{ runner.os }}-docusaurus-build-
+
+ - name: Cache webpack
+ uses: actions/cache@v4
+ with:
+ path: node_modules/.cache
+ key: ${{ runner.os }}-webpack-${{ steps.set-env.outputs.environment }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-webpack-${{ steps.set-env.outputs.environment }}-
+ ${{ runner.os }}-webpack-
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build site
+ run: npm run build
+ env:
+ RENDER_EXTERNAL_URL: ${{ env.SITE_URL }}
+
+ - name: Validate build output
+ run: |
+ if [ ! -d "build" ]; then
+ echo "❌ Build directory not found!"
+ exit 1
+ fi
+ echo "✅ Build directory exists with $(find build -type f | wc -l) files"
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-${{ steps.set-env.outputs.environment }}
+ path: build/
+ retention-days: 1
+
+ # deploy-dev:
+ # name: Deploy to Development
+ # needs: build
+ # runs-on: ubuntu-latest
+ # if: (github.ref == 'refs/heads/dev' || (github.event_name == 'pull_request' && github.base_ref == 'dev')) && needs.build.outputs.environment == 'development'
+ # permissions:
+ # deployments: write
+ # pull-requests: write # Needed to comment on PRs
+ # steps:
+ # - name: Download build artifacts
+ # uses: actions/download-artifact@v4
+ # with:
+ # name: build-development
+ # path: build/
+
+ # - name: Deploy to Render Dev
+ # id: deploy
+ # uses: JorgeLNJunior/render-deploy@v1.4.5
+ # with:
+ # service_id: ${{ secrets.RENDER_SERVICE_ID_DEV }}
+ # api_key: ${{ secrets.RENDER_API_KEY }}
+ # wait_deploy: true
+ # github_deployment: true
+ # deployment_environment: ${{ secrets.RENDER_DEPLOY_ENVIRONMENT_DEV }}
+ # github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ # - name: Comment on PR
+ # if: github.event_name == 'pull_request'
+ # uses: actions/github-script@v7
+ # with:
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
+ # script: |
+ # const deployUrl = '${{ secrets.DEV_URL }}';
+ # const environment = 'Development';
+ # const commentIdentifier = ``;
+
+ # const comment = `${commentIdentifier}
+ # ### 🚀 Preview Deployment Ready!
+
+ # Your changes have been successfully deployed to the ${environment.toLowerCase()} environment.
+
+ # **Preview URL:** ${deployUrl}
+
+ # | Status | Environment | Commit | Time |
+ # |--------|-------------|--------|------|
+ # | ✅ Success | ${environment} | \`${context.sha.substring(0, 7)}\` | ${new Date().toISOString()} |
+
+ # ---
+ # 🤖 This comment was automatically generated by the deployment workflow.`;
+
+ # // Find existing comment
+ # const { data: comments } = await github.rest.issues.listComments({
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # issue_number: context.issue.number,
+ # });
+
+ # const botComment = comments.find(comment =>
+ # comment.body.includes(commentIdentifier)
+ # );
+
+ # if (botComment) {
+ # // Update existing comment
+ # await github.rest.issues.updateComment({
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # comment_id: botComment.id,
+ # body: comment
+ # });
+ # } else {
+ # // Create new comment
+ # await github.rest.issues.createComment({
+ # issue_number: context.issue.number,
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # body: comment
+ # });
+ # }
+
+ # deploy-prod:
+ # name: Deploy to Production
+ # needs: build
+ # runs-on: ubuntu-latest
+ # if: (github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main')) && needs.build.outputs.environment == 'production'
+ # permissions:
+ # deployments: write
+ # pull-requests: write # Needed to comment on PRs
+ # steps:
+ # - name: Download build artifacts
+ # uses: actions/download-artifact@v4
+ # with:
+ # name: build-production
+ # path: build/
+
+ # - name: Deploy to Render Prod
+ # id: deploy
+ # uses: JorgeLNJunior/render-deploy@v1.4.5
+ # with:
+ # service_id: ${{ secrets.RENDER_SERVICE_ID_PROD }}
+ # api_key: ${{ secrets.RENDER_API_KEY }}
+ # wait_deploy: true
+ # github_deployment: true
+ # deployment_environment: ${{ secrets.RENDER_DEPLOY_ENVIRONMENT_PROD }}
+ # github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ # - name: Comment on PR
+ # if: github.event_name == 'pull_request'
+ # uses: actions/github-script@v7
+ # with:
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
+ # script: |
+ # const deployUrl = '${{ secrets.PROD_URL }}';
+ # const environment = 'Production';
+ # const commentIdentifier = ``;
+
+ # const comment = `${commentIdentifier}
+ # ### 🚀 Production Preview Deployment Ready!
+
+ # Your changes have been successfully deployed to the ${environment.toLowerCase()} preview environment.
+
+ # **Preview URL:** ${deployUrl}
+
+ # | Status | Environment | Commit | Time |
+ # |--------|-------------|--------|------|
+ # | ✅ Success | ${environment} | \`${context.sha.substring(0, 7)}\` | ${new Date().toISOString()} |
+
+ # ⚠️ **Note:** This is a preview deployment for the production environment. The actual production deployment will occur after merge.
+
+ # ---
+ # 🤖 This comment was automatically generated by the deployment workflow.`;
+
+ # // Find existing comment
+ # const { data: comments } = await github.rest.issues.listComments({
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # issue_number: context.issue.number,
+ # });
+
+ # const botComment = comments.find(comment =>
+ # comment.body.includes(commentIdentifier)
+ # );
+
+ # if (botComment) {
+ # // Update existing comment
+ # await github.rest.issues.updateComment({
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # comment_id: botComment.id,
+ # body: comment
+ # });
+ # } else {
+ # // Create new comment
+ # await github.rest.issues.createComment({
+ # issue_number: context.issue.number,
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # body: comment
+ # });
+ # }
\ No newline at end of file
diff --git a/.github/workflows/reindex-algolia.yml b/.github/workflows/reindex-algolia.yml
index 6ca445aafc..430ab7b520 100644
--- a/.github/workflows/reindex-algolia.yml
+++ b/.github/workflows/reindex-algolia.yml
@@ -1,35 +1,152 @@
name: Reindex Algolia
on:
- # Manual trigger with inputs
+ # Manual trigger
workflow_dispatch:
+ inputs:
+ environment:
+ description: 'Environment to reindex'
+ required: true
+ default: 'production'
+ type: choice
+ options:
+ - production
+ - development
- # Automatic triggers
- # push:
- # branches:
- # - main
- # paths:
- # - 'docs/**'
- # - 'src/**'
- # - 'static/**'
- # - 'docusaurus.config.js'
- # - 'sidebars/**'
- # - 'package.json'
+ # Triggered after successful deployment workflow
+ workflow_run:
+ workflows: ["Deploy to Render"]
+ types:
+ - completed
+ branches:
+ - main
+
jobs:
reindex-algolia:
name: Reindex Algolia Search
runs-on: ubuntu-latest
- # Only run if push to main/master or manual trigger or scheduled
+ # Only run if the deployment workflow succeeded on main branch or manual trigger
if: |
github.event_name == 'workflow_dispatch' ||
- (github.event_name == 'push' && contains(fromJson('["main", "master"]'), github.ref_name)) ||
+ (github.event.workflow_run.conclusion == 'success' &&
+ github.event.workflow_run.head_branch == 'main')
+
steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set environment variables
+ id: set-env
+ run: |
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
+ if [[ "${{ inputs.environment }}" == "development" ]]; then
+ echo "SITE_URL=${{ secrets.DEV_URL }}" >> $GITHUB_ENV
+ echo "INDEX_NAME=Dev Docs" >> $GITHUB_ENV
+ echo "environment=development" >> $GITHUB_OUTPUT
+ else
+ echo "SITE_URL=${{ secrets.PROD_URL }}" >> $GITHUB_ENV
+ echo "INDEX_NAME=Production Docs" >> $GITHUB_ENV
+ echo "environment=production" >> $GITHUB_OUTPUT
+ fi
+ else
+ # Workflow run trigger - always production
+ echo "SITE_URL=${{ secrets.PROD_URL }}" >> $GITHUB_ENV
+ echo "INDEX_NAME=Production Docs" >> $GITHUB_ENV
+ echo "environment=production" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Wait for deployment to propagate
+ run: |
+ echo "🕐 Waiting 30 seconds for deployment to be fully available..."
+ sleep 30
+
+ - name: Validate site is accessible
+ run: |
+ echo "🔍 Checking if site is accessible at ${{ env.SITE_URL }}..."
+ response=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.SITE_URL }}")
+ if [ "$response" -eq 200 ]; then
+ echo "✅ Site is accessible (HTTP $response)"
+ else
+ echo "❌ Site returned HTTP $response"
+ exit 1
+ fi
+
- name: Algolia crawler creation and crawl
- uses: algolia/algoliasearch-crawler-github-actions@v1.0.10
+ uses: algolia/algoliasearch-crawler-github-actions@v1
id: algolia_crawler
- with: # mandatory parameters
+ with:
crawler-user-id: ${{ secrets.CRAWLER_USER_ID }}
crawler-api-key: ${{ secrets.CRAWLER_API_KEY }}
algolia-app-id: ${{ secrets.ALGOLIA_APP_ID }}
algolia-api-key: ${{ secrets.ALGOLIA_API_KEY }}
- site-url: 'https://product-docs-prod.onrender.com/'
\ No newline at end of file
+ site-url: ${{ env.SITE_URL }}
+
+ - name: Get workflow run details
+ if: github.event_name == 'workflow_run'
+ id: get-pr
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ // Get the workflow run details
+ const workflowRun = context.payload.workflow_run;
+
+ // Find associated pull requests
+ const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ commit_sha: workflowRun.head_sha
+ });
+
+ if (pullRequests.length > 0) {
+ const pr = pullRequests[0];
+ core.setOutput('pr_number', pr.number);
+ core.setOutput('pr_found', 'true');
+ } else {
+ core.setOutput('pr_found', 'false');
+ }
+
+ - name: Comment on PR about reindexing
+ if: |
+ github.event_name == 'workflow_run' &&
+ steps.get-pr.outputs.pr_found == 'true' &&
+ steps.algolia_crawler.outcome == 'success'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const prNumber = ${{ steps.get-pr.outputs.pr_number }};
+ const commentIdentifier = '';
+
+ const comment = `${commentIdentifier}
+ ### 🔍 Search Index Updated!
+
+ The Algolia search index has been successfully updated with your changes.
+
+ | Status | Index | Environment | Time |
+ |--------|-------|-------------|------|
+ | ✅ Success | ${{ env.INDEX_NAME }} | ${{ steps.set-env.outputs.environment }} | ${new Date().toISOString()} |
+
+ Your documentation changes are now searchable in production.
+
+ ---
+ 🤖 This comment was automatically generated after deployment and reindexing.`;
+
+ await github.rest.issues.createComment({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: comment
+ });
+
+ - name: Report crawler status
+ if: always()
+ run: |
+ if [ "${{ steps.algolia_crawler.outcome }}" == "success" ]; then
+ echo "✅ Algolia reindexing completed successfully for ${{ steps.set-env.outputs.environment }} environment"
+ echo "📍 Index: ${{ env.INDEX_NAME }}"
+ echo "🌐 Site URL: ${{ env.SITE_URL }}"
+ else
+ echo "❌ Algolia reindexing failed"
+ exit 1
+ fi
\ No newline at end of file
diff --git a/scripts/build-single.js b/scripts/build-single.js
index d4d92cd46a..2e66ef13e3 100755
--- a/scripts/build-single.js
+++ b/scripts/build-single.js
@@ -58,17 +58,30 @@ function getPluginId(product, version) {
// Function to check if plugin exists in docusaurus config
function validatePlugin(pluginId, product, version) {
- const configPath = path.join(__dirname, '..', 'docusaurus.config.js');
- const configContent = fs.readFileSync(configPath, 'utf8');
-
- // Check if plugin ID exists in config
- if (!configContent.includes(`id: '${pluginId}'`)) {
- console.error(`Plugin "${pluginId}" not found in docusaurus.config.js`);
- if (version) {
- console.error(`Product "${product}" version "${version}" is not configured.`);
- } else {
- console.error(`Product "${product}" is not configured.`);
+ // Import the products configuration to validate against actual product definitions
+ const productsPath = path.join(__dirname, '..', 'src', 'config', 'products.js');
+ const { PRODUCTS } = require(productsPath);
+
+ // Find the product
+ const productConfig = PRODUCTS.find((p) => p.id === product);
+ if (!productConfig) {
+ console.error(`Product "${product}" not found in products configuration.`);
+ console.error('Available products:', PRODUCTS.map((p) => p.id).join(', '));
+ process.exit(1);
+ }
+
+ // If version is provided, check if it exists
+ if (version) {
+ const versionExists = productConfig.versions.some((v) => v.version === version);
+ if (!versionExists) {
+ console.error(`Version "${version}" not found for product "${product}".`);
+ console.error(`Available versions: ${productConfig.versions.map((v) => v.version).join(', ')}`);
+ process.exit(1);
}
+ } else if (!versionlessProducts.includes(product)) {
+ // If no version provided for a versioned product, error
+ console.error(`Product "${product}" requires a version number.`);
+ console.error(`Available versions: ${productConfig.versions.map((v) => v.version).join(', ')}`);
process.exit(1);
}
}
@@ -77,28 +90,38 @@ function validatePlugin(pluginId, product, version) {
function createTempConfig(pluginId, product, version) {
const tempConfigPath = path.join(__dirname, '..', 'docusaurus.single.config.js');
+ // Import the products configuration to get the correct paths
+ const productsPath = path.join(__dirname, '..', 'src', 'config', 'products.js');
+ const { PRODUCTS } = require(productsPath);
+
+ // Find the product configuration
+ const productConfig = PRODUCTS.find((p) => p.id === product);
+
// Get the path and sidebar for this specific product
let docPath, routeBasePath, sidebarPath;
if (version) {
docPath = `docs/${product}/${version}`;
routeBasePath = '/'; // Serve at root for single product builds
+
+ // Find the version configuration to get the sidebar path
+ const versionConfig = productConfig.versions.find((v) => v.version === version);
+ sidebarPath = versionConfig.sidebarFile;
} else {
// Handle special cases
if (pluginId === 'identitymanager_saas') {
docPath = 'docs/identitymanager/saas';
routeBasePath = '/'; // Serve at root for single product builds
+ const versionConfig = productConfig.versions.find((v) => v.version === 'saas');
+ sidebarPath = versionConfig.sidebarFile;
} else {
docPath = `docs/${product}`;
routeBasePath = '/'; // Serve at root for single product builds
- }
- }
- // Determine sidebar path
- if (product === 'threatprevention' && version) {
- sidebarPath = `./sidebars/threatprevention-${version}-sidebar.js`;
- } else {
- sidebarPath = './sidebars/sidebar.js';
+ // For versionless products, get the sidebar from the 'current' version
+ const versionConfig = productConfig.versions.find((v) => v.version === 'current');
+ sidebarPath = versionConfig.sidebarFile;
+ }
}
// Create minimal config with just the single plugin