Skip to content
Open
Show file tree
Hide file tree
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
58 changes: 1 addition & 57 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
28 changes: 3 additions & 25 deletions prepare-authors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,25 +22,22 @@ 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

| Name | Description |
|------|-------------|
| `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

Expand All @@ -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
```
32 changes: 7 additions & 25 deletions prepare-authors/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down Expand Up @@ -74,47 +71,32 @@ 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 }}
fetch-depth: 0
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 }}
Expand Down
75 changes: 3 additions & 72 deletions prepare-authors/prepare_authors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading