This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new bb4da3272e4 Breeze: Add scope metadata to GitHub token retrieval 
(#66753)
bb4da3272e4 is described below

commit bb4da3272e4169b885ad01807e204c15a2529705
Author: Paul <[email protected]>
AuthorDate: Wed Jul 1 09:21:04 2026 -0700

    Breeze: Add scope metadata to GitHub token retrieval (#66753)
    
    Co-authored-by: paullee <[email protected]>
---
 .../src/airflow_breeze/commands/ci_commands.py     |  7 +++---
 .../src/airflow_breeze/commands/issues_commands.py |  7 +++---
 .../commands/release_management_commands.py        | 24 ++++++++++++++++++---
 dev/breeze/src/airflow_breeze/utils/github.py      | 18 +++++++++++++++-
 .../airflow_breeze/utils/provider_dependencies.py  | 10 +++++----
 dev/breeze/tests/test_github_utils.py              | 25 +++++++++++++++++++---
 6 files changed, 74 insertions(+), 17 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/commands/ci_commands.py 
b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
index 686def2fc0b..02b9967f29f 100644
--- a/dev/breeze/src/airflow_breeze/commands/ci_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
@@ -58,7 +58,7 @@ from airflow_breeze.utils.docker_command_utils import (
     fix_ownership_using_docker,
     perform_environment_checks,
 )
-from airflow_breeze.utils.github import retrieve_github_token
+from airflow_breeze.utils.github import format_github_token_scope_guidance, 
retrieve_github_token
 from airflow_breeze.utils.path_utils import AIRFLOW_HOME_PATH, 
AIRFLOW_ROOT_PATH
 from airflow_breeze.utils.run_utils import run_command
 
@@ -780,7 +780,7 @@ def upgrade(
 
     console_print("[info]Running upgrade of important CI environment.[/]")
 
-    github_token = retrieve_github_token(github_token)
+    github_token = retrieve_github_token(github_token, 
description="airflow-ci-upgrade", scopes="public_repo")
 
     # Create a copy of the environment to pass to commands
     command_env = os.environ.copy()
@@ -791,7 +791,8 @@ def upgrade(
     else:
         console_print(
             "[warning]Could not retrieve GitHub token from --github-token, gh 
CLI, or token env. "
-            "Commands may fail if they require authentication.[/]"
+            "Commands may fail if they require authentication. "
+            
f"{format_github_token_scope_guidance(description='airflow-ci-upgrade', 
scopes='public_repo')}[/]"
         )
 
     # All upgrade commands run locally with check=False to continue on errors.
diff --git a/dev/breeze/src/airflow_breeze/commands/issues_commands.py 
b/dev/breeze/src/airflow_breeze/commands/issues_commands.py
index 7acfe45cfaf..90cbbe2910a 100644
--- a/dev/breeze/src/airflow_breeze/commands/issues_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/issues_commands.py
@@ -31,7 +31,7 @@ from airflow_breeze.commands.common_options import (
 from airflow_breeze.utils.click_utils import BreezeGroup
 from airflow_breeze.utils.confirm import Answer, user_confirm
 from airflow_breeze.utils.console import console_print
-from airflow_breeze.utils.github import retrieve_github_token
+from airflow_breeze.utils.github import format_github_token_scope_guidance, 
retrieve_github_token
 from airflow_breeze.utils.shared_options import get_dry_run
 
 
@@ -42,7 +42,7 @@ def issues_group():
 
 def _resolve_github_token(github_token: str | None) -> str | None:
     """Resolve GitHub token from option, environment, or gh CLI."""
-    return retrieve_github_token(github_token)
+    return retrieve_github_token(github_token, 
description="airflow-issues-unassign", scopes="public_repo")
 
 
 def _get_collaborator_logins(repo) -> set[str]:
@@ -159,7 +159,8 @@ def unassign(
     if not token:
         console_print(
             "[error]GitHub token not found. Provide --github-token, "
-            "set GITHUB_TOKEN, or authenticate with `gh auth login`.[/]"
+            "set GITHUB_TOKEN, or authenticate with `gh auth login`. "
+            
f"{format_github_token_scope_guidance(description='airflow-issues-unassign', 
scopes='public_repo')}[/]"
         )
         sys.exit(1)
 
diff --git 
a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py 
b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
index 59bd09c8846..98235ceeec5 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
@@ -2876,7 +2876,11 @@ def generate_issue_content_providers(
             all_prs.update(prs)
             provider_prs[provider_id] = filtered_prs
             all_retrieved_prs.update(provider_prs[provider_id])
-        github_token = retrieve_github_token(github_token)
+        github_token = retrieve_github_token(
+            github_token,
+            description="airflow-generate-provider-release-issue",
+            scopes="repo:status",
+        )
         g = Github(github_token)
         repo = g.get_repo("apache/airflow")
         pull_requests: dict[int, PullRequest.PullRequest | Issue.Issue] = {}
@@ -3486,7 +3490,14 @@ def generate_airflowctl_changelog(
     verbose = get_verbose()
 
     prs = _get_airflowctl_prs(verbose, previous_release, current_release, 
excluded_pr_list)
-    github_token = retrieve_github_token(github_token) or ""
+    github_token = (
+        retrieve_github_token(
+            github_token,
+            description="airflow-generate-airflowctl-changelog",
+            scopes="repo:status",
+        )
+        or ""
+    )
 
     g = Github(github_token)
     repo = g.get_repo("apache/airflow")
@@ -4664,7 +4675,14 @@ def generate_issue_content(
         excluded_prs = []
     prs = [pr for pr in change_prs if pr is not None and pr not in 
excluded_prs]
 
-    github_token = retrieve_github_token(github_token) or ""
+    github_token = (
+        retrieve_github_token(
+            github_token,
+            description="airflow-generate-release-issue",
+            scopes="repo:status",
+        )
+        or ""
+    )
     g = Github(github_token)
     repo = g.get_repo("apache/airflow")
     pull_requests: dict[int, PullRequestOrIssue] = {}
diff --git a/dev/breeze/src/airflow_breeze/utils/github.py 
b/dev/breeze/src/airflow_breeze/utils/github.py
index 29f73c231a7..5ee3ed752ea 100644
--- a/dev/breeze/src/airflow_breeze/utils/github.py
+++ b/dev/breeze/src/airflow_breeze/utils/github.py
@@ -85,10 +85,26 @@ def run_gh_command(
     return subprocess.run(command, env=command_env, check=check, **kwargs)
 
 
-def retrieve_github_token(token: str | None = None, *, env: Mapping[str, str] 
| None = None) -> str | None:
+def format_github_token_scope_guidance(*, description: str | None = None, 
scopes: str | None = None) -> str:
+    purpose = f" for {description}" if description else ""
+    if scopes:
+        return f"If creating a token manually{purpose}, it needs {scopes} 
scope."
+    return f"If creating a token manually{purpose}, make sure it has the 
permissions required by the command."
+
+
+def retrieve_github_token(
+    token: str | None = None,
+    *,
+    description: str | None = None,
+    scopes: str | None = None,
+    env: Mapping[str, str] | None = None,
+) -> str | None:
     """
     Resolve a GitHub token for local Breeze commands.
 
+    ``description`` and ``scopes`` document what the caller needs from the 
token. Retrieval itself
+    does not validate scopes.
+
     Non-empty token arguments are preserved when they do not match 
``GH_TOKEN`` or
     ``GITHUB_TOKEN`` from the environment. Matching values are treated as 
ambient env input because
     Click can populate ``--github-token`` from ``envvar="GITHUB_TOKEN"``. 
Ambient env tokens are used
diff --git a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py 
b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
index a8d489b05ca..a985a65b55d 100644
--- a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
+++ b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
@@ -41,6 +41,7 @@ from airflow_breeze.utils.ci_group import ci_group
 from airflow_breeze.utils.console import console_print
 from airflow_breeze.utils.github import (
     download_constraints_file,
+    format_github_token_scope_guidance,
     get_active_airflow_versions,
     get_tag_date,
     retrieve_github_token,
@@ -252,15 +253,16 @@ def get_all_constraint_files_and_airflow_releases(
         shutil.rmtree(CONSTRAINTS_CACHE_PATH, ignore_errors=True)
     if not CONSTRAINTS_CACHE_PATH.exists():
         if not github_token:
-            github_token = retrieve_github_token()
+            github_token = retrieve_github_token(
+                description="airflow-refresh-constraints", scopes="public_repo"
+            )
             if github_token:
                 console_print("\n[info]Resolved GitHub token for constraints 
refresh[/]\n")
             else:
                 console_print(
                     "[error]You need to provide GITHUB_TOKEN to generate 
providers metadata.[/]\n\n"
-                    "You can generate it with this URL: "
-                    "Please set it to a valid GitHub token with public_repo 
scope. You can create one by clicking "
-                    "the URL:\n\n"
+                    
f"{format_github_token_scope_guidance(description='airflow-refresh-constraints',
 scopes='public_repo')} "
+                    "You can create one by clicking the URL:\n\n"
                     
"https://github.com/settings/tokens/new?scopes=public_repo&description=airflow-refresh-constraints\n\n";
                     "Once you have the token you can prepend prek command with 
GITHUB_TOKEN='<your token>' or"
                     "set it in your environment with export 
GITHUB_TOKEN='<your token>'\n\n"
diff --git a/dev/breeze/tests/test_github_utils.py 
b/dev/breeze/tests/test_github_utils.py
index 142cf226bbc..3b2e2395f3e 100644
--- a/dev/breeze/tests/test_github_utils.py
+++ b/dev/breeze/tests/test_github_utils.py
@@ -23,6 +23,7 @@ import pytest
 
 from airflow_breeze.utils.github import (
     env_without_github_tokens,
+    format_github_token_scope_guidance,
     retrieve_github_token,
     run_gh_command,
 )
@@ -51,7 +52,7 @@ def 
test_retrieve_github_token_prefers_clean_gh_auth_token(mock_run, monkeypatch
     monkeypatch.setenv("GITHUB_TOKEN", "env-github-token")
     mock_run.return_value = _completed_process(returncode=0, 
stdout="stored-gh-token\n")
 
-    assert retrieve_github_token() == "stored-gh-token"
+    assert retrieve_github_token(description="airflow-ci-upgrade", 
scopes="public_repo") == "stored-gh-token"
 
     mock_run.assert_called_once()
     call_env = mock_run.call_args.kwargs["env"]
@@ -65,7 +66,7 @@ def 
test_retrieve_github_token_falls_back_to_env_token(mock_run, monkeypatch):
     monkeypatch.setenv("GITHUB_TOKEN", "env-github-token")
     mock_run.return_value = _completed_process(returncode=1)
 
-    assert retrieve_github_token() == "env-gh-token"
+    assert retrieve_github_token(description="airflow-ci-upgrade", 
scopes="public_repo") == "env-gh-token"
 
 
 @mock.patch("airflow_breeze.utils.github.subprocess.run")
@@ -88,7 +89,10 @@ def 
test_retrieve_github_token_falls_back_to_env_token_when_gh_returns_whitespac
 def test_retrieve_github_token_keeps_explicit_token(mock_run, monkeypatch):
     monkeypatch.setenv("GITHUB_TOKEN", "env-token")
 
-    assert retrieve_github_token("explicit-token") == "explicit-token"
+    assert (
+        retrieve_github_token("explicit-token", 
description="airflow-ci-upgrade", scopes="public_repo")
+        == "explicit-token"
+    )
 
     mock_run.assert_not_called()
 
@@ -101,6 +105,21 @@ def 
test_retrieve_github_token_does_not_treat_env_token_argument_as_explicit(moc
     assert retrieve_github_token("env-token") == "stored-gh-token"
 
 
+def test_format_github_token_scope_guidance_includes_description_and_scope():
+    assert (
+        format_github_token_scope_guidance(description="airflow-ci-upgrade", 
scopes="public_repo")
+        == "If creating a token manually for airflow-ci-upgrade, it needs 
public_repo scope."
+    )
+
+
+def test_format_github_token_scope_guidance_is_generic_without_metadata():
+    guidance = format_github_token_scope_guidance()
+
+    assert (
+        guidance == "If creating a token manually, make sure it has the 
permissions required by the command."
+    )
+
+
 @mock.patch("airflow_breeze.utils.github.subprocess.run")
 def 
test_run_gh_command_retries_with_original_env_after_clean_env_failure(mock_run, 
monkeypatch):
     monkeypatch.setenv("GH_TOKEN", "env-gh-token")

Reply via email to