From cb3c68c951bd7dd1b1eb8fa11c49994c2628c38d Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 26 Mar 2026 13:56:38 -0300 Subject: [PATCH 01/19] Add bundle action and reusable workflow --- .github/workflows/changelog-bundle.yml | 45 ++++++++++ changelog/README.md | 107 ++++++++++++++++++++++ changelog/bundle/action.yml | 120 +++++++++++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 .github/workflows/changelog-bundle.yml create mode 100644 changelog/bundle/action.yml diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml new file mode 100644 index 0000000..f0e4b94 --- /dev/null +++ b/.github/workflows/changelog-bundle.yml @@ -0,0 +1,45 @@ +name: Changelog bundle + +on: + workflow_call: + inputs: + config: + description: 'Path to changelog.yml configuration file' + type: string + default: 'docs/changelog.yml' + release-version: + description: > + GitHub release tag used as the PR filter source (e.g. v9.2.0). + Mutually exclusive with report. + type: string + report: + description: > + Buildkite promotion report URL or local file path. + Mutually exclusive with release-version. + type: string + output: + description: 'Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml)' + type: string + required: true + repo: + description: 'GitHub repository name; falls back to bundle.repo in changelog.yml' + type: string + owner: + description: 'GitHub repository owner; falls back to bundle.owner in changelog.yml, then elastic' + type: string + +jobs: + bundle: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: elastic/docs-actions/changelog/bundle@v1 + with: + config: ${{ inputs.config }} + release-version: ${{ inputs.release-version }} + report: ${{ inputs.report }} + output: ${{ inputs.output }} + repo: ${{ inputs.repo }} + owner: ${{ inputs.owner }} + github-token: ${{ github.token }} diff --git a/changelog/README.md b/changelog/README.md index bf94d86..638648c 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -179,3 +179,110 @@ If a human edits the changelog file directly (i.e., the last commit to the chang ## Output Each PR produces a file at `docs/changelog/{filename}.yaml` on the PR branch (where the filename is determined by the `docs-builder changelog add` command). These files are consumed by `docs-builder` during documentation builds to produce a rendered changelog page. + +## Bundling changelogs + +Individual changelog files accumulate on the default branch as PRs merge. The bundle action generates a fully-resolved YAML file containing only the changelog entries that match a given filter. Two filter sources are supported: + +- **GitHub release version** (`release-version`) — pulls PR references directly from GitHub release notes. Used for stack and product releases triggered by `on: release`. +- **Buildkite promotion report** (`report`) — extracts PR URLs from a promotion report. Used for serverless releases discovered by a scheduled workflow. + +Exactly one filter source must be provided per invocation. The bundle always includes the full content of each matching entry (`--resolve`), so downstream consumers can render changelogs without access to the original files. + +### Prerequisites + +Your `docs/changelog.yml` must include a `bundle` section so docs-builder knows where to find changelog files. Setting `bundle.repo` and `bundle.owner` ensures PR and issue links are generated correctly in the bundle output. + +```yaml +bundle: + directory: docs/changelog + repo: my-repo + owner: elastic +``` + +### Setup + +The bundle action supports two trigger patterns depending on your release process. + +#### Stack / product releases (`on: release`) + +When a GitHub release is published, the action uses `--release-version` to pull PR references directly from the release notes and filter changelog entries accordingly. The release tag provides the version for the output filename. + +**`.github/workflows/changelog-bundle.yml`** + +```yaml +name: changelog-bundle + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + bundle: + uses: elastic/docs-actions/.github/workflows/changelog-bundle.yml@v1 + with: + release-version: ${{ github.event.release.tag_name }} + output: docs/releases/${{ github.event.release.tag_name }}.yaml +``` + +The `github.event.release.tag_name` (e.g. `v9.2.0`) is passed as the release version filter and used to construct the output filename. If you prefer to strip the `v` prefix, you can do so in an earlier job step and pass the result as an input. + +#### Serverless / scheduled releases (`on: schedule`) + +When a Buildkite promotion report provides the list of PRs in a release, a scheduled workflow discovers the report and passes it to the bundle action. The output filename typically uses a date or timestamp. + +**`.github/workflows/changelog-bundle.yml`** + +```yaml +name: changelog-bundle + +on: + schedule: + - cron: '0 8 * * 1-5' + workflow_dispatch: + inputs: + report: + description: 'Buildkite promotion report URL' + required: true + output: + description: 'Output file path for the bundle' + required: true + +permissions: + contents: write + +jobs: + discover-report: + runs-on: ubuntu-latest + outputs: + report-url: ${{ steps.discover.outputs.report-url }} + release-date: ${{ steps.discover.outputs.release-date }} + steps: + - id: discover + run: echo "# your logic to find the latest promotion report" + + bundle: + needs: discover-report + uses: elastic/docs-actions/.github/workflows/changelog-bundle.yml@v1 + with: + report: ${{ needs.discover-report.outputs.report-url }} + output: docs/releases/${{ needs.discover-report.outputs.release-date }}.yaml +``` + +#### Custom config path + +If your changelog configuration is not at `docs/changelog.yml`, pass the path explicitly: + +```yaml + with: + config: path/to/changelog.yml + release-version: ${{ github.event.release.tag_name }} + output: docs/releases/${{ github.event.release.tag_name }}.yaml +``` + +### Output + +The bundle file is written to the path specified by the `output` input (e.g. `docs/releases/v9.2.0.yaml`). It contains the full content of every matching changelog entry — title, type, PR links, areas, description, and all other fields are inlined. If nothing has changed since the last run, no commit is made. diff --git a/changelog/bundle/action.yml b/changelog/bundle/action.yml new file mode 100644 index 0000000..b644b65 --- /dev/null +++ b/changelog/bundle/action.yml @@ -0,0 +1,120 @@ +name: Changelog bundle +description: > + Runs docs-builder changelog bundle in option-based mode to generate a + fully-resolved bundle file and commits the result to the repository. + Supports filtering by GitHub release version or Buildkite promotion report. + +inputs: + config: + description: 'Path to changelog.yml configuration file' + default: 'docs/changelog.yml' + release-version: + description: > + GitHub release tag used as the PR filter source (e.g. v9.2.0). + Mutually exclusive with report. Requires repo to be set in + changelog.yml (bundle.repo) or passed via the repo input. + report: + description: > + Buildkite promotion report URL or local file path used as the + PR filter source. Mutually exclusive with release-version. + output: + description: > + Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml). + Must end in .yml or .yaml. + required: true + repo: + description: > + GitHub repository name. Falls back to bundle.repo in changelog.yml. + owner: + description: > + GitHub repository owner. Falls back to bundle.owner in changelog.yml, + then to elastic. + github-token: + description: 'GitHub token with contents:write permission' + default: '${{ github.token }}' + +outputs: + committed: + description: 'Whether a bundle file was committed and pushed (true/false)' + value: ${{ steps.commit.outputs.committed || 'false' }} + +runs: + using: composite + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup docs-builder + uses: elastic/docs-actions/docs-builder/setup@v1 + with: + version: edge + github-token: ${{ inputs.github-token }} + + - name: Generate changelog bundle + id: generate + shell: bash + env: + CONFIG: ${{ inputs.config }} + RELEASE_VERSION: ${{ inputs.release-version }} + REPORT: ${{ inputs.report }} + OUTPUT: ${{ inputs.output }} + REPO: ${{ inputs.repo }} + OWNER: ${{ inputs.owner }} + GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + if [ -n "$RELEASE_VERSION" ] && [ -n "$REPORT" ]; then + echo "::error::Only one of 'release-version' or 'report' may be provided" + exit 1 + fi + + if [ -z "$RELEASE_VERSION" ] && [ -z "$REPORT" ]; then + echo "::error::Either 'release-version' or 'report' must be provided" + exit 1 + fi + + args=(--config "$CONFIG" --resolve) + + if [ -n "$RELEASE_VERSION" ]; then + args+=(--release-version "$RELEASE_VERSION") + fi + + if [ -n "$REPORT" ]; then + args+=(--report "$REPORT") + fi + + args+=(--output "$OUTPUT") + + if [ -n "$REPO" ]; then + args+=(--repo "$REPO") + fi + + if [ -n "$OWNER" ]; then + args+=(--owner "$OWNER") + fi + + docs-builder changelog bundle "${args[@]}" + + - name: Commit bundle + id: commit + shell: bash + env: + OUTPUT: ${{ inputs.output }} + GH_TOKEN: ${{ inputs.github-token }} + GIT_REPOSITORY: ${{ github.repository }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "$OUTPUT" + + if git diff --cached --quiet; then + echo "No changes to commit" + echo "committed=false" >> "$GITHUB_OUTPUT" + else + git commit -m "Update changelog bundle" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GIT_REPOSITORY}.git" + git push + git remote set-url origin "https://github.com/${GIT_REPOSITORY}.git" + echo "committed=true" >> "$GITHUB_OUTPUT" + fi From c31000be88c56e5f6d8a02805118b2678911c238 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 26 Mar 2026 14:17:03 -0300 Subject: [PATCH 02/19] Add README --- changelog/bundle/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 changelog/bundle/README.md diff --git a/changelog/bundle/README.md b/changelog/bundle/README.md new file mode 100644 index 0000000..d5e13c8 --- /dev/null +++ b/changelog/bundle/README.md @@ -0,0 +1,36 @@ + +# changelog/bundle + +Runs docs-builder changelog bundle in option-based mode to generate a fully-resolved bundle file and commits the result to the repository. Supports filtering by GitHub release version or Buildkite promotion report. + + +## Inputs + +| Name | Description | Required | Default | +|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with report. Requires repo to be set in changelog.yml or via repo input. | `false` | | +| `report` | Buildkite promotion report URL or local file path used as the PR filter source. Mutually exclusive with release-version. | `false` | | +| `output` | Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. | `true` | | +| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml. | `false` | | +| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. | `false` | | +| `github-token` | GitHub token with contents:write permission | `false` | `${{ github.token }}` | + + +## Outputs + +| Name | Description | +|-------------|--------------------------------------------------------------| +| `committed` | Whether a bundle file was committed and pushed (true/false) | + + +## Usage + +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle@v1 + with: + release-version: v9.2.0 + output: docs/releases/v9.2.0.yaml +``` + From 4de3e844477e3308dd2a0b9fb14852214f80b6fa Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 26 Mar 2026 19:37:42 -0300 Subject: [PATCH 03/19] Split action into bundle generation and PR submission. Readjusted naming scheme. --- .github/workflows/changelog-bundle.yml | 20 ++++- changelog/README.md | 6 +- changelog/bundle-create/README.md | 35 ++++++++ changelog/bundle-create/action.yml | 94 +++++++++++++++++++ changelog/bundle-pr/README.md | 29 ++++++ changelog/bundle-pr/action.yml | 69 ++++++++++++++ changelog/bundle/README.md | 36 -------- changelog/bundle/action.yml | 120 ------------------------- 8 files changed, 248 insertions(+), 161 deletions(-) create mode 100644 changelog/bundle-create/README.md create mode 100644 changelog/bundle-create/action.yml create mode 100644 changelog/bundle-pr/README.md create mode 100644 changelog/bundle-pr/action.yml delete mode 100644 changelog/bundle/README.md delete mode 100644 changelog/bundle/action.yml diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml index f0e4b94..6f59af5 100644 --- a/.github/workflows/changelog-bundle.yml +++ b/.github/workflows/changelog-bundle.yml @@ -25,16 +25,16 @@ on: description: 'GitHub repository name; falls back to bundle.repo in changelog.yml' type: string owner: - description: 'GitHub repository owner; falls back to bundle.owner in changelog.yml, then elastic' + description: 'GitHub repository owner; falls back to bundle.owner in changelog.yml' type: string jobs: - bundle: + generate: runs-on: ubuntu-latest permissions: - contents: write + contents: read steps: - - uses: elastic/docs-actions/changelog/bundle@v1 + - uses: elastic/docs-actions/changelog/bundle-create@v1 with: config: ${{ inputs.config }} release-version: ${{ inputs.release-version }} @@ -43,3 +43,15 @@ jobs: repo: ${{ inputs.repo }} owner: ${{ inputs.owner }} github-token: ${{ github.token }} + + create-pr: + needs: generate + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: elastic/docs-actions/changelog/bundle-pr@v1 + with: + output: ${{ inputs.output }} + github-token: ${{ github.token }} diff --git a/changelog/README.md b/changelog/README.md index 638648c..e9365ca 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -200,6 +200,8 @@ bundle: owner: elastic ``` +The reusable workflow splits into two jobs with separate permissions: `generate` (read-only, produces the bundle artifact) and `create-pr` (write access, opens a pull request with the bundle file). + ### Setup The bundle action supports two trigger patterns depending on your release process. @@ -219,6 +221,7 @@ on: permissions: contents: write + pull-requests: write jobs: bundle: @@ -253,6 +256,7 @@ on: permissions: contents: write + pull-requests: write jobs: discover-report: @@ -285,4 +289,4 @@ If your changelog configuration is not at `docs/changelog.yml`, pass the path ex ### Output -The bundle file is written to the path specified by the `output` input (e.g. `docs/releases/v9.2.0.yaml`). It contains the full content of every matching changelog entry — title, type, PR links, areas, description, and all other fields are inlined. If nothing has changed since the last run, no commit is made. +The reusable workflow opens a pull request on a branch named `changelog-bundle/` (e.g. `changelog-bundle/v9.2.0`). The PR contains the fully-resolved bundle file at the path specified by the `output` input. If a PR already exists for that branch, the bundle is updated in place. If the generated bundle is identical to what's already in the repository, no commit or PR is created. diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md new file mode 100644 index 0000000..ba5e7c1 --- /dev/null +++ b/changelog/bundle-create/README.md @@ -0,0 +1,35 @@ + +# changelog/bundle-create + +Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an artifact. Supports filtering by GitHub release version or Buildkite promotion report. Uses --network none for report mode. + + +## Inputs + +| Name | Description | Required | Default | +|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with report. Requires repo to be set in changelog.yml or via repo input. | `false` | | +| `report` | Buildkite promotion report URL or local file path used as the PR filter source. Mutually exclusive with release-version. Local paths must be relative to repo root. | `false` | | +| `output` | Output file path for the bundle, relative to the repo root (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. | `true` | | +| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml. | `false` | | +| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. | `false` | | +| `github-token` | GitHub token (needed for release-version to access GitHub API) | `false` | `${{ github.token }}` | + + +## Outputs + +| Name | Description | +|------|-------------| + + +## Usage + +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle-create@v1 + with: + release-version: v9.2.0 + output: docs/releases/v9.2.0.yaml +``` + diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml new file mode 100644 index 0000000..7e109bd --- /dev/null +++ b/changelog/bundle-create/action.yml @@ -0,0 +1,94 @@ +name: Changelog bundle create +description: > + Checks out the repository, runs docs-builder changelog bundle in Docker + to generate a fully-resolved bundle file, and uploads the result as an + artifact. Supports filtering by GitHub release version or Buildkite + promotion report. Uses --network none for report mode. + +inputs: + config: + description: 'Path to changelog.yml configuration file' + default: 'docs/changelog.yml' + release-version: + description: > + GitHub release tag used as the PR filter source (e.g. v9.2.0). + Mutually exclusive with report. Requires repo to be set in + changelog.yml (bundle.repo) or passed via the repo input. + report: + description: > + Buildkite promotion report URL or local file path used as the + PR filter source. Mutually exclusive with release-version. + Local paths must be relative to the repo root. + output: + description: > + Output file path for the bundle, relative to the repo root + (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. + required: true + repo: + description: > + GitHub repository name. Falls back to bundle.repo in changelog.yml. + owner: + description: > + GitHub repository owner. Falls back to bundle.owner in changelog.yml, + then to elastic. + github-token: + description: 'GitHub token (needed for release-version to access GitHub API)' + default: '${{ github.token }}' + +runs: + using: composite + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Generate changelog bundle + shell: bash + env: + CONFIG: ${{ inputs.config }} + RELEASE_VERSION: ${{ inputs.release-version }} + REPORT: ${{ inputs.report }} + OUTPUT: ${{ inputs.output }} + REPO: ${{ inputs.repo }} + OWNER: ${{ inputs.owner }} + GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + if [ -n "$RELEASE_VERSION" ] && [ -n "$REPORT" ]; then + echo "::error::Only one of 'release-version' or 'report' may be provided" + exit 1 + fi + + if [ -z "$RELEASE_VERSION" ] && [ -z "$REPORT" ]; then + echo "::error::Either 'release-version' or 'report' must be provided" + exit 1 + fi + + docker_args=(--rm -v "${PWD}:/github/workspace" -w /github/workspace) + bundle_args=(changelog bundle --config "$CONFIG" --resolve --output "$OUTPUT") + + if [ -n "$RELEASE_VERSION" ]; then + bundle_args+=(--release-version "$RELEASE_VERSION") + docker_args+=(-e GITHUB_TOKEN) + if [ -n "$REPO" ]; then bundle_args+=(--repo "$REPO"); fi + if [ -n "$OWNER" ]; then bundle_args+=(--owner "$OWNER"); fi + fi + + if [ -n "$REPORT" ]; then + if [[ "$REPORT" == http://* ]] || [[ "$REPORT" == https://* ]]; then + curl -fsSL "$REPORT" -o .bundle-report.html + REPORT=".bundle-report.html" + fi + bundle_args+=(--report "$REPORT") + docker_args+=(--network none) + fi + + docker run "${docker_args[@]}" ghcr.io/elastic/docs-builder:edge "${bundle_args[@]}" + + - name: Upload bundle artifact + uses: actions/upload-artifact@v6 + with: + name: changelog-bundle + path: ${{ inputs.output }} + if-no-files-found: error + retention-days: 1 diff --git a/changelog/bundle-pr/README.md b/changelog/bundle-pr/README.md new file mode 100644 index 0000000..6c2d8f5 --- /dev/null +++ b/changelog/bundle-pr/README.md @@ -0,0 +1,29 @@ + +# changelog/bundle-pr + +Downloads a changelog bundle artifact and opens a pull request to add it to the repository. If a PR already exists for the same bundle, it is updated in place. + + +## Inputs + +| Name | Description | Required | Default | +|----------------|----------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `output` | Output file path for the bundle, relative to the repo root (e.g. docs/releases/v9.2.0.yaml). Must match the bundle-create action. | `true` | | +| `github-token` | GitHub token with contents:write and pull-requests:write permissions | `false` | `${{ github.token }}` | + + +## Outputs + +| Name | Description | +|------|-------------| + + +## Usage + +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle-pr@v1 + with: + output: docs/releases/v9.2.0.yaml +``` + diff --git a/changelog/bundle-pr/action.yml b/changelog/bundle-pr/action.yml new file mode 100644 index 0000000..bde30bc --- /dev/null +++ b/changelog/bundle-pr/action.yml @@ -0,0 +1,69 @@ +name: Changelog bundle PR +description: > + Downloads a changelog bundle artifact and opens a pull request to add it + to the repository. If a PR already exists for the same bundle, it is + updated in place. + +inputs: + output: + description: > + Output file path for the bundle, relative to the repo root + (e.g. docs/releases/v9.2.0.yaml). Must match the path used + by the bundle-create action. + required: true + github-token: + description: 'GitHub token with contents:write and pull-requests:write permissions' + default: '${{ github.token }}' + +runs: + using: composite + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Download bundle artifact + uses: actions/download-artifact@v6 + with: + name: changelog-bundle + path: .bundle-artifact + + - name: Create pull request + shell: bash + env: + OUTPUT: ${{ inputs.output }} + GH_TOKEN: ${{ inputs.github-token }} + GIT_REPOSITORY: ${{ github.repository }} + run: | + BUNDLE_NAME=$(basename "$OUTPUT" .yaml) + BRANCH="changelog-bundle/${BUNDLE_NAME}" + + mkdir -p "$(dirname "$OUTPUT")" + cp ".bundle-artifact/$(basename "$OUTPUT")" "$OUTPUT" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git checkout -b "$BRANCH" + git add "$OUTPUT" + + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + + git commit -m "Add changelog bundle ${BUNDLE_NAME}" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GIT_REPOSITORY}.git" + git push -f origin "$BRANCH" + git remote set-url origin "https://github.com/${GIT_REPOSITORY}.git" + + EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number // empty') + if [ -n "$EXISTING_PR" ]; then + echo "PR #${EXISTING_PR} already exists for branch ${BRANCH}, updated with latest bundle" + else + gh pr create \ + --title "Add changelog bundle ${BUNDLE_NAME}" \ + --body "Auto-generated changelog bundle for ${BUNDLE_NAME}." \ + --head "$BRANCH" + fi diff --git a/changelog/bundle/README.md b/changelog/bundle/README.md deleted file mode 100644 index d5e13c8..0000000 --- a/changelog/bundle/README.md +++ /dev/null @@ -1,36 +0,0 @@ - -# changelog/bundle - -Runs docs-builder changelog bundle in option-based mode to generate a fully-resolved bundle file and commits the result to the repository. Supports filtering by GitHub release version or Buildkite promotion report. - - -## Inputs - -| Name | Description | Required | Default | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| -| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | -| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with report. Requires repo to be set in changelog.yml or via repo input. | `false` | | -| `report` | Buildkite promotion report URL or local file path used as the PR filter source. Mutually exclusive with release-version. | `false` | | -| `output` | Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. | `true` | | -| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml. | `false` | | -| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. | `false` | | -| `github-token` | GitHub token with contents:write permission | `false` | `${{ github.token }}` | - - -## Outputs - -| Name | Description | -|-------------|--------------------------------------------------------------| -| `committed` | Whether a bundle file was committed and pushed (true/false) | - - -## Usage - -```yaml -steps: - - uses: elastic/docs-actions/changelog/bundle@v1 - with: - release-version: v9.2.0 - output: docs/releases/v9.2.0.yaml -``` - diff --git a/changelog/bundle/action.yml b/changelog/bundle/action.yml deleted file mode 100644 index b644b65..0000000 --- a/changelog/bundle/action.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Changelog bundle -description: > - Runs docs-builder changelog bundle in option-based mode to generate a - fully-resolved bundle file and commits the result to the repository. - Supports filtering by GitHub release version or Buildkite promotion report. - -inputs: - config: - description: 'Path to changelog.yml configuration file' - default: 'docs/changelog.yml' - release-version: - description: > - GitHub release tag used as the PR filter source (e.g. v9.2.0). - Mutually exclusive with report. Requires repo to be set in - changelog.yml (bundle.repo) or passed via the repo input. - report: - description: > - Buildkite promotion report URL or local file path used as the - PR filter source. Mutually exclusive with release-version. - output: - description: > - Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml). - Must end in .yml or .yaml. - required: true - repo: - description: > - GitHub repository name. Falls back to bundle.repo in changelog.yml. - owner: - description: > - GitHub repository owner. Falls back to bundle.owner in changelog.yml, - then to elastic. - github-token: - description: 'GitHub token with contents:write permission' - default: '${{ github.token }}' - -outputs: - committed: - description: 'Whether a bundle file was committed and pushed (true/false)' - value: ${{ steps.commit.outputs.committed || 'false' }} - -runs: - using: composite - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - - name: Setup docs-builder - uses: elastic/docs-actions/docs-builder/setup@v1 - with: - version: edge - github-token: ${{ inputs.github-token }} - - - name: Generate changelog bundle - id: generate - shell: bash - env: - CONFIG: ${{ inputs.config }} - RELEASE_VERSION: ${{ inputs.release-version }} - REPORT: ${{ inputs.report }} - OUTPUT: ${{ inputs.output }} - REPO: ${{ inputs.repo }} - OWNER: ${{ inputs.owner }} - GITHUB_TOKEN: ${{ inputs.github-token }} - run: | - if [ -n "$RELEASE_VERSION" ] && [ -n "$REPORT" ]; then - echo "::error::Only one of 'release-version' or 'report' may be provided" - exit 1 - fi - - if [ -z "$RELEASE_VERSION" ] && [ -z "$REPORT" ]; then - echo "::error::Either 'release-version' or 'report' must be provided" - exit 1 - fi - - args=(--config "$CONFIG" --resolve) - - if [ -n "$RELEASE_VERSION" ]; then - args+=(--release-version "$RELEASE_VERSION") - fi - - if [ -n "$REPORT" ]; then - args+=(--report "$REPORT") - fi - - args+=(--output "$OUTPUT") - - if [ -n "$REPO" ]; then - args+=(--repo "$REPO") - fi - - if [ -n "$OWNER" ]; then - args+=(--owner "$OWNER") - fi - - docs-builder changelog bundle "${args[@]}" - - - name: Commit bundle - id: commit - shell: bash - env: - OUTPUT: ${{ inputs.output }} - GH_TOKEN: ${{ inputs.github-token }} - GIT_REPOSITORY: ${{ github.repository }} - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add "$OUTPUT" - - if git diff --cached --quiet; then - echo "No changes to commit" - echo "committed=false" >> "$GITHUB_OUTPUT" - else - git commit -m "Update changelog bundle" - git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GIT_REPOSITORY}.git" - git push - git remote set-url origin "https://github.com/${GIT_REPOSITORY}.git" - echo "committed=true" >> "$GITHUB_OUTPUT" - fi From e43c65899456a5746d19fe0e074763d5860f63f2 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 27 Mar 2026 15:00:41 -0300 Subject: [PATCH 04/19] Security fixes --- .github/workflows/changelog-bundle.yml | 12 ++++++- changelog/README.md | 3 +- changelog/bundle-create/action.yml | 10 ++++-- changelog/bundle-pr/action.yml | 43 ++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml index 6f59af5..08e5383 100644 --- a/.github/workflows/changelog-bundle.yml +++ b/.github/workflows/changelog-bundle.yml @@ -14,13 +14,16 @@ on: type: string report: description: > - Buildkite promotion report URL or local file path. + Buildkite promotion report HTTPS URL or local file path. Mutually exclusive with release-version. type: string output: description: 'Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml)' type: string required: true + base-branch: + description: 'Base branch for the pull request (defaults to repository default branch)' + type: string repo: description: 'GitHub repository name; falls back to bundle.repo in changelog.yml' type: string @@ -28,6 +31,12 @@ on: description: 'GitHub repository owner; falls back to bundle.owner in changelog.yml' type: string +permissions: {} + +concurrency: + group: changelog-bundle-${{ inputs.output }} + cancel-in-progress: false + jobs: generate: runs-on: ubuntu-latest @@ -54,4 +63,5 @@ jobs: - uses: elastic/docs-actions/changelog/bundle-pr@v1 with: output: ${{ inputs.output }} + base-branch: ${{ inputs.base-branch }} github-token: ${{ github.token }} diff --git a/changelog/README.md b/changelog/README.md index e9365ca..36bfa94 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -196,8 +196,6 @@ Your `docs/changelog.yml` must include a `bundle` section so docs-builder knows ```yaml bundle: directory: docs/changelog - repo: my-repo - owner: elastic ``` The reusable workflow splits into two jobs with separate permissions: `generate` (read-only, produces the bundle artifact) and `create-pr` (write access, opens a pull request with the bundle file). @@ -244,6 +242,7 @@ name: changelog-bundle on: schedule: + # At 08:00 AM, Monday through Friday - cron: '0 8 * * 1-5' workflow_dispatch: inputs: diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 7e109bd..6b2f11d 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -31,6 +31,9 @@ inputs: description: > GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. + artifact-name: + description: 'Name for the uploaded artifact (must match bundle-pr artifact-name)' + default: 'changelog-bundle' github-token: description: 'GitHub token (needed for release-version to access GitHub API)' default: '${{ github.token }}' @@ -75,9 +78,12 @@ runs: fi if [ -n "$REPORT" ]; then - if [[ "$REPORT" == http://* ]] || [[ "$REPORT" == https://* ]]; then + if [[ "$REPORT" == https://* ]]; then curl -fsSL "$REPORT" -o .bundle-report.html REPORT=".bundle-report.html" + elif [[ "$REPORT" == http://* ]]; then + echo "::error::Report URL must use HTTPS: ${REPORT}" + exit 1 fi bundle_args+=(--report "$REPORT") docker_args+=(--network none) @@ -88,7 +94,7 @@ runs: - name: Upload bundle artifact uses: actions/upload-artifact@v6 with: - name: changelog-bundle + name: ${{ inputs.artifact-name }} path: ${{ inputs.output }} if-no-files-found: error retention-days: 1 diff --git a/changelog/bundle-pr/action.yml b/changelog/bundle-pr/action.yml index bde30bc..33fabe2 100644 --- a/changelog/bundle-pr/action.yml +++ b/changelog/bundle-pr/action.yml @@ -11,6 +11,12 @@ inputs: (e.g. docs/releases/v9.2.0.yaml). Must match the path used by the bundle-create action. required: true + base-branch: + description: 'Base branch for the pull request (defaults to repository default branch)' + default: '' + artifact-name: + description: 'Name of the artifact uploaded by bundle-create' + default: 'changelog-bundle' github-token: description: 'GitHub token with contents:write and pull-requests:write permissions' default: '${{ github.token }}' @@ -23,20 +29,46 @@ runs: with: persist-credentials: false + - name: Validate output path + shell: bash + env: + OUTPUT: ${{ inputs.output }} + run: | + if [[ "$OUTPUT" != *.yml && "$OUTPUT" != *.yaml ]]; then + echo "::error::Output path must end in .yml or .yaml: ${OUTPUT}" + exit 1 + fi + if [[ "$OUTPUT" == /* ]]; then + echo "::error::Output path must be relative: ${OUTPUT}" + exit 1 + fi + if [[ "$OUTPUT" == *..* ]]; then + echo "::error::Output path must not contain '..': ${OUTPUT}" + exit 1 + fi + - name: Download bundle artifact uses: actions/download-artifact@v6 with: - name: changelog-bundle + name: ${{ inputs.artifact-name }} path: .bundle-artifact - name: Create pull request shell: bash env: OUTPUT: ${{ inputs.output }} + BASE_BRANCH: ${{ inputs.base-branch }} GH_TOKEN: ${{ inputs.github-token }} GIT_REPOSITORY: ${{ github.repository }} run: | BUNDLE_NAME=$(basename "$OUTPUT" .yaml) + BUNDLE_NAME=$(basename "$BUNDLE_NAME" .yml) + + if [[ ! "$BUNDLE_NAME" =~ ^[a-zA-Z0-9._+-]+$ ]]; then + echo "::error::Bundle name contains disallowed characters: ${BUNDLE_NAME}" + exit 1 + fi + BRANCH="changelog-bundle/${BUNDLE_NAME}" mkdir -p "$(dirname "$OUTPUT")" @@ -55,9 +87,15 @@ runs: git commit -m "Add changelog bundle ${BUNDLE_NAME}" git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GIT_REPOSITORY}.git" + # Force-push: branch is ephemeral and always rebuilt from scratch git push -f origin "$BRANCH" git remote set-url origin "https://github.com/${GIT_REPOSITORY}.git" + BASE_FLAG=() + if [ -n "$BASE_BRANCH" ]; then + BASE_FLAG=(--base "$BASE_BRANCH") + fi + EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number // empty') if [ -n "$EXISTING_PR" ]; then echo "PR #${EXISTING_PR} already exists for branch ${BRANCH}, updated with latest bundle" @@ -65,5 +103,6 @@ runs: gh pr create \ --title "Add changelog bundle ${BUNDLE_NAME}" \ --body "Auto-generated changelog bundle for ${BUNDLE_NAME}." \ - --head "$BRANCH" + --head "$BRANCH" \ + "${BASE_FLAG[@]}" fi From f86c77c601f6765a2a5c2822f43cc7c0f4d91fb0 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 27 Mar 2026 15:11:10 -0300 Subject: [PATCH 05/19] Use --force-with-lease instead of -f --- changelog/bundle-pr/action.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog/bundle-pr/action.yml b/changelog/bundle-pr/action.yml index 33fabe2..8ad21ca 100644 --- a/changelog/bundle-pr/action.yml +++ b/changelog/bundle-pr/action.yml @@ -87,8 +87,7 @@ runs: git commit -m "Add changelog bundle ${BUNDLE_NAME}" git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GIT_REPOSITORY}.git" - # Force-push: branch is ephemeral and always rebuilt from scratch - git push -f origin "$BRANCH" + git push --force-with-lease origin "$BRANCH" git remote set-url origin "https://github.com/${GIT_REPOSITORY}.git" BASE_FLAG=() From 06f43225895f2a0f0f84774955ca6851beaa74ed Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 30 Mar 2026 22:24:06 -0300 Subject: [PATCH 06/19] Add --prs support --- .github/workflows/changelog-bundle.yml | 10 ++- changelog/README.md | 63 ++++++++++++++- changelog/bundle-create/README.md | 21 ++--- changelog/bundle-create/action.yml | 59 +++++--------- .../bundle-create/scripts/generate-bundle.js | 80 +++++++++++++++++++ 5 files changed, 180 insertions(+), 53 deletions(-) create mode 100644 changelog/bundle-create/scripts/generate-bundle.js diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml index 08e5383..a7ef002 100644 --- a/.github/workflows/changelog-bundle.yml +++ b/.github/workflows/changelog-bundle.yml @@ -10,12 +10,17 @@ on: release-version: description: > GitHub release tag used as the PR filter source (e.g. v9.2.0). - Mutually exclusive with report. + Mutually exclusive with report and prs. type: string report: description: > Buildkite promotion report HTTPS URL or local file path. - Mutually exclusive with release-version. + Mutually exclusive with release-version and prs. + type: string + prs: + description: > + Comma-separated PR URLs or numbers, or a path to a newline-delimited file. + Mutually exclusive with release-version and report. type: string output: description: 'Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml)' @@ -48,6 +53,7 @@ jobs: config: ${{ inputs.config }} release-version: ${{ inputs.release-version }} report: ${{ inputs.report }} + prs: ${{ inputs.prs }} output: ${{ inputs.output }} repo: ${{ inputs.repo }} owner: ${{ inputs.owner }} diff --git a/changelog/README.md b/changelog/README.md index 36bfa94..7f3d478 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -182,10 +182,11 @@ Each PR produces a file at `docs/changelog/{filename}.yaml` on the PR branch (wh ## Bundling changelogs -Individual changelog files accumulate on the default branch as PRs merge. The bundle action generates a fully-resolved YAML file containing only the changelog entries that match a given filter. Two filter sources are supported: +Individual changelog files accumulate on the default branch as PRs merge. The bundle action generates a fully-resolved YAML file containing only the changelog entries that match a given filter. Three filter sources are supported: - **GitHub release version** (`release-version`) — pulls PR references directly from GitHub release notes. Used for stack and product releases triggered by `on: release`. - **Buildkite promotion report** (`report`) — extracts PR URLs from a promotion report. Used for serverless releases discovered by a scheduled workflow. +- **PR list** (`prs`) — an explicit list of PR URLs or numbers (comma-separated), or a path to a newline-delimited file. Used when the caller already knows which PRs to include. Exactly one filter source must be provided per invocation. The bundle always includes the full content of each matching entry (`--resolve`), so downstream consumers can render changelogs without access to the original files. @@ -202,7 +203,7 @@ The reusable workflow splits into two jobs with separate permissions: `generate` ### Setup -The bundle action supports two trigger patterns depending on your release process. +The bundle action supports multiple trigger patterns depending on your release process. #### Stack / product releases (`on: release`) @@ -275,6 +276,64 @@ jobs: output: docs/releases/${{ needs.discover-report.outputs.release-date }}.yaml ``` +#### Explicit PR list + +When the caller already knows which PRs to include, pass them directly. The `prs` input accepts either comma-separated values or a path to a newline-delimited file. PR numbers can be used instead of full URLs when `bundle.repo` and `bundle.owner` are set in the changelog config. + +**Inline PR numbers** — pass them directly in the workflow call: + +```yaml +name: changelog-bundle + +on: + workflow_dispatch: + inputs: + prs: + description: 'Comma-separated PR URLs or numbers' + required: true + output: + description: 'Output file path for the bundle' + required: true + +permissions: + contents: write + pull-requests: write + +jobs: + bundle: + uses: elastic/docs-actions/.github/workflows/changelog-bundle.yml@v1 + with: + prs: ${{ inputs.prs }} + output: ${{ inputs.output }} +``` + +For example, triggering this workflow with `prs: "12345,67890"` bundles the changelog entries whose `prs` field matches those PR numbers. + +**File-based PR list** — when a prior step produces the list, write it to a file (one PR URL per line) and pass the path: + +```yaml +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + prs-file: ${{ steps.generate.outputs.prs-file }} + steps: + - id: generate + run: | + echo "https://github.com/elastic/my-repo/pull/12345" > prs.txt + echo "https://github.com/elastic/my-repo/pull/67890" >> prs.txt + echo "prs-file=prs.txt" >> "$GITHUB_OUTPUT" + + bundle: + needs: prepare + uses: elastic/docs-actions/.github/workflows/changelog-bundle.yml@v1 + with: + prs: ${{ needs.prepare.outputs.prs-file }} + output: docs/releases/my-bundle.yaml +``` + +When using a file, every line must be a fully-qualified GitHub PR URL (e.g. `https://github.com/owner/repo/pull/123`). Bare numbers are only supported in the comma-separated format. + #### Custom config path If your changelog configuration is not at `docs/changelog.yml`, pass the path explicitly: diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md index ba5e7c1..e5ca016 100644 --- a/changelog/bundle-create/README.md +++ b/changelog/bundle-create/README.md @@ -1,20 +1,21 @@ # changelog/bundle-create -Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an artifact. Supports filtering by GitHub release version or Buildkite promotion report. Uses --network none for report mode. +Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an artifact. Supports filtering by GitHub release version, Buildkite promotion report, or an explicit list of PR URLs/numbers. Uses --network none for report and prs modes. ## Inputs -| Name | Description | Required | Default | -|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| -| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | -| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with report. Requires repo to be set in changelog.yml or via repo input. | `false` | | -| `report` | Buildkite promotion report URL or local file path used as the PR filter source. Mutually exclusive with release-version. Local paths must be relative to repo root. | `false` | | -| `output` | Output file path for the bundle, relative to the repo root (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. | `true` | | -| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml. | `false` | | -| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. | `false` | | -| `github-token` | GitHub token (needed for release-version to access GitHub API) | `false` | `${{ github.token }}` | +| Name | Description | Required | Default | +|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with report and prs. Requires repo to be set in changelog.yml or via repo input. | `false` | | +| `report` | Buildkite promotion report URL or local file path used as the PR filter source. Mutually exclusive with release-version and prs. Local paths must be relative to repo root. | `false` | | +| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with release-version and report. Bare numbers require repo/owner in changelog.yml or inputs. | `false` | | +| `output` | Output file path for the bundle, relative to the repo root (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. | `true` | | +| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml. | `false` | | +| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. | `false` | | +| `github-token` | GitHub token (needed for release-version to access GitHub API) | `false` | `${{ github.token }}` | ## Outputs diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 6b2f11d..61b6210 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -2,8 +2,9 @@ name: Changelog bundle create description: > Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an - artifact. Supports filtering by GitHub release version or Buildkite - promotion report. Uses --network none for report mode. + artifact. Supports filtering by GitHub release version, Buildkite + promotion report, or an explicit list of PR URLs/numbers. + Uses --network none for report and prs modes. inputs: config: @@ -12,13 +13,19 @@ inputs: release-version: description: > GitHub release tag used as the PR filter source (e.g. v9.2.0). - Mutually exclusive with report. Requires repo to be set in + Mutually exclusive with report and prs. Requires repo to be set in changelog.yml (bundle.repo) or passed via the repo input. report: description: > Buildkite promotion report URL or local file path used as the - PR filter source. Mutually exclusive with release-version. + PR filter source. Mutually exclusive with release-version and prs. Local paths must be relative to the repo root. + prs: + description: > + Comma-separated PR URLs or numbers, or a path to a newline-delimited + file. Mutually exclusive with release-version and report. + Bare numbers require repo/owner to be set in changelog.yml or + passed via the repo and owner inputs. output: description: > Output file path for the bundle, relative to the repo root @@ -46,50 +53,24 @@ runs: with: persist-credentials: false - - name: Generate changelog bundle + - name: Prepare bundle arguments + id: prepare shell: bash env: CONFIG: ${{ inputs.config }} RELEASE_VERSION: ${{ inputs.release-version }} REPORT: ${{ inputs.report }} + PRS: ${{ inputs.prs }} OUTPUT: ${{ inputs.output }} REPO: ${{ inputs.repo }} OWNER: ${{ inputs.owner }} - GITHUB_TOKEN: ${{ inputs.github-token }} - run: | - if [ -n "$RELEASE_VERSION" ] && [ -n "$REPORT" ]; then - echo "::error::Only one of 'release-version' or 'report' may be provided" - exit 1 - fi - - if [ -z "$RELEASE_VERSION" ] && [ -z "$REPORT" ]; then - echo "::error::Either 'release-version' or 'report' must be provided" - exit 1 - fi - - docker_args=(--rm -v "${PWD}:/github/workspace" -w /github/workspace) - bundle_args=(changelog bundle --config "$CONFIG" --resolve --output "$OUTPUT") - - if [ -n "$RELEASE_VERSION" ]; then - bundle_args+=(--release-version "$RELEASE_VERSION") - docker_args+=(-e GITHUB_TOKEN) - if [ -n "$REPO" ]; then bundle_args+=(--repo "$REPO"); fi - if [ -n "$OWNER" ]; then bundle_args+=(--owner "$OWNER"); fi - fi + run: node "${{ github.action_path }}/scripts/generate-bundle.js" - if [ -n "$REPORT" ]; then - if [[ "$REPORT" == https://* ]]; then - curl -fsSL "$REPORT" -o .bundle-report.html - REPORT=".bundle-report.html" - elif [[ "$REPORT" == http://* ]]; then - echo "::error::Report URL must use HTTPS: ${REPORT}" - exit 1 - fi - bundle_args+=(--report "$REPORT") - docker_args+=(--network none) - fi - - docker run "${docker_args[@]}" ghcr.io/elastic/docs-builder:edge "${bundle_args[@]}" + - name: Generate changelog bundle + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: docker run ${{ steps.prepare.outputs.docker-args }} ghcr.io/elastic/docs-builder:edge ${{ steps.prepare.outputs.bundle-args }} - name: Upload bundle artifact uses: actions/upload-artifact@v6 diff --git a/changelog/bundle-create/scripts/generate-bundle.js b/changelog/bundle-create/scripts/generate-bundle.js new file mode 100644 index 0000000..5f09088 --- /dev/null +++ b/changelog/bundle-create/scripts/generate-bundle.js @@ -0,0 +1,80 @@ +const fs = require('fs'); +const https = require('https'); +const path = require('path'); + +const config = process.env.CONFIG; +const releaseVersion = process.env.RELEASE_VERSION; +const report = process.env.REPORT; +const prs = process.env.PRS; +const output = process.env.OUTPUT; +const repo = process.env.REPO; +const owner = process.env.OWNER; +const githubOutput = process.env.GITHUB_OUTPUT; + +const filters = [releaseVersion, report, prs].filter(Boolean); +if (filters.length === 0) { + console.error('::error::Exactly one of release-version, report, or prs must be provided'); + process.exit(1); +} +if (filters.length > 1) { + console.error('::error::Only one of release-version, report, or prs may be provided'); + process.exit(1); +} + +function httpsGet(url, dest, maxRedirects = 5) { + return new Promise((resolve, reject) => { + if (maxRedirects <= 0) return reject(new Error('Too many redirects')); + https.get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + return httpsGet(res.headers.location, dest, maxRedirects - 1).then(resolve, reject); + } + if (res.statusCode !== 200) { + return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`)); + } + const stream = fs.createWriteStream(dest); + res.pipe(stream); + stream.on('finish', () => stream.close(resolve)); + stream.on('error', reject); + }).on('error', reject); + }); +} + +async function main() { + const dockerArgs = ['--rm', '-v', `${process.cwd()}:/github/workspace`, '-w', '/github/workspace']; + const bundleArgs = ['changelog', 'bundle', '--config', config, '--resolve', '--output', output]; + + if (releaseVersion) { + bundleArgs.push('--release-version', releaseVersion); + dockerArgs.push('-e', 'GITHUB_TOKEN'); + } + + if (report) { + let effectiveReport = report; + if (report.startsWith('https://')) { + const dest = path.join(process.cwd(), '.bundle-report.html'); + await httpsGet(report, dest); + effectiveReport = '.bundle-report.html'; + } else if (report.startsWith('http://')) { + console.error(`::error::Report URL must use HTTPS: ${report}`); + process.exit(1); + } + bundleArgs.push('--report', effectiveReport); + dockerArgs.push('--network', 'none'); + } + + if (prs) { + bundleArgs.push('--prs', prs); + dockerArgs.push('--network', 'none'); + } + + if (repo) bundleArgs.push('--repo', repo); + if (owner) bundleArgs.push('--owner', owner); + + fs.appendFileSync(githubOutput, `docker-args=${dockerArgs.join(' ')}\n`); + fs.appendFileSync(githubOutput, `bundle-args=${bundleArgs.join(' ')}\n`); +} + +main().catch((err) => { + console.error(`::error::${err.message}`); + process.exit(1); +}); From 803d97929b570171b9caef1f6ba01aeb40c1ac73 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 30 Mar 2026 23:24:07 -0300 Subject: [PATCH 07/19] Add support for profile-based bundling --- .github/workflows/changelog-bundle.yml | 33 ++++-- .gitignore | 1 + changelog/README.md | 55 +++++++++- changelog/bundle-create/README.md | 31 ++++-- changelog/bundle-create/action.yml | 58 +++++++--- changelog/bundle-create/package-lock.json | 30 ++++++ changelog/bundle-create/package.json | 6 ++ .../bundle-create/scripts/discover-output.js | 68 ++++++++++++ .../bundle-create/scripts/generate-bundle.js | 102 ++++++++++++++++-- 9 files changed, 341 insertions(+), 43 deletions(-) create mode 100644 changelog/bundle-create/package-lock.json create mode 100644 changelog/bundle-create/package.json create mode 100644 changelog/bundle-create/scripts/discover-output.js diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml index a7ef002..a44d955 100644 --- a/.github/workflows/changelog-bundle.yml +++ b/.github/workflows/changelog-bundle.yml @@ -7,25 +7,37 @@ on: description: 'Path to changelog.yml configuration file' type: string default: 'docs/changelog.yml' + profile: + description: > + Bundle profile name from bundle.profiles in changelog.yml. + Mutually exclusive with release-version and prs. + type: string + version: + description: > + Version string for profile mode (e.g. 9.2.0). Only valid with profile. + type: string release-version: description: > GitHub release tag used as the PR filter source (e.g. v9.2.0). - Mutually exclusive with report and prs. + Mutually exclusive with profile, report, and prs. type: string report: description: > Buildkite promotion report HTTPS URL or local file path. - Mutually exclusive with release-version and prs. + Mutually exclusive with release-version and prs in option mode. + In profile mode, passed as a positional argument. type: string prs: description: > Comma-separated PR URLs or numbers, or a path to a newline-delimited file. - Mutually exclusive with release-version and report. + Mutually exclusive with profile, release-version, and report. type: string output: - description: 'Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml)' + description: > + Output file path for the bundle (e.g. docs/releases/v9.2.0.yaml). + Optional. When not provided, the path is determined by the config + (bundle.output_directory) and discovered automatically after generation. type: string - required: true base-branch: description: 'Base branch for the pull request (defaults to repository default branch)' type: string @@ -39,7 +51,7 @@ on: permissions: {} concurrency: - group: changelog-bundle-${{ inputs.output }} + group: changelog-bundle-${{ inputs.output || inputs.profile }} cancel-in-progress: false jobs: @@ -47,10 +59,15 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + outputs: + output: ${{ steps.bundle.outputs.output }} steps: - - uses: elastic/docs-actions/changelog/bundle-create@v1 + - id: bundle + uses: elastic/docs-actions/changelog/bundle-create@v1 with: config: ${{ inputs.config }} + profile: ${{ inputs.profile }} + version: ${{ inputs.version }} release-version: ${{ inputs.release-version }} report: ${{ inputs.report }} prs: ${{ inputs.prs }} @@ -68,6 +85,6 @@ jobs: steps: - uses: elastic/docs-actions/changelog/bundle-pr@v1 with: - output: ${{ inputs.output }} + output: ${{ needs.generate.outputs.output }} base-branch: ${{ inputs.base-branch }} github-token: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 9f11b75..1fe1b00 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/ +node_modules/ diff --git a/changelog/README.md b/changelog/README.md index 7f3d478..30f70a6 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -182,13 +182,24 @@ Each PR produces a file at `docs/changelog/{filename}.yaml` on the PR branch (wh ## Bundling changelogs -Individual changelog files accumulate on the default branch as PRs merge. The bundle action generates a fully-resolved YAML file containing only the changelog entries that match a given filter. Three filter sources are supported: +Individual changelog files accumulate on the default branch as PRs merge. The bundle action generates a fully-resolved YAML file containing only the changelog entries that match a given filter. The action supports two modes: + +**Option mode** — you specify the filter and output path directly: - **GitHub release version** (`release-version`) — pulls PR references directly from GitHub release notes. Used for stack and product releases triggered by `on: release`. - **Buildkite promotion report** (`report`) — extracts PR URLs from a promotion report. Used for serverless releases discovered by a scheduled workflow. - **PR list** (`prs`) — an explicit list of PR URLs or numbers (comma-separated), or a path to a newline-delimited file. Used when the caller already knows which PRs to include. -Exactly one filter source must be provided per invocation. The bundle always includes the full content of each matching entry (`--resolve`), so downstream consumers can render changelogs without access to the original files. +Exactly one filter source must be provided. The `output` path is optional — when not provided, docs-builder writes to the config-driven path (`bundle.output_directory`) and the action discovers the file automatically. + +**Profile mode** — all configuration comes from `bundle.profiles` in `changelog.yml`: + +- **Profile** (`profile`) — a named profile that defines the product filter, output filename pattern, and other settings. The `version` input provides the value for `{version}` substitution in profile patterns. +- An optional `report` can be passed as a positional argument to filter by promotion report. +- The `output` path is optional — if not provided, it's discovered from `bundle.output_directory` in the config. +- `bundle.resolve: true` must be set in the config (it cannot be forced via CLI in profile mode). + +The bundle always includes the full content of each matching entry, so downstream consumers can render changelogs without access to the original files. ### Prerequisites @@ -334,6 +345,46 @@ jobs: When using a file, every line must be a fully-qualified GitHub PR URL (e.g. `https://github.com/owner/repo/pull/123`). Bare numbers are only supported in the comma-separated format. +#### Profile-based bundling + +When your repository has `bundle.profiles` configured in `changelog.yml`, the profile drives which changelogs to include and where to write the bundle. Set `bundle.resolve: true` in the config so entry contents are inlined. + +```yaml +bundle: + directory: docs/changelog + output_directory: docs/releases + resolve: true + repo: my-repo + owner: elastic + profiles: + my-release: + products: "my-product {version} {lifecycle}" + output: "{version}.yaml" +``` + +**`.github/workflows/changelog-bundle.yml`** + +```yaml +name: changelog-bundle + +on: + release: + types: [published] + +permissions: + contents: write + pull-requests: write + +jobs: + bundle: + uses: elastic/docs-actions/.github/workflows/changelog-bundle.yml@v1 + with: + profile: my-release + version: ${{ github.event.release.tag_name }} +``` + +The `output` input is not needed — the action discovers the generated file from `bundle.output_directory` in the config. If a promotion report is also needed, pass it via the `report` input. + #### Custom config path If your changelog configuration is not at `docs/changelog.yml`, pass the path explicitly: diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md index e5ca016..0239048 100644 --- a/changelog/bundle-create/README.md +++ b/changelog/bundle-create/README.md @@ -1,7 +1,7 @@ # changelog/bundle-create -Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an artifact. Supports filtering by GitHub release version, Buildkite promotion report, or an explicit list of PR URLs/numbers. Uses --network none for report and prs modes. +Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an artifact. Supports option-based filtering (release-version, report, prs) and profile-based bundling. Uses --network none where possible. ## Inputs @@ -9,23 +9,29 @@ Checks out the repository, runs docs-builder changelog bundle in Docker to gener | Name | Description | Required | Default | |-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| | `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | -| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with report and prs. Requires repo to be set in changelog.yml or via repo input. | `false` | | -| `report` | Buildkite promotion report URL or local file path used as the PR filter source. Mutually exclusive with release-version and prs. Local paths must be relative to repo root. | `false` | | -| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with release-version and report. Bare numbers require repo/owner in changelog.yml or inputs. | `false` | | -| `output` | Output file path for the bundle, relative to the repo root (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. | `true` | | +| `profile` | Bundle profile name from bundle.profiles in changelog.yml. Mutually exclusive with release-version and prs. All paths and filters come from config. | `false` | | +| `version` | Version string for profile mode (e.g. 9.2.0, 2026-03). Used for {version} substitution. Only valid with profile. | `false` | | +| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs. | `false` | | +| `report` | Buildkite promotion report URL or local file path. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. | `false` | | +| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner in config. | `false` | | +| `output` | Output file path for the bundle. Optional. When not provided, determined by config (bundle.output_directory) and discovered automatically. | `false` | | | `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml. | `false` | | | `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. | `false` | | -| `github-token` | GitHub token (needed for release-version to access GitHub API) | `false` | `${{ github.token }}` | +| `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | +| `github-token` | GitHub token (needed for release-version and source: github_release profiles) | `false` | `${{ github.token }}` | ## Outputs -| Name | Description | -|------|-------------| +| Name | Description | +|----------|------------------------------------------| +| `output` | Resolved output file path for the bundle | ## Usage + +Option mode: ```yaml steps: - uses: elastic/docs-actions/changelog/bundle-create@v1 @@ -33,4 +39,13 @@ steps: release-version: v9.2.0 output: docs/releases/v9.2.0.yaml ``` + +Profile mode: +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle-create@v1 + with: + profile: elasticsearch-release + version: 9.2.0 +``` diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 61b6210..6a32b56 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -2,35 +2,44 @@ name: Changelog bundle create description: > Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an - artifact. Supports filtering by GitHub release version, Buildkite - promotion report, or an explicit list of PR URLs/numbers. - Uses --network none for report and prs modes. + artifact. Supports option-based filtering (release-version, report, prs) + and profile-based bundling. Uses --network none where possible. inputs: config: description: 'Path to changelog.yml configuration file' default: 'docs/changelog.yml' + profile: + description: > + Bundle profile name from bundle.profiles in changelog.yml. + Mutually exclusive with release-version and prs. When set, + all paths and filters come from the config. + version: + description: > + Version string for profile mode (e.g. 9.2.0, 2026-03). + Used for {version} substitution in profile patterns. + Only valid with profile. release-version: description: > GitHub release tag used as the PR filter source (e.g. v9.2.0). - Mutually exclusive with report and prs. Requires repo to be set in - changelog.yml (bundle.repo) or passed via the repo input. + Mutually exclusive with profile, report, and prs. report: description: > Buildkite promotion report URL or local file path used as the - PR filter source. Mutually exclusive with release-version and prs. - Local paths must be relative to the repo root. + PR filter source. In option mode, mutually exclusive with + release-version and prs. In profile mode, passed as a positional + argument. Local paths must be relative to the repo root. prs: description: > Comma-separated PR URLs or numbers, or a path to a newline-delimited - file. Mutually exclusive with release-version and report. - Bare numbers require repo/owner to be set in changelog.yml or - passed via the repo and owner inputs. + file. Mutually exclusive with profile, release-version, and report. + Bare numbers require repo/owner to be set in changelog.yml or inputs. output: description: > - Output file path for the bundle, relative to the repo root - (e.g. docs/releases/v9.2.0.yaml). Must end in .yml or .yaml. - required: true + Output file path for the bundle, relative to the repo root. + Optional in both modes. When not provided, docs-builder writes + to the path determined by the config (bundle.output_directory) + and the action discovers the generated file automatically. repo: description: > GitHub repository name. Falls back to bundle.repo in changelog.yml. @@ -42,9 +51,14 @@ inputs: description: 'Name for the uploaded artifact (must match bundle-pr artifact-name)' default: 'changelog-bundle' github-token: - description: 'GitHub token (needed for release-version to access GitHub API)' + description: 'GitHub token (needed for release-version and source: github_release profiles)' default: '${{ github.token }}' +outputs: + output: + description: 'Resolved output file path for the bundle' + value: ${{ steps.discover.outputs.output }} + runs: using: composite steps: @@ -53,11 +67,17 @@ runs: with: persist-credentials: false + - name: Install dependencies + shell: bash + run: npm ci --prefix "${{ github.action_path }}" + - name: Prepare bundle arguments id: prepare shell: bash env: CONFIG: ${{ inputs.config }} + PROFILE: ${{ inputs.profile }} + VERSION: ${{ inputs.version }} RELEASE_VERSION: ${{ inputs.release-version }} REPORT: ${{ inputs.report }} PRS: ${{ inputs.prs }} @@ -72,10 +92,18 @@ runs: GITHUB_TOKEN: ${{ inputs.github-token }} run: docker run ${{ steps.prepare.outputs.docker-args }} ghcr.io/elastic/docs-builder:edge ${{ steps.prepare.outputs.bundle-args }} + - name: Discover output path + id: discover + shell: bash + env: + CONFIG: ${{ inputs.config }} + OUTPUT: ${{ inputs.output }} + run: node "${{ github.action_path }}/scripts/discover-output.js" + - name: Upload bundle artifact uses: actions/upload-artifact@v6 with: name: ${{ inputs.artifact-name }} - path: ${{ inputs.output }} + path: ${{ steps.discover.outputs.output }} if-no-files-found: error retention-days: 1 diff --git a/changelog/bundle-create/package-lock.json b/changelog/bundle-create/package-lock.json new file mode 100644 index 0000000..d328c9c --- /dev/null +++ b/changelog/bundle-create/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "bundle-create", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "js-yaml": "^4.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + } + } +} diff --git a/changelog/bundle-create/package.json b/changelog/bundle-create/package.json new file mode 100644 index 0000000..4267179 --- /dev/null +++ b/changelog/bundle-create/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "dependencies": { + "js-yaml": "^4.1.0" + } +} diff --git a/changelog/bundle-create/scripts/discover-output.js b/changelog/bundle-create/scripts/discover-output.js new file mode 100644 index 0000000..3f77799 --- /dev/null +++ b/changelog/bundle-create/scripts/discover-output.js @@ -0,0 +1,68 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const configPath = process.env.CONFIG || 'docs/changelog.yml'; +const explicitOutput = process.env.OUTPUT; +const githubOutput = process.env.GITHUB_OUTPUT; + +function loadConfig(filePath) { + try { + return yaml.load(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +function findNewestYaml(dir, markerPath) { + if (!fs.existsSync(dir)) { + console.error(`::error::Output directory does not exist: ${dir}`); + process.exit(1); + } + + const markerTime = fs.existsSync(markerPath) + ? fs.statSync(markerPath).mtimeMs + : 0; + + const files = fs.readdirSync(dir) + .filter(f => f.endsWith('.yaml') || f.endsWith('.yml')) + .map(f => { + const full = path.join(dir, f); + return { path: full, mtime: fs.statSync(full).mtimeMs }; + }) + .filter(f => f.mtime > markerTime) + .sort((a, b) => b.mtime - a.mtime); + + return files.length > 0 ? files[0].path : null; +} + +if (explicitOutput) { + if (!fs.existsSync(explicitOutput)) { + console.error(`::error::Expected bundle file not found at: ${explicitOutput}`); + process.exit(1); + } + fs.appendFileSync(githubOutput, `output=${explicitOutput}\n`); + console.log(`Bundle output (explicit): ${explicitOutput}`); + process.exit(0); +} + +const cfg = loadConfig(configPath); +const outputDir = cfg?.bundle?.output_directory; + +if (!outputDir) { + console.error(`::error::Cannot discover bundle output: 'output' input not provided and bundle.output_directory not set in ${configPath}`); + process.exit(1); +} + +const markerPath = path.join(process.cwd(), '.bundle-marker'); +const discovered = findNewestYaml(outputDir, markerPath); + +if (!discovered) { + console.error(`::error::No bundle file found in ${outputDir} after generation`); + process.exit(1); +} + +fs.appendFileSync(githubOutput, `output=${discovered}\n`); +console.log(`Bundle output (discovered): ${discovered}`); diff --git a/changelog/bundle-create/scripts/generate-bundle.js b/changelog/bundle-create/scripts/generate-bundle.js index 5f09088..15d14cb 100644 --- a/changelog/bundle-create/scripts/generate-bundle.js +++ b/changelog/bundle-create/scripts/generate-bundle.js @@ -1,8 +1,13 @@ +'use strict'; + const fs = require('fs'); const https = require('https'); const path = require('path'); +const yaml = require('js-yaml'); const config = process.env.CONFIG; +const profile = process.env.PROFILE; +const version = process.env.VERSION; const releaseVersion = process.env.RELEASE_VERSION; const report = process.env.REPORT; const prs = process.env.PRS; @@ -11,14 +16,12 @@ const repo = process.env.REPO; const owner = process.env.OWNER; const githubOutput = process.env.GITHUB_OUTPUT; -const filters = [releaseVersion, report, prs].filter(Boolean); -if (filters.length === 0) { - console.error('::error::Exactly one of release-version, report, or prs must be provided'); - process.exit(1); -} -if (filters.length > 1) { - console.error('::error::Only one of release-version, report, or prs may be provided'); - process.exit(1); +function loadConfig(configPath) { + try { + return yaml.load(fs.readFileSync(configPath, 'utf8')); + } catch (e) { + return null; + } } function httpsGet(url, dest, maxRedirects = 5) { @@ -39,9 +42,22 @@ function httpsGet(url, dest, maxRedirects = 5) { }); } -async function main() { +async function buildOptionMode() { + const filters = [releaseVersion, report, prs].filter(Boolean); + if (filters.length === 0) { + console.error('::error::Exactly one of release-version, report, or prs must be provided in option mode'); + process.exit(1); + } + if (filters.length > 1) { + console.error('::error::Only one of release-version, report, or prs may be provided'); + process.exit(1); + } const dockerArgs = ['--rm', '-v', `${process.cwd()}:/github/workspace`, '-w', '/github/workspace']; - const bundleArgs = ['changelog', 'bundle', '--config', config, '--resolve', '--output', output]; + const bundleArgs = ['changelog', 'bundle', '--config', config, '--resolve']; + + if (output) { + bundleArgs.push('--output', output); + } if (releaseVersion) { bundleArgs.push('--release-version', releaseVersion); @@ -70,6 +86,72 @@ async function main() { if (repo) bundleArgs.push('--repo', repo); if (owner) bundleArgs.push('--owner', owner); + return { dockerArgs, bundleArgs }; +} + +async function buildProfileMode() { + const optionFilters = [releaseVersion, prs].filter(Boolean); + if (optionFilters.length > 0) { + console.error('::error::release-version and prs cannot be used with profile mode'); + process.exit(1); + } + + const cfg = loadConfig(config); + if (cfg) { + const profiles = cfg?.bundle?.profiles; + if (!profiles || !profiles[profile]) { + const available = profiles ? Object.keys(profiles).join(', ') : 'none'; + console.error(`::error::Profile '${profile}' not found in ${config}. Available: ${available}`); + process.exit(1); + } + + const profileConfig = profiles[profile]; + const needsNetwork = profileConfig.source === 'github_release'; + + if (needsNetwork) { + console.log(`Profile '${profile}' uses source: github_release (network access required)`); + } + } + + const dockerArgs = ['--rm', '-v', `${process.cwd()}:/github/workspace`, '-w', '/github/workspace']; + const bundleArgs = ['changelog', 'bundle', profile]; + + if (version) bundleArgs.push(version); + + if (report) { + let effectiveReport = report; + if (report.startsWith('https://')) { + const dest = path.join(process.cwd(), '.bundle-report.html'); + await httpsGet(report, dest); + effectiveReport = '.bundle-report.html'; + } else if (report.startsWith('http://')) { + console.error(`::error::Report URL must use HTTPS: ${report}`); + process.exit(1); + } + bundleArgs.push(effectiveReport); + } + + const cfg2 = loadConfig(config); + const profileConfig = cfg2?.bundle?.profiles?.[profile]; + const needsNetwork = profileConfig?.source === 'github_release'; + + if (needsNetwork) { + dockerArgs.push('-e', 'GITHUB_TOKEN'); + } else if (!report || !report.startsWith('https://')) { + dockerArgs.push('--network', 'none'); + } + + return { dockerArgs, bundleArgs }; +} + +async function main() { + const isProfileMode = Boolean(profile); + const { dockerArgs, bundleArgs } = isProfileMode + ? await buildProfileMode() + : await buildOptionMode(); + + fs.writeFileSync(path.join(process.cwd(), '.bundle-marker'), ''); + fs.appendFileSync(githubOutput, `docker-args=${dockerArgs.join(' ')}\n`); fs.appendFileSync(githubOutput, `bundle-args=${bundleArgs.join(' ')}\n`); } From 7982c5fc4738e98bcb8efaaf6436f601694a2811 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 30 Mar 2026 23:27:26 -0300 Subject: [PATCH 08/19] Use env vars for docker run --- changelog/bundle-create/action.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 6a32b56..141ef99 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -90,7 +90,9 @@ runs: shell: bash env: GITHUB_TOKEN: ${{ inputs.github-token }} - run: docker run ${{ steps.prepare.outputs.docker-args }} ghcr.io/elastic/docs-builder:edge ${{ steps.prepare.outputs.bundle-args }} + DOCKER_ARGS: ${{ steps.prepare.outputs.docker-args }} + BUNDLE_ARGS: ${{ steps.prepare.outputs.bundle-args }} + run: docker run $DOCKER_ARGS ghcr.io/elastic/docs-builder:edge $BUNDLE_ARGS - name: Discover output path id: discover From 2e773dc7e1f217a2de2dc9fab2f985a0437d1acc Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 30 Mar 2026 23:28:08 -0300 Subject: [PATCH 09/19] Validate paths --- .../bundle-create/scripts/generate-bundle.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/changelog/bundle-create/scripts/generate-bundle.js b/changelog/bundle-create/scripts/generate-bundle.js index 15d14cb..cbf1f45 100644 --- a/changelog/bundle-create/scripts/generate-bundle.js +++ b/changelog/bundle-create/scripts/generate-bundle.js @@ -16,6 +16,28 @@ const repo = process.env.REPO; const owner = process.env.OWNER; const githubOutput = process.env.GITHUB_OUTPUT; +function validatePath(value, name) { + if (!value) return; + if (value.startsWith('/')) { + console.error(`::error::${name} must be a relative path: ${value}`); + process.exit(1); + } + if (value.includes('..')) { + console.error(`::error::${name} must not contain '..': ${value}`); + process.exit(1); + } +} + +validatePath(config, 'config'); +validatePath(output, 'output'); + +if (report && !report.startsWith('https://') && !report.startsWith('http://')) { + validatePath(report, 'report'); +} +if (prs && (prs.includes('/') || prs.endsWith('.txt'))) { + validatePath(prs, 'prs'); +} + function loadConfig(configPath) { try { return yaml.load(fs.readFileSync(configPath, 'utf8')); @@ -90,6 +112,11 @@ async function buildOptionMode() { } async function buildProfileMode() { + if (!/^[a-zA-Z0-9._-]+$/.test(profile)) { + console.error(`::error::Profile name contains disallowed characters: ${profile}`); + process.exit(1); + } + const optionFilters = [releaseVersion, prs].filter(Boolean); if (optionFilters.length > 0) { console.error('::error::release-version and prs cannot be used with profile mode'); From 0e5b77c6b0533810432d7e7eaa1abf680c0f3c59 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 31 Mar 2026 10:27:47 -0300 Subject: [PATCH 10/19] Update README --- changelog/bundle-create/README.md | 28 ++++++++++++++-------------- changelog/bundle-pr/README.md | 12 +++++++----- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md index 0239048..83dae28 100644 --- a/changelog/bundle-create/README.md +++ b/changelog/bundle-create/README.md @@ -1,24 +1,24 @@ -# changelog/bundle-create +# Changelog bundle create Checks out the repository, runs docs-builder changelog bundle in Docker to generate a fully-resolved bundle file, and uploads the result as an artifact. Supports option-based filtering (release-version, report, prs) and profile-based bundling. Uses --network none where possible. ## Inputs -| Name | Description | Required | Default | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| -| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | -| `profile` | Bundle profile name from bundle.profiles in changelog.yml. Mutually exclusive with release-version and prs. All paths and filters come from config. | `false` | | -| `version` | Version string for profile mode (e.g. 9.2.0, 2026-03). Used for {version} substitution. Only valid with profile. | `false` | | -| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs. | `false` | | -| `report` | Buildkite promotion report URL or local file path. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. | `false` | | -| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner in config. | `false` | | -| `output` | Output file path for the bundle. Optional. When not provided, determined by config (bundle.output_directory) and discovered automatically. | `false` | | -| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml. | `false` | | -| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. | `false` | | -| `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | -| `github-token` | GitHub token (needed for release-version and source: github_release profiles) | `false` | `${{ github.token }}` | +| Name | Description | Required | Default | +|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `profile` | Bundle profile name from bundle.profiles in changelog.yml. Mutually exclusive with release-version and prs. When set, all paths and filters come from the config.
| `false` | ` ` | +| `version` | Version string for profile mode (e.g. 9.2.0, 2026-03). Used for {version} substitution in profile patterns. Only valid with profile.
| `false` | ` ` | +| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs.
| `false` | ` ` | +| `report` | Buildkite promotion report URL or local file path used as the PR filter source. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. Local paths must be relative to the repo root.
| `false` | ` ` | +| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner to be set in changelog.yml or inputs.
| `false` | ` ` | +| `output` | Output file path for the bundle, relative to the repo root. Optional in both modes. When not provided, docs-builder writes to the path determined by the config (bundle.output_directory) and the action discovers the generated file automatically.
| `false` | ` ` | +| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml.
| `false` | ` ` | +| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic.
| `false` | ` ` | +| `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | +| `github-token` | GitHub token (needed for release-version and source: github_release profiles) | `false` | `${{ github.token }}` | ## Outputs diff --git a/changelog/bundle-pr/README.md b/changelog/bundle-pr/README.md index 6c2d8f5..8ec07da 100644 --- a/changelog/bundle-pr/README.md +++ b/changelog/bundle-pr/README.md @@ -1,15 +1,17 @@ -# changelog/bundle-pr +# Changelog bundle PR Downloads a changelog bundle artifact and opens a pull request to add it to the repository. If a PR already exists for the same bundle, it is updated in place. ## Inputs -| Name | Description | Required | Default | -|----------------|----------------------------------------------------------------------------------------------------------------------|----------|-----------------------| -| `output` | Output file path for the bundle, relative to the repo root (e.g. docs/releases/v9.2.0.yaml). Must match the bundle-create action. | `true` | | -| `github-token` | GitHub token with contents:write and pull-requests:write permissions | `false` | `${{ github.token }}` | +| Name | Description | Required | Default | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `output` | Output file path for the bundle, relative to the repo root (e.g. docs/releases/v9.2.0.yaml). Must match the path used by the bundle-create action.
| `true` | ` ` | +| `base-branch` | Base branch for the pull request (defaults to repository default branch) | `false` | ` ` | +| `artifact-name` | Name of the artifact uploaded by bundle-create | `false` | `changelog-bundle` | +| `github-token` | GitHub token with contents:write and pull-requests:write permissions | `false` | `${{ github.token }}` | ## Outputs From 687861d0c8988e5ada69b8232bf1e665640b2831 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 2 Apr 2026 18:53:41 -0300 Subject: [PATCH 11/19] Adjust action to use docs-builder changelog bundle --plan --- changelog/bundle-create/README.md | 2 +- changelog/bundle-create/action.yml | 142 +++++++++++-- changelog/bundle-create/package-lock.json | 30 --- changelog/bundle-create/package.json | 6 - .../bundle-create/scripts/discover-output.js | 68 ------- .../bundle-create/scripts/generate-bundle.js | 189 ------------------ 6 files changed, 127 insertions(+), 310 deletions(-) delete mode 100644 changelog/bundle-create/package-lock.json delete mode 100644 changelog/bundle-create/package.json delete mode 100644 changelog/bundle-create/scripts/discover-output.js delete mode 100644 changelog/bundle-create/scripts/generate-bundle.js diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md index 83dae28..634e9db 100644 --- a/changelog/bundle-create/README.md +++ b/changelog/bundle-create/README.md @@ -14,7 +14,7 @@ Checks out the repository, runs docs-builder changelog bundle in Docker to gener | `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs.
| `false` | ` ` | | `report` | Buildkite promotion report URL or local file path used as the PR filter source. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. Local paths must be relative to the repo root.
| `false` | ` ` | | `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner to be set in changelog.yml or inputs.
| `false` | ` ` | -| `output` | Output file path for the bundle, relative to the repo root. Optional in both modes. When not provided, docs-builder writes to the path determined by the config (bundle.output_directory) and the action discovers the generated file automatically.
| `false` | ` ` | +| `output` | Output file path for the bundle, relative to the repo root. Optional in both modes. When not provided, docs-builder writes to the path determined by the config (bundle.output_directory) and the plan resolves the generated file path automatically.
| `false` | ` ` | | `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml.
| `false` | ` ` | | `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic.
| `false` | ` ` | | `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 141ef99..773cbbb 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -39,7 +39,7 @@ inputs: Output file path for the bundle, relative to the repo root. Optional in both modes. When not provided, docs-builder writes to the path determined by the config (bundle.output_directory) - and the action discovers the generated file automatically. + and the plan resolves the generated file path automatically. repo: description: > GitHub repository name. Falls back to bundle.repo in changelog.yml. @@ -57,7 +57,7 @@ inputs: outputs: output: description: 'Resolved output file path for the bundle' - value: ${{ steps.discover.outputs.output }} + value: ${{ steps.plan.outputs.output_path }} runs: using: composite @@ -67,12 +67,45 @@ runs: with: persist-credentials: false - - name: Install dependencies + - name: Validate inputs shell: bash - run: npm ci --prefix "${{ github.action_path }}" + env: + CONFIG: ${{ inputs.config }} + PROFILE: ${{ inputs.profile }} + OUTPUT: ${{ inputs.output }} + REPORT: ${{ inputs.report }} + PRS: ${{ inputs.prs }} + run: | + validate_path() { + local value="$1" name="$2" + [ -z "$value" ] && return + if [[ "$value" == /* ]]; then + echo "::error::${name} must be a relative path: ${value}"; exit 1 + fi + if [[ "$value" == *..* ]]; then + echo "::error::${name} must not contain '..': ${value}"; exit 1 + fi + } + validate_path "$CONFIG" "config" + validate_path "$OUTPUT" "output" + + if [ -n "$REPORT" ] && [[ "$REPORT" != https://* ]] && [[ "$REPORT" != http://* ]]; then + validate_path "$REPORT" "report" + fi + if [ -n "$PRS" ] && { [[ "$PRS" == */* ]] || [[ "$PRS" == *.txt ]]; }; then + validate_path "$PRS" "prs" + fi + + if [ -n "$PROFILE" ] && [[ ! "$PROFILE" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "::error::Profile name contains disallowed characters: ${PROFILE}"; exit 1 + fi - - name: Prepare bundle arguments - id: prepare + if [ -n "$REPORT" ] && [[ "$REPORT" == http://* ]]; then + echo "::error::Report URL must use HTTPS: ${REPORT}"; exit 1 + fi + + - name: Resolve bundle plan + id: plan shell: bash env: CONFIG: ${{ inputs.config }} @@ -84,28 +117,105 @@ runs: OUTPUT: ${{ inputs.output }} REPO: ${{ inputs.repo }} OWNER: ${{ inputs.owner }} - run: node "${{ github.action_path }}/scripts/generate-bundle.js" + run: | + ARGS=() + ARGS+=(changelog bundle) + if [ -n "$PROFILE" ]; then + ARGS+=("$PROFILE") + [ -n "$VERSION" ] && ARGS+=("$VERSION") + [ -n "$REPORT" ] && ARGS+=("$REPORT") + else + ARGS+=(--config "$CONFIG" --resolve) + [ -n "$RELEASE_VERSION" ] && ARGS+=(--release-version "$RELEASE_VERSION") + [ -n "$REPORT" ] && ARGS+=(--report "$REPORT") + [ -n "$PRS" ] && ARGS+=(--prs "$PRS") + [ -n "$OUTPUT" ] && ARGS+=(--output "$OUTPUT") + [ -n "$REPO" ] && ARGS+=(--repo "$REPO") + [ -n "$OWNER" ] && ARGS+=(--owner "$OWNER") + fi - - name: Generate changelog bundle + PLAN_JSON=$(docker run --rm --network none \ + -v "${PWD}:/github/workspace" \ + -w /github/workspace \ + ghcr.io/elastic/docs-builder:edge \ + "${ARGS[@]}" --plan) + + echo "$PLAN_JSON" | jq . + + NEEDS_NETWORK=$(echo "$PLAN_JSON" | jq -r '.needs_network') + NEEDS_GITHUB_TOKEN=$(echo "$PLAN_JSON" | jq -r '.needs_github_token') + OUTPUT_PATH=$(echo "$PLAN_JSON" | jq -r '.output_path // empty') + + echo "needs_network=$NEEDS_NETWORK" >> "$GITHUB_OUTPUT" + echo "needs_github_token=$NEEDS_GITHUB_TOKEN" >> "$GITHUB_OUTPUT" + echo "output_path=$OUTPUT_PATH" >> "$GITHUB_OUTPUT" + + if [ -z "$OUTPUT_PATH" ]; then + echo "::error::Plan did not resolve an output path. Set 'output' input or configure bundle.output_directory in changelog.yml." + exit 1 + fi + + - name: Download report + if: inputs.report != '' && startsWith(inputs.report, 'https://') shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} - DOCKER_ARGS: ${{ steps.prepare.outputs.docker-args }} - BUNDLE_ARGS: ${{ steps.prepare.outputs.bundle-args }} - run: docker run $DOCKER_ARGS ghcr.io/elastic/docs-builder:edge $BUNDLE_ARGS + REPORT: ${{ inputs.report }} + run: curl --fail --silent --show-error --location --max-redirs 5 --max-time 30 "$REPORT" -o .bundle-report.html - - name: Discover output path - id: discover + - name: Generate changelog bundle shell: bash env: + GITHUB_TOKEN: ${{ inputs.github-token }} + NEEDS_NETWORK: ${{ steps.plan.outputs.needs_network }} + NEEDS_GITHUB_TOKEN: ${{ steps.plan.outputs.needs_github_token }} CONFIG: ${{ inputs.config }} + PROFILE: ${{ inputs.profile }} + VERSION: ${{ inputs.version }} + RELEASE_VERSION: ${{ inputs.release-version }} + REPORT: ${{ inputs.report }} + PRS: ${{ inputs.prs }} OUTPUT: ${{ inputs.output }} - run: node "${{ github.action_path }}/scripts/discover-output.js" + REPO: ${{ inputs.repo }} + OWNER: ${{ inputs.owner }} + run: | + DOCKER_ARGS=(--rm -v "${PWD}:/github/workspace" -w /github/workspace) + + if [ "$NEEDS_NETWORK" = "true" ]; then + [ "$NEEDS_GITHUB_TOKEN" = "true" ] && DOCKER_ARGS+=(-e GITHUB_TOKEN) + else + DOCKER_ARGS+=(--network none) + fi + + resolve_report() { + if [ -n "$1" ] && [[ "$1" == https://* ]]; then + echo ".bundle-report.html" + else + echo "$1" + fi + } + + BUNDLE_ARGS=() + BUNDLE_ARGS+=(changelog bundle) + if [ -n "$PROFILE" ]; then + BUNDLE_ARGS+=("$PROFILE") + [ -n "$VERSION" ] && BUNDLE_ARGS+=("$VERSION") + [ -n "$REPORT" ] && BUNDLE_ARGS+=("$(resolve_report "$REPORT")") + else + BUNDLE_ARGS+=(--config "$CONFIG" --resolve) + [ -n "$RELEASE_VERSION" ] && BUNDLE_ARGS+=(--release-version "$RELEASE_VERSION") + [ -n "$REPORT" ] && BUNDLE_ARGS+=(--report "$(resolve_report "$REPORT")") + [ -n "$PRS" ] && BUNDLE_ARGS+=(--prs "$PRS") + [ -n "$OUTPUT" ] && BUNDLE_ARGS+=(--output "$OUTPUT") + [ -n "$REPO" ] && BUNDLE_ARGS+=(--repo "$REPO") + [ -n "$OWNER" ] && BUNDLE_ARGS+=(--owner "$OWNER") + fi + + docker run "${DOCKER_ARGS[@]}" ghcr.io/elastic/docs-builder:edge "${BUNDLE_ARGS[@]}" - name: Upload bundle artifact uses: actions/upload-artifact@v6 with: name: ${{ inputs.artifact-name }} - path: ${{ steps.discover.outputs.output }} + path: ${{ steps.plan.outputs.output_path }} if-no-files-found: error retention-days: 1 diff --git a/changelog/bundle-create/package-lock.json b/changelog/bundle-create/package-lock.json deleted file mode 100644 index d328c9c..0000000 --- a/changelog/bundle-create/package-lock.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "bundle-create", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "js-yaml": "^4.1.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - } - } -} diff --git a/changelog/bundle-create/package.json b/changelog/bundle-create/package.json deleted file mode 100644 index 4267179..0000000 --- a/changelog/bundle-create/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "private": true, - "dependencies": { - "js-yaml": "^4.1.0" - } -} diff --git a/changelog/bundle-create/scripts/discover-output.js b/changelog/bundle-create/scripts/discover-output.js deleted file mode 100644 index 3f77799..0000000 --- a/changelog/bundle-create/scripts/discover-output.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const yaml = require('js-yaml'); - -const configPath = process.env.CONFIG || 'docs/changelog.yml'; -const explicitOutput = process.env.OUTPUT; -const githubOutput = process.env.GITHUB_OUTPUT; - -function loadConfig(filePath) { - try { - return yaml.load(fs.readFileSync(filePath, 'utf8')); - } catch (e) { - return null; - } -} - -function findNewestYaml(dir, markerPath) { - if (!fs.existsSync(dir)) { - console.error(`::error::Output directory does not exist: ${dir}`); - process.exit(1); - } - - const markerTime = fs.existsSync(markerPath) - ? fs.statSync(markerPath).mtimeMs - : 0; - - const files = fs.readdirSync(dir) - .filter(f => f.endsWith('.yaml') || f.endsWith('.yml')) - .map(f => { - const full = path.join(dir, f); - return { path: full, mtime: fs.statSync(full).mtimeMs }; - }) - .filter(f => f.mtime > markerTime) - .sort((a, b) => b.mtime - a.mtime); - - return files.length > 0 ? files[0].path : null; -} - -if (explicitOutput) { - if (!fs.existsSync(explicitOutput)) { - console.error(`::error::Expected bundle file not found at: ${explicitOutput}`); - process.exit(1); - } - fs.appendFileSync(githubOutput, `output=${explicitOutput}\n`); - console.log(`Bundle output (explicit): ${explicitOutput}`); - process.exit(0); -} - -const cfg = loadConfig(configPath); -const outputDir = cfg?.bundle?.output_directory; - -if (!outputDir) { - console.error(`::error::Cannot discover bundle output: 'output' input not provided and bundle.output_directory not set in ${configPath}`); - process.exit(1); -} - -const markerPath = path.join(process.cwd(), '.bundle-marker'); -const discovered = findNewestYaml(outputDir, markerPath); - -if (!discovered) { - console.error(`::error::No bundle file found in ${outputDir} after generation`); - process.exit(1); -} - -fs.appendFileSync(githubOutput, `output=${discovered}\n`); -console.log(`Bundle output (discovered): ${discovered}`); diff --git a/changelog/bundle-create/scripts/generate-bundle.js b/changelog/bundle-create/scripts/generate-bundle.js deleted file mode 100644 index cbf1f45..0000000 --- a/changelog/bundle-create/scripts/generate-bundle.js +++ /dev/null @@ -1,189 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const https = require('https'); -const path = require('path'); -const yaml = require('js-yaml'); - -const config = process.env.CONFIG; -const profile = process.env.PROFILE; -const version = process.env.VERSION; -const releaseVersion = process.env.RELEASE_VERSION; -const report = process.env.REPORT; -const prs = process.env.PRS; -const output = process.env.OUTPUT; -const repo = process.env.REPO; -const owner = process.env.OWNER; -const githubOutput = process.env.GITHUB_OUTPUT; - -function validatePath(value, name) { - if (!value) return; - if (value.startsWith('/')) { - console.error(`::error::${name} must be a relative path: ${value}`); - process.exit(1); - } - if (value.includes('..')) { - console.error(`::error::${name} must not contain '..': ${value}`); - process.exit(1); - } -} - -validatePath(config, 'config'); -validatePath(output, 'output'); - -if (report && !report.startsWith('https://') && !report.startsWith('http://')) { - validatePath(report, 'report'); -} -if (prs && (prs.includes('/') || prs.endsWith('.txt'))) { - validatePath(prs, 'prs'); -} - -function loadConfig(configPath) { - try { - return yaml.load(fs.readFileSync(configPath, 'utf8')); - } catch (e) { - return null; - } -} - -function httpsGet(url, dest, maxRedirects = 5) { - return new Promise((resolve, reject) => { - if (maxRedirects <= 0) return reject(new Error('Too many redirects')); - https.get(url, (res) => { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - return httpsGet(res.headers.location, dest, maxRedirects - 1).then(resolve, reject); - } - if (res.statusCode !== 200) { - return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`)); - } - const stream = fs.createWriteStream(dest); - res.pipe(stream); - stream.on('finish', () => stream.close(resolve)); - stream.on('error', reject); - }).on('error', reject); - }); -} - -async function buildOptionMode() { - const filters = [releaseVersion, report, prs].filter(Boolean); - if (filters.length === 0) { - console.error('::error::Exactly one of release-version, report, or prs must be provided in option mode'); - process.exit(1); - } - if (filters.length > 1) { - console.error('::error::Only one of release-version, report, or prs may be provided'); - process.exit(1); - } - const dockerArgs = ['--rm', '-v', `${process.cwd()}:/github/workspace`, '-w', '/github/workspace']; - const bundleArgs = ['changelog', 'bundle', '--config', config, '--resolve']; - - if (output) { - bundleArgs.push('--output', output); - } - - if (releaseVersion) { - bundleArgs.push('--release-version', releaseVersion); - dockerArgs.push('-e', 'GITHUB_TOKEN'); - } - - if (report) { - let effectiveReport = report; - if (report.startsWith('https://')) { - const dest = path.join(process.cwd(), '.bundle-report.html'); - await httpsGet(report, dest); - effectiveReport = '.bundle-report.html'; - } else if (report.startsWith('http://')) { - console.error(`::error::Report URL must use HTTPS: ${report}`); - process.exit(1); - } - bundleArgs.push('--report', effectiveReport); - dockerArgs.push('--network', 'none'); - } - - if (prs) { - bundleArgs.push('--prs', prs); - dockerArgs.push('--network', 'none'); - } - - if (repo) bundleArgs.push('--repo', repo); - if (owner) bundleArgs.push('--owner', owner); - - return { dockerArgs, bundleArgs }; -} - -async function buildProfileMode() { - if (!/^[a-zA-Z0-9._-]+$/.test(profile)) { - console.error(`::error::Profile name contains disallowed characters: ${profile}`); - process.exit(1); - } - - const optionFilters = [releaseVersion, prs].filter(Boolean); - if (optionFilters.length > 0) { - console.error('::error::release-version and prs cannot be used with profile mode'); - process.exit(1); - } - - const cfg = loadConfig(config); - if (cfg) { - const profiles = cfg?.bundle?.profiles; - if (!profiles || !profiles[profile]) { - const available = profiles ? Object.keys(profiles).join(', ') : 'none'; - console.error(`::error::Profile '${profile}' not found in ${config}. Available: ${available}`); - process.exit(1); - } - - const profileConfig = profiles[profile]; - const needsNetwork = profileConfig.source === 'github_release'; - - if (needsNetwork) { - console.log(`Profile '${profile}' uses source: github_release (network access required)`); - } - } - - const dockerArgs = ['--rm', '-v', `${process.cwd()}:/github/workspace`, '-w', '/github/workspace']; - const bundleArgs = ['changelog', 'bundle', profile]; - - if (version) bundleArgs.push(version); - - if (report) { - let effectiveReport = report; - if (report.startsWith('https://')) { - const dest = path.join(process.cwd(), '.bundle-report.html'); - await httpsGet(report, dest); - effectiveReport = '.bundle-report.html'; - } else if (report.startsWith('http://')) { - console.error(`::error::Report URL must use HTTPS: ${report}`); - process.exit(1); - } - bundleArgs.push(effectiveReport); - } - - const cfg2 = loadConfig(config); - const profileConfig = cfg2?.bundle?.profiles?.[profile]; - const needsNetwork = profileConfig?.source === 'github_release'; - - if (needsNetwork) { - dockerArgs.push('-e', 'GITHUB_TOKEN'); - } else if (!report || !report.startsWith('https://')) { - dockerArgs.push('--network', 'none'); - } - - return { dockerArgs, bundleArgs }; -} - -async function main() { - const isProfileMode = Boolean(profile); - const { dockerArgs, bundleArgs } = isProfileMode - ? await buildProfileMode() - : await buildOptionMode(); - - fs.writeFileSync(path.join(process.cwd(), '.bundle-marker'), ''); - - fs.appendFileSync(githubOutput, `docker-args=${dockerArgs.join(' ')}\n`); - fs.appendFileSync(githubOutput, `bundle-args=${bundleArgs.join(' ')}\n`); -} - -main().catch((err) => { - console.error(`::error::${err.message}`); - process.exit(1); -}); From f7750eb54fc4817fcc34b9aec3f0a79a90fbd2cb Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 2 Apr 2026 19:11:12 -0300 Subject: [PATCH 12/19] Set outputs instead --- changelog/bundle-create/action.yml | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 773cbbb..57b75f2 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -104,6 +104,12 @@ runs: echo "::error::Report URL must use HTTPS: ${REPORT}"; exit 1 fi + - name: Setup docs-builder + uses: elastic/docs-actions/docs-builder/setup@v1 + with: + version: edge + github-token: ${{ inputs.github-token }} + - name: Resolve bundle plan id: plan shell: bash @@ -119,7 +125,7 @@ runs: OWNER: ${{ inputs.owner }} run: | ARGS=() - ARGS+=(changelog bundle) + ARGS+=(changelog bundle --plan) if [ -n "$PROFILE" ]; then ARGS+=("$PROFILE") [ -n "$VERSION" ] && ARGS+=("$VERSION") @@ -134,22 +140,13 @@ runs: [ -n "$OWNER" ] && ARGS+=(--owner "$OWNER") fi - PLAN_JSON=$(docker run --rm --network none \ - -v "${PWD}:/github/workspace" \ - -w /github/workspace \ - ghcr.io/elastic/docs-builder:edge \ - "${ARGS[@]}" --plan) - - echo "$PLAN_JSON" | jq . - - NEEDS_NETWORK=$(echo "$PLAN_JSON" | jq -r '.needs_network') - NEEDS_GITHUB_TOKEN=$(echo "$PLAN_JSON" | jq -r '.needs_github_token') - OUTPUT_PATH=$(echo "$PLAN_JSON" | jq -r '.output_path // empty') - - echo "needs_network=$NEEDS_NETWORK" >> "$GITHUB_OUTPUT" - echo "needs_github_token=$NEEDS_GITHUB_TOKEN" >> "$GITHUB_OUTPUT" - echo "output_path=$OUTPUT_PATH" >> "$GITHUB_OUTPUT" + docs-builder "${ARGS[@]}" + - name: Verify plan output + shell: bash + env: + OUTPUT_PATH: ${{ steps.plan.outputs.output_path }} + run: | if [ -z "$OUTPUT_PATH" ]; then echo "::error::Plan did not resolve an output path. Set 'output' input or configure bundle.output_directory in changelog.yml." exit 1 From 20bacab5f4a310d834fd419a24ad0c9680a572f8 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 2 Apr 2026 19:15:08 -0300 Subject: [PATCH 13/19] Update README --- changelog/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog/README.md b/changelog/README.md index 884501e..6d55388 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -189,13 +189,13 @@ Individual changelog files accumulate on the default branch as PRs merge. The bu - **Buildkite promotion report** (`report`) — extracts PR URLs from a promotion report. Used for serverless releases discovered by a scheduled workflow. - **PR list** (`prs`) — an explicit list of PR URLs or numbers (comma-separated), or a path to a newline-delimited file. Used when the caller already knows which PRs to include. -Exactly one filter source must be provided. The `output` path is optional — when not provided, docs-builder writes to the config-driven path (`bundle.output_directory`) and the action discovers the file automatically. +Exactly one filter source must be provided. The `output` path is optional — when not provided, the action runs `docs-builder changelog bundle --plan` to resolve the output path from the config (`bundle.output_directory`) before generating the bundle. **Profile mode** — all configuration comes from `bundle.profiles` in `changelog.yml`: - **Profile** (`profile`) — a named profile that defines the product filter, output filename pattern, and other settings. The `version` input provides the value for `{version}` substitution in profile patterns. - An optional `report` can be passed as a positional argument to filter by promotion report. -- The `output` path is optional — if not provided, it's discovered from `bundle.output_directory` in the config. +- The `output` path is optional — if not provided, it's resolved from `bundle.output_directory` in the config via the `--plan` step. - `bundle.resolve: true` must be set in the config (it cannot be forced via CLI in profile mode). The bundle always includes the full content of each matching entry, so downstream consumers can render changelogs without access to the original files. @@ -382,7 +382,7 @@ jobs: version: ${{ github.event.release.tag_name }} ``` -The `output` input is not needed — the action discovers the generated file from `bundle.output_directory` in the config. If a promotion report is also needed, pass it via the `report` input. +The `output` input is not needed — the action resolves the output path from `bundle.output_directory` and the profile's `output` pattern via the `--plan` step. If a promotion report is also needed, pass it via the `report` input. #### Custom config path From b2e6a149cd9457fa35e02d4dea09b8c62e027927 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 2 Apr 2026 19:27:30 -0300 Subject: [PATCH 14/19] Fix README --- changelog/bundle-create/README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md index 634e9db..c3f9b2c 100644 --- a/changelog/bundle-create/README.md +++ b/changelog/bundle-create/README.md @@ -6,19 +6,19 @@ Checks out the repository, runs docs-builder changelog bundle in Docker to gener ## Inputs -| Name | Description | Required | Default | -|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| -| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | -| `profile` | Bundle profile name from bundle.profiles in changelog.yml. Mutually exclusive with release-version and prs. When set, all paths and filters come from the config.
| `false` | ` ` | -| `version` | Version string for profile mode (e.g. 9.2.0, 2026-03). Used for {version} substitution in profile patterns. Only valid with profile.
| `false` | ` ` | -| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs.
| `false` | ` ` | -| `report` | Buildkite promotion report URL or local file path used as the PR filter source. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. Local paths must be relative to the repo root.
| `false` | ` ` | -| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner to be set in changelog.yml or inputs.
| `false` | ` ` | +| Name | Description | Required | Default | +|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `profile` | Bundle profile name from bundle.profiles in changelog.yml. Mutually exclusive with release-version and prs. When set, all paths and filters come from the config.
| `false` | ` ` | +| `version` | Version string for profile mode (e.g. 9.2.0, 2026-03). Used for {version} substitution in profile patterns. Only valid with profile.
| `false` | ` ` | +| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs.
| `false` | ` ` | +| `report` | Buildkite promotion report URL or local file path used as the PR filter source. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. Local paths must be relative to the repo root.
| `false` | ` ` | +| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner to be set in changelog.yml or inputs.
| `false` | ` ` | | `output` | Output file path for the bundle, relative to the repo root. Optional in both modes. When not provided, docs-builder writes to the path determined by the config (bundle.output_directory) and the plan resolves the generated file path automatically.
| `false` | ` ` | -| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml.
| `false` | ` ` | -| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic.
| `false` | ` ` | -| `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | -| `github-token` | GitHub token (needed for release-version and source: github_release profiles) | `false` | `${{ github.token }}` | +| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml.
| `false` | ` ` | +| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic.
| `false` | ` ` | +| `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | +| `github-token` | GitHub token (needed for release-version and source: github_release profiles) | `false` | `${{ github.token }}` | ## Outputs From 5bc07602a5a8bb65e57875fa4fd1db08a64ccd43 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 7 Apr 2026 12:28:16 -0300 Subject: [PATCH 15/19] Harden input validation --- changelog/bundle-create/action.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 57b75f2..5b9764b 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -72,9 +72,13 @@ runs: env: CONFIG: ${{ inputs.config }} PROFILE: ${{ inputs.profile }} + VERSION: ${{ inputs.version }} + RELEASE_VERSION: ${{ inputs.release-version }} OUTPUT: ${{ inputs.output }} REPORT: ${{ inputs.report }} PRS: ${{ inputs.prs }} + REPO: ${{ inputs.repo }} + OWNER: ${{ inputs.owner }} run: | validate_path() { local value="$1" name="$2" @@ -86,6 +90,15 @@ runs: echo "::error::${name} must not contain '..': ${value}"; exit 1 fi } + + validate_identifier() { + local value="$1" name="$2" pattern="$3" + [ -z "$value" ] && return + if [[ ! "$value" =~ $pattern ]]; then + echo "::error::${name} contains disallowed characters: ${value}"; exit 1 + fi + } + validate_path "$CONFIG" "config" validate_path "$OUTPUT" "output" @@ -96,9 +109,11 @@ runs: validate_path "$PRS" "prs" fi - if [ -n "$PROFILE" ] && [[ ! "$PROFILE" =~ ^[a-zA-Z0-9._-]+$ ]]; then - echo "::error::Profile name contains disallowed characters: ${PROFILE}"; exit 1 - fi + validate_identifier "$PROFILE" "profile" '^[a-zA-Z0-9._-]+$' + validate_identifier "$VERSION" "version" '^[a-zA-Z0-9._+-]+$' + validate_identifier "$RELEASE_VERSION" "release-version" '^[a-zA-Z0-9._+-]+$' + validate_identifier "$REPO" "repo" '^[a-zA-Z0-9._-]+$' + validate_identifier "$OWNER" "owner" '^[a-zA-Z0-9._-]+$' if [ -n "$REPORT" ] && [[ "$REPORT" == http://* ]]; then echo "::error::Report URL must use HTTPS: ${REPORT}"; exit 1 From 40e06ddbb1d2abefe5926913128da5eff99f8781 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 7 Apr 2026 12:46:27 -0300 Subject: [PATCH 16/19] Pin docs-builder version using attestation-verify --- .github/workflows/changelog-bundle.yml | 8 ++++++++ changelog/bundle-create/action.yml | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml index a44d955..304f510 100644 --- a/.github/workflows/changelog-bundle.yml +++ b/.github/workflows/changelog-bundle.yml @@ -47,6 +47,12 @@ on: owner: description: 'GitHub repository owner; falls back to bundle.owner in changelog.yml' type: string + docs-builder-version: + description: > + docs-builder version to use (e.g. 0.1.100, latest, edge). + Non-edge versions are attestation-verified by the setup action. + type: string + default: 'edge' permissions: {} @@ -59,6 +65,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + packages: read outputs: output: ${{ steps.bundle.outputs.output }} steps: @@ -74,6 +81,7 @@ jobs: output: ${{ inputs.output }} repo: ${{ inputs.repo }} owner: ${{ inputs.owner }} + docs-builder-version: ${{ inputs.docs-builder-version }} github-token: ${{ github.token }} create-pr: diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 5b9764b..84ce7f4 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -47,6 +47,11 @@ inputs: description: > GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic. + docs-builder-version: + description: > + docs-builder version to use (e.g. 0.1.100, latest, edge). + Non-edge versions are attestation-verified by the setup action. + default: 'edge' artifact-name: description: 'Name for the uploaded artifact (must match bundle-pr artifact-name)' default: 'changelog-bundle' @@ -122,7 +127,7 @@ runs: - name: Setup docs-builder uses: elastic/docs-actions/docs-builder/setup@v1 with: - version: edge + version: ${{ inputs.docs-builder-version }} github-token: ${{ inputs.github-token }} - name: Resolve bundle plan @@ -174,12 +179,25 @@ runs: REPORT: ${{ inputs.report }} run: curl --fail --silent --show-error --location --max-redirs 5 --max-time 30 "$REPORT" -o .bundle-report.html + - name: Pull and pin Docker image + id: docker-image + shell: bash + env: + BUILDER_VERSION: ${{ inputs.docs-builder-version }} + run: | + IMAGE="ghcr.io/elastic/docs-builder:${BUILDER_VERSION}" + docker pull "$IMAGE" + DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE") + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "::notice title=Docker image digest::${DIGEST}" + - name: Generate changelog bundle shell: bash env: GITHUB_TOKEN: ${{ inputs.github-token }} NEEDS_NETWORK: ${{ steps.plan.outputs.needs_network }} NEEDS_GITHUB_TOKEN: ${{ steps.plan.outputs.needs_github_token }} + IMAGE_DIGEST: ${{ steps.docker-image.outputs.digest }} CONFIG: ${{ inputs.config }} PROFILE: ${{ inputs.profile }} VERSION: ${{ inputs.version }} @@ -222,7 +240,7 @@ runs: [ -n "$OWNER" ] && BUNDLE_ARGS+=(--owner "$OWNER") fi - docker run "${DOCKER_ARGS[@]}" ghcr.io/elastic/docs-builder:edge "${BUNDLE_ARGS[@]}" + docker run "${DOCKER_ARGS[@]}" "$IMAGE_DIGEST" "${BUNDLE_ARGS[@]}" - name: Upload bundle artifact uses: actions/upload-artifact@v6 From c21dbdccc48b0511bbe4bf2b4dd5c2f47f1aa6f3 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 7 Apr 2026 12:46:52 -0300 Subject: [PATCH 17/19] Add domain allowlist for report URLs --- changelog/bundle-create/action.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 84ce7f4..1a58b9d 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -124,6 +124,22 @@ runs: echo "::error::Report URL must use HTTPS: ${REPORT}"; exit 1 fi + if [ -n "$REPORT" ] && [[ "$REPORT" == https://* ]]; then + REPORT_HOST=$(echo "$REPORT" | sed -E 's|^https://([^/:]+).*|\1|') + ALLOWED_DOMAINS="github.com buildkite.com" + DOMAIN_OK=false + for domain in $ALLOWED_DOMAINS; do + if [[ "$REPORT_HOST" == "$domain" || "$REPORT_HOST" == *."$domain" ]]; then + DOMAIN_OK=true + break + fi + done + if [ "$DOMAIN_OK" != "true" ]; then + echo "::error::Report URL domain not allowed: ${REPORT_HOST} (allowed: ${ALLOWED_DOMAINS})" + exit 1 + fi + fi + - name: Setup docs-builder uses: elastic/docs-actions/docs-builder/setup@v1 with: From 07c29bc7f4f4b806d7df22b7a34d1221b188a13b Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 7 Apr 2026 12:54:00 -0300 Subject: [PATCH 18/19] revert tackling ssrf in the action --- changelog/bundle-create/action.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml index 1a58b9d..84ce7f4 100644 --- a/changelog/bundle-create/action.yml +++ b/changelog/bundle-create/action.yml @@ -124,22 +124,6 @@ runs: echo "::error::Report URL must use HTTPS: ${REPORT}"; exit 1 fi - if [ -n "$REPORT" ] && [[ "$REPORT" == https://* ]]; then - REPORT_HOST=$(echo "$REPORT" | sed -E 's|^https://([^/:]+).*|\1|') - ALLOWED_DOMAINS="github.com buildkite.com" - DOMAIN_OK=false - for domain in $ALLOWED_DOMAINS; do - if [[ "$REPORT_HOST" == "$domain" || "$REPORT_HOST" == *."$domain" ]]; then - DOMAIN_OK=true - break - fi - done - if [ "$DOMAIN_OK" != "true" ]; then - echo "::error::Report URL domain not allowed: ${REPORT_HOST} (allowed: ${ALLOWED_DOMAINS})" - exit 1 - fi - fi - - name: Setup docs-builder uses: elastic/docs-actions/docs-builder/setup@v1 with: From 8a2220602c617d4233743c8974d5b0c1b9ec998a Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 7 Apr 2026 13:09:36 -0300 Subject: [PATCH 19/19] Update README --- changelog/bundle-create/README.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md index c3f9b2c..fa43529 100644 --- a/changelog/bundle-create/README.md +++ b/changelog/bundle-create/README.md @@ -6,19 +6,20 @@ Checks out the repository, runs docs-builder changelog bundle in Docker to gener ## Inputs -| Name | Description | Required | Default | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| -| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | -| `profile` | Bundle profile name from bundle.profiles in changelog.yml. Mutually exclusive with release-version and prs. When set, all paths and filters come from the config.
| `false` | ` ` | -| `version` | Version string for profile mode (e.g. 9.2.0, 2026-03). Used for {version} substitution in profile patterns. Only valid with profile.
| `false` | ` ` | -| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs.
| `false` | ` ` | -| `report` | Buildkite promotion report URL or local file path used as the PR filter source. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. Local paths must be relative to the repo root.
| `false` | ` ` | -| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner to be set in changelog.yml or inputs.
| `false` | ` ` | -| `output` | Output file path for the bundle, relative to the repo root. Optional in both modes. When not provided, docs-builder writes to the path determined by the config (bundle.output_directory) and the plan resolves the generated file path automatically.
| `false` | ` ` | -| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml.
| `false` | ` ` | -| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic.
| `false` | ` ` | -| `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | -| `github-token` | GitHub token (needed for release-version and source: github_release profiles) | `false` | `${{ github.token }}` | +| Name | Description | Required | Default | +|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `profile` | Bundle profile name from bundle.profiles in changelog.yml. Mutually exclusive with release-version and prs. When set, all paths and filters come from the config.
| `false` | ` ` | +| `version` | Version string for profile mode (e.g. 9.2.0, 2026-03). Used for {version} substitution in profile patterns. Only valid with profile.
| `false` | ` ` | +| `release-version` | GitHub release tag used as the PR filter source (e.g. v9.2.0). Mutually exclusive with profile, report, and prs.
| `false` | ` ` | +| `report` | Buildkite promotion report URL or local file path used as the PR filter source. In option mode, mutually exclusive with release-version and prs. In profile mode, passed as a positional argument. Local paths must be relative to the repo root.
| `false` | ` ` | +| `prs` | Comma-separated PR URLs or numbers, or a path to a newline-delimited file. Mutually exclusive with profile, release-version, and report. Bare numbers require repo/owner to be set in changelog.yml or inputs.
| `false` | ` ` | +| `output` | Output file path for the bundle, relative to the repo root. Optional in both modes. When not provided, docs-builder writes to the path determined by the config (bundle.output_directory) and the plan resolves the generated file path automatically.
| `false` | ` ` | +| `repo` | GitHub repository name. Falls back to bundle.repo in changelog.yml.
| `false` | ` ` | +| `owner` | GitHub repository owner. Falls back to bundle.owner in changelog.yml, then to elastic.
| `false` | ` ` | +| `docs-builder-version` | docs-builder version to use (e.g. 0.1.100, latest, edge). Non-edge versions are attestation-verified by the setup action.
| `false` | `edge` | +| `artifact-name` | Name for the uploaded artifact (must match bundle-pr artifact-name) | `false` | `changelog-bundle` | +| `github-token` | GitHub token (needed for release-version and source: github_release profiles) | `false` | `${{ github.token }}` | ## Outputs