Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 80 additions & 7 deletions .github/workflows/docs-preview-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: none
deployments: write
id-token: write
pull-requests: none
outputs:
Expand All @@ -216,10 +216,8 @@ jobs:
persist-credentials: false

- name: Create Deployment
# disabled: deployments are not enabled on this branch
if: >
false
&& env.MATCH == 'true'
env.MATCH == 'true'
&& needs.check.outputs.any_modified != 'false'
&& (
github.event_name == 'push'
Expand Down Expand Up @@ -363,10 +361,8 @@ jobs:

- name: Update deployment status
uses: actions/github-script@v8
# disabled: deployments are not enabled on this branch
if: >
false
&& env.MATCH == 'true'
env.MATCH == 'true'
&& always()
&& steps.deployment.outputs.result
env:
Expand All @@ -382,3 +378,80 @@ jobs:
environment_url: `https://docs-v3-preview.elastic.dev${process.env.LANDING_PAGE_PATH}`,
log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
})

comment:
if: >
github.event_name == 'pull_request'
&& needs.check.outputs.any_modified == 'true'
&& needs.build.outputs.path_prefix != ''
&& needs.build.result == 'success'
runs-on: ubuntu-latest
needs:
- check
- build
permissions:
contents: none
deployments: none
id-token: none
pull-requests: write
steps:
- name: Comment handbook preview links on PR
uses: actions/github-script@v8
env:
PATH_PREFIX: ${{ needs.build.outputs.path_prefix }}
ALL_CHANGED_FILES: ${{ needs.check.outputs.all_changed_files }}
with:
script: |
const title = '## Docs preview (local build)'
const base = `https://docs-v3-preview.elastic.dev${process.env.PATH_PREFIX}`
const changedMdFiles = process.env.ALL_CHANGED_FILES
.split(/\s+/)
.filter(Boolean)
.filter((i) => i.endsWith('.md'))
.filter((i) => !i.includes('/_snippets/'))

const lines = [title, '', `**Handbook preview:** ${base}/`, '']

if (changedMdFiles.length > 0) {
lines.push('Sample changed pages:')
for (const file of changedMdFiles.slice(0, 15)) {
const path = file
.replace(/^docs\//, '')
.replace(/\/index\.md$/, '')
.replace(/\.md$/, '')
lines.push(`- [${file}](${base}/${path})`)
}
if (changedMdFiles.length > 15) {
lines.push(`- …and ${changedMdFiles.length - 15} more`)
}
}

const body = lines.join('\n')
const owner = context.repo.owner
const repo = context.repo.repo
const issue_number = context.payload.pull_request.number

const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number,
})
const existing = comments.find(
(c) =>
c.user.type === 'Bot' && c.body.startsWith(title)
)
Comment on lines +434 to +442
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat .github/workflows/docs-preview-local.yml | wc -l

Repository: elastic/docs-builder

Length of output: 68


🏁 Script executed:

sed -n '420,460p' .github/workflows/docs-preview-local.yml

Repository: elastic/docs-builder

Length of output: 1297


🏁 Script executed:

# Search for context around the comment upsert logic
sed -n '400,480p' .github/workflows/docs-preview-local.yml

Repository: elastic/docs-builder

Length of output: 2169


🌐 Web query:

GitHub Actions @actions/github paginate listComments API documentation

💡 Result:

@actions/github “paginate” for listComments: use Octokit pagination (Link-header driven) on the REST endpoint that lists issue comments (issues/{issue_number}/comments). The REST endpoint supports pagination via the page and per_page query parameters (max 100) and also supports since filtering; GitHub paginates results using the response Link header when there are more results. [1][2][3] In GitHub Actions, the @actions/github package provides a hydrated Octokit client (github.getOctokit / octokit.rest.*). [4] Use octokit.paginate with the list-comments endpoint so it automatically fetches all pages until the last page, returning a single array of items. [2][3] Example (actions/github-script / Node JS): const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: 123, per_page: 100, }); This pattern is also used in the official actions/github-script discussion as the fix when listComments exceeds the single-page default of 30. [5] Key REST details for listComments / issue comments: - Endpoint: GET /repos/{owner}/{repo}/issues/{issue_number}/comments [1] - Pagination query parameters: page (default 1), per_page (default 30, max 100) [1] Octokit pagination mechanics: - GitHub Docs’ pagination guide states that Octokit’s paginate fetches next pages until the last page and returns all items as a single array. [3]

