grr-cli 2.3.0

Google tools from the terminal, at maximum performance: 401 methods across 14 Google APIs in one generated command tree, zero-config, over HTTP/3
Documentation
name: Changelog

# CHANGELOG.md is generated from the commit history by
# scripts/generate-changelog.ts. It cannot be hand-maintained without drifting
# from the tags, so the only job here is to keep the committed copy equal to
# what the generator produces.
#
# Why a pull request and not a direct commit to main:
#   - Generated content still deserves a reviewable diff. `git log` is the
#     source of truth, so the interesting question is not "is this text right"
#     but "did the generator mis-group something", and a PR is where that gets
#     noticed.
#   - A direct push to main is not available if the branch is protected, and
#     adding a bypass to work around that would be worse than a PR.
#   - peter-evans/create-pull-request pushes a throwaway branch and force-pushes
#     *that* branch (never main), which is the safe way to keep a generated file
#     current. This is the same mechanism .github/workflows/discovery.yml
#     already uses for the generated Discovery index, so there is one pattern in
#     this repository rather than two.
#   - Consequence: main is never force-pushed, and no job here needs
#     --force-with-lease at all.
#
# The script is pure and idempotent: the same history always renders the same
# bytes, so a run that finds no change opens no PR and this workflow stays
# quiet.

on:
  workflow_dispatch:
    inputs:
      dry_run:
        description: "Generate and show the diff, but do not open a pull request"
        type: boolean
        default: false
      verify:
        description: "Also run the release-only sync check (used by the release watcher)"
        type: boolean
        default: false
  push:
    branches: [main]
  release:
    types: [published]

permissions:
  contents: read

concurrency:
  # Static, NOT keyed on the ref: every trigger pushes the SAME throwaway
  # branch (`chore/changelog-regenerate`), so a push-to-main run and a release
  # run must serialise. Keying on `github.ref` put them in two different
  # groups and let them force-push the same branch concurrently.
  group: changelog
  # Serialised, not cancelled: a release-triggered regeneration must finish
  # even if another push lands behind it. The job is ~2 minutes, so queueing
  # is cheaper than losing the run.
  cancel-in-progress: false

