diff --git a/Project.toml b/Project.toml index 32c40de..402ab5c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "MassApplyPatch" uuid = "a374f348-d733-473a-88bc-8a13261ed0c1" -version = "0.2.30" +version = "0.2.31" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/main.jl b/src/main.jl index 548e594..f1fa7e4 100644 --- a/src/main.jl +++ b/src/main.jl @@ -50,28 +50,73 @@ end const git = Git.git() -function clone_repo(repo::AbstractString, destdir::AbstractString) - url = "git@github.com:$repo.git" +""" + BotAuth(; token, user_name, user_email) + +Credentials for attributing MassApplyPatch's clone, push, commit, and +PR-creation operations to a dedicated bot account instead of the local +user. + + - `token`: a GitHub PAT under the bot account. Used for HTTPS clone / + push auth and for GitHub API calls. + - `user_name`, `user_email`: git commit author identity. The typical + pattern for the email is `+@users.noreply.github.com`, + which GitHub links back to the bot's profile. + +Pass to `make_patch_pr` via the `auth` kwarg. When `auth === nothing` +(default), MassApplyPatch falls back to SSH clone + the user's local +git config + `gh auth token` for the API. +""" +Base.@kwdef struct BotAuth + token::String + user_name::String + user_email::String +end + +function clone_repo( + repo::AbstractString, destdir::AbstractString; + auth::Union{BotAuth, Nothing} = nothing + ) + url = if isnothing(auth) + "git@github.com:$repo.git" + else + "https://x-access-token:$(auth.token)@github.com/$repo.git" + end try run_with_output(`$git clone $url $destdir`) catch e @error "Failed to clone repository $repo: $e" + return nothing + end + if !isnothing(auth) + run_with_output(`$git -C $destdir config user.name $(auth.user_name)`) + run_with_output(`$git -C $destdir config user.email $(auth.user_email)`) end return nothing end -function github_auth() - # Try gh CLI first, then fall back to ENV - token = get(ENV, "GITHUB_AUTH", "") - if isempty(token) - try - token = readchomp(`gh auth token`) - catch e - @error "Failed to get GitHub auth token from gh CLI: $e" +function github_auth(; auth::Union{BotAuth, Nothing} = nothing) + token = if !isnothing(auth) + auth.token + else + # Fall back to legacy ENV[\"GITHUB_AUTH\"] or gh CLI. + env_token = get(ENV, "GITHUB_AUTH", "") + if !isempty(env_token) + env_token + else + cli_token = "" + try + cli_token = readchomp(`gh auth token`) + catch e + @error "Failed to get GitHub auth token from gh CLI: $e" + end + cli_token end end isempty(token) && - error("Install and authenticate the `gh` CLI, or set ENV[\"GITHUB_AUTH\"].") + error( + "Pass `auth = BotAuth(...)`, install+authenticate the `gh` CLI, or set ENV[\"GITHUB_AUTH\"]." + ) return GitHub.authenticate(token) end @@ -123,11 +168,12 @@ function make_patch_pr( notrigger_patchname = String[], branch::AbstractString = default_kwarg(patchname, :branch), title::AbstractString = default_kwarg(patchname, :title), - body::AbstractString = default_kwarg(patchname, :body) + body::AbstractString = default_kwarg(patchname, :body), + auth::Union{BotAuth, Nothing} = nothing ) tmpdir = mktempdir() repodir = joinpath(tmpdir, split(repo, "/"; limit = 2)[2]) - clone_repo(repo, repodir) + clone_repo(repo, repodir; auth) !isdir(repodir) && return nothing url = cd(repodir) do branchname = unique_branch_name(branch, git) @@ -148,8 +194,8 @@ function make_patch_pr( run_with_output(`$git add .`) run_with_output(`$git commit -m $title`) run_with_output(`$git push origin $branchname`) - auth = github_auth() - return open_pr(repo, branchname; title, body, auth) + gh_auth = github_auth(; auth) + return open_pr(repo, branchname; title, body, auth = gh_auth) end return url end @@ -167,7 +213,7 @@ Arguments: The patch function should be provided as a Julia file, which is included and must define a function `patch(repo_path)`. """ -function main(argv) +function main(argv; auth::Union{BotAuth, Nothing} = nothing) # Get the patch names to determine which patches to apply. patchargs = filter(arg -> startswith(arg, "--patch="), argv) patchnames = map(arg -> split(arg, "="; limit = 2)[2], patchargs) @@ -207,7 +253,7 @@ function main(argv) make_patch_pr( patchnames, repo; notrigger_patchname = notrigger_patchnames, branch, title, - body + body, auth ) catch error @error "Error patching $repo: $error"