Citations:


🌐 Web query:

@actions/github github.paginate method documentation

💡 Result:

In GitHub Actions, @actions/github exposes an authenticated Octokit client and supports pagination via octokit.paginate (and related iterator helpers) when using the underlying Octokit REST pagination plugin. What documentation exists for github.paginate / octokit.paginate 1) Official GitHub REST API pagination docs (Octokit integration) GitHub documents that for paginated REST API results you can use octokit.paginate, which fetches the next page until the last page and returns all items as a single array; it also supports a map function and octokit.paginate.iterator to iterate page-by-page. [1][2] 2) Authoritative Octokit plugin documentation: @octokit/plugin-paginate-rest The plugin-paginate-rest package documentation states that it adds octokit.paginate which accepts the same parameters as octokit.request; it wraps octokit.request and continues requesting while there is a rel="next" link in the response Link header. It also notes only “List...” endpoints are supported for pagination and that non-list endpoints behave like octokit.request. [3] What the method does (high-confidence behavior) - octokit.paginate wraps octokit.request and keeps requesting the “next” URL as long as the response Link header contains rel="next". [3] - It returns all fetched items as a single array (even when the underlying raw REST result was an object, octokit.paginate returns an array of items). [1][2] - You can supply a map function to process each page’s results and optionally end early by calling done. [1] - Alternatively, octokit.paginate.iterator lets you iterate one page/response at a time, more memory-efficient than collecting everything. [1] How this maps to @actions/github - @actions/github is a hydrated Octokit client returned by getOctokit(token). [4][5] - As part of the standard usage, the client retains pagination APIs (github.paginate) consistent with Octokit.js (used in github-script examples). For example, actions/github-script shows calling await github.paginate(opts) to get all pages of a REST list endpoint. [6] Practical example pattern (from official docs) - For REST: use octokit.paginate with the REST “list” endpoint route/params, and optionally map. GitHub’s docs explicitly recommend octokit.paginate for fetching paginated results via Octokit.js. [1] If you meant pagination for GraphQL - There is a separate plugin for GraphQL pagination: octokit.graphql.paginate, which paginates by scanning for pageInfo and using endCursor when hasNextPage is true. [7] Most relevant references to read - GitHub Docs: “Using pagination in the REST API” (Octokit.paginate + iterator + map function). [1][2] - Octokit plugin: @octokit/plugin-paginate-rest.js (how octokit.paginate is implemented via Link header rel="next"). [3] - @actions/github package overview (shows it returns an authenticated Octokit client and is how Actions code gets an Octokit instance). [4][5]

Citations:


Fetch all comment pages to avoid duplicates and strict bot identity match.

This only fetches the first page (~30 comments) and matches any bot. On busy PRs, it misses the existing comment, creating duplicates. Use github.paginate() to fetch all pages and match specifically against github-actions[bot] to avoid accidentally updating other bots' comments.

Proposed fix
-            const { data: comments } = await github.rest.issues.listComments({
-              owner,
-              repo,
-              issue_number,
-            })
+            const comments = await github.paginate(
+              github.rest.issues.listComments,
+              { owner, repo, issue_number, per_page: 100 }
+            )
             const existing = comments.find(
               (c) =>
-                c.user.type === 'Bot' && c.body.startsWith(title)
+                c.user?.login === 'github-actions[bot]' &&
+                c.body?.startsWith(title)
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs-preview-local.yml around lines 434 - 442, Replace the
single-page listComments call with a full pagination call and tighten the bot
identity check: use github.paginate(github.rest.issues.listComments, { owner,
repo, issue_number }) to retrieve all comment pages into comments, then find
existing by matching c.user.login === 'github-actions[bot]' &&
c.body.startsWith(title) (instead of c.user.type) so you won't miss existing
comments on busy PRs or accidentally match other bots.

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
})
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,24 @@
using Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry;
using Elastic.Documentation.Api.IntegrationTests.Fixtures;
using FakeItEasy;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Elastic.Documentation.Api.IntegrationTests;