jobs:
  regenerate:
    name: regenerate changelog
    runs-on: ubuntu-24.04-arm
    timeout-minutes: 10
    permissions:
      # Push the regeneration branch; never main.
      contents: write
      pull-requests: write
      # gh workflow run below — waking the merge poller.
      actions: write
    steps:
      - uses: actions/checkout@v7
        timeout-minutes: 5
        with:
          # The generator diffs consecutive tags, so it needs the full history
          # and every tag. A shallow clone would silently render "no changes"
          # for older releases.
          fetch-depth: 0
          fetch-tags: true

      - uses: actions/setup-node@v7
        timeout-minutes: 5
        with:
          # Matches the Node version the other script-running workflow uses.
          node-version: "26"

      - name: Generate CHANGELOG.md
        id: gen
        timeout-minutes: 5
        run: |
          set -euo pipefail
          # pipefail so a non-zero exit from the generator (for example the
          # refuse-to-overwrite-a-hand-written-changelog guard) fails this step
          # instead of being swallowed by `tee`.
          node scripts/generate-changelog.ts | tee changelog-gen.log

          # The script prints machine-readable `key=value` lines; reuse them
          # instead of re-deriving the newest tag in shell.
          TAG=$(grep -m1 '^changelog:newest-tag=' changelog-gen.log | cut -d= -f2- || true)
          echo "newest_tag=${TAG:-unreleased}" >> "$GITHUB_OUTPUT"

          # Heredoc form: a single-line `echo` cannot carry a multi-line value.
          {
            echo 'summary<<CHANGELOG_GEN_EOF'
            cat changelog-gen.log
            echo 'CHANGELOG_GEN_EOF'
          } >> "$GITHUB_OUTPUT"

          # `git status --porcelain` rather than `git diff --quiet`, so a
          # first-time generated file (untracked) counts as a change too.
          # Both outputs are named: the generator writes CHANGELOG.md *and*
          # site/src/data/changelog.json, and the changelog page renders the
          # latter. Detecting only the Markdown file meant a run that moved
          # the JSON alone opened no PR and left the site stale.
          if [ -z "$(git status --porcelain -- CHANGELOG.md site/src/data/changelog.json)" ]; then
            echo "changed=false" >> "$GITHUB_OUTPUT"
            echo "CHANGELOG.md and site/src/data/changelog.json are in sync."
          else
            echo "changed=true" >> "$GITHUB_OUTPUT"
            echo "Changelog output is out of date:"
            git --no-pager diff --stat -- CHANGELOG.md site/src/data/changelog.json
          fi

      - name: Summarise
        if: always()
        run: |
          {
            echo "### Changelog generator"
            echo
            echo '```'
            cat changelog-gen.log 2>/dev/null || echo "(generator produced no log)"
            echo '```'
            if [ "${{ steps.gen.outputs.changed }}" = "true" ]; then
              echo
              echo "#### Diff"
              echo
              echo '```diff'
              git --no-pager diff -- CHANGELOG.md
              echo '```'
            fi
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Report (dry run)
        if: steps.gen.outputs.changed == 'true' && inputs.dry_run == true
        run: |
          echo "::notice title=Dry run::CHANGELOG.md is out of date but no pull"
          echo "::notice::request was opened, because dry_run was set."
          git --no-pager diff -- CHANGELOG.md
          exit 1

      - name: Open a pull request
        id: pr
        if: steps.gen.outputs.changed == 'true' && inputs.dry_run != true
        uses: peter-evans/create-pull-request@v8
        timeout-minutes: 5
        with:
          author: "debanjanbasu <10209115+debanjanbasu@users.noreply.github.com>"
          committer: "debanjanbasu <10209115+debanjanbasu@users.noreply.github.com>"
          branch: chore/changelog-regenerate
          title: "docs(changelog): regenerate for ${{ steps.gen.outputs.newest_tag }}"
          commit-message: "docs(changelog): regenerate for ${{ steps.gen.outputs.newest_tag }}"
          # Same triage label every other generated-artifact automation PR
          # carries (stats/demo/discovery/benchmark).
          labels: dependencies
          add-paths: |
            CHANGELOG.md
            site/src/data/changelog.json
          body: |
            Automated regeneration of `CHANGELOG.md` by
            `.github/workflows/changelog.yml`, run after
            `${{ github.event_name }}`.

            The file is derived entirely from the git tags and the commits
            between them, so this PR is not asking for a wording review — it is
            asking for a sanity check that commits landed in the right section.
            A commit that is missing from the changelog entirely means its
            subject did not parse as a Conventional Commit and it was filed
            under **Other**.

            Re-running the workflow without new commits produces no diff and
            opens no PR.

            <details>
            <summary>Generator output</summary>

            ```
            ${{ steps.gen.outputs.summary }}
            ```

            </details>

      - name: Wake the merge poller
        # CI on a token-authored PR waits in action_required until the poller
        # approves it (GitHub's 2026-06 bot-event gate). Waking the poller now
        # instead of waiting for the next cron tick removes the measured 5-7
        # minutes of approval latency before any CI compute starts. The poller
        # is idempotent, and the cron remains the safety net.
        if: steps.pr.outputs.pull-request-number != ''
        timeout-minutes: 2
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
        run: gh workflow run automation-merge.yml --ref main || echo "::warning::poller dispatch failed"

  verify:
    name: verify changelog is in sync
    # Release-only, on purpose. On a `main` push the generator legitimately
    # wants to add the new commits to `## [Unreleased]`, so a sync check there
    # would fail on every push and train everyone to ignore it. A published
    # release is the moment where "the changelog must describe this version" is
    # a real invariant.
    #
    # The `release` event cannot be relied on alone: releases are created with
    # GITHUB_TOKEN, and a token-authored release starts no workflows, so this
    # job would never run. release-binaries.yml's watcher therefore dispatches
    # this workflow with `verify=true`, which is the second arm below. The
    # `release` arm is kept for the day a release is published by a human.
    #
    # Report-only on the section check, deliberately: the generator emits one
    # heading per TAG, so the commit a tag points at cannot contain its own
    # section — the tag did not exist when that commit was made. The old
    # check grepped the tagged tree and therefore failed on every release,
    # forever (v0.11.0 and v0.11.1 both went red this way). What is checked
    # here instead is main's CHANGELOG.md, and a miss warns: the regeneration
    # PR normally merges minutes after the release, so lagging is the expected
    # state at release time. The hard, grace-windowed gate is the daily
    # verify-delivery check (scripts/verify-delivery.ts), which fails once the
    # release leaves the window with the section still missing.
    if: (github.event_name == 'release' && github.event.action == 'published') || inputs.verify == true
    runs-on: ubuntu-24.04-arm
    timeout-minutes: 10
    permissions:
      # Read-only: this job verifies, it never writes.
      contents: read
    steps:
      - uses: actions/checkout@v7
        timeout-minutes: 5
        with:
          fetch-depth: 0
          fetch-tags: true

      - uses: actions/setup-node@v7
        timeout-minutes: 5
        with:
          node-version: "26"

      - name: Report on the released version's changelog section
        timeout-minutes: 5
        run: |
          set -euo pipefail
          # Two checks with different strictness, because they answer
          # different questions:
          #
          # 1. REPORT: does main's CHANGELOG.md carry a section for the version
          #    being released? main, not the checked-out tree — on the
          #    `release` event the checkout is the tag, and a tag's commit can
          #    never contain its own section (see the job comment above). A
          #    miss warns here; verify-delivery.yml fails on it once the
          #    release leaves its grace window, which is the enforcement.
          VERSION="$(grep -m1 '^version' Cargo.toml | sed 's/version = "\(.*\)"/\1/')"
          # A failed fetch falls back to HEAD rather than failing a report:
          # HEAD is the best available snapshot, and the enforcement lives in
          # verify-delivery.yml.
          ref=HEAD
          git fetch --no-tags origin main && ref=origin/main || true
          # No grep -q: it exits at the first match, git show takes SIGPIPE on
          # the still-open pipe, and pipefail would read that as "no match".
          if git show "$ref:CHANGELOG.md" | grep -E "^## \[?v?${VERSION}\]?" > /dev/null; then
            echo "CHANGELOG.md on $ref documents ${VERSION}."
          else
            echo "::warning title=CHANGELOG.md has no section for ${VERSION} yet::The section is generated from the tag, so it can only land after the regeneration PR (chore/changelog-regenerate) merges — a snapshot taken minutes after a release is expected to lag. No action needed unless it is still missing tomorrow: verify-delivery.yml fails once the release leaves the 12h grace window."
          fi
          #
          # 2. ADVISORY: the generated file matching the moving HEAD exactly.
          #    On a repo that regenerates on every push this is true only in
          #    quiet windows — a bot merge minutes later re-dilutes it — so a
          #    mismatch is a warning here, not a failure. The regeneration PR
          #    is the mechanism that converges it; failing the release verify
          #    on a race nobody can win would just train everyone to ignore it.
          if node scripts/generate-changelog.ts --check; then
            echo "CHANGELOG.md is byte-identical to the generator output."
          else
            echo "::warning title=CHANGELOG.md is one regeneration behind::A push landed after the last regeneration; the changelog workflow's PR will converge it. No action needed unless it persists."
          fi