Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions .github/workflows/upstream-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: LightGBM upstream release watcher

# Weekly (and on-demand) watch on microsoft/LightGBM. When a release newer than the
# version pinned in build.rs appears, Claude reviews the upstream C API header diff and
# edits the crate; a dedicated action then opens ONE pull request. Nothing here can push
# to the base branch or publish a release — the only remote write is the PR itself.
#
# Auth: workload identity federation (no stored API key). Requires repo secrets:
# ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID
# (ANTHROPIC_WORKSPACE_ID only if the federation rule spans multiple workspaces)
# Recommended: a branch-protection rule on the default branch requiring PRs.

on:
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC
workflow_dispatch:

permissions:
id-token: write # fetch the GitHub OIDC token for Anthropic WIF
contents: write # minimum GitHub allows for creating a PR branch
pull-requests: write # open/update the PR
Comment on lines +18 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does the GitHub REST "Create a label" endpoint require issues:write permission on GITHUB_TOKEN, or is pull-requests:write sufficient?

💡 Result:

For the GitHub REST API "Create a label" endpoint, possessing issues:write permission is generally recommended and recognized as the effective requirement for success [1][2]. While technical documentation for fine-grained personal access tokens often lists both issues:write and pull-requests:write as potential permission sets for label management [3][2], real-world usage in GitHub Actions with the GITHUB_TOKEN frequently shows that pull-requests:write alone is insufficient for creating new labels [1][2]. Users consistently report encountering authorization errors when attempting to create labels with only pull-requests:write and resolve them by explicitly adding issues:write to their workflow permissions [1][2]. Therefore, if your workflow needs to create labels, you should include issues:write in your permissions configuration: permissions: issues: write pull-requests: write # Optional, depending on your other needs This configuration ensures the GITHUB_TOKEN has the necessary access to manage repository labels effectively [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files .github/workflows/upstream-release.yml
echo '---'
nl -ba .github/workflows/upstream-release.yml | sed -n '130,190p'
echo '--- search ---'
rg -n "gh label create|create-pull-request|permissions:" .github/workflows/upstream-release.yml

Repository: aryehlev/lightgbm-rust

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail
nl -ba .github/workflows/upstream-release.yml | sed -n '145,180p'
echo '---'
rg -n "GH_TOKEN|GITHUB_TOKEN|gh label create|issues: write|pull-requests: write|contents: write|id-token: write" .github/workflows/upstream-release.yml

Repository: aryehlev/lightgbm-rust

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- permissions and label steps ---'
sed -n '1,230p' .github/workflows/upstream-release.yml | cat -n | sed -n '1,230p'

echo '--- focused search ---'
rg -n "gh label create|create-pull-request|GH_TOKEN|GITHUB_TOKEN|issues: write|pull-requests: write|contents: write|id-token: write" .github/workflows/upstream-release.yml

Repository: aryehlev/lightgbm-rust

Length of output: 10905


Add issues: write to the workflow permissions.

gh label create runs against GITHUB_TOKEN, and pull-requests: write doesn’t cover label creation. Without issues: write, the label-ensure step can silently no-op under || true, so dependencies and upstream-release may never be created on a fresh repo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/upstream-release.yml around lines 18 - 21, Add issues:
write to the workflow-level permissions block alongside id-token, contents, and
pull-requests so the label-ensure step can create repository labels using
GITHUB_TOKEN.


jobs:
propose-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false # the edit step gets NO git push credentials