public class OtlpProxyIntegrationTests : IAsyncLifetime
public class OtlpProxyIntegrationTests
{
private const string OtlpEndpoint = "http://localhost:4318";

public ValueTask InitializeAsync()
{
Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpEndpoint);
return ValueTask.CompletedTask;
}

public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT", null);
return ValueTask.CompletedTask;
}
/// <summary>
/// Ensure OTLP routes are mapped by wiring the endpoint into host configuration (same key Program.cs reads).
/// Relying only on IAsyncLifetime + env vars is flaky under parallel test execution on CI.
/// </summary>
private static WebApplicationFactory<Program> WithOtlpConfigured(WebApplicationFactory<Program> factory) =>
factory.WithWebHostBuilder(builder =>
builder.UseSetting("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpEndpoint));

[Fact]
public async Task OtlpProxyTracesEndpointForwardsToCorrectUrl()
Expand All @@ -49,12 +46,12 @@ public async Task OtlpProxyTracesEndpointForwardsToCorrectUrl()
.Invokes((HttpRequestMessage req, CancellationToken ct) => capturedRequest = req)
.Returns(Task.FromResult(mockResponse));

using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
using var factory = WithOtlpConfigured(ApiWebApplicationFactory.WithMockedServices(services =>
{
// Replace the named HttpClient with our mock
_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
.ConfigurePrimaryHttpMessageHandler(() => mockHandler);
});
}));

var client = factory.CreateClient();
var otlpPayload = /*lang=json,strict*/ """
Expand Down Expand Up @@ -114,11 +111,11 @@ public async Task OtlpProxyLogsEndpointForwardsToCorrectUrl()
.Invokes((HttpRequestMessage req, CancellationToken ct) => capturedRequest = req)
.Returns(Task.FromResult(mockResponse));

using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
using var factory = WithOtlpConfigured(ApiWebApplicationFactory.WithMockedServices(services =>
{
_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
.ConfigurePrimaryHttpMessageHandler(() => mockHandler);
});
}));

var client = factory.CreateClient();
var otlpPayload = /*lang=json,strict*/ """
Expand Down Expand Up @@ -171,11 +168,11 @@ public async Task OtlpProxyMetricsEndpointForwardsToCorrectUrl()
.Invokes((HttpRequestMessage req, CancellationToken ct) => capturedRequest = req)
.Returns(Task.FromResult(mockResponse));

using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
using var factory = WithOtlpConfigured(ApiWebApplicationFactory.WithMockedServices(services =>
{
_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
.ConfigurePrimaryHttpMessageHandler(() => mockHandler);
});
}));

var client = factory.CreateClient();
var otlpPayload = /*lang=json,strict*/ """
Expand Down Expand Up @@ -222,14 +219,14 @@ public async Task OtlpProxyReturnsCollectorErrorStatusCode()
.WithReturnType<Task<HttpResponseMessage>>()
.Returns(Task.FromResult(mockResponse));

using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
using var factory = WithOtlpConfigured(ApiWebApplicationFactory.WithMockedServices(services =>
{
#pragma warning disable EXTEXP0001 // Experimental API - needed for test to bypass resilience handlers
_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
.ConfigurePrimaryHttpMessageHandler(() => mockHandler)
.RemoveAllResilienceHandlers();
#pragma warning restore EXTEXP0001
});
}));

var client = factory.CreateClient();
using var content = new StringContent("{}", Encoding.UTF8, "application/json");
Expand All @@ -250,7 +247,7 @@ public async Task OtlpProxyReturnsCollectorErrorStatusCode()
public async Task OtlpProxyInvalidSignalTypeReturns404()
{
// Arrange
using var factory = new ApiWebApplicationFactory();
using var factory = WithOtlpConfigured(new ApiWebApplicationFactory());
using var client = factory.CreateClient();
using var content = new StringContent("{}", Encoding.UTF8, "application/json");

Expand Down
Loading