diff --git a/.github/workflows/changelog-bundle-pr.yml b/.github/workflows/changelog-bundle-pr.yml new file mode 100644 index 0000000..73a4e39 --- /dev/null +++ b/.github/workflows/changelog-bundle-pr.yml @@ -0,0 +1,97 @@ +name: Changelog bundle PR + +on: + workflow_call: + inputs: + config: + 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). + Used for {version} substitution in profile patterns. + type: string + release-version: + description: > + GitHub release tag used as the PR filter source (e.g. v9.2.0). + 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 in option mode. + type: string + prs: + description: > + Comma-separated PR URLs or numbers, or a path to a newline-delimited file. + 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). + Optional. When not provided, the path is determined by the config. + 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 + base-branch: + description: 'Base branch for the pull request (defaults to repository default branch)' + 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: {} + +concurrency: + group: changelog-bundle-pr-${{ inputs.output || inputs.profile }} + cancel-in-progress: false + +jobs: + generate: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + outputs: + output: ${{ steps.bundle.outputs.output }} + steps: + - 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 }} + output: ${{ inputs.output }} + repo: ${{ inputs.repo }} + owner: ${{ inputs.owner }} + docs-builder-version: ${{ inputs.docs-builder-version }} + 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: ${{ needs.generate.outputs.output }} + base-branch: ${{ inputs.base-branch }} + github-token: ${{ github.token }} diff --git a/.github/workflows/changelog-bundle.yml b/.github/workflows/changelog-bundle.yml new file mode 100644 index 0000000..9448a9b --- /dev/null +++ b/.github/workflows/changelog-bundle.yml @@ -0,0 +1,116 @@ +name: Changelog bundle + +on: + workflow_call: + inputs: + mode: + description: > + Operation mode: 'bundle' (default) runs changelog bundle, + 'gh-release' runs changelog gh-release to create changelogs + from a GitHub release. In gh-release mode, repo and version + are required. + type: string + default: 'bundle' + config: + 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 (e.g. 9.2.0). In bundle profile mode, used + for {version} substitution. In gh-release mode, used as the + release tag to fetch. + type: string + release-version: + description: > + GitHub release tag used as the PR filter source (e.g. v9.2.0). + 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 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 profile, release-version, and report. + type: string + output: + 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 + 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 + strip-title-prefix: + description: 'Remove [Prefix]: from PR titles (gh-release mode only)' + type: boolean + default: false + 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' + aws-account-id: + description: 'AWS account ID for OIDC. Only override if provisioned for the target account.' + type: string + default: '197730964718' + +permissions: {} + +concurrency: + group: changelog-bundle-${{ inputs.output || inputs.profile }} + cancel-in-progress: false + +jobs: + generate: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + outputs: + output: ${{ steps.bundle.outputs.output }} + steps: + - id: bundle + uses: elastic/docs-actions/changelog/bundle-create@v1 + with: + mode: ${{ inputs.mode }} + config: ${{ inputs.config }} + profile: ${{ inputs.profile }} + version: ${{ inputs.version }} + release-version: ${{ inputs.release-version }} + report: ${{ inputs.report }} + prs: ${{ inputs.prs }} + output: ${{ inputs.output }} + repo: ${{ inputs.repo }} + owner: ${{ inputs.owner }} + strip-title-prefix: ${{ inputs.strip-title-prefix }} + docs-builder-version: ${{ inputs.docs-builder-version }} + github-token: ${{ github.token }} + + upload: + needs: generate + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: elastic/docs-actions/changelog/bundle-upload@v1 + with: + output: ${{ needs.generate.outputs.output }} + config: ${{ inputs.config }} + docs-builder-version: ${{ inputs.docs-builder-version }} + aws-account-id: ${{ inputs.aws-account-id }} diff --git a/changelog/README.md b/changelog/README.md index 68ac54f..6c329af 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -235,3 +235,190 @@ The workflow uses a per-repository concurrency group so that rapid successive pu > **Note:** The composite action accepts an `aws-account-id` input (default: the Elastic docs account). Overriding this is only valid when OIDC trust and IAM roles have been provisioned for the target account. In practice, most repositories should use the default. > **Note:** The `github-token` input defaults to the workflow's `GITHUB_TOKEN`, which is scoped to the job's declared permissions. Do not substitute a broader PAT unless `docs-builder/setup` explicitly requires it. + +## 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, then uploads it to the `elastic-docs-v3-changelog-bundles` S3 bucket. + +Two reusable workflows are available: + +- **`changelog-bundle.yml`** (primary) — generates a bundle and uploads it to S3. Used for release-triggered workflows. +- **`changelog-bundle-pr.yml`** (opt-in) — generates a bundle and opens a pull request. Used for teams that need a committed bundle before a tag exists. + +The bundle always includes the full content of each matching entry, 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 + output_directory: docs/releases + repo: my-repo + owner: elastic +``` + +Your repository must also be listed in the `elastic-docs-v3-changelog-bundles` infrastructure to have an IAM role provisioned for OIDC-based S3 uploads. Contact the docs-engineering team to add your repository. + +### Setup + +#### Profile-based bundling with S3 upload (`on: release`) + +The recommended setup for stack and product releases. The caller triggers on `release`, passes a profile and version, and the bundle is uploaded to S3 automatically. + +```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: {} + +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 resolves the output path from `bundle.output_directory` and the profile's `output` pattern via the `--plan` step. + +#### GitHub release mode (`mode: gh-release`) + +For repositories that do not use the validate/submit workflow to accumulate individual changelog files, `gh-release` mode creates changelogs directly from a GitHub release's notes and bundles them in a single step. + +**`.github/workflows/changelog-bundle.yml`** + +```yaml +name: changelog-bundle + +on: + release: + types: [published] + +permissions: {} + +jobs: + bundle: + uses: elastic/docs-actions/.github/workflows/changelog-bundle.yml@v1 + with: + mode: gh-release + repo: my-repo + version: ${{ github.event.release.tag_name }} +``` + +#### Option-based bundling with S3 upload + +You can also use option-based filtering instead of profiles. The `release-version`, `report`, and `prs` inputs are supported. + +**Stack / product releases:** + +```yaml +name: changelog-bundle + +on: + release: + types: [published] + +permissions: {} + +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 +``` + +**Serverless / scheduled releases:** + +```yaml +name: changelog-bundle + +on: + schedule: + # At 08:00 AM, Monday through Friday + - cron: '0 8 * * 1-5' + +permissions: {} + +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 + profile: my-release + version: ${{ github.event.release.tag_name }} +``` + +### Output + +The primary workflow (`changelog-bundle.yml`) uploads the bundle to the `elastic-docs-v3-changelog-bundles` S3 bucket under `{product}/bundles/{filename}`. The bundle is available to downstream rendering workflows immediately after upload. + +### Bundle PR workflow (opt-in) + +For teams that need a committed bundle file before a tag exists (e.g. feature-freeze branches), use the PR workflow instead. This generates the bundle and opens a pull request. + +**`.github/workflows/changelog-bundle-pr.yml`** + +```yaml +name: changelog-bundle-pr + +on: + workflow_dispatch: + inputs: + version: + description: 'Version string (e.g. 9.2.0)' + required: true + +permissions: {} + +jobs: + bundle: + uses: elastic/docs-actions/.github/workflows/changelog-bundle-pr.yml@v1 + with: + profile: my-release + version: ${{ inputs.version }} +``` + +The PR workflow opens a pull request on a branch named `changelog-bundle/` (e.g. `changelog-bundle/v9.2.0`). 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. + +> **Note:** The PR workflow does not upload to S3. If you need both S3 upload and a PR, run both workflows or use the composite actions (`bundle-create`, `bundle-upload`, `bundle-pr`) directly. diff --git a/changelog/bundle-create/README.md b/changelog/bundle-create/README.md new file mode 100644 index 0000000..0fe5613 --- /dev/null +++ b/changelog/bundle-create/README.md @@ -0,0 +1,63 @@ +# Changelog bundle create + +Checks out the repository, runs docs-builder in Docker to generate a fully-resolved bundle file, and uploads the result as an artifact. Supports option-based filtering (release-version, report, prs), profile-based bundling, and gh-release mode (creates changelogs from a GitHub release). Uses `--network none` where possible. + +## Modes + +- **`bundle`** (default) — runs `docs-builder changelog bundle` with profile or option-based filtering. Requires changelogs to already exist in the repository. +- **`gh-release`** — runs `docs-builder changelog gh-release` to create changelogs directly from a GitHub release's notes. Requires `repo` and optionally `version` (defaults to `latest`). + +## Inputs + +| Name | Description | Required | Default | +|------------------------|-----------------------------------------------------------------------------------------------------|----------|-----------------------| +| `mode` | Operation mode: `bundle` or `gh-release` | `false` | `bundle` | +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `profile` | Bundle profile name (bundle mode only) | `false` | | +| `version` | Version string (e.g. 9.2.0). Profile substitution in bundle mode; release tag in gh-release mode | `false` | | +| `release-version` | GitHub release tag for PR filtering (bundle mode, option-based only) | `false` | | +| `report` | Buildkite promotion report URL or local file path | `false` | | +| `prs` | Comma-separated PR URLs/numbers, or path to a newline-delimited file | `false` | | +| `output` | Output file path, relative to repo root | `false` | | +| `repo` | GitHub repository name. Required for gh-release mode | `false` | | +| `owner` | GitHub repository owner | `false` | | +| `strip-title-prefix` | Remove `[Prefix]:` from PR titles (gh-release mode only) | `false` | `false` | +| `docs-builder-version` | docs-builder version (e.g. 0.1.100, latest, edge) | `false` | `edge` | +| `artifact-name` | Name for the uploaded artifact | `false` | `changelog-bundle` | +| `github-token` | GitHub token | `false` | `${{ github.token }}` | + +## Outputs + +| Name | Description | +|----------|------------------------------------------| +| `output` | Resolved output file path for the bundle | + +## Usage + +Bundle mode (profile): +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle-create@v1 + with: + profile: elasticsearch-release + version: 9.2.0 +``` + +Bundle mode (option-based): +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle-create@v1 + with: + release-version: v9.2.0 + output: docs/releases/v9.2.0.yaml +``` + +GitHub release mode: +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle-create@v1 + with: + mode: gh-release + repo: elasticsearch + version: v9.2.0 +``` diff --git a/changelog/bundle-create/action.yml b/changelog/bundle-create/action.yml new file mode 100644 index 0000000..36bf3fe --- /dev/null +++ b/changelog/bundle-create/action.yml @@ -0,0 +1,337 @@ +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 option-based filtering (release-version, report, prs), + profile-based bundling, and gh-release mode (creates changelogs from a + GitHub release). Uses --network none where possible. + +inputs: + mode: + description: > + Operation mode: 'bundle' (default) runs changelog bundle, + 'gh-release' runs changelog gh-release to create changelogs + from a GitHub release. In gh-release mode, repo and version + are required; most other inputs are ignored. + default: 'bundle' + 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 profile, report, and prs. + report: + description: > + 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. + prs: + description: > + 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. + output: + description: > + 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. + 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. + 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' + strip-title-prefix: + description: 'Remove [Prefix]: from PR titles (gh-release mode only)' + default: 'false' + github-token: + 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.plan.outputs.output_path || steps.gh-release-output.outputs.output_path }} + +runs: + using: composite + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Validate inputs + shell: bash + env: + MODE: ${{ inputs.mode }} + 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 }} + ARTIFACT_NAME: ${{ inputs.artifact-name }} + run: | + if [[ "$MODE" != "bundle" && "$MODE" != "gh-release" ]]; then + echo "::error::mode must be 'bundle' or 'gh-release', got: ${MODE}" + exit 1 + fi + + if [[ "$MODE" == "gh-release" && -z "$REPO" ]]; then + echo "::error::repo is required in gh-release mode" + exit 1 + fi + + 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 + if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then + echo "::error::${name} must not contain newlines"; 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" + + 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 + + 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._-]+$' + validate_identifier "$ARTIFACT_NAME" "artifact-name" '^[a-zA-Z0-9._-]+$' + + if [ -n "$REPORT" ] && [[ "$REPORT" == http://* ]]; then + 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: ${{ inputs.docs-builder-version }} + github-token: ${{ inputs.github-token }} + + - name: Resolve bundle plan + if: inputs.mode == 'bundle' + id: plan + shell: bash + env: + CONFIG: ${{ inputs.config }} + PROFILE: ${{ inputs.profile }} + VERSION: ${{ inputs.version }} + RELEASE_VERSION: ${{ inputs.release-version }} + REPORT: ${{ inputs.report }} + PRS: ${{ inputs.prs }} + OUTPUT: ${{ inputs.output }} + REPO: ${{ inputs.repo }} + OWNER: ${{ inputs.owner }} + run: | + ARGS=() + ARGS+=(changelog bundle --plan) + 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 + + docs-builder "${ARGS[@]}" + + - name: Verify plan output + if: inputs.mode == 'bundle' + 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 + fi + + - name: Download report + if: inputs.mode == 'bundle' && inputs.report != '' && startsWith(inputs.report, 'https://') + shell: bash + env: + 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 + if: inputs.mode == '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 }} + RELEASE_VERSION: ${{ inputs.release-version }} + REPORT: ${{ inputs.report }} + PRS: ${{ inputs.prs }} + OUTPUT: ${{ inputs.output }} + 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[@]}" "$IMAGE_DIGEST" "${BUNDLE_ARGS[@]}" + + - name: Generate changelogs from GitHub release + if: inputs.mode == 'gh-release' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + IMAGE_DIGEST: ${{ steps.docker-image.outputs.digest }} + CONFIG: ${{ inputs.config }} + VERSION: ${{ inputs.version }} + OUTPUT: ${{ inputs.output }} + REPO: ${{ inputs.repo }} + OWNER: ${{ inputs.owner }} + STRIP_TITLE_PREFIX: ${{ inputs.strip-title-prefix }} + run: | + DOCKER_ARGS=(--rm -v "${PWD}:/github/workspace" -w /github/workspace -e GITHUB_TOKEN) + + GH_RELEASE_ARGS=(changelog gh-release) + + REPO_ARG="$REPO" + if [ -n "$OWNER" ]; then + REPO_ARG="${OWNER}/${REPO}" + fi + GH_RELEASE_ARGS+=("$REPO_ARG") + + [ -n "$VERSION" ] && GH_RELEASE_ARGS+=("$VERSION") || GH_RELEASE_ARGS+=("latest") + [ -n "$CONFIG" ] && GH_RELEASE_ARGS+=(--config "$CONFIG") + [ -n "$OUTPUT" ] && GH_RELEASE_ARGS+=(--output "$OUTPUT") + [ "$STRIP_TITLE_PREFIX" = "true" ] && GH_RELEASE_ARGS+=(--strip-title-prefix) + + docker run "${DOCKER_ARGS[@]}" "$IMAGE_DIGEST" "${GH_RELEASE_ARGS[@]}" + + - name: Discover gh-release output + if: inputs.mode == 'gh-release' + id: gh-release-output + shell: bash + env: + OUTPUT: ${{ inputs.output }} + CONFIG: ${{ inputs.config }} + run: | + SEARCH_DIR="${OUTPUT:-docs/releases}" + if [ -f "$SEARCH_DIR" ]; then + SEARCH_DIR="$(dirname "$SEARCH_DIR")" + fi + + BUNDLE_DIR="${SEARCH_DIR}/bundles" + if [ ! -d "$BUNDLE_DIR" ]; then + BUNDLE_DIR="$SEARCH_DIR" + fi + + BUNDLE_FILE=$(find "$BUNDLE_DIR" -maxdepth 1 -name '*.yaml' -o -name '*.yml' | head -1) + if [ -z "$BUNDLE_FILE" ]; then + echo "::error::No bundle file found in ${BUNDLE_DIR}" + exit 1 + fi + echo "output_path=${BUNDLE_FILE}" >> "$GITHUB_OUTPUT" + echo "::notice title=Bundle output::${BUNDLE_FILE}" + + - name: Upload bundle artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ inputs.artifact-name }} + path: ${{ steps.plan.outputs.output_path || steps.gh-release-output.outputs.output_path }} + 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..8ec07da --- /dev/null +++ b/changelog/bundle-pr/README.md @@ -0,0 +1,31 @@ + +# 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 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 + +| 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..8eb7083 --- /dev/null +++ b/changelog/bundle-pr/action.yml @@ -0,0 +1,121 @@ +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 inputs + shell: bash + env: + OUTPUT: ${{ inputs.output }} + BASE_BRANCH: ${{ inputs.base-branch }} + ARTIFACT_NAME: ${{ inputs.artifact-name }} + 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 + if [[ "$OUTPUT" == *$'\n'* || "$OUTPUT" == *$'\r'* ]]; then + echo "::error::Output path must not contain newlines" + exit 1 + fi + if [ -n "$BASE_BRANCH" ] && [[ ! "$BASE_BRANCH" =~ ^[a-zA-Z0-9._/+-]+$ ]]; then + echo "::error::Base branch contains disallowed characters: ${BASE_BRANCH}" + exit 1 + fi + if [[ ! "$ARTIFACT_NAME" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "::error::Artifact name contains disallowed characters: ${ARTIFACT_NAME}" + 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 diff --git a/changelog/bundle-upload/README.md b/changelog/bundle-upload/README.md new file mode 100644 index 0000000..b280717 --- /dev/null +++ b/changelog/bundle-upload/README.md @@ -0,0 +1,29 @@ +# Changelog bundle upload + +Downloads a changelog bundle artifact and uploads it to the `elastic-docs-v3-changelog-bundles` S3 bucket. Uses OIDC for AWS authentication and docs-builder's incremental upload (only files whose content has changed are transferred). + +## Prerequisites + +Your repository must be listed in the `elastic-docs-v3-changelog-bundles` infrastructure to have an IAM role provisioned for OIDC-based S3 uploads. Contact the docs-engineering team to add your repository. + +## Inputs + +| Name | Description | Required | Default | +|------------------------|-------------------------------------------------------------------------------------------------------|----------|-----------------------| +| `output` | Output file path for the bundle, relative to repo root. Must match the path used by bundle-create | `true` | | +| `config` | Path to changelog.yml configuration file | `false` | `docs/changelog.yml` | +| `artifact-name` | Name of the artifact uploaded by bundle-create | `false` | `changelog-bundle` | +| `docs-builder-version` | docs-builder version (e.g. 0.1.100, latest, edge) | `false` | `edge` | +| `github-token` | GitHub token (used by docs-builder setup). Use the default GITHUB_TOKEN; do not substitute a broader PAT | `false` | `${{ github.token }}` | +| `aws-account-id` | AWS account ID. Only override if OIDC trust and IAM roles have been provisioned for the target account | `false` | `197730964718` | + +## Usage + +```yaml +steps: + - uses: elastic/docs-actions/changelog/bundle-upload@v1 + with: + output: docs/releases/v9.2.0.yaml +``` + +This action is typically used as the second job in the `changelog-bundle.yml` reusable workflow, after `bundle-create` generates the artifact. The S3 key for each bundle is `{product}/bundles/{filename}`, where the product is read from the bundle's YAML `products` array. diff --git a/changelog/bundle-upload/action.yml b/changelog/bundle-upload/action.yml new file mode 100644 index 0000000..273c3c7 --- /dev/null +++ b/changelog/bundle-upload/action.yml @@ -0,0 +1,110 @@ +name: Changelog bundle upload +description: > + Downloads a changelog bundle artifact and uploads it to the private S3 bucket. A scrubber Lambda mirrors + sanitized copies to the public CDN bucket. Uses OIDC for AWS authentication + and docs-builder's incremental upload. + +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 + config: + description: 'Path to changelog.yml configuration file (repo-relative, no ".." or absolute paths)' + default: 'docs/changelog.yml' + artifact-name: + description: 'Name of the artifact uploaded by bundle-create' + default: 'changelog-bundle' + 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' + github-token: + description: 'GitHub token (used by docs-builder setup). Use the default GITHUB_TOKEN; do not substitute a broader PAT.' + default: '${{ github.token }}' + aws-account-id: + description: 'The AWS account ID. Only override if OIDC trust and IAM roles have been provisioned for the target account.' + default: '197730964718' + +runs: + using: composite + steps: + - name: Validate inputs + shell: bash + env: + OUTPUT: ${{ inputs.output }} + CONFIG: ${{ inputs.config }} + ARTIFACT_NAME: ${{ inputs.artifact-name }} + 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 + if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then + echo "::error::${name} must not contain newlines"; exit 1 + fi + } + + if [[ "$OUTPUT" != *.yml && "$OUTPUT" != *.yaml ]]; then + echo "::error::Output path must end in .yml or .yaml: ${OUTPUT}" + exit 1 + fi + validate_path "$OUTPUT" "output" + validate_path "$CONFIG" "config" + + if [[ ! "$ARTIFACT_NAME" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "::error::Artifact name contains disallowed characters: ${ARTIFACT_NAME}" + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Download bundle artifact + uses: actions/download-artifact@v6 + with: + name: ${{ inputs.artifact-name }} + path: .bundle-artifact + + - name: Place bundle in output path + shell: bash + env: + OUTPUT: ${{ inputs.output }} + run: | + mkdir -p "$(dirname "$OUTPUT")" + cp ".bundle-artifact/$(basename "$OUTPUT")" "$OUTPUT" + + - name: Setup docs-builder + uses: elastic/docs-actions/docs-builder/setup@v1 + with: + version: ${{ inputs.docs-builder-version }} + github-token: ${{ inputs.github-token }} + + - name: Authenticate with AWS + uses: elastic/docs-actions/aws/auth@v1 + with: + aws_account_id: ${{ inputs.aws-account-id }} + aws_role_name_prefix: elastic-docs-v3-changelog- + + - name: Upload bundle to S3 + shell: bash + env: + CONFIG: ${{ inputs.config }} + OUTPUT: ${{ inputs.output }} + run: | + docs-builder changelog upload \ + --artifact-type bundle \ + --target s3 \ + --s3-bucket-name elastic-docs-v3-changelog-bundles-private \ + --config "$CONFIG" \ + --directory "$(dirname "$OUTPUT")"