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