Skip to content

Add bundle action and reusable workflow#42

Draft
cotti wants to merge 7 commits intomainfrom
feature/bundle_action
Draft

Add bundle action and reusable workflow#42
cotti wants to merge 7 commits intomainfrom
feature/bundle_action

Conversation

@cotti
Copy link
Copy Markdown
Contributor

@cotti cotti commented Mar 26, 2026

This pull request introduces an action and reusable workflow for bundling changelog entries, allowing teams to generate a fully-resolved changelog bundle file filtered by release version or promotion report. The changes add a reusable GitHub Actions workflow, a composite action, and comprehensive documentation for integrating this process into various release pipelines.

Changelog bundling infrastructure:

  • Added .github/workflows/changelog-bundle.yml, a reusable workflow that generates a changelog bundle file by invoking the new composite action. This workflow supports filtering by GitHub release version or Buildkite promotion report, and outputs the bundled changelog to a specified path.
  • Introduced changelog/bundle/action.yml, a composite GitHub Action that runs docs-builder changelog bundle with flexible input options, commits the result to the repository, and handles filtering, configuration, and output management.

@cotti cotti self-assigned this Mar 26, 2026
@cotti cotti added the enhancement New feature or request label Mar 26, 2026
@cotti cotti marked this pull request as draft March 26, 2026 17:30
args+=(--owner "$OWNER")
fi

docs-builder changelog bundle "${args[@]}"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should run this in docker similar to

I think we can also add --network none in this case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 4de3e84. We get the file via curl and send it to docs-builder inside docker with --network none. It goes to code that only tries to regex parse it.

Comment on lines +107 to +120
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this needed if you are already doing elastic/docs-actions/docs-builder/setup@v1?

Also, if this is supposed to run on the main branch, we must create a PR instead of committing directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oof, yeah. 4de3e84 Puts a get artifact -> submit PR action after getting the artifact from the bundle gen action.

value: ${{ steps.commit.outputs.committed || 'false' }}

runs:
using: composite
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should split this whole thing into two jobs.

one that creates the bundle file and uploads the artifact and has only contents: write permissions

and one that downloads the artifact and has permissions to create a PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 4de3e84. We have bundle-create for the first step (contents: read) and bundle-pr for the rest (contents/pull-request: write). contents: write would be needed because the PR branch needs to be created there. Unless there's a different way?

@@ -0,0 +1,45 @@
name: Changelog bundle
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is a reusable workflow necessary if it has only one step?

