From 549c20745929e4b6f9a3b49c747fb144820a3f5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:33:26 +0000 Subject: [PATCH 1/7] Add chronus-verify CI workflow and update changelog docs for azpysdk Agent-Logs-Url: https://github.com/Azure/azure-sdk-for-python/sessions/5b569801-2fe6-444b-9f82-e75c3eff1f9b Co-authored-by: l0lawrence <100643745+l0lawrence@users.noreply.github.com> --- .github/workflows/chronus-verify.yml | 39 ++++++++++++++++++++++++++ doc/dev/changelog_updates.md | 41 +++++++++++++++++++++------- 2 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/chronus-verify.yml diff --git a/.github/workflows/chronus-verify.yml b/.github/workflows/chronus-verify.yml new file mode 100644 index 000000000000..18bee7cd37a9 --- /dev/null +++ b/.github/workflows/chronus-verify.yml @@ -0,0 +1,39 @@ +name: Chronus Verify + +on: + pull_request: + branches: [main] + paths: + - "sdk/*/*/**" + +jobs: + chronus-verify: + name: Verify Chronus Change Descriptions + if: >- + github.event.pull_request.user.login != 'azure-sdk' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # needed so chronus can diff against base branch + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: npm + cache-dependency-path: .github/chronus/package-lock.json + + - name: Install pinned dependencies + run: npm ci + working-directory: .github/chronus + + - name: Run chronus verify + run: .github/chronus/node_modules/.bin/chronus verify + + - name: Chronus verification failed – see docs + if: failure() + run: | + echo "::error::Chronus verification failed. Add a change description with 'azpysdk changelog add' (or 'npx chronus add' if you prefer raw Chronus)." + echo "::error::See https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/changelog_updates.md for instructions." diff --git a/doc/dev/changelog_updates.md b/doc/dev/changelog_updates.md index c39d8d49e25e..95fb154bba4f 100644 --- a/doc/dev/changelog_updates.md +++ b/doc/dev/changelog_updates.md @@ -10,21 +10,23 @@ The repository configuration lives in [`.chronus/config.yaml`](https://github.co ## Prerequisites -Chronus is distributed as an npm package. To use it, you need [Node.js](https://nodejs.org/) installed (LTS version recommended). You can then run Chronus without a global install using `npx`: +The recommended way to interact with Chronus is through the `azpysdk` CLI, which is already available in this repository's developer environment and handles installing Chronus automatically. + +If you prefer to invoke Chronus directly, it is distributed as an npm package and requires [Node.js](https://nodejs.org/) (LTS version recommended). You can run it without a global install using `npx`: ```bash npx chronus ``` -Alternatively, install it globally: +## Adding a Change Description + +When you make changes to a package that has a `pyproject.toml`, add a change description by running the following from the repository root or from within the package directory: ```bash -npm install -g @chronus/chronus +azpysdk changelog add ``` -## Adding a Change Description - -When you make changes to a package that has a `pyproject.toml`, run `chronus add` from the root of the repository: +Alternatively, using raw Chronus: ```bash npx chronus add @@ -55,7 +57,13 @@ The following change kinds are defined for this repository: ### Specifying a Package Directly -You can skip the interactive prompt by passing the package path(s) directly: +You can skip the interactive prompt by passing the package path and change details directly: + +```bash +azpysdk changelog add sdk/storage/azure-storage-blob --kind fix --message "Fixed upload failure on large files" +``` + +Or using raw Chronus: ```bash npx chronus add sdk/storage/azure-storage-blob @@ -65,9 +73,9 @@ npx chronus add sdk/storage/azure-storage-blob ```bash # After making changes to azure-storage-blob, add a change description -npx chronus add sdk/storage/azure-storage-blob +azpysdk changelog add sdk/storage/azure-storage-blob -# Chronus will prompt you: +# You will be prompted to select: # ? What kind of change is this? â€ē fix # ? Describe the change: â€ē Fixed an issue where upload would fail on large files ``` @@ -90,10 +98,17 @@ You commit this file along with your code changes. To check whether all modified packages have a corresponding change description (e.g., before opening a PR), run: +```bash +azpysdk changelog verify +``` + +Or using raw Chronus: + ```bash npx chronus verify ``` +> **Note:** The CI workflow (`Chronus Verify`) runs `chronus verify` automatically on every pull request that modifies files under `sdk/`. If it fails, add a change description with `azpysdk changelog add`. If your changes don't need a changelog entry (e.g., pure documentation or test-only changes unrelated to package behavior), you can add an `internal` change kind entry to satisfy the requirement without bumping the version. @@ -101,6 +116,12 @@ If your changes don't need a changelog entry (e.g., pure documentation or test-o To see a summary of all pending changes and the resulting version bumps: +```bash +azpysdk changelog status +``` + +Or using raw Chronus: + ```bash npx chronus status ``` @@ -109,7 +130,7 @@ npx chronus status Packages in this repository that use `pyproject.toml` (instead of or alongside `setup.py`) are fully supported by Chronus. The `pyproject.toml` is used for package metadata, while the `CHANGELOG.md` in the package directory remains the canonical user-facing changelog. -Chronus reads the package version from the Python package metadata and writes changelog entries into the `CHANGELOG.md` file with `npx chronus changelog`. You do not need to manually edit `CHANGELOG.md` for your changes. +Chronus reads the package version from the Python package metadata and writes changelog entries into the `CHANGELOG.md` file with `azpysdk changelog create` (or `npx chronus changelog`). You do not need to manually edit `CHANGELOG.md` for your changes. ## Further Reading From 3c3c6aa05ca59e0637bd46fc266ff83c2ec27799 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:34:22 +0000 Subject: [PATCH 2/7] Fix doc: accurately reflect chronus-verify workflow path filter Agent-Logs-Url: https://github.com/Azure/azure-sdk-for-python/sessions/5b569801-2fe6-444b-9f82-e75c3eff1f9b Co-authored-by: l0lawrence <100643745+l0lawrence@users.noreply.github.com> --- doc/dev/changelog_updates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dev/changelog_updates.md b/doc/dev/changelog_updates.md index 95fb154bba4f..ff9c6e42a0d3 100644 --- a/doc/dev/changelog_updates.md +++ b/doc/dev/changelog_updates.md @@ -108,7 +108,7 @@ Or using raw Chronus: npx chronus verify ``` -> **Note:** The CI workflow (`Chronus Verify`) runs `chronus verify` automatically on every pull request that modifies files under `sdk/`. If it fails, add a change description with `azpysdk changelog add`. +> **Note:** The CI workflow (`Chronus Verify`) runs `chronus verify` automatically on every pull request that modifies source files under `sdk/` (specifically files matching `sdk/*/*/**`). If it fails, add a change description with `azpysdk changelog add`. If your changes don't need a changelog entry (e.g., pure documentation or test-only changes unrelated to package behavior), you can add an `internal` change kind entry to satisfy the requirement without bumping the version. From fdd8e8f6bb2d01a8c790955015ca4724c870d914 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 1 May 2026 08:55:41 -0700 Subject: [PATCH 3/7] Add /chronus add slash-command auto-fix workflow On chronus-verify failure, post a sticky PR comment instructing the contributor to comment '/chronus add [kind]' for a one-click fix. Add chronus-fix.yml: an issue_comment-triggered workflow that verifies commenter permissions, rejects fork PRs (security), parses the requested kind, runs 'chronus add --kind X --message ' for each missing package, and pushes the result back to the PR branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/chronus-fix.yml | 312 +++++++++++++++++++++++++++ .github/workflows/chronus-verify.yml | 67 +++++- doc/dev/changelog_updates.md | 5 +- 3 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/chronus-fix.yml diff --git a/.github/workflows/chronus-fix.yml b/.github/workflows/chronus-fix.yml new file mode 100644 index 000000000000..2a97a567d997 --- /dev/null +++ b/.github/workflows/chronus-fix.yml @@ -0,0 +1,312 @@ +name: Chronus Fix (slash command) + +# Listens for `/chronus add [kind]` comments on PRs and commits a Chronus +# change entry derived from the PR title back to the PR branch. +# +# Security note: this workflow runs in default-branch context with a +# write-scoped GITHUB_TOKEN. Because we install the chronus tooling pinned +# at .github/chronus/* from the PR head, we restrict execution to PRs that +# originate from the same repository. Fork PRs receive a comment instructing +# the contributor to run the command locally instead. + +on: + issue_comment: + types: [created] + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: chronus-fix-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + chronus-fix: + name: Apply chronus add via slash command + runs-on: ubuntu-latest + if: >- + github.event.issue.pull_request != null && + startsWith(github.event.comment.body, '/chronus add') + steps: + - name: Verify commenter has write permission + uses: actions/github-script@v7 + with: + script: | + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.comment.user.login, + }); + if (!['admin', 'maintain', 'write'].includes(data.permission)) { + core.setFailed( + `User ${context.payload.comment.user.login} lacks write permission ` + + `(has: ${data.permission}).` + ); + } + + - name: React 👀 to the comment + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes', + }); + + - name: Get PR metadata and reject fork PRs + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.issue.number, + }); + + const isFork = pr.head.repo.full_name !== pr.base.repo.full_name; + if (isFork) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: [ + 'âš ī¸ `/chronus add` cannot push to fork PR branches.', + '', + 'Please run the following locally and push the resulting file:', + '', + '```bash', + 'azpysdk changelog add', + '```', + ].join('\n'), + }); + core.setFailed('Fork PRs are not supported by /chronus add.'); + return; + } + + // Validate ref shape (defensive — refs come from the API but we + // pass them to git below). Allow standard branch characters only. + if (!/^[A-Za-z0-9._/\-]+$/.test(pr.head.ref) || + !/^[A-Za-z0-9._/\-]+$/.test(pr.base.ref)) { + core.setFailed(`Refusing unusual ref name. head=${pr.head.ref} base=${pr.base.ref}`); + return; + } + + core.setOutput('head_ref', pr.head.ref); + core.setOutput('head_sha', pr.head.sha); + core.setOutput('base_ref', pr.base.ref); + core.setOutput('title', pr.title); + + - name: Parse requested kind from comment + id: kind + env: + BODY: ${{ github.event.comment.body }} + run: | + set -euo pipefail + # Extract the third whitespace-separated token: "/chronus add ". + token="$(printf '%s' "$BODY" | awk '{print $3}' | tr -d '\r')" + case "${token,,}" in + ""|internal) kind="internal" ;; + fix|bug) kind="fix" ;; + feature|features) kind="feature" ;; + breaking) kind="breaking" ;; + deprecation|deprecated) kind="deprecation" ;; + dependencies|deps) kind="dependencies" ;; + *) + echo "::error::Unknown kind '$token'. Allowed: internal, fix, feature, breaking, deprecation, dependencies." + exit 1 + ;; + esac + echo "kind=$kind" >> "$GITHUB_OUTPUT" + + - name: Checkout PR head + uses: actions/checkout@v4 + with: + # We're already restricted to same-repo PRs above, so the default + # repository (github.repository) is the right source. + ref: ${{ steps.pr.outputs.head_sha }} + fetch-depth: 0 + persist-credentials: true + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fetch base branch for chronus diff + env: + BASE_REF: ${{ steps.pr.outputs.base_ref }} + run: | + set -euo pipefail + # Chronus reads `baseBranch` from .chronus/config.yaml (currently + # "main") and diffs against it via git. Make sure that branch ref + # exists locally. + git fetch origin "$BASE_REF:$BASE_REF" || git fetch origin "$BASE_REF" + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: npm + cache-dependency-path: .github/chronus/package-lock.json + + - name: Install pinned chronus + run: npm ci + working-directory: .github/chronus + + - name: Determine packages missing a change entry + id: missing + run: | + set -uo pipefail + # `chronus verify` exits non-zero when entries are missing. Capture + # output and parse the package list. Don't fail this step on + # non-zero exit because the missing-entries case is exactly what + # we want to handle. + out="$(.github/chronus/node_modules/.bin/chronus verify 2>&1)" || true + printf '%s\n' "$out" + + # Extract anything that looks like sdk//. + pkgs="$(printf '%s\n' "$out" | grep -oE 'sdk/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+' | sort -u || true)" + if [ -z "$pkgs" ]; then + echo "found=0" >> "$GITHUB_OUTPUT" + else + echo "found=1" >> "$GITHUB_OUTPUT" + { + echo 'packages<> "$GITHUB_OUTPUT" + fi + + - name: Add chronus entry per package + if: steps.missing.outputs.found == '1' + env: + KIND: ${{ steps.kind.outputs.kind }} + MESSAGE: ${{ steps.pr.outputs.title }} + PACKAGES: ${{ steps.missing.outputs.packages }} + run: | + set -euo pipefail + while IFS= read -r pkg_path; do + [ -z "$pkg_path" ] && continue + pkg_name="$(basename "$pkg_path")" + echo "::group::chronus add $pkg_name ($KIND)" + # `chronus add` takes the package name as a positional argument. + .github/chronus/node_modules/.bin/chronus add \ + "$pkg_name" \ + --kind "$KIND" \ + --message "$MESSAGE" + echo "::endgroup::" + done <<< "$PACKAGES" + + - name: Commit and push + id: push + if: steps.missing.outputs.found == '1' + env: + HEAD_REF: ${{ steps.pr.outputs.head_ref }} + ACTOR: ${{ github.event.comment.user.login }} + ACTOR_ID: ${{ github.event.comment.user.id }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + set -euo pipefail + # Defensive validation — reject unusual branch names. + git check-ref-format --branch "$HEAD_REF" + + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .chronus/changes/ + if git diff --cached --quiet; then + echo "Nothing staged — chronus add did not produce a file." + echo "pushed=0" >> "$GITHUB_OUTPUT" + exit 0 + fi + + msg="$(printf 'Add chronus changelog entry [skip ci]\n\nTriggered by /chronus add comment on PR #%s.\n\nCo-authored-by: %s <%s+%s@users.noreply.github.com>' \ + "$PR_NUMBER" "$ACTOR" "$ACTOR_ID" "$ACTOR")" + git commit -m "$msg" + + if git push origin "HEAD:refs/heads/$HEAD_REF"; then + echo "pushed=1" >> "$GITHUB_OUTPUT" + else + echo "pushed=0" >> "$GITHUB_OUTPUT" + echo "::warning::Push failed. The PR branch may have moved; ask the user to retry." + fi + + - name: React 🚀 and reply on success + if: success() && steps.push.outputs.pushed == '1' + uses: actions/github-script@v7 + env: + KIND: ${{ steps.kind.outputs.kind }} + TITLE: ${{ steps.pr.outputs.title }} + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket', + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: [ + '✅ Pushed a Chronus change entry to this PR.', + '', + '- **Kind:** `' + process.env.KIND + '`', + '- **Description:** _' + process.env.TITLE + '_', + '', + 'Edit the file under `.chronus/changes/` if you\'d like to refine the description.', + ].join('\n'), + }); + + - name: Reply on no-op + if: success() && steps.missing.outputs.found != '1' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: [ + 'â„šī¸ Could not detect any packages missing a Chronus change entry.', + '', + 'Either every changed package already has an entry, or `chronus verify`', + 'failed for an unrelated reason — check the workflow run for details.', + ].join('\n'), + }); + + - name: Reply on push failure + if: success() && steps.missing.outputs.found == '1' && steps.push.outputs.pushed != '1' + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'confused', + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: [ + 'âš ī¸ Could not push a Chronus entry to this PR branch.', + '', + 'The branch may have moved while the bot was working. Please', + 'comment `/chronus add` again, or run `azpysdk changelog add` locally.', + ].join('\n'), + }); + + - name: React 👎 on failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1', + }); diff --git a/.github/workflows/chronus-verify.yml b/.github/workflows/chronus-verify.yml index 18bee7cd37a9..faf702890cdf 100644 --- a/.github/workflows/chronus-verify.yml +++ b/.github/workflows/chronus-verify.yml @@ -14,6 +14,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + pull-requests: write steps: - uses: actions/checkout@v4 with: @@ -30,10 +31,70 @@ jobs: working-directory: .github/chronus - name: Run chronus verify + id: verify run: .github/chronus/node_modules/.bin/chronus verify - - name: Chronus verification failed – see docs - if: failure() + - name: Post sticky PR comment with one-click fix instructions + if: failure() && steps.verify.conclusion == 'failure' && github.event.pull_request.head.repo.full_name == github.repository + uses: actions/github-script@v7 + with: + script: | + const HEADER = ''; + const body = [ + HEADER, + '### 📝 Missing changelog entry', + '', + 'This PR touches package source under `sdk/*/*/**` but no Chronus', + 'change description was found. CI requires every user-affecting', + 'change to have one.', + '', + '#### ⚡ One-click fix', + '', + '**Comment `/chronus add` on this PR** and a bot will commit a', + 'changelog entry for you, derived from your PR title.', + '', + 'Customise the entry kind by appending it to the command:', + '', + '- `/chronus add`  â†’  defaults to `internal`', + '- `/chronus add fix`  â†’  bug fix', + '- `/chronus add feature`  â†’  new feature', + '- `/chronus add breaking`  â†’  breaking change', + '- `/chronus add deprecation`  â†’  deprecation', + '- `/chronus add dependencies`  â†’  dependency bump', + '', + '> â„šī¸ For PRs from forks, run the command locally instead:', + '>', + '> ```bash', + '> azpysdk changelog add', + '> ```', + '>', + 'See [`doc/dev/changelog_updates.md`](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/changelog_updates.md) for full instructions.', + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + }); + const existing = comments.find(c => c.body && c.body.startsWith(HEADER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); + } + + - name: Emit annotation on failure + if: failure() && steps.verify.conclusion == 'failure' run: | - echo "::error::Chronus verification failed. Add a change description with 'azpysdk changelog add' (or 'npx chronus add' if you prefer raw Chronus)." + echo "::error::Chronus verification failed. Comment '/chronus add' on this PR for an automated fix, or run 'azpysdk changelog add' locally." echo "::error::See https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/changelog_updates.md for instructions." diff --git a/doc/dev/changelog_updates.md b/doc/dev/changelog_updates.md index ff9c6e42a0d3..f7daf8143ec5 100644 --- a/doc/dev/changelog_updates.md +++ b/doc/dev/changelog_updates.md @@ -108,7 +108,10 @@ Or using raw Chronus: npx chronus verify ``` -> **Note:** The CI workflow (`Chronus Verify`) runs `chronus verify` automatically on every pull request that modifies source files under `sdk/` (specifically files matching `sdk/*/*/**`). If it fails, add a change description with `azpysdk changelog add`. +> **Note:** The CI workflow (`Chronus Verify`) runs `chronus verify` automatically on every pull request that modifies source files under `sdk/` (specifically files matching `sdk/*/*/**`). If it fails, you have two options: +> +> - **One-click fix:** comment `/chronus add` on the PR (optionally followed by a kind, e.g. `/chronus add fix`). A bot will commit a Chronus entry derived from the PR title back to the branch. *Available for PRs from the main repository only — fork PRs should run the command locally.* +> - **Locally:** run `azpysdk changelog add` and push the resulting `.chronus/changes/*.md` file. If your changes don't need a changelog entry (e.g., pure documentation or test-only changes unrelated to package behavior), you can add an `internal` change kind entry to satisfy the requirement without bumping the version. From 8fe8a55c7ba9de0ea924e7dfefd91cd4e41b00bf Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 1 May 2026 14:26:04 -0700 Subject: [PATCH 4/7] Harden chronus workflows: SHA-pinned actions, base-branch tooling, tighter perms - Pin actions/checkout, setup-node, github-script to SHA + version comment. - chronus-fix: restore .github/chronus from base branch before npm ci/run, so PR-head changes to the tooling cannot execute under the write token. - persist-credentials: false on both checkouts; push uses an explicit x-access-token URL only at push time. - Permissions: verify drops pull-requests:write for issues:write only; fix drops pull-requests:write to pull-requests:read. - Slash command parsed by strict regex; rejects /chronus additive etc. - Add concurrency groups on both workflows. - Consolidate 4 small github-script steps into one auth+metadata step. - Merge 4 terminal reply steps into one always() switch step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/chronus-fix.yml | 276 +++++++++++---------------- .github/workflows/chronus-verify.yml | 23 ++- 2 files changed, 123 insertions(+), 176 deletions(-) diff --git a/.github/workflows/chronus-fix.yml b/.github/workflows/chronus-fix.yml index 2a97a567d997..d2af72438f0e 100644 --- a/.github/workflows/chronus-fix.yml +++ b/.github/workflows/chronus-fix.yml @@ -1,13 +1,15 @@ name: Chronus Fix (slash command) -# Listens for `/chronus add [kind]` comments on PRs and commits a Chronus -# change entry derived from the PR title back to the PR branch. +# Listens for `/chronus add [kind]` PR comments and commits a Chronus change +# entry derived from the PR title back to the PR branch. # -# Security note: this workflow runs in default-branch context with a -# write-scoped GITHUB_TOKEN. Because we install the chronus tooling pinned -# at .github/chronus/* from the PR head, we restrict execution to PRs that -# originate from the same repository. Fork PRs receive a comment instructing -# the contributor to run the command locally instead. +# Security model: +# - Restricted to same-repository PRs (fork PRs get a polite reply with +# local-run instructions). +# - Commenter must have write permission. +# - The Chronus tooling under `.github/chronus` is restored from the BASE +# branch before install/run, so PR-head changes to the tooling cannot +# execute under this workflow's write-scoped token. on: issue_comment: @@ -16,7 +18,7 @@ on: permissions: contents: write issues: write - pull-requests: write + pull-requests: read concurrency: group: chronus-fix-${{ github.event.issue.number }} @@ -30,50 +32,61 @@ jobs: github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/chronus add') steps: - - name: Verify commenter has write permission - uses: actions/github-script@v7 + - name: Authorize command and load PR metadata + id: pr + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + const comment = context.payload.comment; + const issue = context.payload.issue; + + const match = comment.body.trim().match(/^\/chronus\s+add(?:\s+([A-Za-z]+))?\s*$/i); + if (!match) { + core.setFailed('Invalid command. Use `/chronus add [kind]`.'); + return; + } + const aliases = { + internal: 'internal', + fix: 'fix', bug: 'fix', + feature: 'feature', features: 'feature', + breaking: 'breaking', + deprecation: 'deprecation', deprecated: 'deprecation', + dependencies: 'dependencies', deps: 'dependencies', + }; + const kind = aliases[(match[1] || 'internal').toLowerCase()]; + if (!kind) { + core.setFailed('Unknown kind. Allowed: internal, fix, feature, breaking, deprecation, dependencies.'); + return; + } + + const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ owner: context.repo.owner, repo: context.repo.repo, - username: context.payload.comment.user.login, + username: comment.user.login, }); - if (!['admin', 'maintain', 'write'].includes(data.permission)) { - core.setFailed( - `User ${context.payload.comment.user.login} lacks write permission ` + - `(has: ${data.permission}).` - ); + if (!['admin', 'maintain', 'write'].includes(perm.permission)) { + core.setFailed(`User ${comment.user.login} lacks write permission (has: ${perm.permission}).`); + return; } - - name: React 👀 to the comment - uses: actions/github-script@v7 - with: - script: | await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, - comment_id: context.payload.comment.id, + comment_id: comment.id, content: 'eyes', }); - - name: Get PR metadata and reject fork PRs - id: pr - uses: actions/github-script@v7 - with: - script: | const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: context.payload.issue.number, + pull_number: issue.number, }); - const isFork = pr.head.repo.full_name !== pr.base.repo.full_name; - if (isFork) { + if (pr.head.repo.full_name !== pr.base.repo.full_name) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.payload.issue.number, + issue_number: issue.number, body: [ 'âš ī¸ `/chronus add` cannot push to fork PR branches.', '', @@ -88,10 +101,8 @@ jobs: return; } - // Validate ref shape (defensive — refs come from the API but we - // pass them to git below). Allow standard branch characters only. - if (!/^[A-Za-z0-9._/\-]+$/.test(pr.head.ref) || - !/^[A-Za-z0-9._/\-]+$/.test(pr.base.ref)) { + const refOk = /^[A-Za-z0-9._/\-]+$/; + if (!refOk.test(pr.head.ref) || !refOk.test(pr.base.ref)) { core.setFailed(`Refusing unusual ref name. head=${pr.head.ref} base=${pr.base.ref}`); return; } @@ -100,51 +111,26 @@ jobs: core.setOutput('head_sha', pr.head.sha); core.setOutput('base_ref', pr.base.ref); core.setOutput('title', pr.title); - - - name: Parse requested kind from comment - id: kind - env: - BODY: ${{ github.event.comment.body }} - run: | - set -euo pipefail - # Extract the third whitespace-separated token: "/chronus add ". - token="$(printf '%s' "$BODY" | awk '{print $3}' | tr -d '\r')" - case "${token,,}" in - ""|internal) kind="internal" ;; - fix|bug) kind="fix" ;; - feature|features) kind="feature" ;; - breaking) kind="breaking" ;; - deprecation|deprecated) kind="deprecation" ;; - dependencies|deps) kind="dependencies" ;; - *) - echo "::error::Unknown kind '$token'. Allowed: internal, fix, feature, breaking, deprecation, dependencies." - exit 1 - ;; - esac - echo "kind=$kind" >> "$GITHUB_OUTPUT" + core.setOutput('kind', kind); - name: Checkout PR head - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # We're already restricted to same-repo PRs above, so the default - # repository (github.repository) is the right source. ref: ${{ steps.pr.outputs.head_sha }} fetch-depth: 0 - persist-credentials: true - token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false - - name: Fetch base branch for chronus diff + - name: Fetch base branch and restore trusted tooling env: BASE_REF: ${{ steps.pr.outputs.base_ref }} run: | set -euo pipefail - # Chronus reads `baseBranch` from .chronus/config.yaml (currently - # "main") and diffs against it via git. Make sure that branch ref - # exists locally. - git fetch origin "$BASE_REF:$BASE_REF" || git fetch origin "$BASE_REF" + git fetch --no-tags origin "$BASE_REF:$BASE_REF" + # Replace .github/chronus with the base-branch version so the PR + # cannot influence what we install or execute below. + git restore --source="$BASE_REF" -- .github/chronus - - name: Setup Node - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: lts/* cache: npm @@ -154,68 +140,49 @@ jobs: run: npm ci working-directory: .github/chronus - - name: Determine packages missing a change entry - id: missing + - name: Run chronus add for missing packages + id: add + env: + KIND: ${{ steps.pr.outputs.kind }} + MESSAGE: ${{ steps.pr.outputs.title }} run: | set -uo pipefail - # `chronus verify` exits non-zero when entries are missing. Capture - # output and parse the package list. Don't fail this step on - # non-zero exit because the missing-entries case is exactly what - # we want to handle. out="$(.github/chronus/node_modules/.bin/chronus verify 2>&1)" || true printf '%s\n' "$out" - # Extract anything that looks like sdk//. pkgs="$(printf '%s\n' "$out" | grep -oE 'sdk/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+' | sort -u || true)" if [ -z "$pkgs" ]; then echo "found=0" >> "$GITHUB_OUTPUT" - else - echo "found=1" >> "$GITHUB_OUTPUT" - { - echo 'packages<> "$GITHUB_OUTPUT" + exit 0 fi + echo "found=1" >> "$GITHUB_OUTPUT" - - name: Add chronus entry per package - if: steps.missing.outputs.found == '1' - env: - KIND: ${{ steps.kind.outputs.kind }} - MESSAGE: ${{ steps.pr.outputs.title }} - PACKAGES: ${{ steps.missing.outputs.packages }} - run: | - set -euo pipefail + set -e while IFS= read -r pkg_path; do [ -z "$pkg_path" ] && continue pkg_name="$(basename "$pkg_path")" echo "::group::chronus add $pkg_name ($KIND)" - # `chronus add` takes the package name as a positional argument. - .github/chronus/node_modules/.bin/chronus add \ - "$pkg_name" \ - --kind "$KIND" \ - --message "$MESSAGE" + .github/chronus/node_modules/.bin/chronus add "$pkg_name" --kind "$KIND" --message "$MESSAGE" echo "::endgroup::" - done <<< "$PACKAGES" + done <<< "$pkgs" - name: Commit and push id: push - if: steps.missing.outputs.found == '1' + if: steps.add.outputs.found == '1' env: HEAD_REF: ${{ steps.pr.outputs.head_ref }} ACTOR: ${{ github.event.comment.user.login }} ACTOR_ID: ${{ github.event.comment.user.id }} PR_NUMBER: ${{ github.event.issue.number }} + GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail - # Defensive validation — reject unusual branch names. git check-ref-format --branch "$HEAD_REF" git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add .chronus/changes/ if git diff --cached --quiet; then - echo "Nothing staged — chronus add did not produce a file." echo "pushed=0" >> "$GITHUB_OUTPUT" exit 0 fi @@ -224,89 +191,62 @@ jobs: "$PR_NUMBER" "$ACTOR" "$ACTOR_ID" "$ACTOR")" git commit -m "$msg" - if git push origin "HEAD:refs/heads/$HEAD_REF"; then + if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$HEAD_REF"; then echo "pushed=1" >> "$GITHUB_OUTPUT" else echo "pushed=0" >> "$GITHUB_OUTPUT" echo "::warning::Push failed. The PR branch may have moved; ask the user to retry." fi - - name: React 🚀 and reply on success - if: success() && steps.push.outputs.pushed == '1' - uses: actions/github-script@v7 + - name: Reply with final result + if: always() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 env: - KIND: ${{ steps.kind.outputs.kind }} + KIND: ${{ steps.pr.outputs.kind }} TITLE: ${{ steps.pr.outputs.title }} + FOUND: ${{ steps.add.outputs.found }} + PUSHED: ${{ steps.push.outputs.pushed }} + JOB_STATUS: ${{ job.status }} with: script: | - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket', + const issue_number = context.payload.issue.number; + const comment_id = context.payload.comment.id; + const react = (content) => github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id, content, }); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - body: [ - '✅ Pushed a Chronus change entry to this PR.', - '', - '- **Kind:** `' + process.env.KIND + '`', - '- **Description:** _' + process.env.TITLE + '_', - '', - 'Edit the file under `.chronus/changes/` if you\'d like to refine the description.', - ].join('\n'), + const reply = (body) => github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number, body, }); - - name: Reply on no-op - if: success() && steps.missing.outputs.found != '1' - uses: actions/github-script@v7 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - body: [ + if (process.env.JOB_STATUS !== 'success') { + await react('-1'); + return; + } + if (process.env.FOUND !== '1') { + await reply([ 'â„šī¸ Could not detect any packages missing a Chronus change entry.', '', 'Either every changed package already has an entry, or `chronus verify`', 'failed for an unrelated reason — check the workflow run for details.', - ].join('\n'), - }); - - - name: Reply on push failure - if: success() && steps.missing.outputs.found == '1' && steps.push.outputs.pushed != '1' - uses: actions/github-script@v7 - with: - script: | - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'confused', - }); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - body: [ - 'âš ī¸ Could not push a Chronus entry to this PR branch.', + ].join('\n')); + return; + } + if (process.env.PUSHED === '1') { + await react('rocket'); + await reply([ + '✅ Pushed a Chronus change entry to this PR.', '', - 'The branch may have moved while the bot was working. Please', - 'comment `/chronus add` again, or run `azpysdk changelog add` locally.', - ].join('\n'), - }); - - - name: React 👎 on failure - if: failure() - uses: actions/github-script@v7 - with: - script: | - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '-1', - }); + '- **Kind:** `' + process.env.KIND + '`', + '- **Description:** _' + process.env.TITLE + '_', + '', + 'Edit the file under `.chronus/changes/` if you\'d like to refine the description.', + ].join('\n')); + return; + } + await react('confused'); + await reply([ + 'âš ī¸ Could not push a Chronus entry to this PR branch.', + '', + 'The branch may have moved while the bot was working. Please', + 'comment `/chronus add` again, or run `azpysdk changelog add` locally.', + ].join('\n')); diff --git a/.github/workflows/chronus-verify.yml b/.github/workflows/chronus-verify.yml index faf702890cdf..7814f2c216dc 100644 --- a/.github/workflows/chronus-verify.yml +++ b/.github/workflows/chronus-verify.yml @@ -6,21 +6,25 @@ on: paths: - "sdk/*/*/**" +concurrency: + group: chronus-verify-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: chronus-verify: name: Verify Chronus Change Descriptions - if: >- - github.event.pull_request.user.login != 'azure-sdk' + if: github.event.pull_request.user.login != 'azure-sdk' runs-on: ubuntu-latest permissions: contents: read - pull-requests: write + issues: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # needed so chronus can diff against base branch + persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: lts/* cache: npm @@ -34,9 +38,12 @@ jobs: id: verify run: .github/chronus/node_modules/.bin/chronus verify - - name: Post sticky PR comment with one-click fix instructions + # Sticky comment is only post-able when GITHUB_TOKEN has write scope — + # i.e. PRs from the main repo. Fork PRs see only the error annotation + # below, which is fine because /chronus add doesn't work for forks anyway. + - name: Post sticky one-click-fix PR comment on failure if: failure() && steps.verify.conclusion == 'failure' && github.event.pull_request.head.repo.full_name == github.repository - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const HEADER = ''; @@ -67,7 +74,7 @@ jobs: '> ```bash', '> azpysdk changelog add', '> ```', - '>', + '', 'See [`doc/dev/changelog_updates.md`](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/changelog_updates.md) for full instructions.', ].join('\n'); From 6e100c55481de5451ef735f78190de2657bdb572 Mon Sep 17 00:00:00 2001 From: l0lawrence Date: Fri, 5 Jun 2026 10:38:46 -0700 Subject: [PATCH 5/7] Remove /chronus add bot; point verify failures at local azpysdk + Copilot autofix The custom slash-command bot (chronus-fix.yml) parsed 'chronus verify' output for sdk/ paths, but verify emits package names, so it could never detect packages and never added an entry. Per review feedback, drop the bot entirely and rely on running 'azpysdk changelog add' locally or using GitHub Copilot to fix the failing check. Reword the Chronus Verify sticky comment, annotation, and docs accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/chronus-fix.yml | 252 --------------------------- .github/workflows/chronus-verify.yml | 33 ++-- doc/dev/changelog_updates.md | 5 +- 3 files changed, 15 insertions(+), 275 deletions(-) delete mode 100644 .github/workflows/chronus-fix.yml diff --git a/.github/workflows/chronus-fix.yml b/.github/workflows/chronus-fix.yml deleted file mode 100644 index d2af72438f0e..000000000000 --- a/.github/workflows/chronus-fix.yml +++ /dev/null @@ -1,252 +0,0 @@ -name: Chronus Fix (slash command) - -# Listens for `/chronus add [kind]` PR comments and commits a Chronus change -# entry derived from the PR title back to the PR branch. -# -# Security model: -# - Restricted to same-repository PRs (fork PRs get a polite reply with -# local-run instructions). -# - Commenter must have write permission. -# - The Chronus tooling under `.github/chronus` is restored from the BASE -# branch before install/run, so PR-head changes to the tooling cannot -# execute under this workflow's write-scoped token. - -on: - issue_comment: - types: [created] - -permissions: - contents: write - issues: write - pull-requests: read - -concurrency: - group: chronus-fix-${{ github.event.issue.number }} - cancel-in-progress: false - -jobs: - chronus-fix: - name: Apply chronus add via slash command - runs-on: ubuntu-latest - if: >- - github.event.issue.pull_request != null && - startsWith(github.event.comment.body, '/chronus add') - steps: - - name: Authorize command and load PR metadata - id: pr - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const comment = context.payload.comment; - const issue = context.payload.issue; - - const match = comment.body.trim().match(/^\/chronus\s+add(?:\s+([A-Za-z]+))?\s*$/i); - if (!match) { - core.setFailed('Invalid command. Use `/chronus add [kind]`.'); - return; - } - const aliases = { - internal: 'internal', - fix: 'fix', bug: 'fix', - feature: 'feature', features: 'feature', - breaking: 'breaking', - deprecation: 'deprecation', deprecated: 'deprecation', - dependencies: 'dependencies', deps: 'dependencies', - }; - const kind = aliases[(match[1] || 'internal').toLowerCase()]; - if (!kind) { - core.setFailed('Unknown kind. Allowed: internal, fix, feature, breaking, deprecation, dependencies.'); - return; - } - - const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: comment.user.login, - }); - if (!['admin', 'maintain', 'write'].includes(perm.permission)) { - core.setFailed(`User ${comment.user.login} lacks write permission (has: ${perm.permission}).`); - return; - } - - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: comment.id, - content: 'eyes', - }); - - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: issue.number, - }); - - if (pr.head.repo.full_name !== pr.base.repo.full_name) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: [ - 'âš ī¸ `/chronus add` cannot push to fork PR branches.', - '', - 'Please run the following locally and push the resulting file:', - '', - '```bash', - 'azpysdk changelog add', - '```', - ].join('\n'), - }); - core.setFailed('Fork PRs are not supported by /chronus add.'); - return; - } - - const refOk = /^[A-Za-z0-9._/\-]+$/; - if (!refOk.test(pr.head.ref) || !refOk.test(pr.base.ref)) { - core.setFailed(`Refusing unusual ref name. head=${pr.head.ref} base=${pr.base.ref}`); - return; - } - - core.setOutput('head_ref', pr.head.ref); - core.setOutput('head_sha', pr.head.sha); - core.setOutput('base_ref', pr.base.ref); - core.setOutput('title', pr.title); - core.setOutput('kind', kind); - - - name: Checkout PR head - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{ steps.pr.outputs.head_sha }} - fetch-depth: 0 - persist-credentials: false - - - name: Fetch base branch and restore trusted tooling - env: - BASE_REF: ${{ steps.pr.outputs.base_ref }} - run: | - set -euo pipefail - git fetch --no-tags origin "$BASE_REF:$BASE_REF" - # Replace .github/chronus with the base-branch version so the PR - # cannot influence what we install or execute below. - git restore --source="$BASE_REF" -- .github/chronus - - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: lts/* - cache: npm - cache-dependency-path: .github/chronus/package-lock.json - - - name: Install pinned chronus - run: npm ci - working-directory: .github/chronus - - - name: Run chronus add for missing packages - id: add - env: - KIND: ${{ steps.pr.outputs.kind }} - MESSAGE: ${{ steps.pr.outputs.title }} - run: | - set -uo pipefail - out="$(.github/chronus/node_modules/.bin/chronus verify 2>&1)" || true - printf '%s\n' "$out" - - pkgs="$(printf '%s\n' "$out" | grep -oE 'sdk/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+' | sort -u || true)" - if [ -z "$pkgs" ]; then - echo "found=0" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "found=1" >> "$GITHUB_OUTPUT" - - set -e - while IFS= read -r pkg_path; do - [ -z "$pkg_path" ] && continue - pkg_name="$(basename "$pkg_path")" - echo "::group::chronus add $pkg_name ($KIND)" - .github/chronus/node_modules/.bin/chronus add "$pkg_name" --kind "$KIND" --message "$MESSAGE" - echo "::endgroup::" - done <<< "$pkgs" - - - name: Commit and push - id: push - if: steps.add.outputs.found == '1' - env: - HEAD_REF: ${{ steps.pr.outputs.head_ref }} - ACTOR: ${{ github.event.comment.user.login }} - ACTOR_ID: ${{ github.event.comment.user.id }} - PR_NUMBER: ${{ github.event.issue.number }} - GITHUB_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - git check-ref-format --branch "$HEAD_REF" - - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add .chronus/changes/ - if git diff --cached --quiet; then - echo "pushed=0" >> "$GITHUB_OUTPUT" - exit 0 - fi - - msg="$(printf 'Add chronus changelog entry [skip ci]\n\nTriggered by /chronus add comment on PR #%s.\n\nCo-authored-by: %s <%s+%s@users.noreply.github.com>' \ - "$PR_NUMBER" "$ACTOR" "$ACTOR_ID" "$ACTOR")" - git commit -m "$msg" - - if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$HEAD_REF"; then - echo "pushed=1" >> "$GITHUB_OUTPUT" - else - echo "pushed=0" >> "$GITHUB_OUTPUT" - echo "::warning::Push failed. The PR branch may have moved; ask the user to retry." - fi - - - name: Reply with final result - if: always() - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - env: - KIND: ${{ steps.pr.outputs.kind }} - TITLE: ${{ steps.pr.outputs.title }} - FOUND: ${{ steps.add.outputs.found }} - PUSHED: ${{ steps.push.outputs.pushed }} - JOB_STATUS: ${{ job.status }} - with: - script: | - const issue_number = context.payload.issue.number; - const comment_id = context.payload.comment.id; - const react = (content) => github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id, content, - }); - const reply = (body) => github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number, body, - }); - - if (process.env.JOB_STATUS !== 'success') { - await react('-1'); - return; - } - if (process.env.FOUND !== '1') { - await reply([ - 'â„šī¸ Could not detect any packages missing a Chronus change entry.', - '', - 'Either every changed package already has an entry, or `chronus verify`', - 'failed for an unrelated reason — check the workflow run for details.', - ].join('\n')); - return; - } - if (process.env.PUSHED === '1') { - await react('rocket'); - await reply([ - '✅ Pushed a Chronus change entry to this PR.', - '', - '- **Kind:** `' + process.env.KIND + '`', - '- **Description:** _' + process.env.TITLE + '_', - '', - 'Edit the file under `.chronus/changes/` if you\'d like to refine the description.', - ].join('\n')); - return; - } - await react('confused'); - await reply([ - 'âš ī¸ Could not push a Chronus entry to this PR branch.', - '', - 'The branch may have moved while the bot was working. Please', - 'comment `/chronus add` again, or run `azpysdk changelog add` locally.', - ].join('\n')); diff --git a/.github/workflows/chronus-verify.yml b/.github/workflows/chronus-verify.yml index 7814f2c216dc..ac8fc2b027c2 100644 --- a/.github/workflows/chronus-verify.yml +++ b/.github/workflows/chronus-verify.yml @@ -39,9 +39,8 @@ jobs: run: .github/chronus/node_modules/.bin/chronus verify # Sticky comment is only post-able when GITHUB_TOKEN has write scope — - # i.e. PRs from the main repo. Fork PRs see only the error annotation - # below, which is fine because /chronus add doesn't work for forks anyway. - - name: Post sticky one-click-fix PR comment on failure + # i.e. PRs from the main repo. Fork PRs see only the error annotation below. + - name: Post sticky fix-instructions PR comment on failure if: failure() && steps.verify.conclusion == 'failure' && github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: @@ -55,25 +54,21 @@ jobs: 'change description was found. CI requires every user-affecting', 'change to have one.', '', - '#### ⚡ One-click fix', + '#### How to fix', '', - '**Comment `/chronus add` on this PR** and a bot will commit a', - 'changelog entry for you, derived from your PR title.', + 'Run the following from the repo root (or from within the package', + 'directory) and push the new `.chronus/changes/*.md` file:', '', - 'Customise the entry kind by appending it to the command:', + '```bash', + 'azpysdk changelog add', + '```', '', - '- `/chronus add`  â†’  defaults to `internal`', - '- `/chronus add fix`  â†’  bug fix', - '- `/chronus add feature`  â†’  new feature', - '- `/chronus add breaking`  â†’  breaking change', - '- `/chronus add deprecation`  â†’  deprecation', - '- `/chronus add dependencies`  â†’  dependency bump', + 'This adds a change entry for each modified package. If your change', + 'is not user-facing (e.g. tests or docs only), choose the `internal`', + 'kind to satisfy the check without bumping the version.', '', - '> â„šī¸ For PRs from forks, run the command locally instead:', - '>', - '> ```bash', - '> azpysdk changelog add', - '> ```', + '> 💡 You can also ask **GitHub Copilot** to fix this failing check', + '> for you directly from the PR\'s *Checks* tab.', '', 'See [`doc/dev/changelog_updates.md`](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/changelog_updates.md) for full instructions.', ].join('\n'); @@ -103,5 +98,5 @@ jobs: - name: Emit annotation on failure if: failure() && steps.verify.conclusion == 'failure' run: | - echo "::error::Chronus verification failed. Comment '/chronus add' on this PR for an automated fix, or run 'azpysdk changelog add' locally." + echo "::error::Chronus verification failed. Run 'azpysdk changelog add' locally and push the new .chronus/changes/*.md file, or ask GitHub Copilot to fix this check from the PR's Checks tab." echo "::error::See https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/changelog_updates.md for instructions." diff --git a/doc/dev/changelog_updates.md b/doc/dev/changelog_updates.md index f7daf8143ec5..fc29752d0eee 100644 --- a/doc/dev/changelog_updates.md +++ b/doc/dev/changelog_updates.md @@ -108,10 +108,7 @@ Or using raw Chronus: npx chronus verify ``` -> **Note:** The CI workflow (`Chronus Verify`) runs `chronus verify` automatically on every pull request that modifies source files under `sdk/` (specifically files matching `sdk/*/*/**`). If it fails, you have two options: -> -> - **One-click fix:** comment `/chronus add` on the PR (optionally followed by a kind, e.g. `/chronus add fix`). A bot will commit a Chronus entry derived from the PR title back to the branch. *Available for PRs from the main repository only — fork PRs should run the command locally.* -> - **Locally:** run `azpysdk changelog add` and push the resulting `.chronus/changes/*.md` file. +> **Note:** The CI workflow (`Chronus Verify`) runs `chronus verify` automatically on every pull request that modifies source files under `sdk/` (specifically files matching `sdk/*/*/**`). If it fails, run `azpysdk changelog add` locally and push the resulting `.chronus/changes/*.md` file. You can also ask GitHub Copilot to fix the failing check directly from the pull request's *Checks* tab. If your changes don't need a changelog entry (e.g., pure documentation or test-only changes unrelated to package behavior), you can add an `internal` change kind entry to satisfy the requirement without bumping the version. From 0dd5ca05d972193f96e85a6c76820a5b82f2cbf0 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Mon, 15 Jun 2026 13:56:20 -0700 Subject: [PATCH 6/7] Pin Node version to 22 in chronus-verify workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/chronus-verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/chronus-verify.yml b/.github/workflows/chronus-verify.yml index ac8fc2b027c2..1b56e2262201 100644 --- a/.github/workflows/chronus-verify.yml +++ b/.github/workflows/chronus-verify.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: lts/* + node-version: "22" cache: npm cache-dependency-path: .github/chronus/package-lock.json From 1097a19c48f1e3fdc699976378ac7128c85c0854 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 16 Jun 2026 09:18:31 -0700 Subject: [PATCH 7/7] Address review: use npm exec and paginate listComments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/chronus-verify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/chronus-verify.yml b/.github/workflows/chronus-verify.yml index 1b56e2262201..158a5157b98c 100644 --- a/.github/workflows/chronus-verify.yml +++ b/.github/workflows/chronus-verify.yml @@ -36,7 +36,7 @@ jobs: - name: Run chronus verify id: verify - run: .github/chronus/node_modules/.bin/chronus verify + run: npm exec --no --prefix .github/chronus/ -- chronus verify # Sticky comment is only post-able when GITHUB_TOKEN has write scope — # i.e. PRs from the main repo. Fork PRs see only the error annotation below. @@ -73,7 +73,7 @@ jobs: 'See [`doc/dev/changelog_updates.md`](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/changelog_updates.md) for full instructions.', ].join('\n'); - const { data: comments } = await github.rest.issues.listComments({ + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number,