Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Version [v1.14.1] - 2025-07-09

### Added

* Support self-hosted GitHub instances.

### Fixed

* Fixed the `GITHUB_EVENT_NAME` message to include `release` as one of the options. ([#2750])
Expand Down
51 changes: 38 additions & 13 deletions src/deployconfig.jl
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,35 @@ struct GitHubActions <: DeployConfig
github_repository::String
github_event_name::String
github_ref::String
github_host::String
github_api::String
github_pages_url::String
end

function GitHubActions()
github_repository = get(ENV, "GITHUB_REPOSITORY", "") # "JuliaDocs/Documenter.jl"
github_event_name = get(ENV, "GITHUB_EVENT_NAME", "") # "push", "pull_request" or "cron" (?)
github_ref = get(ENV, "GITHUB_REF", "") # "refs/heads/$(branchname)" for branch, "refs/tags/$(tagname)" for tags
return GitHubActions(github_repository, github_event_name, github_ref)
github_api = get(ENV, "GITHUB_API_URL", "") # https://api.github.com

# Compute GitHub Pages URL from repository
parts = split(github_repository, "/")
github_pages_url = if length(parts) == 2
owner, repo = parts
"https://$(owner).github.io/$(repo)/"
Copy link
Collaborator

@fingolfin fingolfin Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course you are just moving this around, and that's already a good step; so what I say below is not a change request, but a general thought (perhaps we should record it it into an issue)

Instead of hardcoding the pages URL like that (which won't if e.g. a custom domain name is used, as e.g. Documenter.jl does) one could also query the pages URL from the GitHub API. E.g. if the gh tool is installed:

$ gh api repos/JuliaDocs/Documenter.jl --jq '.homepage'
https://documenter.juliadocs.org

But of course one also do that without, using just the github_api:

curl -s https://api.github.com/repos/JuliaDocs/Documenter.jl | jq -r '.homepage'

Obviously in Julia we'd use the JSON module, not jq, to parse this data.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be great, however I can't find any public API that will tell me where github pages are located.
homepage is not reliable and can point somewhere else
There is private api /repos/{owner}/{repo}/pages but it requires authentication.

else
""
end

return GitHubActions(github_repository, github_event_name, github_ref, "github.com", github_api, github_pages_url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we hard-code github_host here, it feels like we should also hard-code the API URL? I also have a minor security concern here, where this could allow someone redirect the API calls somewhere (including the token) by somehow attacking the GITHUB_API_URL environment variable (for normal GitHub-hosted repos).

Suggested change
github_api = get(ENV, "GITHUB_API_URL", "") # https://api.github.com
# Compute GitHub Pages URL from repository
parts = split(github_repository, "/")
github_pages_url = if length(parts) == 2
owner, repo = parts
"https://$(owner).github.io/$(repo)/"
else
""
end
return GitHubActions(github_repository, github_event_name, github_ref, "github.com", github_api, github_pages_url)
# Compute GitHub Pages URL from repository
parts = split(github_repository, "/")
github_pages_url = if length(parts) == 2
owner, repo = parts
"https://$(owner).github.io/$(repo)/"
else
""
end
return GitHubActions(github_repository, github_event_name, github_ref, "github.com", "https://api.github.com", github_pages_url)

end

function GitHubActions(host, pages_url)
github_repository = get(ENV, "GITHUB_REPOSITORY", "") # "JuliaDocs/Documenter.jl"
github_event_name = get(ENV, "GITHUB_EVENT_NAME", "") # "push", "pull_request" or "cron" (?)
github_ref = get(ENV, "GITHUB_REF", "") # "refs/heads/$(branchname)" for branch, "refs/tags/$(tagname)" for tags
github_api = get(ENV, "GITHUB_API_URL", "") # https://api.github.com
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is GITHUB_API_URL something that is configurable for a self-hosted instance, or can we assume it's api.$(host)?

There's also GITHUB_SERVER_URL, which could potentially be used for determining host automatically? Or would that not be reliable and/or two automagical?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can assume anything about it, I don't know exactly if it is configurable but I would expect that it is as some orgs have strict rules about domain names

I need to check GITHUB_SERVER_URL , for some reason I haven't used that, don't know if it is because we don't have it or because I've missed it

if something like that exists I would prefer to used it indeed instead of specifying it manually

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that GITHUB_API_URL is mentioned in the GitHub manual as a variable they set in GitHub Action workflows.

And in the corresponding enterprise docs at https://docs.github.com/en/[email protected]/actions/reference/workflows-and-actions/variables the example value given is http(s)://HOSTNAME/api/v3 -- so no, we can't just assume it is api.$(host) (I assume that's the URL one gest if one enables "subdomain isolation")

return GitHubActions(github_repository, github_event_name, github_ref, host, github_api, pages_url)
end

# Check criteria for deployment
Expand Down Expand Up @@ -393,7 +416,7 @@ function deploy_folder(
all_ok &= pr_ok
println(io, "- $(marker(pr_ok)) ENV[\"GITHUB_REF\"] corresponds to a PR number")
if pr_ok
pr_origin_matches_repo = verify_github_pull_repository(cfg.github_repository, pr_number)
pr_origin_matches_repo = verify_github_pull_repository(cfg, pr_number)
all_ok &= pr_origin_matches_repo
println(io, "- $(marker(pr_origin_matches_repo)) PR originates from the same repository")
end
Expand Down Expand Up @@ -452,7 +475,7 @@ end

authentication_method(::GitHubActions) = env_nonempty("DOCUMENTER_KEY") ? SSH : HTTPS
function authenticated_repo_url(cfg::GitHubActions)
return "https://$(ENV["GITHUB_ACTOR"]):$(ENV["GITHUB_TOKEN"])@github.com/$(cfg.github_repository).git"
return "https://$(ENV["GITHUB_ACTOR"]):$(ENV["GITHUB_TOKEN"])@$(cfg.github_host)/$(cfg.github_repository).git"
end

function version_tag_strip_build(tag; tag_prefix = "")
Expand All @@ -469,7 +492,7 @@ function version_tag_strip_build(tag; tag_prefix = "")
return "$s0$s1$s2$s3$s4"
end

function post_status(::GitHubActions; type, repo::String, subfolder = nothing, kwargs...)
function post_status(cfg::GitHubActions; type, repo::String, subfolder = nothing, kwargs...)
try # make this non-fatal and silent
# If we got this far it usually means everything is in
# order so no need to check everything again.
Expand All @@ -489,17 +512,18 @@ function post_status(::GitHubActions; type, repo::String, subfolder = nothing, k
sha = get(ENV, "GITHUB_SHA", nothing)
end
sha === nothing && return
return post_github_status(type, repo, sha, subfolder)
return post_github_status(cfg, type, repo, sha, subfolder)
catch
@debug "Failed to post status"
end
end

function post_github_status(type::S, deploydocs_repo::S, sha::S, subfolder = nothing) where {S <: String}

function post_github_status(cfg::GitHubActions, type::S, deploydocs_repo::S, sha::S, subfolder = nothing) where {S <: String}
try
Sys.which("curl") === nothing && return
## Extract owner and repository name
m = match(r"^github.com\/(.+?)\/(.+?)(.git)?$", deploydocs_repo)
m = match(Regex("^(?:https?://)?$(cfg.github_host)\\/(.+?)\\/(.+?)(.git)?\$"), deploydocs_repo)
m === nothing && return
owner = String(m.captures[1])
repo = String(m.captures[2])
Expand All @@ -517,9 +541,9 @@ function post_github_status(type::S, deploydocs_repo::S, sha::S, subfolder = not
json["description"] = "Documentation build in progress"
elseif type == "success"
json["description"] = "Documentation build succeeded"
target_url = "https://$(owner).github.io/$(repo)/"
if subfolder !== nothing
target_url *= "$(subfolder)/"
target_url = cfg.github_pages_url
if !isempty(target_url) && subfolder !== nothing
target_url = rstrip(target_url, '/') * "/$(subfolder)/"
end
json["target_url"] = target_url
elseif type == "error"
Expand All @@ -530,7 +554,7 @@ function post_github_status(type::S, deploydocs_repo::S, sha::S, subfolder = not
error("unsupported type: $type")
end
push!(cmd.exec, "-d", JSON.json(json))
push!(cmd.exec, "https://api.github.com/repos/$(owner)/$(repo)/statuses/$(sha)")
push!(cmd.exec, "$(cfg.github_api)/repos/$(owner)/$(repo)/statuses/$(sha)")
# Run the command (silently)
io = IOBuffer()
res = run(pipeline(cmd; stdout = io, stderr = devnull))
Expand All @@ -541,7 +565,8 @@ function post_github_status(type::S, deploydocs_repo::S, sha::S, subfolder = not
return nothing
end

function verify_github_pull_repository(repo, prnr)
function verify_github_pull_repository(cfg::GitHubActions, prnr)
repo = cfg.github_repository
github_token = get(ENV, "GITHUB_TOKEN", nothing)
if github_token === nothing
@warn "GITHUB_TOKEN is missing, unable to verify if PR comes from destination repository -- assuming it doesn't."
Expand All @@ -552,7 +577,7 @@ function verify_github_pull_repository(repo, prnr)
push!(cmd.exec, "-H", "Authorization: token $(github_token)")
push!(cmd.exec, "-H", "User-Agent: Documenter.jl")
push!(cmd.exec, "--fail")
push!(cmd.exec, "https://api.github.com/repos/$(repo)/pulls/$(prnr)")
push!(cmd.exec, "$(cfg.github_api)/repos/$(repo)/pulls/$(prnr)")
try
# Run the command (silently)
response = run_and_capture(cmd)
Expand Down
12 changes: 9 additions & 3 deletions src/utilities/Remotes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ function repofile(remote::Remote, ref, filename, linerange = nothing)
return fileurl(remote, ref, filename, isnothing(linerange) ? nothing : Int(first(linerange)):Int(last(linerange)))
end


const GITHUB_HOST = "github.com"

"""
GitHub(user :: AbstractString, repo :: AbstractString)
GitHub(remote :: AbstractString)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the liberty of removing host for the single-argument method. I think it would be better if it stays single-argument. If we want to add support for overriding the host, then let's figure out some syntax for the string, like github.selfhosted:Org/Repo.jl, which we can regex.

Expand All @@ -121,12 +124,15 @@ a slash (e.g. `JuliaDocs/Documenter.jl`).
struct GitHub <: Remote
user::String
repo::String
host::String

GitHub(user::AbstractString, repo::AbstractString, host::AbstractString = GITHUB_HOST) = new(user, repo, host)
end
function GitHub(remote::AbstractString)
function GitHub(remote::AbstractString; host::AbstractString = GITHUB_HOST)
user, repo = split(remote, '/')
return GitHub(user, repo)
return GitHub(user, repo, host)
end
repourl(remote::GitHub) = "https://github.com/$(remote.user)/$(remote.repo)"
repourl(remote::GitHub) = "https://$(remote.host)/$(remote.user)/$(remote.repo)"
function fileurl(remote::GitHub, ref::AbstractString, filename::AbstractString, linerange)
url = "$(repourl(remote))/blob/$(ref)/$(filename)"
isnothing(linerange) && return url
Expand Down
24 changes: 23 additions & 1 deletion test/deployconfig.jl
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,28 @@ end
)
@test !d.all_ok
end
# Self-hosted GitHub installation
# Regular tag build with GITHUB_TOKEN
withenv(
"GITHUB_EVENT_NAME" => "push",
"GITHUB_REPOSITORY" => "JuliaDocs/Documenter.jl",
"GITHUB_REF" => "refs/tags/v1.2.3",
"GITHUB_ACTOR" => "github-actions",
"GITHUB_TOKEN" => "SGVsbG8sIHdvcmxkLg==",
"DOCUMENTER_KEY" => nothing,
) do
cfg = Documenter.GitHubActions("github.selfhosted", "pages.selfhosted/something/JuliaDocs/Documenter.jl")
d = Documenter.deploy_folder(
cfg; repo = "github.selfhosted/JuliaDocs/Documenter.jl.git",
devbranch = "master", devurl = "dev", push_preview = true
)
@test d.all_ok
@test d.subfolder == "v1.2.3"
@test d.repo == "github.selfhosted/JuliaDocs/Documenter.jl.git"
@test d.branch == "gh-pages"
@test Documenter.authentication_method(cfg) === Documenter.HTTPS
@test Documenter.authenticated_repo_url(cfg) === "https://github-actions:[email protected]/JuliaDocs/Documenter.jl.git"
end
end
end

Expand Down Expand Up @@ -1396,7 +1418,7 @@ end
"CI" => "woodpecker",
"GITHUB_REPOSITORY" => nothing
) do
@test_throws KeyError cfg = Documenter.auto_detect_deploy_system()
@test_throws KeyError cfg = Documenter.auto_detect_deploy_system()
end
# Drone compatibility ends post-1.0.0
withenv(
Expand Down
Loading