it would be necessary, if we split it into 2 jobs (https://github.com/elastic/docs-actions/pull/42/changes#r2997494919)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. I've split them with their specific permissions now.

@cotti cotti requested a review from reakaleek March 26, 2026 22:46
Comment on lines +198 to +200
directory: docs/changelog
repo: my-repo
owner: elastic
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All of these are optional right?

I don't think we should document hardcoding the repo and owner, for most this would be equal to the one holding changelog.yml ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that was by convention but they shouldn't be exposed willy-nilly. Removed


on:
schedule:
- cron: '0 8 * * 1-5'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
- cron: '0 8 * * 1-5'
# At 08:00 AM, Monday through Friday
- cron: '0 8 * * 1-5'

I can never read cron syntax :D

Copy link
Copy Markdown
Member

@Mpdreamz Mpdreamz left a comment

Choose a reason for hiding this comment

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

Security & logic review

Compared against the patterns established in changelog-validate.yml, changelog-submit.yml, changelog/submit/action.yml, and docs-deploy.yml.

Seven issues found — two are high severity and block merge; the rest are medium/low or best-practice gaps.


🔴 High

1. Missing top-level permissions: {} on the reusable workflow.github/workflows/changelog-bundle.yml

Both sibling workflows (changelog-validate.yml:11, changelog-submit.yml:15) open with permissions: {} to deny all permissions at workflow scope, then grant only what is needed per job. This file has no top-level permissions block, so it inherits the repository default token permissions (typically includes contents: write, packages: read, etc.). The generate job silently runs with write access it does not need.

Fix: add permissions: {} between the on: block and jobs:.


2. Missing concurrency block.github/workflows/changelog-bundle.yml

Both changelog-validate.yml (line 13) and changelog-submit.yml (lines 17–19) define concurrency groups. Without one here, two simultaneous calls for the same output path will race: both generate jobs upload an artifact named changelog-bundle (last write wins), and both create-pr jobs will race on the same branch with a force-push.

Fix: add something like:

concurrency:
  group: changelog-bundle-${{ inputs.output }}-${{ github.run_id }}
  cancel-in-progress: false

🟡 Medium

3. Force-push without branch-name validationchangelog/bundle-pr/action.yml lines 39–58

BUNDLE_NAME=$(basename "$OUTPUT" .yaml) is then used directly as a branch name component and in a git push -f. basename strips the directory prefix but does not validate characters. Compare changelog/submit/action.yml lines 66–77, which has an explicit ref-name guard: ^[a-zA-Z0-9._/+-]+$.

The force-push (-f) is also destructive and inconsistent with the pattern in changelog/submit/action.yml which uses a plain git push. If a team member has pushed additional commits to the bundle branch, a force-push will silently discard them.

Fix: validate $BUNDLE_NAME against ^[a-zA-Z0-9._+-]+$ before constructing $BRANCH; replace -f with a regular push (or justify explicitly in a comment).


4. Unvalidated $OUTPUT used as cp destinationchangelog/bundle-pr/action.yml lines 42–43

mkdir -p "$(dirname "$OUTPUT")"
cp ".bundle-artifact/$(basename "$OUTPUT")" "$OUTPUT"

$OUTPUT is a caller-controlled string used directly as the copy destination. The action description says "Must match the path used by bundle-create" but there is no enforcement. A value containing ../ sequences or an absolute path outside the workspace would be silently honoured.

Fix: validate that $OUTPUT is a relative path with no .. components and that it ends in .yml or .yaml (as the docs promise but the code does not check).


5. Unvalidated URL passed to curlchangelog/bundle-create/action.yml lines 78–80

curl -fsSL "$REPORT" -o .bundle-report.html

$REPORT is a caller-controlled URL fetched on the runner before --network none is applied to the Docker container. A misconfigured (or malicious) caller could pass an internal endpoint (e.g. the EC2 instance metadata service at http://169.254.169.254/...) and the runner would fetch it. The downloaded content then becomes input to the Docker container.

The attack surface is limited to workflow_call authors in the same org, but the risk should be documented or mitigated (e.g. allowlist known Buildkite URL prefixes).


🟠 Logic

6. No --base branch on gh pr createchangelog/bundle-pr/action.yml line 65–68

The PR is created without --base, so it targets the repo default branch at call time. If the workflow is ever used to produce bundles for a non-default release branch, this will silently target the wrong base. Making it explicit is a one-word fix and matches the defensive posture of the rest of the repo.

Fix: add --base "$GITHUB_REF_NAME" or a dedicated base-branch input.


7. Hardcoded artifact name changelog-bundlechangelog/bundle-create/action.yml line 91 / changelog/bundle-pr/action.yml line 29

If a consuming workflow ever calls bundle-create twice in parallel (e.g. matrix), the second upload will overwrite the first and bundle-pr will silently process the wrong file. Consider parameterising the artifact name or including the output basename.


Summary

File Line(s) Severity Finding
changelog-bundle.yml after line 30 🔴 Missing permissions: {}
changelog-bundle.yml after line 30 🔴 Missing concurrency block
bundle-pr/action.yml 39–58 🟡 Force-push + no branch-name validation
bundle-pr/action.yml 42–43 🟡 Unvalidated $OUTPUT as cp destination
bundle-create/action.yml 78–80 🟡 Unvalidated URL to curl (SSRF potential)
bundle-pr/action.yml 65–68 🟠 gh pr create missing --base
bundle-create/action.yml 91 🟠 Hardcoded artifact name risks collision

description: 'GitHub repository owner; falls back to bundle.owner in changelog.yml'
type: string

jobs:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Missing permissions: {} at workflow scope. Both changelog-validate.yml (line 11) and changelog-submit.yml (line 15) deny all permissions at the top level and then grant only what each job needs. Without this, the workflow inherits the repository default token permissions — the generate job silently runs with write access it does not need.

Add before jobs::

permissions: {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changes from this report:

.github/workflows/changelog-bundle.yml

  • permissions: {} added at workflow scope
  • concurrency block added, keyed on inputs.output with cancel-in-progress: false. Two calls for the same output path will now queue instead of racing.
  • base-branch input propagated to bundle-pr action.
  • report description updated to specify HTTPS.

changelog/bundle-create/action.yml

  • HTTPS-only for report URLs — http:// URLs are now rejected with an error.
  • artifact-name input added (default changelog-bundle) — the upload step uses it instead of a hardcoded string, preventing collisions in matrix scenarios.

changelog/bundle-pr/action.yml

  • Output path validation step added: rejects absolute paths, .. traversal, and files not ending in .yml/.yaml.
  • Branch name validation using ^[a-zA-Z0-9._+-]+$ — matches the guard in changelog/submit/action.yml lines 72-77.
  • basename strips both .yaml and .yml extensions to handle either.
  • --base flag on gh pr create when base-branch input is provided.
  • artifact-name input added to match the bundle-create parameterization.
  • Force-push has been changed to --force-with-lease.

| `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` | |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be nice (in follow-up PR?) to make the github action support profiles. Then more of these options could be picked up automatically from the config file (e.g. output, output_products, etc)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants