diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml new file mode 100644 index 0000000..08e5383 --- /dev/null +++ b/.github/workflows/changelog-bundle.yml @@ -0,0 +1,67 @@ +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 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 + owner: + 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 + permissions: + contents: read + steps: + - uses: elastic/docs-actions/changelog/bundle-create@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 }} + + 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 }} + base-branch: ${{ inputs.base-branch }} + github-token: ${{ github.token }} diff --git a/changelog/README.md b/changelog/README.md index bf94d86..36bfa94 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -179,3 +179,113 @@ 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 +``` + +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. + +#### 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 + pull-requests: 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: + # At 08:00 AM, Monday through Friday + - 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 + pull-requests: 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 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..6b2f11d --- /dev/null +++ b/changelog/bundle-create/action.yml @@ -0,0 +1,100 @@ +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. + 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 }}' + +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" == 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: Upload bundle artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ inputs.artifact-name }} + 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..8ad21ca --- /dev/null +++ b/changelog/bundle-pr/action.yml @@ -0,0 +1,107 @@ +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 + 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 }}' + +runs: + using: composite + steps: + - name: Checkout + uses: actions/checkout@v6 + 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: ${{ 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")" + 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 --force-with-lease 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" + else + gh pr create \ + --title "Add changelog bundle ${BUNDLE_NAME}" \ + --body "Auto-generated changelog bundle for ${BUNDLE_NAME}." \ + --head "$BRANCH" \ + "${BASE_FLAG[@]}" + fi