diff --git a/.github/workflows/assembler-preview.yml b/.github/workflows/assembler-preview.yml new file mode 100644 index 000000000..cc626e8da --- /dev/null +++ b/.github/workflows/assembler-preview.yml @@ -0,0 +1,154 @@ +name: assembler-preview + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + workflow_dispatch: + inputs: + pr_number: + description: 'Pull Request number to build the assembler preview for' + required: true + type: string + +permissions: + contents: read + deployments: write + id-token: write + pull-requests: read + +concurrency: + group: assembler-preview-${{ github.event.pull_request.number || inputs.pr_number }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + env: + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} + steps: + - name: Get PR details + if: github.event_name == 'workflow_dispatch' + id: pr-details + uses: actions/github-script@v8 + env: + PR_NUMBER: ${{ inputs.pr_number }} + with: + result-encoding: json + script: | + const { owner, repo } = context.repo; + const prNumber = parseInt(process.env.PR_NUMBER, 10); + + if (isNaN(prNumber) || prNumber <= 0) { + core.setFailed(`Invalid PR number: ${process.env.PR_NUMBER}`); + return; + } + + try { + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: prNumber, + }); + + return { + sha: pr.head.sha, + ref: pr.head.ref, + base_ref: pr.base.ref, + }; + } catch (error) { + core.setFailed(`Failed to get PR #${prNumber}: ${error.message}`); + } + + - name: Set PR SHA (workflow_dispatch) + id: pr-sha-dispatch + if: github.event_name == 'workflow_dispatch' + run: echo "sha=${{ fromJSON(steps.pr-details.outputs.result).sha }}" >> $GITHUB_OUTPUT + + - name: Set PR SHA (pull_request) + id: pr-sha-pr + if: github.event_name == 'pull_request' + run: echo "sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT + + - name: Resolve PR SHA + id: pr-sha + run: echo "sha=${{ steps.pr-sha-dispatch.outputs.sha || steps.pr-sha-pr.outputs.sha }}" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ steps.pr-sha.outputs.sha }} + persist-credentials: false + + - name: Create Deployment + uses: actions/github-script@v8 + id: deployment + env: + PR_SHA: ${{ steps.pr-sha.outputs.sha }} + with: + result-encoding: string + script: | + const { owner, repo } = context.repo; + const prNumber = process.env.PR_NUMBER; + const environment = 'assembler-preview'; + const task = `assembler-preview-${prNumber}`; + const deployment = await github.rest.repos.createDeployment({ + owner, + repo, + environment, + task, + ref: process.env.PR_SHA, + auto_merge: false, + transient_environment: true, + required_contexts: [], + }) + await github.rest.repos.createDeploymentStatus({ + deployment_id: deployment.data.id, + owner, + repo, + state: "in_progress", + log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + }) + return deployment.data.id + + - name: Bootstrap Action Workspace + uses: elastic/docs-builder/.github/actions/bootstrap@main + + - name: Build assembled documentation + id: assembler-build + env: + ASSEMBLER_PREVIEW_PATH_PREFIX: ${{ github.repository }}/docs/${{ env.PR_NUMBER }} + run: | + echo "ASSEMBLER_PREVIEW_PATH_PREFIX=${ASSEMBLER_PREVIEW_PATH_PREFIX}" >> $GITHUB_ENV + yq -i ".environments.preview.path_prefix = \"${ASSEMBLER_PREVIEW_PATH_PREFIX}\"" config/assembler.yml + dotnet run --project src/tooling/docs-builder -- assemble --skip-private-repositories --environment preview + + - uses: elastic/docs-builder/.github/actions/aws-auth@main + + - name: Upload assembled docs to S3 + id: s3-upload + env: + AWS_RETRY_MODE: standard + AWS_MAX_ATTEMPTS: 6 + run: | + aws s3 sync .artifacts/assembly/${ASSEMBLER_PREVIEW_PATH_PREFIX} "s3://elastic-docs-v3-website-preview/${ASSEMBLER_PREVIEW_PATH_PREFIX}" --delete --no-follow-symlinks + aws cloudfront create-invalidation \ + --distribution-id EKT7LT5PM8RKS \ + --paths "/${ASSEMBLER_PREVIEW_PATH_PREFIX}" "/${ASSEMBLER_PREVIEW_PATH_PREFIX}/*" + + - name: Update Deployment Status + if: always() && steps.deployment.outputs.result + uses: actions/github-script@v8 + with: + script: | + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.deployment.outputs.result }}, + state: "${{ steps.s3-upload.outcome == 'success' && 'success' || 'failure' }}", + environment_url: `https://docs-v3-preview.elastic.dev/${process.env.ASSEMBLER_PREVIEW_PATH_PREFIX}`, + log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + }) diff --git a/config/assembler.yml b/config/assembler.yml index 40b2c7311..5c9106742 100644 --- a/config/assembler.yml +++ b/config/assembler.yml @@ -36,6 +36,14 @@ environments: path_prefix: docs feature_flags: SEARCH_OR_ASK_AI: true + preview: + uri: https://docs-v3-preview.elastic.dev + path_prefix: ${ASSEMBLER_PREVIEW_PATH_PREFIX} + content_source: current + google_tag_manager: + enabled: false + feature_flags: + SEARCH_OR_ASK_AI: true shared_configuration: stack: &stack diff --git a/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs index 2ac6eab21..09e78ddad 100644 --- a/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs +++ b/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs @@ -48,7 +48,7 @@ public class LlmMarkdownExporter : IMarkdownExporter public async ValueTask FinishExportAsync(IDirectoryInfo outputFolder, Cancel ctx) { - var outputDirectory = Path.Combine(outputFolder.FullName, "docs"); + var outputDirectory = outputFolder.FullName; var zipPath = Path.Combine(outputDirectory, "llm.zip"); // Create the llms.txt file with boilerplate content diff --git a/src/services/Elastic.Documentation.Assembler/AssembleContext.cs b/src/services/Elastic.Documentation.Assembler/AssembleContext.cs index def15404a..c85c2953b 100644 --- a/src/services/Elastic.Documentation.Assembler/AssembleContext.cs +++ b/src/services/Elastic.Documentation.Assembler/AssembleContext.cs @@ -42,6 +42,12 @@ public class AssembleContext : IDocumentationConfigurationContext public IDirectoryInfo OutputDirectory { get; } + /// + /// The output directory with the path prefix applied. + /// This is where assembled content (sitemap.xml, llms.txt, link-index.snapshot.json, etc.) should be written. + /// + public IDirectoryInfo OutputWithPathPrefixDirectory { get; } + /// public IFileInfo ConfigurationPath { get; } @@ -83,5 +89,11 @@ public AssembleContext( CheckoutDirectory = ReadFileSystem.DirectoryInfo.New(checkoutDirectory ?? defaultCheckoutDirectory); var defaultOutputDirectory = Path.Combine(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"); OutputDirectory = ReadFileSystem.DirectoryInfo.New(output ?? defaultOutputDirectory); + + // Calculate the output directory with path prefix once + var pathPrefix = Environment.PathPrefix; + OutputWithPathPrefixDirectory = string.IsNullOrEmpty(pathPrefix) + ? OutputDirectory + : WriteFileSystem.DirectoryInfo.New(WriteFileSystem.Path.Combine(OutputDirectory.FullName, pathPrefix)); } } diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs index 5e72221a7..bd0a6e760 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs @@ -120,7 +120,7 @@ Cancel ctx if (exporters.Contains(Exporter.Html)) { - var sitemapBuilder = new SitemapBuilder(navigation.NavigationItems, assembleContext.WriteFileSystem, assembleContext.OutputDirectory); + var sitemapBuilder = new SitemapBuilder(navigation.NavigationItems, assembleContext.WriteFileSystem, assembleContext.OutputWithPathPrefixDirectory); sitemapBuilder.Generate(); } @@ -140,13 +140,13 @@ Cancel ctx private static async Task EnhanceLlmsTxtFile(AssembleContext context, SiteNavigation navigation, LlmsNavigationEnhancer enhancer, Cancel ctx) { - var llmsTxtPath = Path.Combine(context.OutputDirectory.FullName, "docs", "llms.txt"); + var pathPrefixedOutputFolder = context.OutputWithPathPrefixDirectory; + var llmsTxtPath = context.ReadFileSystem.Path.Combine(pathPrefixedOutputFolder.FullName, "llms.txt"); - var readFs = context.ReadFileSystem; - if (!readFs.File.Exists(llmsTxtPath)) + if (!context.ReadFileSystem.File.Exists(llmsTxtPath)) return; // No llms.txt file to enhance - var existingContent = await readFs.File.ReadAllTextAsync(llmsTxtPath, ctx); + var existingContent = await context.ReadFileSystem.File.ReadAllTextAsync(llmsTxtPath, ctx); // Assembler always uses the production URL as canonical base URL var canonicalBaseUrl = new Uri(context.Environment.Uri); var navigationSections = enhancer.GenerateNavigationSections(navigation, canonicalBaseUrl); @@ -154,7 +154,6 @@ private static async Task EnhanceLlmsTxtFile(AssembleContext context, SiteNaviga // Append the navigation sections to the existing boilerplate var enhancedContent = existingContent + Environment.NewLine + navigationSections; - var writeFs = context.WriteFileSystem; - await writeFs.File.WriteAllTextAsync(llmsTxtPath, enhancedContent, Encoding.UTF8, ctx); + await context.WriteFileSystem.File.WriteAllTextAsync(llmsTxtPath, enhancedContent, Encoding.UTF8, ctx); } } diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs index a6410ffbf..611ed801d 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs @@ -75,7 +75,7 @@ public async Task BuildAllAsync(PublishEnvironment environment, FrozenDictionary foreach (var exporter in markdownExporters) { _logger.LogInformation("Calling FinishExportAsync on {ExporterName}", exporter.GetType().Name); - _ = await exporter.FinishExportAsync(context.OutputDirectory, ctx); + _ = await exporter.FinishExportAsync(context.OutputWithPathPrefixDirectory, ctx); } if (exportOptions.Contains(Exporter.Redirects)) diff --git a/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs b/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs index 2059be9d9..2e9d355c8 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs @@ -16,7 +16,7 @@ namespace Elastic.Documentation.Assembler.Building; public class SitemapBuilder( IReadOnlyCollection navigationItems, IFileSystem fileSystem, - IDirectoryInfo outputFolder + IDirectoryInfo pathPrefixedOutputFolder ) { private static readonly Uri BaseUri = new("https://www.elastic.co"); @@ -54,7 +54,7 @@ public void Generate() doc.Add(root); - using var fileStream = fileSystem.File.Create(Path.Combine(outputFolder.ToString() ?? string.Empty, "docs", "sitemap.xml")); + using var fileStream = fileSystem.File.Create(fileSystem.Path.Combine(pathPrefixedOutputFolder.FullName, "sitemap.xml")); doc.Save(fileStream); } diff --git a/src/services/Elastic.Documentation.Assembler/Sourcing/RepositorySourcesFetcher.cs b/src/services/Elastic.Documentation.Assembler/Sourcing/RepositorySourcesFetcher.cs index 5f7fa220e..c291ed1b7 100644 --- a/src/services/Elastic.Documentation.Assembler/Sourcing/RepositorySourcesFetcher.cs +++ b/src/services/Elastic.Documentation.Assembler/Sourcing/RepositorySourcesFetcher.cs @@ -118,7 +118,7 @@ await context.WriteFileSystem.File.WriteAllTextAsync( } public async Task WriteLinkRegistrySnapshot(LinkRegistry linkRegistrySnapshot, Cancel ctx = default) => await context.WriteFileSystem.File.WriteAllTextAsync( - Path.Combine(context.OutputDirectory.FullName, "docs", CheckoutResult.LinkRegistrySnapshotFileName), + context.WriteFileSystem.Path.Combine(context.OutputWithPathPrefixDirectory.FullName, CheckoutResult.LinkRegistrySnapshotFileName), LinkRegistry.Serialize(linkRegistrySnapshot), ctx );