diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json
index 82e4910..91373f4 100644
--- a/.github/aw/actions-lock.json
+++ b/.github/aw/actions-lock.json
@@ -10,6 +10,11 @@
"version": "v5.0.5",
"sha": "27d5ce7f107fe9357f9df03efb73ab90386fccae"
},
+ "actions/checkout@v6": {
+ "repo": "actions/checkout",
+ "version": "v6",
+ "sha": "df4cb1c069e1874edd31b4311f1884172cec0e10"
+ },
"actions/checkout@v6.0.2": {
"repo": "actions/checkout",
"version": "v6.0.2",
diff --git a/.github/workflows/changelog-submit.yml b/.github/workflows/changelog-submit.yml
index 0dc03b2..9e9a4b4 100644
--- a/.github/workflows/changelog-submit.yml
+++ b/.github/workflows/changelog-submit.yml
@@ -33,38 +33,6 @@ jobs:
should-submit: ${{ steps.evaluate.outputs.should-submit }}
is-org-member: ${{ steps.check-org-membership.outputs.is-member }}
steps:
- - name: Resolve PR author
- id: pr-author
- if: github.event.workflow_run.head_repository.full_name != github.repository
- uses: actions/github-script@v9
- with:
- # language=js
- script: |
- const run = context.payload.workflow_run;
- const { owner, repo } = context.repo;
-
- let prNumber;
- if (run.pull_requests?.length > 0) {
- prNumber = run.pull_requests[0].number;
- } else {
- const headLabel = `${run.head_repository.owner.login}:${run.head_branch}`;
- const { data: prs } = await github.rest.pulls.list({
- owner, repo, state: 'open', head: headLabel
- });
- const match = prs.find(pr => pr.head.sha === run.head_sha);
- if (match) prNumber = match.number;
- }
-
- if (!prNumber) {
- core.setFailed('Could not resolve PR number for fork — cannot verify org membership. Failing closed.');
- return;
- }
-
- const { data: pr } = await github.rest.pulls.get({
- owner, repo, pull_number: prNumber
- });
- core.setOutput('login', pr.user.login);
-
- name: Fetch ephemeral GitHub token
if: github.event.workflow_run.head_repository.full_name != github.repository
id: fetch-ephemeral-token
@@ -78,7 +46,6 @@ jobs:
if: github.event.workflow_run.head_repository.full_name != github.repository
uses: elastic/docs-actions/github/is-elastic-org-member@v1
with:
- username: ${{ steps.pr-author.outputs.login }}
token: ${{ steps.fetch-ephemeral-token.outputs.token }}
- name: Evaluate
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index 2690e17..91fa2c4 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -176,6 +176,7 @@ jobs:
core.setOutput('base-ref', pr.base.ref);
core.setOutput('pr-author', pr.user.login);
+
// --- Changed-files check (docs-relevant files only) ---
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: prNumber
@@ -259,7 +260,6 @@ jobs:
if: steps.context.outputs.is-fork == 'true' && steps.context.outputs.event == 'pull_request'
uses: elastic/docs-actions/github/is-elastic-org-member@v1
with:
- username: ${{ steps.context.outputs.pr-author }}
token: ${{ steps.fetch-ephemeral-token.outputs.token }}
- name: Evaluate
@@ -375,6 +375,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
+ packages: read
outputs:
build_outcome: ${{ steps.docs-build.outcome == 'success' && 'success' || '' }}
skip: ${{ steps.docs-build.outputs.skip }}
@@ -437,11 +438,30 @@ jobs:
echo "PATH_PREFIX=${path_prefix}" >> "$GITHUB_ENV"
echo "result=${path_prefix}" >> "$GITHUB_OUTPUT"
+ # Resolve the mutable :edge tag to an immutable RepoDigest before
+ # running the container.
+ - name: Pull and pin docs-builder image
+ id: docker-image
+ # language=bash
+ run: |
+ IMAGE="ghcr.io/elastic/docs-builder:edge"
+ docker pull "$IMAGE"
+ DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
+ if [ -z "$DIGEST" ]; then
+ echo "::error::Failed to resolve RepoDigest for ${IMAGE}"
+ exit 1
+ fi
+ echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
+ echo "::notice title=docs-builder image digest::${DIGEST}"
+
# Run docs-builder in Docker isolation. Only explicitly listed env vars are
# passed to the container — ACTIONS_RUNTIME_TOKEN, ACTIONS_CACHE_URL, and
# OIDC env vars are excluded to prevent cache poisoning and credential
# theft if the build tool is compromised via malicious content.
#
+ # The image is referenced by digest (resolved above) so the run is
+ # immutable for this workflow execution.
+ #
# Future: add --network none once docs-builder has an init command to
# preload the link index before the build.
- name: Build documentation
@@ -465,7 +485,7 @@ jobs:
-e GITHUB_REF="refs/heads/${HEAD_BRANCH}" \
-e INPUT_PREFIX="${PATH_PREFIX}" \
-e INPUT_STRICT="${STRICT_FLAG}" \
- ghcr.io/elastic/docs-builder:edge || EXIT_CODE=$?
+ "${IMAGE_DIGEST}" || EXIT_CODE=$?
if [ -s "$CONTAINER_OUTPUT" ]; then
cat "$CONTAINER_OUTPUT" >> "$GITHUB_OUTPUT"
@@ -474,6 +494,7 @@ jobs:
exit $EXIT_CODE
env:
STRICT_FLAG: ${{ fromJSON(inputs.strict != '' && inputs.strict || 'true') }}
+ IMAGE_DIGEST: ${{ steps.docker-image.outputs.digest }}
- name: Upload links artifact
id: upload-links
diff --git a/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml b/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml
index 899750b..48d27c9 100644
--- a/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-applies-to-sweep.lock.yml
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"0fed7cba1098883ebd07c37ad7926ab3adc379e9f9577fcbf70a99c024c5a5a5","compiler_version":"v0.75.0","agent_id":"copilot"}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -43,7 +43,7 @@
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -628,7 +628,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout source docs repo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 30
persist-credentials: false
diff --git a/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml b/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml
index 4c257ec..e3ae6ed 100644
--- a/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-coherence-sweep.lock.yml
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"da184731259d4126ad359ba043c1c4730c4ef2437f3364fe0336575e32c82368","compiler_version":"v0.75.0","agent_id":"copilot"}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -44,7 +44,7 @@
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -643,7 +643,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout source docs repo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 30
persist-credentials: false
diff --git a/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml b/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml
index 125f908..748c5d1 100644
--- a/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-frontmatter-sweep.lock.yml
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"0092af990d55dc70c7e1d94ccbd243fefa27c2f8e2be5042f7659897b24b6c31","compiler_version":"v0.75.0","agent_id":"copilot"}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -44,7 +44,7 @@
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -631,7 +631,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout source docs repo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 30
persist-credentials: false
diff --git a/.github/workflows/gh-aw-docs-openings-sweep.lock.yml b/.github/workflows/gh-aw-docs-openings-sweep.lock.yml
index 2499bb2..cf9a7de 100644
--- a/.github/workflows/gh-aw-docs-openings-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-openings-sweep.lock.yml
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6f92b7dd643845ebdd84409b45c39f5eba3493453cabd41e9fe06738576586d8","compiler_version":"v0.75.0","agent_id":"copilot"}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -43,7 +43,7 @@
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -639,7 +639,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout source docs repo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 30
persist-credentials: false
diff --git a/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml b/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml
index 1ad4cc3..0777e4f 100644
--- a/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-staleness-sweep.lock.yml
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"83730e892c4686ac69269d501b9fd18336ef3252ddd9da9245805b0dfcf2b865","compiler_version":"v0.75.0","agent_id":"copilot"}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -44,7 +44,7 @@
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -650,7 +650,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout source docs repo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
persist-credentials: false
diff --git a/.github/workflows/gh-aw-docs-style-sweep.lock.yml b/.github/workflows/gh-aw-docs-style-sweep.lock.yml
index ce2c2bc..a1c3ef6 100644
--- a/.github/workflows/gh-aw-docs-style-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-style-sweep.lock.yml
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"3a87b6e135615b0fde1701690ded47f089dd65413730cf7dc8118c04758314d0","compiler_version":"v0.75.0","agent_id":"copilot"}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -43,7 +43,7 @@
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -631,7 +631,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout source docs repo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 30
persist-credentials: false
diff --git a/.github/workflows/gh-aw-docs-typos-sweep.lock.yml b/.github/workflows/gh-aw-docs-typos-sweep.lock.yml
index c79391c..5d23879 100644
--- a/.github/workflows/gh-aw-docs-typos-sweep.lock.yml
+++ b/.github/workflows/gh-aw-docs-typos-sweep.lock.yml
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"3be42550899dd610111872c641e30947ba676b126cb692bf2bdb7d8e0ce5cf12","compiler_version":"v0.75.0","agent_id":"copilot"}
-# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"f889c9c3c06adeaabccefc06e29c42733ee05dff","version":"v0.75.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -42,7 +42,7 @@
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -593,7 +593,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout source docs repo
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (source v6)
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 1
persist-credentials: false
diff --git a/Makefile b/Makefile
index 083245f..1081d6d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
.PHONY: help setup compile lint
-GH_AW_VERSION ?= v0.71.5
+GH_AW_VERSION ?= v0.75.0
help:
@echo "docs-actions — Elastic documentation GitHub Actions and agentic workflows"
@@ -13,7 +13,7 @@ help:
@echo "Workflow sources live in .github/workflows/gh-aw-*.md. Edit those, then run 'make compile'."
setup:
- @if gh extension list | rg -q '^gh aw\s+github/gh-aw\s+$(GH_AW_VERSION)$$'; then \
+ @if [ "$$(gh extension list | awk '$$1 == "gh" && $$2 == "aw" { print $$4 }')" = "$(GH_AW_VERSION)" ]; then \
echo "gh-aw $(GH_AW_VERSION) already installed"; \
else \
echo "Installing gh-aw extension $(GH_AW_VERSION)..."; \
diff --git a/changelog/submit/apply/scripts/comment-helper.js b/changelog/submit/apply/scripts/comment-helper.js
index 05aa77f..a9b367f 100644
--- a/changelog/submit/apply/scripts/comment-helper.js
+++ b/changelog/submit/apply/scripts/comment-helper.js
@@ -1,6 +1,51 @@
+// Trust boundary:
+// All env-var inputs that flow into the comment body — CHANGELOG_FILE,
+// CHANGELOG_DIR, HEAD_REF, LABEL_TABLE, PRODUCT_LABEL_TABLE, SKIP_LABELS,
+// CONFIG_FILE, and the staged YAML content — originate from PR metadata or
+// repo configuration that an attacker can influence. Use the helpers below
+// when interpolating any of those values into a Markdown comment:
+// - escapeMarkdown() for inline text. Escapes Markdown punctuation *and*
+// HTML-significant characters (<, >, &) so a hostile value cannot
+// introduce raw HTML.
+// - wrapCodeFence() for multi-line content embedded as a code block.
+// Picks a backtick fence longer than any sequence in the content so a
+// stray ``` cannot break out of the block.
const TITLE = '### 📋 Changelog';
-const escapeMarkdown = (s) => s.replace(/([[\]()\\`*_{}#+\-.!|])/g, '\\$1');
+// Escapes Markdown punctuation and HTML-significant characters. Sticking
+// to ASCII printable range; the OutputSanitizer in docs-builder already
+// strips C0/DEL controls before these values reach the runner.
+const escapeMarkdown = (s) =>
+ String(s ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/([[\]()\\`*_{}#+\-.!|])/g, '\\$1');
+
+// Returns content wrapped in a backtick fence whose length is one greater
+// than the longest run of backticks already present in `content`. Prevents
+// the embedded content from closing the outer fence prematurely.
+const wrapCodeFence = (content, language = '') => {
+ const matches = String(content ?? '').match(/`+/g) ?? [];
+ const longest = matches.reduce((max, run) => Math.max(max, run.length), 0);
+ const fence = '`'.repeat(Math.max(3, longest + 1));
+ return `${fence}${language}\n${content}\n${fence}`;
+};
+
+// Returns the text wrapped as inline code using a backtick run longer than
+// any run inside the text. Prefer this over `escapeMarkdown(s)` wrapped in
+// single backticks when `s` may itself contain backticks (e.g., user-
+// supplied label or path values).
+const wrapInlineCode = (s) => {
+ const text = String(s ?? '');
+ const matches = text.match(/`+/g) ?? [];
+ const longest = matches.reduce((max, run) => Math.max(max, run.length), 0);
+ const tick = '`'.repeat(longest + 1);
+ // CommonMark: pad with a single space if the content starts or ends with
+ // a backtick, so the boundary backticks aren't absorbed into the run.
+ const padded = (text.startsWith('`') || text.endsWith('`')) ? ` ${text} ` : text;
+ return `${tick}${padded}${tick}`;
+};
async function upsertComment({ github, context, prNumber, body }) {
const { owner, repo } = context.repo;
@@ -17,4 +62,4 @@ async function upsertComment({ github, context, prNumber, body }) {
}
}
-module.exports = { TITLE, upsertComment, escapeMarkdown };
+module.exports = { TITLE, upsertComment, escapeMarkdown, wrapCodeFence, wrapInlineCode };
diff --git a/changelog/submit/apply/scripts/post-comment-only.js b/changelog/submit/apply/scripts/post-comment-only.js
index 0f38fb8..3486178 100644
--- a/changelog/submit/apply/scripts/post-comment-only.js
+++ b/changelog/submit/apply/scripts/post-comment-only.js
@@ -1,5 +1,5 @@
const fs = require('fs');
-const { TITLE, upsertComment, escapeMarkdown } = require('./comment-helper');
+const { TITLE, upsertComment, escapeMarkdown, wrapCodeFence } = require('./comment-helper');
module.exports = async ({ github, context, core }) => {
const prNumber = parseInt(process.env.PR_NUMBER, 10);
@@ -16,9 +16,10 @@ module.exports = async ({ github, context, core }) => {
bodyParts.push(
`Generated changelog entry for \`${escapeMarkdown(changelogDir + '/' + files[0])}\`:`,
'',
- '```yaml',
- content,
- '```',
+ // wrapCodeFence picks a backtick run longer than any in `content`, so
+ // attacker-supplied YAML cannot close the fence early and inject
+ // arbitrary Markdown after the block.
+ wrapCodeFence(content, 'yaml'),
'',
'This comment is informational — editing it does not change what gets uploaded. On merge, the entry is regenerated from the live PR record (title, labels) and uploaded to S3. To change the preview, edit the PR title or labels and let the changelog workflow re-run.',
);
diff --git a/changelog/submit/apply/scripts/post-failure-comment.js b/changelog/submit/apply/scripts/post-failure-comment.js
index 41ee66f..b6eb2e1 100644
--- a/changelog/submit/apply/scripts/post-failure-comment.js
+++ b/changelog/submit/apply/scripts/post-failure-comment.js
@@ -1,4 +1,4 @@
-const { TITLE, upsertComment } = require('./comment-helper');
+const { TITLE, upsertComment, wrapInlineCode } = require('./comment-helper');
module.exports = async ({ github, context, core }) => {
const prNumber = parseInt(process.env.PR_NUMBER, 10);
@@ -7,6 +7,8 @@ module.exports = async ({ github, context, core }) => {
const productLabelRows = (process.env.PRODUCT_LABEL_TABLE || '').trim();
const skipLabels = process.env.SKIP_LABELS || '';
+ const configFileCode = wrapInlineCode(configFile);
+
const hasTypeIssue = !!labelRows;
const hasProductIssue = !!productLabelRows;
@@ -24,7 +26,7 @@ module.exports = async ({ github, context, core }) => {
if (hasTypeIssue) {
sections.push(['', '🔖 Add one of these **type** labels to your PR:', '', labelRows].join('\n'));
} else if (!hasProductIssue) {
- sections.push(`\nAdd a type label that matches your \`pivot.types\` configuration in \`${configFile}\`.`);
+ sections.push(`\nAdd a type label that matches your ${wrapInlineCode('pivot.types')} configuration in ${configFileCode}.`);
}
if (hasProductIssue) {
@@ -33,10 +35,10 @@ module.exports = async ({ github, context, core }) => {
let skipSection;
if (skipLabels.trim()) {
- const formatted = skipLabels.split(',').map(l => `\`${l.trim()}\``).join(', ');
+ const formatted = skipLabels.split(',').map(l => wrapInlineCode(l.trim())).join(', ');
skipSection = `\n⏭️ To skip changelog generation, add one of these labels: ${formatted}`;
} else {
- skipSection = `\n⏭️ No skip labels are configured. To allow skipping changelog generation, add a label to \`rules.create.exclude\` in \`${configFile}\`.`;
+ skipSection = `\n⏭️ No skip labels are configured. To allow skipping changelog generation, add a label to ${wrapInlineCode('rules.create.exclude')} in ${configFileCode}.`;
}
const body = [
@@ -46,7 +48,7 @@ module.exports = async ({ github, context, core }) => {
...sections,
skipSection,
'',
- `📄 See \`${configFile}\` for the full changelog configuration.`,
+ `📄 See ${configFileCode} for the full changelog configuration.`,
].join('\n');
await upsertComment({ github, context, prNumber, body });
diff --git a/changelog/submit/apply/scripts/post-success-comment.js b/changelog/submit/apply/scripts/post-success-comment.js
index 232742a..07a9b35 100644
--- a/changelog/submit/apply/scripts/post-success-comment.js
+++ b/changelog/submit/apply/scripts/post-success-comment.js
@@ -1,5 +1,10 @@
-const { TITLE, upsertComment, escapeMarkdown } = require('./comment-helper');
+const { TITLE, upsertComment, wrapInlineCode } = require('./comment-helper');
+// changelogFile / branch are validated upstream by ref-name regex
+// (`^[a-zA-Z0-9._/+-]+$`) plus OutputSanitizer in docs-builder, so they
+// are constrained to a small alphabet. wrapInlineCode is still used for
+// the visible filename so a stray backtick (or future loosening of the
+// upstream regex) cannot break out of the inline code span.
module.exports = async ({ github, context, core }) => {
const prNumber = parseInt(process.env.PR_NUMBER, 10);
const branch = process.env.HEAD_REF;
@@ -13,7 +18,7 @@ module.exports = async ({ github, context, core }) => {
const body = [
TITLE,
'',
- `📝 Changelog entry committed: [\`${escapeMarkdown(changelogFile)}\`](${viewUrl})`,
+ `📝 Changelog entry committed: [${wrapInlineCode(changelogFile)}](${viewUrl})`,
'',
`✏️ [Edit this changelog](${editUrl})`,
].join('\n');
diff --git a/changelog/submit/evaluate/action.yml b/changelog/submit/evaluate/action.yml
index 2bd8dde..0bade61 100644
--- a/changelog/submit/evaluate/action.yml
+++ b/changelog/submit/evaluate/action.yml
@@ -162,7 +162,7 @@ runs:
REPO_NAME: ${{ github.event.repository.name }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
PR_TITLE: ${{ steps.pr-data.outputs.title }}
- PR_BODY: ${{ steps.pr-data.outputs.body }}
+ PR_BODY_FILE: ${{ steps.pr-data.outputs.body-file }}
PR_LABELS: ${{ steps.pr-data.outputs.labels }}
HEAD_REF: ${{ steps.pr-data.outputs.head-ref }}
HEAD_SHA: ${{ steps.pr-data.outputs.head-sha }}
diff --git a/changelog/submit/evaluate/scripts/fetch-pr-data.js b/changelog/submit/evaluate/scripts/fetch-pr-data.js
index f5dab1f..973e2bc 100644
--- a/changelog/submit/evaluate/scripts/fetch-pr-data.js
+++ b/changelog/submit/evaluate/scripts/fetch-pr-data.js
@@ -1,3 +1,15 @@
+const fs = require('fs');
+const path = require('path');
+
+const TITLE_MAX_LEN = 200;
+const BODY_FILE_MAX_BYTES = 64 * 1024;
+
+const sanitizeInline = (value, maxLen) =>
+ (value || '')
+ .replace(/\u0000/g, '')
+ .replace(/\r/g, '')
+ .slice(0, maxLen);
+
module.exports = async ({ github, context, core }) => {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
@@ -8,9 +20,38 @@ module.exports = async ({ github, context, core }) => {
core.info(`PR #${pr.number} is ${pr.state} — skipping`);
return;
}
- core.setOutput('title', pr.title);
- core.setOutput('body', pr.body || '');
- core.setOutput('labels', pr.labels.map(l => l.name).join(','));
+
+ const labelNames = pr.labels.map(l => l.name);
+ const offendingLabel = labelNames.find(name => name.includes(','));
+ if (offendingLabel) {
+ core.setFailed(
+ `Label name contains ',' which would corrupt comma-joined parsing: ${JSON.stringify(offendingLabel)}`
+ );
+ return;
+ }
+
+ // Stage the body in a file rather than passing it inline.
+ const runnerTemp = process.env.RUNNER_TEMP;
+ if (!runnerTemp) {
+ core.setFailed('RUNNER_TEMP is not set; cannot stage PR body file');
+ return;
+ }
+ const bodyFile = path.join(runnerTemp, 'changelog-pr-body.md');
+ const rawBody = (pr.body || '').replace(/\u0000/g, '');
+ const bodyBytes = Buffer.from(rawBody, 'utf8');
+ const cappedBody = bodyBytes.length > BODY_FILE_MAX_BYTES
+ ? bodyBytes.subarray(0, BODY_FILE_MAX_BYTES).toString('utf8')
+ : rawBody;
+ if (bodyBytes.length > BODY_FILE_MAX_BYTES) {
+ core.warning(
+ `PR body exceeds ${BODY_FILE_MAX_BYTES} bytes (${bodyBytes.length}); truncating.`
+ );
+ }
+ fs.writeFileSync(bodyFile, cappedBody, { encoding: 'utf8', mode: 0o600 });
+
+ core.setOutput('title', sanitizeInline(pr.title, TITLE_MAX_LEN));
+ core.setOutput('body-file', bodyFile);
+ core.setOutput('labels', labelNames.join(','));
core.setOutput('is-fork', String(pr.head.repo?.full_name !== pr.base.repo?.full_name));
core.setOutput('head-repo', pr.head.repo?.full_name || '');
core.setOutput('maintainer-can-modify', String(pr.maintainer_can_modify ?? false));
diff --git a/changelog/validate/action.yml b/changelog/validate/action.yml
index 6df464f..efb1b45 100644
--- a/changelog/validate/action.yml
+++ b/changelog/validate/action.yml
@@ -32,6 +32,17 @@ runs:
version: edge
github-token: ${{ inputs.github-token }}
+ - name: Stage PR body
+ id: stage-body
+ shell: bash
+ env:
+ PR_BODY: ${{ github.event.pull_request.body }}
+ run: |
+ BODY_FILE="${RUNNER_TEMP}/changelog-pr-body.md"
+ printf '%s' "${PR_BODY:-}" > "$BODY_FILE"
+ chmod 600 "$BODY_FILE"
+ echo "path=${BODY_FILE}" >> "$GITHUB_OUTPUT"
+
- name: Evaluate PR
id: evaluate
shell: bash
@@ -42,7 +53,7 @@ runs:
REPO_NAME: ${{ github.event.repository.name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
- PR_BODY: ${{ github.event.pull_request.body }}
+ PR_BODY_FILE: ${{ steps.stage-body.outputs.path }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
diff --git a/docs-builder/setup/action.yml b/docs-builder/setup/action.yml
index 134c8b5..3a98a1f 100644
--- a/docs-builder/setup/action.yml
+++ b/docs-builder/setup/action.yml
@@ -22,7 +22,19 @@ runs:
mkdir -p "${INSTALL_DIR}"
if [[ "${DOCS_BUILDER_VERSION}" == "edge" ]]; then
- docker cp $(docker create --name tc ghcr.io/elastic/docs-builder:edge):/app/docs-builder "${INSTALL_DIR}/docs-builder" && docker rm tc
+ # Resolve :edge to a RepoDigest before extracting the binary so
+ # the rest of this run uses an immutable image reference.
+ EDGE_IMAGE="ghcr.io/elastic/docs-builder:edge"
+ docker pull "${EDGE_IMAGE}"
+ EDGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "${EDGE_IMAGE}")
+ if [ -z "${EDGE_DIGEST}" ]; then
+ echo "::error::Failed to resolve RepoDigest for ${EDGE_IMAGE}"
+ exit 1
+ fi
+ echo "::notice title=docs-builder image digest::${EDGE_DIGEST}"
+ CONTAINER_ID=$(docker create "${EDGE_DIGEST}")
+ trap "docker rm -f ${CONTAINER_ID} >/dev/null 2>&1 || true" EXIT
+ docker cp "${CONTAINER_ID}:/app/docs-builder" "${INSTALL_DIR}/docs-builder"
else
if [[ "${DOCS_BUILDER_VERSION}" == "latest" ]]; then
DOCS_BUILDER_VERSION="" # empty string to get the latest version
diff --git a/github/is-elastic-org-member/README.md b/github/is-elastic-org-member/README.md
index 55e2251..9dae05f 100644
--- a/github/is-elastic-org-member/README.md
+++ b/github/is-elastic-org-member/README.md
@@ -1,25 +1,68 @@
# github/is-elastic-org-member
-Checks whether a GitHub user is a member of the elastic org using a GitHub token with read:org scope.
+Verifies that one or more GitHub users are members of the elastic org using a GitHub token with read:org scope. When `collect-from-workflow-run` is true, also auto-collects every login involved in the upstream workflow_run (PR opener + actor + triggering_actor + head-commit author/committer) so callers don't duplicate that logic. All resolved users must be confirmed members for `is-member` to be `true` (fail-closed semantics for trust gates — see elastic/docs-eng-team#511).
## Inputs
-| Name | Description | Required | Default |
-|------------|---------------------------------------|----------|---------|
-| `username` | GitHub username to check | `true` | ` ` |
-| `token` | GitHub token with the necessary scope | `true` | ` ` |
+| Name | Description | Required | Default |
+|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|
+| `username` | Single GitHub username to check. Backward-compat input for callers that only need to verify one user. Merged with `usernames` and any auto-collected logins.
| `false` | ` ` |
+| `usernames` | Newline- or comma-separated list of additional GitHub usernames. Merged with `username` and any auto-collected logins.
| `false` | ` ` |
+| `collect-from-workflow-run` | When 'true' (default) and the calling workflow was triggered by `workflow_run`, automatically include the PR opener, the upstream run's `actor` and `triggering_actor`, and the head commit's author and committer in the membership check. Set to 'false' to validate only the explicit `username` / `usernames` inputs.
| `false` | `true` |
+| `token` | GitHub token with `read:org` scope (for membership checks) and at minimum read access to public commits (for fork head-commit lookups). Typically an ephemeral Vault-issued token.
| `true` | ` ` |
## Outputs
-| Name | Description |
-|-------------|-------------------------------------------------------------------------|
-| `is-member` | 'true' if the user is a confirmed elastic org member, 'false' otherwise |
+| Name | Description |
+|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `is-member` | "'true' if every resolved user is a confirmed elastic org member, 'false' otherwise (including when no usernames were resolved)."
|
+| `non-members` | Newline-separated list of usernames that failed the membership check. Empty when `is-member` is `true`. Useful for logging which account(s) caused the gate to fail.
|
+| `checked-usernames` | Newline-separated list of usernames that were validated and checked (after deduplication and filtering of bots/invalid logins). Useful for diagnostic logging.
|
+## Behavior
+
+- The action is implemented in JavaScript via `actions/github-script@v9`. All
+ string handling, splitting, deduplication, and validation happens in JS to
+ avoid shell-quoting and `grep`/`sed` portability concerns.
+- Inputs are split on newline **and** comma, trimmed, and case-insensitively
+ deduped.
+- The GitHub `web-flow` ghost account (used as committer for web-UI edits and
+ squash merges) and `noreply` are filtered out before validation.
+- Each remaining value is validated against the GitHub username grammar
+ (`^[A-Za-z0-9](?:-?[A-Za-z0-9]){0,38}$`) before being passed to the API.
+ Anything that doesn't match (including bot logins ending in `[bot]`) is
+ silently dropped.
+- If no valid usernames remain after filtering, the action fails closed
+ (`is-member=false`).
+- Each membership check uses `octokit.orgs.checkMembershipForUser`. Any
+ non-200 response (including the documented `302 Found` for
+ not-publicly-visible memberships and `404 Not Found` for non-members) is
+ treated as "not a confirmed member" and added to `non-members`.
+
## Usage
+
+### Workflow_run trust gate (recommended)
+
+Pass only the token and let the action collect every login that touched the
+upstream run:
+
+```yaml
+- uses: elastic/docs-actions/github/is-elastic-org-member@v1
+ with:
+ token: ${{ steps.fetch-ephemeral-token.outputs.token }}
+```
+
+This is equivalent to listing the PR opener, `workflow_run.actor.login`,
+`workflow_run.triggering_actor.login`, and the head commit's `author.login`
+and `committer.login` — and requiring **all** of them to be elastic
+members.
+
+### Single user (legacy)
+
```yaml
on: push
@@ -27,3 +70,15 @@ steps:
- uses: elastic/docs-actions/github/is-elastic-org-member@v1
```
+
+### Explicit username list (auto-collection disabled)
+
+```yaml
+- uses: elastic/docs-actions/github/is-elastic-org-member@v1
+ with:
+ collect-from-workflow-run: 'false'
+ usernames: |
+ ${{ inputs.user-a }}
+ ${{ inputs.user-b }}
+ token: ${{ steps.fetch-ephemeral-token.outputs.token }}
+```
diff --git a/github/is-elastic-org-member/action.yml b/github/is-elastic-org-member/action.yml
index baf3a04..ddb5b7e 100644
--- a/github/is-elastic-org-member/action.yml
+++ b/github/is-elastic-org-member/action.yml
@@ -1,46 +1,210 @@
name: github/is-elastic-org-member
description: >
- Checks whether a GitHub user is a member of the elastic org using a
- GitHub token with read:org scope.
+ Verifies that one or more GitHub users are members of the elastic org
+ using a GitHub token with read:org scope. When `collect-from-workflow-run`
+ is true, also auto-collects every login involved in the upstream
+ workflow_run (PR opener + actor + triggering_actor + head-commit
+ author/committer) so callers don't duplicate that logic. All resolved
+ users must be confirmed members for `is-member` to be `true` (fail-closed
+ semantics for trust gates — see elastic/docs-eng-team#511).
inputs:
username:
- description: GitHub username to check
- required: true
+ description: >
+ Single GitHub username to check. Backward-compat input for callers
+ that only need to verify one user. Merged with `usernames` and any
+ auto-collected logins.
+ required: false
+ default: ''
+ usernames:
+ description: >
+ Newline- or comma-separated list of additional GitHub usernames.
+ Merged with `username` and any auto-collected logins.
+ required: false
+ default: ''
+ collect-from-workflow-run:
+ description: >
+ When 'true' (default) and the calling workflow was triggered by
+ `workflow_run`, automatically include the PR opener, the upstream
+ run's `actor` and `triggering_actor`, and the head commit's author
+ and committer in the membership check. Set to 'false' to validate
+ only the explicit `username` / `usernames` inputs.
+ required: false
+ default: 'true'
token:
- description: GitHub token with the necessary scope
+ description: >
+ GitHub token with `read:org` scope (for membership checks) and at
+ minimum read access to public commits (for fork head-commit
+ lookups). Typically an ephemeral Vault-issued token.
required: true
outputs:
is-member:
- description: "'true' if the user is a confirmed elastic org member, 'false' otherwise"
+ description: >
+ "'true' if every resolved user is a confirmed elastic org member,
+ 'false' otherwise (including when no usernames were resolved)."
value: ${{ steps.check.outputs.is-member }}
+ non-members:
+ description: >
+ Newline-separated list of usernames that failed the membership
+ check. Empty when `is-member` is `true`. Useful for logging which
+ account(s) caused the gate to fail.
+ value: ${{ steps.check.outputs.non-members }}
+ checked-usernames:
+ description: >
+ Newline-separated list of usernames that were validated and
+ checked (after deduplication and filtering of bots/invalid logins).
+ Useful for diagnostic logging.
+ value: ${{ steps.check.outputs.checked-usernames }}
runs:
using: composite
steps:
- - name: Check org membership
+ - name: Check elastic org membership
id: check
- shell: bash
+ uses: actions/github-script@v9
env:
- GH_TOKEN: ${{ inputs.token }}
- USERNAME: ${{ inputs.username }}
- run: |
- if [[ -z "$GH_TOKEN" || -z "$USERNAME" ]]; then
- echo "is-member=false" >> "$GITHUB_OUTPUT"
- echo "::warning::Token or username not available — cannot verify org membership"
- exit 0
- fi
-
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
- -H "Authorization: token $GH_TOKEN" \
- -H "Accept: application/vnd.github+json" \
- "https://api.github.com/orgs/elastic/members/${USERNAME}")
-
- if [[ "$HTTP_CODE" == "204" ]]; then
- echo "is-member=true" >> "$GITHUB_OUTPUT"
- echo "::notice::${USERNAME} is a member of the elastic org"
- else
- echo "is-member=false" >> "$GITHUB_OUTPUT"
- echo "::notice::${USERNAME} is not a confirmed member of the elastic org (HTTP ${HTTP_CODE})"
- fi
+ ORG_MEMBER_TOKEN: ${{ inputs.token }}
+ EXPLICIT_USERNAME: ${{ inputs.username }}
+ EXPLICIT_USERNAMES: ${{ inputs.usernames }}
+ COLLECT_FROM_WORKFLOW_RUN: ${{ inputs.collect-from-workflow-run }}
+ with:
+ # Use a separate Octokit instance so the org-membership token
+ # (read:org) is not conflated with the default GITHUB_TOKEN.
+ github-token: ${{ inputs.token }}
+ # language=js
+ script: |
+ // GitHub username grammar: 1-39 chars, alphanumeric + dash,
+ // no leading or trailing dash, no consecutive dashes.
+ // Source: https://github.com/shinnn/github-username-regex.
+ const USERNAME_REGEX = /^[A-Za-z0-9](?:-?[A-Za-z0-9]){0,38}$/;
+
+ // Logins that are real GitHub accounts but never represent a
+ // human committer for our trust-gate purposes:
+ // - `web-flow` → squash-merge / web-UI commit signer
+ // - `noreply` → defensive; no real user
+ // Anything ending in `[bot]` is filtered by USERNAME_REGEX
+ // (the brackets are not in the username alphabet).
+ const FILTERED_LOGINS = new Set(['web-flow', 'noreply']);
+
+ const split = (s) =>
+ String(s ?? '')
+ .split(/[\n,]/)
+ .map((x) => x.trim())
+ .filter(Boolean);
+
+ const explicit = [
+ ...split(process.env.EXPLICIT_USERNAME),
+ ...split(process.env.EXPLICIT_USERNAMES),
+ ];
+
+ const collected = [];
+
+ if (process.env.COLLECT_FROM_WORKFLOW_RUN === 'true') {
+ const run = context.payload?.workflow_run;
+ if (!run) {
+ core.info('collect-from-workflow-run is true but no workflow_run payload is present — skipping auto-collection');
+ } else {
+ // The PR opener (upstream-resolved). Falls back to nothing
+ // if no PR is associated with the run.
+ try {
+ const prRef = run.pull_requests?.[0];
+ if (prRef?.number) {
+ const { data: pr } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: prRef.number,
+ });
+ if (pr.user?.login) collected.push(pr.user.login);
+ }
+ } catch (err) {
+ core.warning(`Could not resolve PR opener: ${err.message}`);
+ }
+
+ // The user(s) who triggered the upstream workflow_run.
+ if (run.actor?.login) collected.push(run.actor.login);
+ if (run.triggering_actor?.login) collected.push(run.triggering_actor.login);
+
+ // Head commit author/committer — fetched against the
+ // (possibly fork) head repo. Fail-soft: if we cannot
+ // resolve the commit, the explicit + actor logins still
+ // constrain the check.
+ const headRepoFull = run.head_repository?.full_name;
+ const headSha = run.head_sha;
+ if (headRepoFull && headSha) {
+ const [forkOwner, forkRepo] = headRepoFull.split('/');
+ try {
+ const { data: commit } = await github.rest.repos.getCommit({
+ owner: forkOwner,
+ repo: forkRepo,
+ ref: headSha,
+ });
+ if (commit.author?.login) collected.push(commit.author.login);
+ if (commit.committer?.login) collected.push(commit.committer.login);
+ } catch (err) {
+ core.warning(`Could not fetch head commit ${headSha} from ${headRepoFull}: ${err.message}`);
+ }
+ }
+ }
+ }
+
+ // Merge, filter bots, validate against the GitHub username
+ // grammar, and dedupe. Validation happens before any value is
+ // interpolated into an API path.
+ const candidates = [...explicit, ...collected];
+ const seen = new Set();
+ const checked = [];
+ for (const raw of candidates) {
+ if (FILTERED_LOGINS.has(raw)) continue;
+ if (!USERNAME_REGEX.test(raw)) {
+ core.info(`Skipping invalid login candidate: ${JSON.stringify(raw)}`);
+ continue;
+ }
+ const key = raw.toLowerCase();
+ if (seen.has(key)) continue;
+ seen.add(key);
+ checked.push(raw);
+ }
+
+ core.setOutput('checked-usernames', checked.join('\n'));
+
+ if (!process.env.ORG_MEMBER_TOKEN) {
+ core.warning('No token supplied — failing closed (is-member=false)');
+ core.setOutput('is-member', 'false');
+ core.setOutput('non-members', checked.join('\n'));
+ return;
+ }
+
+ if (checked.length === 0) {
+ core.warning('No valid usernames resolved — failing closed (is-member=false)');
+ core.setOutput('is-member', 'false');
+ core.setOutput('non-members', '');
+ return;
+ }
+
+ const nonMembers = [];
+ for (const username of checked) {
+ try {
+ await github.rest.orgs.checkMembershipForUser({
+ org: 'elastic',
+ username,
+ });
+ core.info(`${username} is a member of the elastic org`);
+ } catch (err) {
+ // Octokit raises a HttpError with status 302 (not visible
+ // to the caller — public membership check) or 404 (not a
+ // member) for non-members. Treat any non-200 as "not a
+ // confirmed member".
+ const status = err.status ?? 'unknown';
+ core.info(`${username} is not a confirmed member of the elastic org (status ${status})`);
+ nonMembers.push(username);
+ }
+ }
+
+ if (nonMembers.length === 0) {
+ core.setOutput('is-member', 'true');
+ core.setOutput('non-members', '');
+ } else {
+ core.setOutput('is-member', 'false');
+ core.setOutput('non-members', nonMembers.join('\n'));
+ }
diff --git a/scripts/run-gh-aw-compile.sh b/scripts/run-gh-aw-compile.sh
index 7ad59fa..7407401 100755
--- a/scripts/run-gh-aw-compile.sh
+++ b/scripts/run-gh-aw-compile.sh
@@ -9,15 +9,21 @@ installed_version="$(
| awk '$1 == "gh" && $2 == "aw" { print $4 }'
)"
+# `gh extension install --pin --force` is a no-op when gh-aw is already
+# installed at a different version (it prints "already up to date" and keeps
+# the old binary). Removing first then installing pinned is the reliable way
+# to land an exact version.
+install_hint="gh extension remove gh-aw 2>/dev/null; gh extension install github/gh-aw --pin ${EXPECTED_GH_AW_VERSION}"
+
if [[ -z "${installed_version}" ]]; then
echo "gh-aw is not installed."
- echo "Install it with: gh extension install github/gh-aw --pin ${EXPECTED_GH_AW_VERSION} --force"
+ echo "Install it with: ${install_hint}"
exit 1
fi
if [[ "${installed_version}" != "${EXPECTED_GH_AW_VERSION}" ]]; then
echo "gh-aw version mismatch: expected ${EXPECTED_GH_AW_VERSION}, found ${installed_version}."
- echo "Install the expected version with: gh extension install github/gh-aw --pin ${EXPECTED_GH_AW_VERSION} --force"
+ echo "Install the expected version with: ${install_hint}"
exit 1
fi