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")