- name: Determine current and latest versions
id: versions
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
CURRENT=$(grep 'unwrap_or_else' build.rs | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
[ -n "$CURRENT" ] || { echo "::error::Could not find pinned version in build.rs"; exit 1; }
LATEST=$(gh api "repos/microsoft/LightGBM/releases/latest" --jq '.tag_name' | sed 's/^v//')
[ -n "$LATEST" ] || { echo "::error::Could not fetch latest release"; exit 1; }

echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
echo "latest=$LATEST" >> "$GITHUB_OUTPUT"
echo "Current pinned: $CURRENT | Latest upstream: $LATEST"

if [ "$CURRENT" = "$LATEST" ]; then
echo "update=false" >> "$GITHUB_OUTPUT"
elif [ "$(printf '%s\n%s\n' "$CURRENT" "$LATEST" | sort -V | tail -1)" = "$LATEST" ]; then
echo "update=true" >> "$GITHUB_OUTPUT"
else
echo "Pinned version is newer than upstream latest; nothing to do."
echo "update=false" >> "$GITHUB_OUTPUT"
fi

- name: Skip if an update PR is already open
id: existing
if: steps.versions.outputs.update == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
BRANCH="chore/lightgbm-${{ steps.versions.outputs.latest }}"
if gh pr list --state open --head "$BRANCH" --json number --jq '.[0].number' | grep -q '[0-9]'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
Comment on lines +56 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Template-injection risk: unsanitized upstream tag spliced directly into a shell script.

LATEST (Line 40) is taken verbatim from tag_name of microsoft/LightGBM's latest release with only a v prefix stripped — unlike CURRENT, it is never constrained to a semver shape. It is then substituted directly via ${{ }} into this run: script rather than via an env:-indirected $VAR reference (as is correctly done elsewhere in this same file, e.g. Lines 74-75). This is exactly the classic GitHub Actions template-injection pattern zizmor flags at Line 63: if the upstream tag ever contained shell metacharacters (`, $(), quotes), it would execute arbitrary commands in a job holding contents: write, pull-requests: write, and an OIDC token for Anthropic — not merely break string comparison. The same unvalidated LATEST is also interpolated straight into the Claude prompt text (Lines 114-151), giving it a secondary prompt-injection surface against an agent with Bash access.

Harden at the source and at this use site:

🔒 Proposed fix
           echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
           echo "latest=$LATEST"   >> "$GITHUB_OUTPUT"
+          [[ "$LATEST" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Unexpected release tag format: $LATEST"; exit 1; }
       - name: Skip if an update PR is already open
         id: existing
         if: steps.versions.outputs.update == 'true'
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          LATEST: ${{ steps.versions.outputs.latest }}
         run: |
           set -euo pipefail
-          BRANCH="chore/lightgbm-${{ steps.versions.outputs.latest }}"
+          BRANCH="chore/lightgbm-${LATEST}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Skip if an update PR is already open
id: existing
if: steps.versions.outputs.update == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
BRANCH="chore/lightgbm-${{ steps.versions.outputs.latest }}"
if gh pr list --state open --head "$BRANCH" --json number --jq '.[0].number' | grep -q '[0-9]'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Skip if an update PR is already open
id: existing
if: steps.versions.outputs.update == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LATEST: ${{ steps.versions.outputs.latest }}
run: |
set -euo pipefail
BRANCH="chore/lightgbm-${LATEST}"
if gh pr list --state open --head "$BRANCH" --json number --jq '.[0].number' | grep -q '[0-9]'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
🧰 Tools
🪛 zizmor (1.26.1)

[info] 63-63: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/upstream-release.yml around lines 56 - 68, Validate the
upstream tag in the workflow before assigning or using LATEST, requiring the
expected semver-shaped value after removing the optional v prefix and failing
otherwise. In the “Skip if an update PR is already open” step, pass the
validated value through env and reference it as a shell variable when
constructing BRANCH; apply the same indirect env-based handling to the Claude
prompt interpolation of LATEST, preserving the existing release-update flow.

Source: Linters/SAST tools


- name: Build upstream C API header diff
id: diff
if: steps.versions.outputs.update == 'true' && steps.existing.outputs.skip == 'false'
env:
CURRENT: ${{ steps.versions.outputs.current }}
LATEST: ${{ steps.versions.outputs.latest }}
run: |
set -uo pipefail
OUT="$RUNNER_TEMP/header-diff.txt"
: > "$OUT"
for h in include/LightGBM/c_api.h include/LightGBM/export.h include/LightGBM/arrow.h; do
old=$(curl -fsSL "https://raw.githubusercontent.com/microsoft/LightGBM/v${CURRENT}/${h}" 2>/dev/null || true)
new=$(curl -fsSL "https://raw.githubusercontent.com/microsoft/LightGBM/v${LATEST}/${h}" 2>/dev/null || true)
[ -z "$old" ] && [ -z "$new" ] && continue
echo "===== ${h} (v${CURRENT} -> v${LATEST}) =====" >> "$OUT"
diff -u <(printf '%s' "$old") <(printf '%s' "$new") >> "$OUT" || true
echo >> "$OUT"
done
# Seed the PR body so it always exists even if the edit step is skipped.
printf '## LightGBM v%s\n\nBump from v%s. Release notes: https://github.com/microsoft/LightGBM/releases/tag/v%s\n' \
"$LATEST" "$CURRENT" "$LATEST" > "$RUNNER_TEMP/pr-body.md"
echo "path=$OUT" >> "$GITHUB_OUTPUT"
echo "Wrote header diff to $OUT ($(wc -l < "$OUT") lines)"

- name: Claude — review diff and edit the crate (no push access)
if: steps.versions.outputs.update == 'true' && steps.existing.outputs.skip == 'false'
uses: anthropics/claude-code-action@v1
with:
# Workload identity federation — exchanges the GitHub OIDC token for a
# short-lived Anthropic credential. Do NOT also set anthropic_api_key.
anthropic_federation_rule_id: ${{ secrets.ANTHROPIC_FEDERATION_RULE_ID }}
anthropic_organization_id: ${{ secrets.ANTHROPIC_ORGANIZATION_ID }}
anthropic_service_account_id: ${{ secrets.ANTHROPIC_SERVICE_ACCOUNT_ID }}
anthropic_workspace_id: ${{ secrets.ANTHROPIC_WORKSPACE_ID }}
claude_args: |
--allowedTools Bash,Edit,Write,Read,Glob,Grep
--max-turns 40
--model claude-opus-4-8
prompt: |
You are preparing a dependency update for the Rust crate `lightgbm-rust`,
which wraps the LightGBM C API. You can ONLY edit files. Do NOT run git, do
NOT commit, push, or open a PR — a later workflow step does that from your
changes. There are no push credentials available to you.

Upstream microsoft/LightGBM has a newer release:
current pinned version: v${{ steps.versions.outputs.current }}
new version: v${{ steps.versions.outputs.latest }}

Do the following:

1. In build.rs, bump the default version returned by `get_lightgbm_version()`
from ${{ steps.versions.outputs.current }} to ${{ steps.versions.outputs.latest }}.
Update stale references to the old version in README.md / PLATFORM_SUPPORT.md
where clearly appropriate.

2. Read the upstream C API header diff: `cat "${{ steps.diff.outputs.path }}"`.
Analyze it for changes affecting this crate: changed/removed function
signatures the safe wrappers call, valuable new functions, changed
structs/enums/constants.

IMPORTANT: the raw FFI in src/sys.rs is generated by bindgen at build time
(`include!(concat!(env!("OUT_DIR"), "/bindings.rs"))`) — do NOT hand-write or
edit FFI declarations. Only modify the SAFE WRAPPER layer (src/model.rs,
src/lib.rs, src/error.rs, src/polars_ext.rs) where the diff actually requires
it. Be conservative and minimal; do not invent wrappers for unused functions
unless a new capability is clearly worth exposing — if so, keep it small.

3. If feasible run `cargo build` (then `cargo test`) to sanity-check. The build
downloads platform libraries and may fail for environment reasons — if so,
note it and continue.

4. Write the pull-request body to the file "${{ runner.temp }}/pr-body.md"
(overwrite it). It must contain:
- the bump (v${{ steps.versions.outputs.current }} -> v${{ steps.versions.outputs.latest }})
and the release-notes link.
- a "## Code review" section: an honest review of the C API diff — what
changed, what it means for this crate, and exactly what you changed and
why (or that no wrapper changes were required).
- a "## Needs human verification" checklist for anything you could not
confirm (release assets/wheels present for every platform, build/test on
real targets, etc.).

- name: Ensure PR labels exist
if: steps.versions.outputs.update == 'true' && steps.existing.outputs.skip == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh label create dependencies --force >/dev/null 2>&1 || true
gh label create upstream-release --force >/dev/null 2>&1 || true

- name: Create pull request
if: steps.versions.outputs.update == 'true' && steps.existing.outputs.skip == 'false'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: chore/lightgbm-${{ steps.versions.outputs.latest }}
commit-message: "chore: update LightGBM to v${{ steps.versions.outputs.latest }}"
title: "chore: update LightGBM to v${{ steps.versions.outputs.latest }}"
body-path: ${{ runner.temp }}/pr-body.md
labels: dependencies, upstream-release
delete-branch: true
Loading