Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package kubelogin for openSUSE:Factory checked in at 2026-03-02 17:36:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/kubelogin (Old) and /work/SRC/openSUSE:Factory/.kubelogin.new.29461 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "kubelogin" Mon Mar 2 17:36:26 2026 rev:25 rq:1335655 version:0.2.15 Changes: -------- --- /work/SRC/openSUSE:Factory/kubelogin/kubelogin.changes 2026-01-12 11:49:58.021241009 +0100 +++ /work/SRC/openSUSE:Factory/.kubelogin.new.29461/kubelogin.changes 2026-03-02 17:36:29.933757244 +0100 @@ -1,0 +2,11 @@ +Wed Feb 25 09:19:07 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.2.15: + * Bug Fixes + - PoP token flow crash with nil pointer in cache.Replace when + running non-root by @vineeth-thumma in #736 + * Enhancements + - feat: automate CHANGELOG.md generation for releases by + @Copilot in #737 + +------------------------------------------------------------------- Old: ---- kubelogin-0.2.14.obscpio New: ---- kubelogin-0.2.15.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ kubelogin.spec ++++++ --- /var/tmp/diff_new_pack.9dZW8d/_old 2026-03-02 17:36:31.869838802 +0100 +++ /var/tmp/diff_new_pack.9dZW8d/_new 2026-03-02 17:36:31.921840993 +0100 @@ -17,7 +17,7 @@ Name: kubelogin -Version: 0.2.14 +Version: 0.2.15 Release: 0 Summary: Kubernetes client credential plugin implementing Azure authentication License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.9dZW8d/_old 2026-03-02 17:36:32.301857001 +0100 +++ /var/tmp/diff_new_pack.9dZW8d/_new 2026-03-02 17:36:32.337858517 +0100 @@ -2,7 +2,7 @@ <service name="obs_scm" mode="manual"> <param name="url">https://github.com/Azure/kubelogin.git</param> <param name="scm">git</param> - <param name="revision">v0.2.14</param> + <param name="revision">v0.2.15</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.9dZW8d/_old 2026-03-02 17:36:32.585868965 +0100 +++ /var/tmp/diff_new_pack.9dZW8d/_new 2026-03-02 17:36:32.625870650 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/Azure/kubelogin.git</param> - <param name="changesrevision">6e8b5babc994d57e752e24297dd9c6f59247a1f8</param></service></servicedata> + <param name="changesrevision">7936910173e517c5c651d1bbd56d3143c4b1fe4e</param></service></servicedata> (No newline at EOF) ++++++ kubelogin-0.2.14.obscpio -> kubelogin-0.2.15.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/.bingo/go.mod new/kubelogin-0.2.15/.bingo/go.mod --- old/kubelogin-0.2.14/.bingo/go.mod 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/.bingo/go.mod 2026-02-20 03:16:58.000000000 +0100 @@ -1 +1 @@ -module _ // Fake go.mod auto-created by 'bingo' for go -moddir compatibility with non-Go projects. Commit this file, together with other .mod files. \ No newline at end of file +module _ // Fake go.mod auto-created by 'bingo' for go -moddir compatibility with non-Go projects. Commit this file, together with other .mod files. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/.github/skills/update-changelog/SKILL.md new/kubelogin-0.2.15/.github/skills/update-changelog/SKILL.md --- old/kubelogin-0.2.14/.github/skills/update-changelog/SKILL.md 1970-01-01 01:00:00.000000000 +0100 +++ new/kubelogin-0.2.15/.github/skills/update-changelog/SKILL.md 2026-02-20 03:16:58.000000000 +0100 @@ -0,0 +1,74 @@ +--- +name: update-changelog +description: > + Generate and update CHANGELOG.md for a new kubelogin release. Use this + when asked to prepare release notes or update the changelog for a new + version. Fetches merged pull requests since the previous release, + categorizes them, identifies new contributors, and prepares a formatted + entry for CHANGELOG.md. +--- + +## Overview + +The `make changelog` target runs `hack/changelog-generator/main.go`, which: + +1. Calls `gh api repos/Azure/kubelogin/releases/latest` to determine the + previous release tag (when `SINCE_TAG` is not supplied). +2. Calls `gh api repos/Azure/kubelogin/commits/<tag>` to get the tag date. +3. Fetches all merged pull requests since that date via + `gh api --paginate repos/Azure/kubelogin/pulls?state=closed&...`. +4. Categorizes each PR by GitHub label, then by title prefix: + - **Bug Fixes** — label `bug`/`fix`; prefix `fix:`, `bugfix:`, `hotfix:` + - **Enhancements** — label `enhancement`/`feature`; prefix `feat:` + - **Maintenance** — label `dependencies`/`chore`; prefix `bump `, `update ` + - **Doc Update** — label `documentation`/`docs`; prefix `docs:` + - **What's Changed** — everything else +5. Identifies first-time contributors by comparing PR authors against all + prior merged PR authors. +6. Writes a formatted entry to `changelog-entry.md`. + +## Steps to follow + +1. Determine the new version number (e.g. `0.2.15`) and, optionally, the + previous tag to compare from (e.g. `v0.2.14`). + - If the previous tag is not provided, the tool will auto-detect the + latest stable release. + +2. Run `make changelog` to generate the changelog entry: + + ```bash + # SINCE_TAG is optional – omit to auto-detect the latest release tag + VERSION=0.2.15 make changelog + # or explicitly: + VERSION=0.2.15 SINCE_TAG=v0.2.14 make changelog + ``` + + This writes the formatted entry to `changelog-entry.md`. When using the + [GitHub Actions workflow](../../.github/workflows/update-changelog.yml), + the workflow then inserts `changelog-entry.md` after the header of + `CHANGELOG.md` and opens a pull request automatically. + + When running locally, insert the content manually: + + ```bash + # Insert after the "# Change Log" header + { + head -n 2 CHANGELOG.md + echo "" + cat changelog-entry.md + echo "" + tail -n +3 CHANGELOG.md + } > CHANGELOG.md.new && mv CHANGELOG.md.new CHANGELOG.md + rm changelog-entry.md + ``` + + Authentication is handled automatically by the `gh` CLI. Ensure you + are authenticated (`gh auth login`) or that `GH_TOKEN`/`GITHUB_TOKEN` + is set in the environment. + +3. Review the updated `CHANGELOG.md` and edit entries as needed for + clarity before committing. + +4. After the changelog is merged to the default branch, trigger the + [Release workflow](../../.github/workflows/release.yml) to create the + GitHub release and build binaries. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/.github/workflows/update-changelog.yml new/kubelogin-0.2.15/.github/workflows/update-changelog.yml --- old/kubelogin-0.2.14/.github/workflows/update-changelog.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/kubelogin-0.2.15/.github/workflows/update-changelog.yml 2026-02-20 03:16:58.000000000 +0100 @@ -0,0 +1,81 @@ +--- +name: Update Changelog + +"on": + workflow_dispatch: + inputs: + version: + description: 'Version number (e.g., 0.2.15)' + required: true + type: string + since_tag: + description: >- + Previous version tag (e.g., v0.2.14); defaults to latest tag + required: false + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + update-changelog: + name: Generate and Update Changelog + runs-on: ubuntu-latest + steps: + - name: Checkout repository + # v4.1.1 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + fetch-depth: 0 # Fetch all history + + - name: Set up Go + # v4.1.0 + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe + with: + go-version-file: "go.mod" + cache: true + + - name: Generate changelog entry + id: generate + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION=${{ inputs.version }} \ + SINCE_TAG=${{ inputs.since_tag }} \ + make changelog + + - name: Update CHANGELOG.md + run: | + # Create a temporary file with entry after header + { + head -n 2 CHANGELOG.md + echo "" + cat changelog-entry.md + echo "" + tail -n +3 CHANGELOG.md + } > CHANGELOG.md.new + + mv CHANGELOG.md.new CHANGELOG.md + rm changelog-entry.md + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: >- + docs: update CHANGELOG.md for v${{ inputs.version }} + title: "v${{ inputs.version }} release" + body: | + This PR updates the CHANGELOG.md with release notes for + version ${{ inputs.version }}. + + The changelog entry has been automatically generated from + merged pull requests. Please review and edit as needed + before merging. + + After merging, you can trigger the release workflow to + create the release. + branch: changelog/v${{ inputs.version }} + delete-branch: true + labels: documentation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/.gitignore new/kubelogin-0.2.15/.gitignore --- old/kubelogin-0.2.14/.gitignore 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/.gitignore 2026-02-20 03:16:58.000000000 +0100 @@ -21,3 +21,6 @@ # JetBrains IDE folder .idea + +# hack/changelog-generator binary +hack/changelog-generator/changelog-generator diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/CHANGELOG.md new/kubelogin-0.2.15/CHANGELOG.md --- old/kubelogin-0.2.14/CHANGELOG.md 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/CHANGELOG.md 2026-02-20 03:16:58.000000000 +0100 @@ -1,5 +1,17 @@ # Change Log +## [0.2.15] + +### Bug Fixes + +* PoP token flow crash with nil pointer in cache.Replace when running non-root by @vineeth-thumma in https://github.com/Azure/kubelogin/pull/736 + +### Enhancements + +* feat: automate CHANGELOG.md generation for releases by @Copilot in https://github.com/Azure/kubelogin/pull/737 + +**Full Changelog**: https://github.com/Azure/kubelogin/compare/v0.2.14...v0.2.15 + ## [0.2.14] ### Maintenance diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/Makefile new/kubelogin-0.2.15/Makefile --- old/kubelogin-0.2.14/Makefile 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/Makefile 2026-02-20 03:16:58.000000000 +0100 @@ -25,15 +25,23 @@ @echo " test - Run tests (includes linting)" @echo " clean - Remove built binaries" @echo " build-image - Build Docker image with kubelogin binary" + @echo " changelog - Generate a CHANGELOG.md entry for a new release" @echo "" @echo "Docker image build options:" @echo " make build-image # Build with 'latest' tag" @echo " GIT_TAG=v1.0.0 make build-image # Build with specific tag" @echo "" + @echo "Changelog generation options:" + @echo " VERSION=0.2.15 make changelog # auto-detect previous tag" + @echo " VERSION=0.2.15 SINCE_TAG=v0.2.14 make changelog # explicit previous tag" + @echo " Token is read from GITHUB_TOKEN env var or 'gh auth token' automatically" + @echo "" @echo "Environment variables:" @echo " GOOS - Target OS (default: $(OS))" @echo " GOARCH - Target architecture (default: $(ARCH))" @echo " GIT_TAG - Git tag for version info and Docker tagging" + @echo " VERSION - New version number for changelog generation" + @echo " SINCE_TAG - Previous tag to compare from for changelog (optional)" lint: $(GOLANGCI_LINT) $(GOLANGCI_LINT) run @@ -47,6 +55,16 @@ clean: -rm -f $(BIN) +changelog: + @if [ -z "$(VERSION)" ]; then \ + echo "Error: VERSION is required. Usage: VERSION=0.2.15 make changelog"; \ + exit 1; \ + fi + go run hack/changelog-generator/main.go \ + --version="$(VERSION)" \ + $(if $(SINCE_TAG),--since-tag="$(SINCE_TAG)",) \ + --repo="Azure/kubelogin" + # Docker image build target IMAGE_NAME := ghcr.io/azure/kubelogin IMAGE_TAG := $(if $(GIT_TAG),$(GIT_TAG),latest) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/docs/book/src/development.md new/kubelogin-0.2.15/docs/book/src/development.md --- old/kubelogin-0.2.14/docs/book/src/development.md 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/docs/book/src/development.md 2026-02-20 03:16:58.000000000 +0100 @@ -44,3 +44,53 @@ ``` **Note**: Tests require the system dependencies listed above. If you encounter errors related to `libsecret-1.so` or "encrypted storage isn't possible", ensure the libsecret library is installed. + +## Releases + +### Automated Changelog Generation + +The project includes an automated changelog generation tool that creates properly formatted CHANGELOG.md entries from merged pull requests. + +#### Using the GitHub Actions Workflow + +1. Navigate to the [Update Changelog workflow](https://github.com/Azure/kubelogin/actions/workflows/update-changelog.yml) +2. Click "Run workflow" +3. Provide the required inputs: + - **Version number**: The new version (e.g., `0.2.15`) without the 'v' prefix + - **Previous version tag**: The tag to compare from (e.g., `v0.2.14`) with the 'v' prefix +4. Click "Run workflow" + +The workflow will: +- Fetch all merged PRs since the previous version +- Categorize them (What's Changed, Maintenance, Enhancements, Bug Fixes, Doc Update) +- Identify new contributors +- Generate a formatted changelog entry +- Create a pull request with the updated CHANGELOG.md + +#### Running Locally + +You can generate a changelog entry locally using the `gh` CLI and `make`: + +```bash +# Authenticate with the gh CLI (one-time setup) +gh auth login + +VERSION=0.2.15 make changelog +``` + +The tool uses `gh api` for all GitHub API calls, so only `gh auth login` +is required. In CI the `GH_TOKEN` environment variable is used instead. + +See [hack/changelog-generator/README.md](../../hack/changelog-generator/README.md) for more details. + +### Release Process + +After the changelog is updated: + +1. Review and merge the changelog PR +2. Trigger the [Release workflow](https://github.com/Azure/kubelogin/actions/workflows/release.yml) +3. The workflow will: + - Read the version from CHANGELOG.md + - Create a draft GitHub release + - Build binaries for all platforms + - Upload artifacts to the release diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/hack/changelog-generator/README.md new/kubelogin-0.2.15/hack/changelog-generator/README.md --- old/kubelogin-0.2.14/hack/changelog-generator/README.md 1970-01-01 01:00:00.000000000 +0100 +++ new/kubelogin-0.2.15/hack/changelog-generator/README.md 2026-02-20 03:16:58.000000000 +0100 @@ -0,0 +1,140 @@ +# Changelog Generator + +This tool automatically generates CHANGELOG.md entries for kubelogin releases by fetching merged pull requests from GitHub and categorizing them appropriately. + +## Features + +- Fetches merged PRs since the last release tag +- Automatically categorizes PRs into: + - What's Changed (general changes) + - Enhancements (new features) + - Bug Fixes (bug fixes) + - Maintenance (dependency updates, CVE fixes, chores) + - Doc Update (documentation changes) +- Identifies and lists new contributors +- Generates a Full Changelog comparison link +- Follows the existing CHANGELOG.md format + +## Quick Start + +### Via GitHub Actions (Recommended) + +1. Go to the [Actions tab](https://github.com/Azure/kubelogin/actions/workflows/update-changelog.yml) +2. Click "Run workflow" +3. Fill in the required inputs: + - **Version number**: e.g., `0.2.15` (without the 'v' prefix) + - **Previous version tag**: e.g., `v0.2.14` (with the 'v' prefix) +4. Click "Run workflow" +5. Review and merge the generated PR +6. Trigger the [Release workflow](https://github.com/Azure/kubelogin/actions/workflows/release.yml) + +### Via Make Target + +```bash +# Authenticate once with the gh CLI (one-time setup): +gh auth login + +# SINCE_TAG is optional; omit it to auto-detect the latest tag +VERSION=0.2.15 make changelog + +# Or specify the previous tag explicitly +VERSION=0.2.15 SINCE_TAG=v0.2.14 make changelog +``` + +This generates a `changelog-entry.md` file that you can manually insert into CHANGELOG.md. + +### Running Directly + +```bash +# Authenticate once with the gh CLI (one-time setup): +gh auth login +go run hack/changelog-generator/main.go \ + --version="0.2.15" \ + --repo="Azure/kubelogin" \ + --output="changelog-entry.md" + +# Or specify the previous tag explicitly +go run hack/changelog-generator/main.go \ + --version="0.2.15" \ + --since-tag="v0.2.14" \ + --repo="Azure/kubelogin" \ + --output="changelog-entry.md" +``` + +## PR Categorization + +PRs are categorized first by **GitHub labels**, then by **title patterns**: + +| Category | Labels | Title prefixes / patterns | +|---|---|---| +| Bug Fixes | `bug`, `fix` | `fix:`, `bugfix:`, `bug fix:`, `hotfix:` | +| Enhancements | `enhancement`, `feature` | `feat:`, `feature:`, `add support`, `new feature` | +| Maintenance | `maintenance`, `dependencies`, `chore` | `bump `, `update `, `CVE-`, `fix cve`, `chore` | +| Doc Update | `documentation`, `docs` | `docs:`, `doc:`, `documentation`, `install doc` | +| What's Changed | *(default)* | *(everything else)* | + +### New Contributor Detection + +A contributor is marked as "new" if they have a merged PR in the current release but **no** merged PRs before the previous release tag. + +## Example Output + +```markdown +## [0.2.15] + +### What's Changed + +* Add new authentication method by @username in https://github.com/Azure/kubelogin/pull/123 + +### Enhancements + +* Add Y support by @username in https://github.com/Azure/kubelogin/pull/124 + +### Bug Fixes + +* Fix nil pointer in cache.Replace by @username in https://github.com/Azure/kubelogin/pull/127 + +### Maintenance + +* Bump Go to 1.24.12 by @dependabot in https://github.com/Azure/kubelogin/pull/125 + +### Doc Update + +* Update installation guide by @username in https://github.com/Azure/kubelogin/pull/126 + +### New Contributors + +* @newuser made their first contribution in https://github.com/Azure/kubelogin/pull/123 + +**Full Changelog**: https://github.com/Azure/kubelogin/compare/v0.2.14...v0.2.15 +``` + +## Integration with Release Workflow + +1. **Generate** changelog entry (this tool) → creates a PR +2. **Merge** the changelog PR +3. **Trigger** the [Release workflow](https://github.com/Azure/kubelogin/actions/workflows/release.yml) + - Reads version from CHANGELOG.md + - Creates a draft release + - Builds binaries for all platforms + - Publishes artifacts + +## Troubleshooting + +**"No PRs found"** +- Verify the tag exists: `git tag -l | grep <tag>` +- Check that PRs were merged after the `since_tag` date + +**"API rate limit exceeded"** +- Run `gh auth login` if you haven't already +- Wait for rate limit reset (typically 1 hour) + +**Wrong categorization** +- Add appropriate labels to PRs before running the tool +- Or manually edit the generated changelog before merging + +## Requirements + +- [`gh` CLI](https://cli.github.com/) authenticated via `gh auth login` +- Go 1.24.11 or later + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/hack/changelog-generator/go.mod new/kubelogin-0.2.15/hack/changelog-generator/go.mod --- old/kubelogin-0.2.14/hack/changelog-generator/go.mod 1970-01-01 01:00:00.000000000 +0100 +++ new/kubelogin-0.2.15/hack/changelog-generator/go.mod 2026-02-20 03:16:58.000000000 +0100 @@ -0,0 +1,3 @@ +module github.com/Azure/kubelogin/hack/changelog-generator + +go 1.24.11 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/hack/changelog-generator/main.go new/kubelogin-0.2.15/hack/changelog-generator/main.go --- old/kubelogin-0.2.14/hack/changelog-generator/main.go 1970-01-01 01:00:00.000000000 +0100 +++ new/kubelogin-0.2.15/hack/changelog-generator/main.go 2026-02-20 03:16:58.000000000 +0100 @@ -0,0 +1,397 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + "os/exec" + "regexp" + "sort" + "strings" + "time" +) + +// GitHubPR represents a GitHub pull request +type GitHubPR struct { + Number int `json:"number"` + Title string `json:"title"` + HTMLURL string `json:"html_url"` + User User `json:"user"` + MergedAt time.Time `json:"merged_at"` + Labels []Label `json:"labels"` + CreatedAt time.Time `json:"created_at"` +} + +// User represents a GitHub user +type User struct { + Login string `json:"login"` +} + +// Label represents a GitHub label +type Label struct { + Name string `json:"name"` +} + +// Contributor tracks first-time contributors +type Contributor struct { + Login string + PRURL string + Number int +} + +func main() { + version := flag.String("version", "", "Version number (e.g., 0.2.15)") + sinceTag := flag.String("since-tag", "", "Previous version tag (e.g., v0.2.14); defaults to the latest tag") + repo := flag.String("repo", "Azure/kubelogin", "Repository in format owner/repo") + output := flag.String("output", "changelog-entry.md", "Output file path") + flag.Parse() + + if *version == "" { + log.Fatal("--version is required") + } + + // Resolve the previous tag if not provided + if *sinceTag == "" { + resolved, err := getLatestTag(*repo) + if err != nil { + log.Fatalf("Failed to resolve previous tag: %v", err) + } + log.Printf("No --since-tag provided; using latest tag: %s", resolved) + *sinceTag = resolved + } + + // Get the date of the previous tag + tagDate, err := getTagDate(*repo, *sinceTag) + if err != nil { + log.Fatalf("Failed to get tag date: %v", err) + } + + // Fetch merged PRs since the tag + prs, err := getMergedPRsSince(*repo, tagDate) + if err != nil { + log.Fatalf("Failed to fetch PRs: %v", err) + } + + if len(prs) == 0 { + log.Println("No merged PRs found since", *sinceTag) + } + + // Get all contributors before this tag to identify new ones + allContributorsBefore, err := getAllContributorsBefore(*repo, tagDate) + if err != nil { + log.Printf("Warning: Failed to get historical contributors: %v", err) + allContributorsBefore = make(map[string]bool) + } + + // Categorize PRs and identify new contributors + categories := categorizePRs(prs, allContributorsBefore) + + // Generate the changelog entry + entry := generateChangelogEntry(*version, *sinceTag, categories, *repo) + + // Write to output file + if err := os.WriteFile(*output, []byte(entry), 0644); err != nil { + log.Fatalf("Failed to write output file: %v", err) + } + + log.Printf("Successfully generated changelog entry for version %s", *version) +} + +// ghAPI runs "gh api <args>" and returns the output. +// Authentication is handled automatically by the gh CLI +// (GITHUB_TOKEN env var or the credential stored by "gh auth login"). +func ghAPI(args ...string) ([]byte, error) { + out, err := exec.Command("gh", append([]string{"api"}, args...)...).Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("gh api %v: %w\n%s", args, err, exitErr.Stderr) + } + return nil, fmt.Errorf("gh api %v: %w", args, err) + } + return out, nil +} + +// getLatestTag returns the tag of the most recent stable release. +func getLatestTag(repo string) (string, error) { + out, err := ghAPI(fmt.Sprintf("repos/%s/releases/latest", repo), "--jq", ".tag_name") + if err != nil { + return "", err + } + tag := strings.TrimSpace(string(out)) + if tag == "" { + return "", fmt.Errorf("no releases found in repository %s", repo) + } + return tag, nil +} + +// getTagDate returns the author date of the commit the tag points to. +func getTagDate(repo, tag string) (time.Time, error) { + out, err := ghAPI( + fmt.Sprintf("repos/%s/commits/%s", repo, tag), + "--jq", ".commit.author.date", + ) + if err != nil { + return time.Time{}, err + } + return time.Parse(time.RFC3339, strings.TrimSpace(string(out))) +} + +// decodePRStream decodes newline-delimited JSON objects produced by +// "gh api --paginate ... --jq '.[]'". +func decodePRStream(data []byte) ([]GitHubPR, error) { + var prs []GitHubPR + dec := json.NewDecoder(strings.NewReader(string(data))) + for dec.More() { + var pr GitHubPR + if err := dec.Decode(&pr); err != nil { + return nil, err + } + prs = append(prs, pr) + } + return prs, nil +} + +// releasePRTitle matches PR titles that represent a version release +// (e.g. "v0.2.14 release") so they can be excluded from the changelog. +var releasePRTitle = regexp.MustCompile(`(?i)^v?\d+\.\d+\.\d+`) + +// isReleasePR returns true when the PR title looks like a release commit +// (e.g. "v0.2.14 release", "0.2.14 release"). +func isReleasePR(title string) bool { + return releasePRTitle.MatchString(strings.TrimSpace(title)) +} + +// hasLabel returns true if the PR has a label with the given name (case-insensitive). +func hasLabel(pr GitHubPR, name string) bool { + for _, l := range pr.Labels { + if strings.EqualFold(l.Name, name) { + return true + } + } + return false +} + +// getMergedPRsSince returns all merged PRs after the given time. +func getMergedPRsSince(repo string, since time.Time) ([]GitHubPR, error) { + out, err := ghAPI( + "--paginate", + fmt.Sprintf("repos/%s/pulls?state=closed&sort=created&direction=desc&per_page=100", repo), + "--jq", ".[]", + ) + if err != nil { + return nil, err + } + all, err := decodePRStream(out) + if err != nil { + return nil, err + } + var prs []GitHubPR + for _, pr := range all { + if !pr.MergedAt.IsZero() && pr.MergedAt.After(since) && !isReleasePR(pr.Title) && !hasLabel(pr, "release") { + prs = append(prs, pr) + } + } + return prs, nil +} + +// getAllContributorsBefore returns the set of logins that contributed before the given time. +func getAllContributorsBefore(repo string, before time.Time) (map[string]bool, error) { + out, err := ghAPI( + "--paginate", + fmt.Sprintf("repos/%s/pulls?state=closed&sort=created&direction=asc&per_page=100", repo), + "--jq", ".[]", + ) + if err != nil { + return nil, err + } + all, err := decodePRStream(out) + if err != nil { + return nil, err + } + contributors := make(map[string]bool) + for _, pr := range all { + if !pr.MergedAt.IsZero() && pr.MergedAt.Before(before) { + contributors[pr.User.Login] = true + } + } + return contributors, nil +} + +// Categories holds all categorized PRs +type Categories struct { + Changes []GitHubPR + BugFixes []GitHubPR + Maintenance []GitHubPR + Enhancements []GitHubPR + DocUpdates []GitHubPR + NewContributors []Contributor +} + +func categorizePRs(prs []GitHubPR, existingContributors map[string]bool) Categories { + cats := Categories{ + Changes: make([]GitHubPR, 0), + BugFixes: make([]GitHubPR, 0), + Maintenance: make([]GitHubPR, 0), + Enhancements: make([]GitHubPR, 0), + DocUpdates: make([]GitHubPR, 0), + NewContributors: make([]Contributor, 0), + } + + seenNewContributors := make(map[string]bool) + + for _, pr := range prs { + // Check for new contributors + if !existingContributors[pr.User.Login] && !seenNewContributors[pr.User.Login] { + cats.NewContributors = append(cats.NewContributors, Contributor{ + Login: pr.User.Login, + PRURL: pr.HTMLURL, + Number: pr.Number, + }) + seenNewContributors[pr.User.Login] = true + } + + // Categorize based on labels and title + category := categorizeByLabelsAndTitle(pr) + switch category { + case "bugfix": + cats.BugFixes = append(cats.BugFixes, pr) + case "maintenance": + cats.Maintenance = append(cats.Maintenance, pr) + case "enhancement": + cats.Enhancements = append(cats.Enhancements, pr) + case "documentation": + cats.DocUpdates = append(cats.DocUpdates, pr) + default: + cats.Changes = append(cats.Changes, pr) + } + } + + return cats +} + +func categorizeByLabelsAndTitle(pr GitHubPR) string { + title := strings.ToLower(pr.Title) + + // Check labels first + for _, label := range pr.Labels { + labelName := strings.ToLower(label.Name) + if strings.Contains(labelName, "bug") || + strings.Contains(labelName, "fix") { + return "bugfix" + } + if strings.Contains(labelName, "maintenance") || + strings.Contains(labelName, "dependencies") || + strings.Contains(labelName, "chore") { + return "maintenance" + } + if strings.Contains(labelName, "enhancement") || + strings.Contains(labelName, "feature") { + return "enhancement" + } + if strings.Contains(labelName, "documentation") || + strings.Contains(labelName, "docs") { + return "documentation" + } + } + + // Check title patterns + if strings.HasPrefix(title, "fix:") || + strings.HasPrefix(title, "bugfix:") || + strings.HasPrefix(title, "bug fix:") || + strings.HasPrefix(title, "hotfix:") { + return "bugfix" + } + + if strings.HasPrefix(title, "bump ") || + strings.HasPrefix(title, "update ") || + strings.Contains(title, "cve-") || + strings.Contains(title, "fix cve") || + strings.Contains(title, "dependencies") || + strings.HasPrefix(title, "chore:") || + strings.HasPrefix(title, "chore ") { + return "maintenance" + } + + if strings.HasPrefix(title, "docs:") || + strings.HasPrefix(title, "doc:") || + strings.Contains(title, "documentation") || + strings.Contains(title, "install doc") { + return "documentation" + } + + if strings.HasPrefix(title, "feat:") || + strings.HasPrefix(title, "feature:") || + strings.Contains(title, "add support") || + strings.Contains(title, "new feature") { + return "enhancement" + } + + return "change" +} + +func generateChangelogEntry(version, sinceTag string, cats Categories, repo string) string { + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("## [%s]\n", version)) + + // What's Changed + if len(cats.Changes) > 0 { + sb.WriteString("\n### What's Changed\n\n") + for _, pr := range cats.Changes { + sb.WriteString(fmt.Sprintf("* %s by @%s in %s\n", pr.Title, pr.User.Login, pr.HTMLURL)) + } + } + + // Enhancements + if len(cats.Enhancements) > 0 { + sb.WriteString("\n### Enhancements\n\n") + for _, pr := range cats.Enhancements { + sb.WriteString(fmt.Sprintf("* %s by @%s in %s\n", pr.Title, pr.User.Login, pr.HTMLURL)) + } + } + + // Bug Fixes + if len(cats.BugFixes) > 0 { + sb.WriteString("\n### Bug Fixes\n\n") + for _, pr := range cats.BugFixes { + sb.WriteString(fmt.Sprintf("* %s by @%s in %s\n", pr.Title, pr.User.Login, pr.HTMLURL)) + } + } + + // Maintenance + if len(cats.Maintenance) > 0 { + sb.WriteString("\n### Maintenance\n\n") + for _, pr := range cats.Maintenance { + sb.WriteString(fmt.Sprintf("* %s by @%s in %s\n", pr.Title, pr.User.Login, pr.HTMLURL)) + } + } + + // Doc Updates + if len(cats.DocUpdates) > 0 { + sb.WriteString("\n### Doc Update\n\n") + for _, pr := range cats.DocUpdates { + sb.WriteString(fmt.Sprintf("* %s by @%s in %s\n", pr.Title, pr.User.Login, pr.HTMLURL)) + } + } + + // New Contributors + if len(cats.NewContributors) > 0 { + sb.WriteString("\n### New Contributors\n\n") + // Sort by username for consistency + sort.Slice(cats.NewContributors, func(i, j int) bool { + return cats.NewContributors[i].Login < cats.NewContributors[j].Login + }) + for _, c := range cats.NewContributors { + sb.WriteString(fmt.Sprintf("* @%s made their first contribution in %s\n", c.Login, c.PRURL)) + } + } + + // Full Changelog link + currentTag := "v" + version + sb.WriteString(fmt.Sprintf("\n**Full Changelog**: https://github.com/%s/compare/%s...%s\n", + repo, sinceTag, currentTag)) + + return sb.String() +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/hack/changelog-generator/main_test.go new/kubelogin-0.2.15/hack/changelog-generator/main_test.go --- old/kubelogin-0.2.14/hack/changelog-generator/main_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/kubelogin-0.2.15/hack/changelog-generator/main_test.go 2026-02-20 03:16:58.000000000 +0100 @@ -0,0 +1,179 @@ +package main + +import ( + "fmt" + "strings" + "testing" + "time" +) + +func TestCategorizeByLabelsAndTitle(t *testing.T) { + tests := []struct { + name string + pr GitHubPR + expected string + }{ + // Label-based categorization takes precedence + {name: "bug label", pr: GitHubPR{Title: "something", Labels: []Label{{Name: "bug"}}}, expected: "bugfix"}, + {name: "fix label", pr: GitHubPR{Title: "something", Labels: []Label{{Name: "fix"}}}, expected: "bugfix"}, + {name: "enhancement label", pr: GitHubPR{Title: "something", Labels: []Label{{Name: "enhancement"}}}, expected: "enhancement"}, + {name: "feature label", pr: GitHubPR{Title: "something", Labels: []Label{{Name: "feature"}}}, expected: "enhancement"}, + {name: "dependencies label", pr: GitHubPR{Title: "something", Labels: []Label{{Name: "dependencies"}}}, expected: "maintenance"}, + {name: "documentation label", pr: GitHubPR{Title: "something", Labels: []Label{{Name: "documentation"}}}, expected: "documentation"}, + {name: "docs label", pr: GitHubPR{Title: "something", Labels: []Label{{Name: "docs"}}}, expected: "documentation"}, + + // Title prefix — bug fixes + {name: "fix: prefix", pr: GitHubPR{Title: "fix: nil pointer"}, expected: "bugfix"}, + {name: "bugfix: prefix", pr: GitHubPR{Title: "bugfix: something"}, expected: "bugfix"}, + {name: "bug fix: prefix", pr: GitHubPR{Title: "bug fix: something"}, expected: "bugfix"}, + {name: "hotfix: prefix", pr: GitHubPR{Title: "hotfix: something"}, expected: "bugfix"}, + + // Title prefix — maintenance + {name: "bump prefix", pr: GitHubPR{Title: "Bump Go to 1.24"}, expected: "maintenance"}, + {name: "update prefix", pr: GitHubPR{Title: "Update dependency"}, expected: "maintenance"}, + {name: "cve in title", pr: GitHubPR{Title: "address cve-2024-1234"}, expected: "maintenance"}, + {name: "fix cve", pr: GitHubPR{Title: "fix cve issues"}, expected: "maintenance"}, + {name: "chore: prefix", pr: GitHubPR{Title: "chore: tidy modules"}, expected: "maintenance"}, + {name: "chore space prefix", pr: GitHubPR{Title: "chore bump version"}, expected: "maintenance"}, + {name: "choreography not matched", pr: GitHubPR{Title: "choreography work"}, expected: "change"}, + + // Title prefix — documentation + {name: "docs: prefix", pr: GitHubPR{Title: "docs: update readme"}, expected: "documentation"}, + {name: "doc: prefix", pr: GitHubPR{Title: "doc: fix typo"}, expected: "documentation"}, + + // Title prefix — enhancements + {name: "feat: prefix", pr: GitHubPR{Title: "feat: add new auth"}, expected: "enhancement"}, + {name: "feature: prefix", pr: GitHubPR{Title: "feature: new login"}, expected: "enhancement"}, + {name: "add support", pr: GitHubPR{Title: "add support for X"}, expected: "enhancement"}, + + // Default + {name: "unrecognized", pr: GitHubPR{Title: "Refactor internal logic"}, expected: "change"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := categorizeByLabelsAndTitle(tc.pr) + if got != tc.expected { + t.Errorf("categorizeByLabelsAndTitle(%q, labels=%v) = %q; want %q", + tc.pr.Title, tc.pr.Labels, got, tc.expected) + } + }) + } +} + +func prURL(n int) string { + return fmt.Sprintf("https://github.com/Azure/kubelogin/pull/%d", n) +} + +func TestCategorizePRs(t *testing.T) { + now := time.Now() + prs := []GitHubPR{ + {Number: 1, Title: "feat: add thing", User: User{Login: "alice"}, HTMLURL: prURL(1), MergedAt: now}, + {Number: 2, Title: "fix: nil pointer", User: User{Login: "bob"}, HTMLURL: prURL(2), MergedAt: now}, + {Number: 3, Title: "Bump Go to 1.24", User: User{Login: "dependabot"}, HTMLURL: prURL(3), MergedAt: now}, + {Number: 4, Title: "docs: update readme", User: User{Login: "carol"}, HTMLURL: prURL(4), MergedAt: now}, + {Number: 5, Title: "General change", User: User{Login: "alice"}, HTMLURL: prURL(5), MergedAt: now}, + } + // bob and carol are new; alice and dependabot are existing + existing := map[string]bool{"alice": true, "dependabot": true} + + cats := categorizePRs(prs, existing) + + if len(cats.Enhancements) != 1 || cats.Enhancements[0].Number != 1 { + t.Errorf("expected 1 enhancement (PR#1), got %d", len(cats.Enhancements)) + } + if len(cats.BugFixes) != 1 || cats.BugFixes[0].Number != 2 { + t.Errorf("expected 1 bug fix (PR#2), got %d", len(cats.BugFixes)) + } + if len(cats.Maintenance) != 1 || cats.Maintenance[0].Number != 3 { + t.Errorf("expected 1 maintenance (PR#3), got %d", len(cats.Maintenance)) + } + if len(cats.DocUpdates) != 1 || cats.DocUpdates[0].Number != 4 { + t.Errorf("expected 1 doc update (PR#4), got %d", len(cats.DocUpdates)) + } + if len(cats.Changes) != 1 || cats.Changes[0].Number != 5 { + t.Errorf("expected 1 general change (PR#5), got %d", len(cats.Changes)) + } + + // New contributors: bob (PR#2) and carol (PR#4) + if len(cats.NewContributors) != 2 { + t.Errorf("expected 2 new contributors, got %d", len(cats.NewContributors)) + } + newLogins := map[string]bool{} + for _, c := range cats.NewContributors { + newLogins[c.Login] = true + } + if !newLogins["bob"] || !newLogins["carol"] { + t.Errorf("expected bob and carol as new contributors, got %v", newLogins) + } +} + +func TestGenerateChangelogEntry(t *testing.T) { + now := time.Now() + cats := Categories{ + Enhancements: []GitHubPR{{Number: 1, Title: "feat: new thing", User: User{Login: "alice"}, HTMLURL: "https://github.com/Azure/kubelogin/pull/1", MergedAt: now}}, + BugFixes: []GitHubPR{{Number: 2, Title: "fix: crash", User: User{Login: "bob"}, HTMLURL: "https://github.com/Azure/kubelogin/pull/2", MergedAt: now}}, + NewContributors: []Contributor{{Login: "bob", PRURL: "https://github.com/Azure/kubelogin/pull/2"}}, + } + + entry := generateChangelogEntry("0.2.15", "v0.2.14", cats, "Azure/kubelogin") + + for _, want := range []string{ + "## [0.2.15]", + "### Enhancements", + "feat: new thing", + "@alice", + "### Bug Fixes", + "fix: crash", + "@bob", + "### New Contributors", + "@bob made their first contribution", + "**Full Changelog**: https://github.com/Azure/kubelogin/compare/v0.2.14...v0.2.15", + } { + if !strings.Contains(entry, want) { + t.Errorf("expected changelog entry to contain %q\nGot:\n%s", want, entry) + } + } +} + +func TestIsReleasePR(t *testing.T) { + cases := []struct { + title string + expected bool + }{ + {"v0.2.14 release", true}, + {"0.2.14 release", true}, + {"v1.0.0", true}, + {"v0.2.14", true}, + // Regular PRs — must NOT be filtered + {"fix: nil pointer", false}, + {"feat: add new login flow", false}, + {"Bump Go to 1.24.11", false}, + {"docs: update readme", false}, + {"[Bug Fix] - PoP token crash", false}, + {"chore: update CHANGELOG.md for v0.2.15", false}, + } + for _, tc := range cases { + got := isReleasePR(tc.title) + if got != tc.expected { + t.Errorf("isReleasePR(%q) = %v; want %v", tc.title, got, tc.expected) + } + } +} + +func TestHasLabel(t *testing.T) { + pr := GitHubPR{Labels: []Label{{Name: "release"}, {Name: "chore"}}} + if !hasLabel(pr, "release") { + t.Error("expected hasLabel to return true for 'release'") + } + if !hasLabel(pr, "Release") { + t.Error("expected hasLabel to be case-insensitive") + } + if hasLabel(pr, "bug") { + t.Error("expected hasLabel to return false for 'bug'") + } + empty := GitHubPR{} + if hasLabel(empty, "release") { + t.Error("expected hasLabel to return false for PR with no labels") + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/pkg/internal/token/execCredentialPlugin.go new/kubelogin-0.2.15/pkg/internal/token/execCredentialPlugin.go --- old/kubelogin-0.2.14/pkg/internal/token/execCredentialPlugin.go 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/pkg/internal/token/execCredentialPlugin.go 2026-02-20 03:16:58.000000000 +0100 @@ -31,18 +31,17 @@ func New(o *Options) (ExecCredentialPlugin, error) { klog.V(10).Info(o.ToString()) - // Initialize PoP token cache in Options if enabled if o.IsPoPTokenEnabled && o.popTokenCache == nil { // Create PoP token cache using the official MSAL & MSAL extension libraries. popTokenCache, err := popcache.NewCache(o.AuthRecordCacheDir) if err != nil { // Fallback: Log warning and continue without PoP token caching when cache creation fails - klog.V(2).Infof("PoP token caching disabled due to secure storage failure (likely container environment): %v", err) - popTokenCache = nil - // Continue execution without using cached PoP tokens + // Leave popTokenCache unset (nil field) so GetPoPTokenCache() returns an untyped nil interface. + klog.Warningf("PoP token caching disabled due to secure storage failure (likely container environment): %v", err) + } else { + o.setPoPTokenCache(popTokenCache) } - o.setPoPTokenCache(popTokenCache) } return &execCredentialPlugin{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/pkg/internal/token/options.go new/kubelogin-0.2.15/pkg/internal/token/options.go --- old/kubelogin-0.2.14/pkg/internal/token/options.go 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/pkg/internal/token/options.go 2026-02-20 03:16:58.000000000 +0100 @@ -17,6 +17,7 @@ "github.com/Azure/kubelogin/pkg/internal/env" "github.com/Azure/kubelogin/pkg/internal/pop" popcache "github.com/Azure/kubelogin/pkg/internal/pop/cache" + msalcache "github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache" ) // PoPKeyProvider provides PoP keys based on the configured cache policy @@ -378,9 +379,14 @@ }) } -// GetPoPTokenCache returns the PoP token cache if available. -// Returns nil if PoP is disabled or cache creation failed (e.g., container environments). -func (o *Options) GetPoPTokenCache() *popcache.Cache { +// GetPoPTokenCache returns the PoP token cache if available, or nil if PoP is +// disabled or cache creation failed (e.g., container environments). +// It returns the interface type so that a nil field produces an untyped nil, +// which correctly compares as nil in downstream interface checks. +func (o *Options) GetPoPTokenCache() msalcache.ExportReplace { + if o.popTokenCache == nil { + return nil + } return o.popTokenCache } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kubelogin-0.2.14/pkg/internal/token/options_test.go new/kubelogin-0.2.15/pkg/internal/token/options_test.go --- old/kubelogin-0.2.14/pkg/internal/token/options_test.go 2026-01-06 19:59:04.000000000 +0100 +++ new/kubelogin-0.2.15/pkg/internal/token/options_test.go 2026-02-20 03:16:58.000000000 +0100 @@ -681,3 +681,40 @@ }) } } + +// TestGetPoPTokenCache_NilReturnsUntypedNil verifies that GetPoPTokenCache() +// returns a true untyped nil (not a typed nil *Cache wrapped in an interface) +// when the cache field is not set. +func TestGetPoPTokenCache_NilReturnsUntypedNil(t *testing.T) { + t.Run("nil cache field returns interface-nil", func(t *testing.T) { + o := &Options{} + // popTokenCache is its zero value (nil *popcache.Cache) + + cache := o.GetPoPTokenCache() + + // This is the critical assertion: the returned interface must be nil. + if cache != nil { + t.Fatal("GetPoPTokenCache() returned non-nil interface for nil cache field; this would cause a nil pointer panic in MSAL") + } + }) + + t.Run("valid cache field returns non-nil", func(t *testing.T) { + tmpDir := t.TempDir() + o := &Options{ + IsPoPTokenEnabled: true, + AuthRecordCacheDir: tmpDir, + } + + // Initialize via New() which calls setPoPTokenCache on success + _, err := New(o) + if err != nil { + t.Fatalf("New() failed: %v", err) + } + + // If cache creation succeeded (depends on environment), verify it's non-nil + cache := o.GetPoPTokenCache() + if o.popTokenCache != nil && cache == nil { + t.Fatal("GetPoPTokenCache() returned nil interface for non-nil cache field") + } + }) +} ++++++ kubelogin.obsinfo ++++++ --- /var/tmp/diff_new_pack.9dZW8d/_old 2026-03-02 17:36:34.213937548 +0100 +++ /var/tmp/diff_new_pack.9dZW8d/_new 2026-03-02 17:36:34.249939064 +0100 @@ -1,5 +1,5 @@ name: kubelogin -version: 0.2.14 -mtime: 1767725944 -commit: 6e8b5babc994d57e752e24297dd9c6f59247a1f8 +version: 0.2.15 +mtime: 1771553818 +commit: 7936910173e517c5c651d1bbd56d3143c4b1fe4e ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/kubelogin/vendor.tar.gz /work/SRC/openSUSE:Factory/.kubelogin.new.29461/vendor.tar.gz differ: char 13, line 1
