diff --git a/.github/workflows/assembler-preview.yml b/.github/workflows/assembler-preview.yml new file mode 100644 index 000000000..3cab1d97e --- /dev/null +++ b/.github/workflows/assembler-preview.yml @@ -0,0 +1,127 @@ +name: assembler-preview + +on: + 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-${{ inputs.pr_number }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + env: + PR_NUMBER: ${{ inputs.pr_number }} + ASSEMBLER_PREVIEW_PATH_PREFIX: ${{ github.repository }}/docs/${{ inputs.pr_number }} + steps: + - name: Get PR details + id: pr-details + uses: actions/github-script@v8 + 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: Checkout PR + uses: actions/checkout@v6 + with: + ref: ${{ fromJSON(steps.pr-details.outputs.result).sha }} + persist-credentials: false + + - name: Create Deployment + uses: actions/github-script@v8 + id: deployment + 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: '${{ fromJSON(steps.pr-details.outputs.result).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 + run: | + 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/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 837cec41c..a6476c6a9 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -63,14 +63,14 @@ on: type: boolean default: false required: false - + permissions: contents: read deployments: write id-token: write pull-requests: write - + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.ref }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull_request') }} @@ -79,7 +79,7 @@ jobs: match: if: github.event.repository.fork == false # Skip running the job on the fork itself (It still runs on PRs on the upstream from forks) runs-on: ubuntu-latest - permissions: + permissions: contents: none deployments: none pull-requests: none @@ -104,7 +104,7 @@ jobs: with: ref_name: ${{ steps.merge-group-base-branch.outputs.base_ref }} repository: ${{ github.repository }} - + - name: Match for PR events id: pr-check if: contains(fromJSON('["pull_request", "pull_request_target"]'), github.event_name) @@ -117,7 +117,7 @@ jobs: run: | echo "ref=${{ github.base_ref }}" echo "repo=${{ github.repository }}" - + - name: Match for push events id: push-check if: contains(fromJSON('["push"]'), github.event_name) @@ -130,7 +130,7 @@ jobs: run: | echo "ref=${{ github.ref_name }}" echo "repo=${{ github.repository }}" - + - name: Debug outputs run: | echo "content-source-match: ${{ format('{0}{1}{2}', steps.pr-check.outputs.content-source-match, steps.push-check.outputs.content-source-match, steps.merge-group-check.outputs.content-source-match) }}" @@ -141,7 +141,7 @@ jobs: check: runs-on: ubuntu-latest - needs: + needs: - match permissions: contents: read @@ -236,7 +236,7 @@ jobs: deployments: write id-token: write pull-requests: none - outputs: + outputs: deployment_result: ${{ steps.deployment.outputs.result }} path_prefix: ${{ steps.generate-path-prefix.outputs.result }} env: @@ -261,7 +261,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} persist-credentials: false - + - name: Create Deployment if: > env.MATCH == 'true' @@ -408,7 +408,7 @@ jobs: ) ) uses: elastic/docs-builder/actions/validate-inbound-local@main - + - name: 'Validate inbound links' if: > env.MATCH == 'true' @@ -444,7 +444,7 @@ jobs: ) ) uses: elastic/docs-builder/actions/validate-path-prefixes-local@main - + - name: 'Validate local path prefixes against those claimed by global navigation.yml' if: > env.MATCH == 'true' @@ -526,7 +526,7 @@ jobs: needs: - check - build - permissions: + permissions: contents: none deployments: none id-token: none @@ -619,7 +619,7 @@ jobs: const owner = context.repo.owner; const repo = context.repo.repo; - + const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: prNum }); @@ -656,7 +656,7 @@ jobs: ### 🤔 Need help? - Check out the [cumulative docs guidelines](https://www.elastic.co/docs/contribute-docs/how-to/cumulative-docs/) - Reach out in the [#docs](https://elastic.slack.com/archives/C0JF80CJZ) Slack channel`; - + await github.rest.issues.createComment({ owner, repo, issue_number: prNum, @@ -684,7 +684,7 @@ jobs: persist-credentials: false - name: Run Vale Linter uses: elastic/vale-rules/lint@main - with: + with: files: ${{ needs.check.outputs.all_changed_files }} - name: Post Vale Results uses: elastic/vale-rules/report@main 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/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs index 5e72221a7..e9e3524ce 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs @@ -120,7 +120,11 @@ Cancel ctx if (exporters.Contains(Exporter.Html)) { - var sitemapBuilder = new SitemapBuilder(navigation.NavigationItems, assembleContext.WriteFileSystem, assembleContext.OutputDirectory); + var pathPrefix = assembleContext.Environment.PathPrefix; + var outputWithPrefix = string.IsNullOrEmpty(pathPrefix) + ? assembleContext.OutputDirectory + : assembleContext.WriteFileSystem.DirectoryInfo.New(assembleContext.WriteFileSystem.Path.Combine(assembleContext.OutputDirectory.FullName, pathPrefix)); + var sitemapBuilder = new SitemapBuilder(navigation.NavigationItems, assembleContext.WriteFileSystem, outputWithPrefix); sitemapBuilder.Generate(); } @@ -140,9 +144,10 @@ 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 readFs = context.ReadFileSystem; + var pathPrefix = context.Environment.PathPrefix ?? "docs"; + var llmsTxtPath = readFs.Path.Combine(context.OutputDirectory.FullName, pathPrefix, "llms.txt"); + if (!readFs.File.Exists(llmsTxtPath)) return; // No llms.txt file to enhance diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs index a6410ffbf..de2f94d81 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuilder.cs @@ -75,7 +75,11 @@ 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); + var pathPrefix = context.Environment.PathPrefix; + var outputWithPrefix = string.IsNullOrEmpty(pathPrefix) + ? context.OutputDirectory + : context.ReadFileSystem.DirectoryInfo.New(Path.Join(context.OutputDirectory.FullName, pathPrefix)); + _ = await exporter.FinishExportAsync(outputWithPrefix, 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..a45a2ec51 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/SitemapBuilder.cs @@ -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(outputFolder.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..90bb4066b 100644 --- a/src/services/Elastic.Documentation.Assembler/Sourcing/RepositorySourcesFetcher.cs +++ b/src/services/Elastic.Documentation.Assembler/Sourcing/RepositorySourcesFetcher.cs @@ -117,11 +117,15 @@ 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), + public async Task WriteLinkRegistrySnapshot(LinkRegistry linkRegistrySnapshot, Cancel ctx = default) + { + var pathPrefix = context.Environment.PathPrefix ?? "docs"; + await context.WriteFileSystem.File.WriteAllTextAsync( + context.WriteFileSystem.Path.Combine(context.OutputDirectory.FullName, pathPrefix, CheckoutResult.LinkRegistrySnapshotFileName), LinkRegistry.Serialize(linkRegistrySnapshot), ctx ); + } }