diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7df78a23..0320f9f0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,62 +54,6 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env] Repository-level CI upload token. - prepare-authors: - name: Prepare authors - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout Source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Create Test Author Metadata - env: - AUTHORS_PATH: ${{ runner.temp }}/prepare-authors-smoke.json - run: | - python3 - <<'PY' - import json - import os - import subprocess - from pathlib import Path - - output = subprocess.check_output( - ["git", "log", "-z", "--format=%ae%x00%an", "HEAD"], - text=True, - ) - fields = output.split("\0") - if fields[-1] == "": - fields.pop() - authors = {} - for offset in range(0, len(fields), 2): - email, name = fields[offset : offset + 2] - entry = authors.setdefault(email, {"name": name, "email": email}) - if name != entry["name"]: - aliases = entry.setdefault("aliases", []) - if name not in aliases: - aliases.append(name) - Path(os.environ["AUTHORS_PATH"]).write_text( - json.dumps(list(authors.values())), - encoding="utf-8", - ) - PY - - - name: Run Prepare Authors - id: prepare-authors - uses: ./prepare-authors - with: - authors-path: ${{ runner.temp }}/prepare-authors-smoke.json - git-remote: prepare-authors-smoke - mode: check - - - name: Check Result - env: - CHANGED: ${{ steps.prepare-authors.outputs.changed }} - run: test "$CHANGED" = false - read-file: name: Read file runs-on: ubuntu-latest @@ -254,7 +198,7 @@ jobs: # required check analyze: name: Analyze - needs: [pytest, prepare-authors, read-file, template-files, canary-release] + needs: [pytest, read-file, template-files, canary-release] if: '!cancelled()' runs-on: ubuntu-latest permissions: diff --git a/prepare-authors/README.md b/prepare-authors/README.md index f58d37c0..437fa634 100644 --- a/prepare-authors/README.md +++ b/prepare-authors/README.md @@ -8,9 +8,7 @@ used when the commit’s resolved GitHub login conflicts with the entry’s `git key (the commit is treated as a different person, or matched by that login). Duplicate emails fail. Duplicate names or GitHub logins require the other identifier to select one author entry. -In `check` mode it validates only and fails when new contributors or -alternate-email/alias updates are needed; missing `github:` keys warn but do not fail. -In `prepare` mode unresolved missing `github:` keys fail the step after lookup +Unresolved missing `github:` keys fail the step after lookup attempts (no commit or PR). That includes newly discovered contributors whose commit author could not be mapped to a GitHub login. Rever still updates `.mailmap` and `AUTHORS.md` at release time. @@ -24,10 +22,9 @@ commit author could not be mapped to a GitHub login. Rever still updates | `base-branch` | Base branch for the generated authors PR. | `main` | | `branch-prefix` | Non-empty prefix for the generated authors branch. | `prepare-authors-` | | `git-remote` | Git remote alias used to resolve owner/repo for gh api. | `origin` | -| `mode` | `prepare` updates `.authors.yml` and opens a PR; `check` validates only. | `prepare` | | `git-author-name` | Git author name for the generated commit. | `Conda Bot` | | `git-author-email` | Git author email for the generated commit. | `18747875+conda-bot@users.noreply.github.com` | -| `token` | Token for checkout and prepare-mode push/PR (`contents:write`, `pull-requests:write`). Author login lookups use `GITHUB_TOKEN` / `github.token`. | `${{ github.token }}` | +| `token` | Token for checkout and push/PR (`contents:write`, `pull-requests:write`). Author login lookups use `GITHUB_TOKEN` / `github.token`. | `${{ github.token }}` | ## Action Outputs @@ -35,14 +32,12 @@ commit author could not be mapped to a GitHub login. Rever still updates |------|-------------| | `changed` | Whether `.authors.yml` needed updates. | | `branch` | Generated authors branch. | -| `pull-request-url` | Generated or updated authors PR URL in prepare mode. | +| `pull-request-url` | Generated or updated authors PR URL. | ## Sample Workflows In your GitHub repository include this action in your workflows: -### Prepare mode (weekly) - ```yaml name: Prepare authors @@ -65,20 +60,3 @@ jobs: base-branch: main token: ${{ secrets.SYNC_TOKEN }} ``` - -### Check mode - -Use `mode: check` to validate `.authors.yml` without writing files or opening a -PR. Check mode checks out the triggering revision (not `base-branch`). New -contributors and alternate-email or alias updates fail the step. Missing `github:` keys -emit warnings but do not fail (unlike prepare mode, which fails when those keys -stay unresolved after lookup, including for newly discovered contributors). -Lookups use the job `GITHUB_TOKEN` -(`contents: read` is enough). - -```yaml -- uses: conda/actions/prepare-authors@main - with: - mode: check - since: tag -``` diff --git a/prepare-authors/action.yml b/prepare-authors/action.yml index 630124ae..2b49fddc 100644 --- a/prepare-authors/action.yml +++ b/prepare-authors/action.yml @@ -23,9 +23,6 @@ inputs: git-remote: description: Git remote alias used to resolve owner/repo for gh api. default: origin - mode: - description: prepare updates .authors.yml and opens a PR; check validates only. - default: prepare git-author-name: description: Git author name for the generated commit. default: Conda Bot @@ -34,7 +31,7 @@ inputs: default: 18747875+conda-bot@users.noreply.github.com token: description: >- - Token for checkout and, in prepare mode, push/PR writes + Token for checkout and push/PR writes (contents:write and pull-requests:write). Commit author lookups always use GITHUB_TOKEN / github.token instead. default: ${{ github.token }} @@ -74,7 +71,6 @@ runs: run: pip install --quiet -r "$GITHUB_ACTION_PATH/requirements.txt" - name: Checkout Repository - if: inputs.mode == 'prepare' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.base-branch }} @@ -82,39 +78,25 @@ runs: persist-credentials: false token: ${{ inputs.token }} - - name: Checkout Repository - if: inputs.mode == 'check' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - token: ${{ inputs.token }} - - name: Prepare Authors id: run shell: bash run: | args=( - "$INPUT_MODE" --authors-path "$INPUT_AUTHORS_PATH" --since "$INPUT_SINCE" --git-remote "$INPUT_GIT_REMOTE" + --base-branch "$INPUT_BASE_BRANCH" + --branch-prefix "$INPUT_BRANCH_PREFIX" + --git-author-name "$INPUT_GIT_AUTHOR_NAME" + --git-author-email "$INPUT_GIT_AUTHOR_EMAIL" + --repository "$GITHUB_REPOSITORY" + --token "$INPUT_TOKEN" ) - if [ "$INPUT_MODE" = prepare ]; then - args+=( - --base-branch "$INPUT_BASE_BRANCH" - --branch-prefix "$INPUT_BRANCH_PREFIX" - --git-author-name "$INPUT_GIT_AUTHOR_NAME" - --git-author-email "$INPUT_GIT_AUTHOR_EMAIL" - --repository "$GITHUB_REPOSITORY" - --token "$INPUT_TOKEN" - ) - fi python "$GITHUB_ACTION_PATH/prepare_authors.py" "${args[@]}" env: # Keep reads on the job token; do not set GH_TOKEN to inputs.token here. GITHUB_TOKEN: ${{ github.token }} - INPUT_MODE: ${{ inputs.mode }} INPUT_AUTHORS_PATH: ${{ inputs.authors-path }} INPUT_SINCE: ${{ inputs.since }} INPUT_GIT_REMOTE: ${{ inputs.git-remote }} diff --git a/prepare-authors/prepare_authors.py b/prepare-authors/prepare_authors.py index 19ba0b5e..0cb5615f 100644 --- a/prepare-authors/prepare_authors.py +++ b/prepare-authors/prepare_authors.py @@ -47,19 +47,6 @@ class AuthorAnalysis: def parse_args(argv: list[str] | None = None) -> Namespace: parser = ArgumentParser(description="Prepare .authors.yml updates for rever.") - subparsers = parser.add_subparsers(dest="command", required=True) - - sub_check = subparsers.add_parser("check") - add_shared_args(sub_check) - - sub_prepare = subparsers.add_parser("prepare") - add_shared_args(sub_prepare) - add_prepare_args(sub_prepare) - - return parser.parse_args(argv) - - -def add_shared_args(parser: ArgumentParser) -> None: parser.add_argument("--authors-path", default=".authors.yml") parser.add_argument( "--since", @@ -72,9 +59,6 @@ def add_shared_args(parser: ArgumentParser) -> None: default="origin", help="Git remote alias used to resolve owner/repo for gh api.", ) - - -def add_prepare_args(parser: ArgumentParser) -> None: parser.add_argument("--base-branch", default="main") parser.add_argument("--branch-prefix", default="prepare-authors-") parser.add_argument("--git-author-name", default="Conda Bot") @@ -87,15 +71,13 @@ def add_prepare_args(parser: ArgumentParser) -> None: default=os.environ.get("GITHUB_REPOSITORY", ""), ) parser.add_argument("--token", default=os.environ.get("GITHUB_TOKEN", "")) + return parser.parse_args(argv) def main(argv: list[str] | None = None) -> int: try: args = parse_args(argv) - if args.command == "check": - check_authors(args) - else: - prepare_authors(args) + prepare_authors(args) except ActionError as err: print(f"::error::{err}", file=sys.stderr) return 1 @@ -139,61 +121,10 @@ def github_required_authors(analysis: AuthorAnalysis) -> list[tuple[str, str]]: ] -def check_authors(args: Namespace) -> None: - read_token = require_github_token() - authors_path = Path(args.authors_path) - metadata, _ = load_metadata(authors_path) - repo_full = get_repo_full(args.git_remote) - warn_if_missing_repo_full(repo_full, args.git_remote) - analysis = analyze_authors( - metadata, - since=args.since, - repo_full=repo_full, - get_github_login_fn=make_github_login_fn(read_token), - ) - - summary_lines = emit_missing_github_warnings(analysis.missing_github_keys) - - if not analysis.alternate_email_updates and not analysis.new_authors: - if summary_lines: - write_summary("\n".join(summary_lines)) - else: - write_summary("All authors are present in `.authors.yml`.") - write_output("changed", "false") - return - - messages: list[str] = [] - if analysis.alternate_email_updates: - messages.append( - f"Found {len(analysis.alternate_email_updates)} existing contributor(s) " - "needing alternate_emails or aliases updates:" - ) - for entry, commit, _github_login in analysis.alternate_email_updates: - parts: list[str] = [] - if commit.email != entry["email"] and commit.email not in entry.get( - "alternate_emails", [] - ): - parts.append(f"add {commit.email!r} to alternate_emails") - if commit.name != entry["name"] and commit.name not in entry.get( - "aliases", [] - ): - parts.append(f"add {commit.name!r} to aliases") - detail = "; ".join(parts) if parts else "update metadata" - messages.append(f"- {entry['name']}: {detail}") - if analysis.new_authors: - messages.append(f"Found {len(analysis.new_authors)} new contributor(s):") - for commit in analysis.new_authors: - messages.append(f"- {commit['name']} <{commit['email']}>") - - write_summary("\n".join(summary_lines + messages)) - write_output("changed", "true") - raise ActionError("\n".join(messages)) - - def prepare_authors(args: Namespace) -> None: branch = f"{args.branch_prefix}{args.base_branch}" if branch == args.base_branch: - raise ActionError("branch-prefix must not be empty in prepare mode.") + raise ActionError("branch-prefix must not be empty.") read_token = require_github_token() if not args.token: diff --git a/prepare-authors/test_prepare_authors.py b/prepare-authors/test_prepare_authors.py index de2a3fce..c9db8611 100644 --- a/prepare-authors/test_prepare_authors.py +++ b/prepare-authors/test_prepare_authors.py @@ -14,7 +14,6 @@ analyze_authors, apply_updates, build_author_indexes, - check_authors, classify_commits, create_or_update_pr, emit_missing_github_warnings, @@ -1158,54 +1157,6 @@ def stateful_login(_repo: str, commit_hash: str) -> str | None: assert metadata[1]["github"] == "bob" -def test_check_authors_fails_when_updates_needed( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - init_repo(tmp_path, monkeypatch) - monkeypatch.setenv("GITHUB_TOKEN", "read-token") - authors = tmp_path / ".authors.yml" - write_authors(authors, "- name: Alice Example\n email: alice@example.com\n") - subprocess.run(["git", "add", ".authors.yml"], check=True) - subprocess.run(["git", "commit", "-m", "authors"], check=True, capture_output=True) - subprocess.run(["git", "tag", "1.0.0"], check=True) - (tmp_path / "feature.txt").write_text("feature\n", encoding="utf-8") - subprocess.run(["git", "add", "feature.txt"], check=True) - subprocess.run( - ["git", "commit", "-m", "feature", "--author", "Bob Example "], - check=True, - capture_output=True, - ) - - class Args: - authors_path = ".authors.yml" - since = "tag" - git_remote = "origin" - - with pytest.raises(ActionError, match="new contributor"): - check_authors(Args()) - - -def test_check_authors_passes_when_complete( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - init_repo(tmp_path, monkeypatch) - monkeypatch.setenv("GITHUB_TOKEN", "read-token") - authors = tmp_path / ".authors.yml" - write_authors(authors, "- name: Test User\n email: test@example.com\n") - subprocess.run(["git", "add", ".authors.yml"], check=True) - subprocess.run(["git", "commit", "-m", "authors"], check=True, capture_output=True) - subprocess.run(["git", "tag", "1.0.0"], check=True) - - class Args: - authors_path = ".authors.yml" - since = "tag" - git_remote = "origin" - - check_authors(Args()) - - def test_unresolved_missing_github_keys() -> None: metadata = [ {"name": "Alice Example", "email": "alice@example.com", "github": "alice"}, @@ -1265,78 +1216,6 @@ def test_emit_missing_github_warnings_empty() -> None: assert emit_missing_github_warnings([]) == [] -def test_check_authors_warns_but_passes_on_missing_github_only( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - init_repo(tmp_path, monkeypatch) - monkeypatch.setenv("GITHUB_TOKEN", "read-token") - authors = tmp_path / ".authors.yml" - write_authors( - authors, - ( - "- name: Alice Example\n" - " email: alice@example.com\n" - " github: alice\n" - "- name: Bob Example\n" - " email: bob@example.com\n" - ), - ) - subprocess.run(["git", "add", ".authors.yml"], check=True) - subprocess.run(["git", "commit", "-m", "authors"], check=True, capture_output=True) - subprocess.run(["git", "tag", "1.0.0"], check=True) - - class Args: - authors_path = ".authors.yml" - since = "tag" - git_remote = "origin" - - check_authors(Args()) - captured = capsys.readouterr() - assert "missing a github key" in captured.err - assert "Bob Example" in captured.err - - -def test_check_authors_does_not_apply_resolved_github_update( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - monkeypatch.setenv("GITHUB_TOKEN", "read-token") - github_output = tmp_path / "github_output" - monkeypatch.setenv("GITHUB_OUTPUT", str(github_output)) - metadata = [ - {"name": "Alice Example", "email": "alice@example.com", "github": "alice"}, - {"name": "Bob Example", "email": "bob@example.com"}, - ] - commits = [CommitAuthor("abc", "bob@example.com", "Bob Example", "fix")] - - class Args: - authors_path = ".authors.yml" - since = "tag" - git_remote = "origin" - - with ( - patch("prepare_authors.load_metadata", return_value=(metadata, None)), - patch("prepare_authors.get_repo_full", return_value="conda/example"), - patch( - "prepare_authors.get_commits_since", - return_value=(commits, "tag 1.0.0"), - ), - patch( - "prepare_authors.make_github_login_fn", - return_value=lambda _repo, _hash: "bob", - ), - ): - check_authors(Args()) - - captured = capsys.readouterr() - assert "missing a github key" in captured.err - assert "changed=false" in github_output.read_text(encoding="utf-8") - assert "github" not in metadata[1] - - def test_prepare_authors_rejects_empty_branch_prefix_before_mutation() -> None: class Args: base_branch = "main" @@ -1535,24 +1414,6 @@ def test_require_github_token_fails_when_missing( require_github_token() -def test_check_authors_requires_github_token( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - init_repo(tmp_path, monkeypatch) - monkeypatch.delenv("GITHUB_TOKEN", raising=False) - authors = tmp_path / ".authors.yml" - write_authors(authors, "- name: Test User\n email: test@example.com\n") - - class Args: - authors_path = ".authors.yml" - since = "tag" - git_remote = "origin" - - with pytest.raises(ActionError, match="GITHUB_TOKEN is required"): - check_authors(Args()) - - def test_get_github_login_passes_read_token_as_gh_token( monkeypatch: pytest.MonkeyPatch, ) -> None: