From cb3c68c951bd7dd1b1eb8fa11c49994c2628c38d Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 26 Mar 2026 13:56:38 -0300 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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=()