Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Support self-hosted GitHub instances. ([#2755])

Comment on lines +10 to +11
Copy link
Collaborator

@fingolfin fingolfin Nov 20, 2025

Choose a reason for hiding this comment

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

This now needs to be moved up (it currently is in the 1.16.0 section but that version has already been released)

* Added option `treat_markdown_warnings_as_error` which throws an error when encountering a markdown/interpolation warning ([#2792], [#2751])
* Footnotes can now be previewed by hovering over the link. ([#2080])
* The version selector now attempts to stay on the same page when switching between documentation versions. If the page doesn't exist in the target version, it falls back to the version homepage. ([#2801])
Expand Down Expand Up @@ -2196,6 +2198,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#2751]: https://github.com/JuliaDocs/Documenter.jl/issues/2751
[#2752]: https://github.com/JuliaDocs/Documenter.jl/issues/2752
[#2753]: https://github.com/JuliaDocs/Documenter.jl/issues/2753
[#2755]: https://github.com/JuliaDocs/Documenter.jl/issues/2755
[#2761]: https://github.com/JuliaDocs/Documenter.jl/issues/2761
[#2762]: https://github.com/JuliaDocs/Documenter.jl/issues/2762
[#2772]: https://github.com/JuliaDocs/Documenter.jl/issues/2772
Expand Down
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ RegistryInstances = "2792f1a3-b283-48e8-9a74-f99dce5104f3"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[compat]
Expand All @@ -50,6 +51,7 @@ RegistryInstances = "0.1"
SHA = "0.7, 1"
TOML = "1"
Test = "1.6"
URIs = "1.6.1"
UUIDs = "1.6"
Unicode = "1.6"
julia = "1.6"
Expand Down
2 changes: 2 additions & 0 deletions docs/src/lib/internals/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ Pages = ["utilities/utilities.jl"]
```@docs
Remotes.URL
Remotes.repofile
Remotes.github_host
Remotes.parse_url
```
89 changes: 67 additions & 22 deletions src/deployconfig.jl
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ end

Implementation of `DeployConfig` for deploying from GitHub Actions.

For self-hosted GitHub installation use the `GitHubActions(pages_url)` constructor
to specify the URL to the GitHub pages location.

The following environment variables influences the build
when using the `GitHubActions` configuration:

Expand All @@ -311,19 +314,61 @@ when using the `GitHubActions` configuration:
- `GITHUB_TOKEN` or `DOCUMENTER_KEY`: used for authentication with GitHub,
see the manual section for [GitHub Actions](@ref) for more information.

- `GITHUB_API_URL`: specifies the GitHub API URL, which generally is `https://api.github.com`,
but may be different for self-hosted GitHub instances.

- `GITHUB_ACTOR`: name of the person or app that initiated the workflow. For
example, `octocat`. This is used to construct API calls.

The `GITHUB_*` variables are set automatically on GitHub Actions, see the
[documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables).
"""
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()
# Try to deduce GitHub Pages URL using GitHub API
github_repository = get(ENV, "GITHUB_REPOSITORY", "") # "JuliaDocs/Documenter.jl"
github_api = get(ENV, "GITHUB_API_URL", "") # https://api.github.com
github_token = get(ENV, "GITHUB_TOKEN", "")

try
# Construct the curl call
cmd = `curl -s`
push!(cmd.exec, "-H", "Authorization: token $(github_token)")
push!(cmd.exec, "-H", "User-Agent: Documenter.jl")
push!(cmd.exec, "--fail")
push!(cmd.exec, "$(github_api)/repos/$(github_repository)/pages")

# Run the command (silently)
response = run_and_capture(cmd)
response = JSON.parse(response.stdout)

return GitHubActions(response["html_url"])
catch
@warn "Unable to deduce GitHub Pages URL using GitHub API; falling back guess from the repository name."
parts = split(github_repository, "/")
if length(parts) == 2
owner, repo = parts
return GitHubActions("https://$(owner).github.io/$(repo)/")
else
return GitHubActions("")
end
end
end

function GitHubActions(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
return GitHubActions(github_repository, github_event_name, github_ref)
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, Remotes.github_host(), github_api, pages_url)
end

# Check criteria for deployment
Expand Down Expand Up @@ -393,7 +438,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 +497,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 +514,7 @@ function version_tag_strip_build(tag; tag_prefix = "")
return "$s0$s1$s2$s3$s4"
end

function post_status(gha::GitHubActions; type, repo::String, subfolder = nothing, kwargs...)
function post_status(gha::GitHubActions; type, 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,21 +534,20 @@ function post_status(gha::GitHubActions; type, repo::String, subfolder = nothing
sha = get(ENV, "GITHUB_SHA", nothing)
end
sha === nothing && return
return post_github_status(type, gha.github_repository, repo, sha, subfolder)
catch
@debug "Failed to post status"
return post_github_status(gha, type, sha, subfolder)
catch e
@debug "Failed to post status" exception = e
end
end

function post_github_status(type::S, source::S, deploydocs_repo::S, sha::S, subfolder = nothing) where {S <: String}
function post_github_status(gha::GitHubActions, type::S, sha::S, subfolder = nothing) where {S <: String}
try
Sys.which("curl") === nothing && return
if Sys.which("curl") === nothing
@warn "curl not found in PATH, cannot post status"
return
end
## Extract owner and repository names
source_owner, source_repo = split(source, '/')
m = match(r"^github.com\/(.+?)\/(.+?)(.git)?$", deploydocs_repo)
m === nothing && return
deploy_owner = String(m.captures[1])
deploy_repo = String(m.captures[2])
source_owner, source_repo = split(gha.github_repository, '/')

## Need an access token for this
auth = get(ENV, "GITHUB_TOKEN", nothing)
Expand All @@ -518,9 +562,9 @@ function post_github_status(type::S, source::S, deploydocs_repo::S, sha::S, subf
json["description"] = "Documentation build in progress"
elseif type == "success"
json["description"] = "Documentation build succeeded"
target_url = "https://$(deploy_owner).github.io/$(deploy_repo)/"
if subfolder !== nothing
target_url *= "$(subfolder)/"
target_url = gha.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 @@ -531,18 +575,19 @@ function post_github_status(type::S, source::S, deploydocs_repo::S, sha::S, subf
error("unsupported type: $type")
end
push!(cmd.exec, "-d", JSON.json(json))
push!(cmd.exec, "https://api.github.com/repos/$(source_owner)/$(source_repo)/statuses/$(sha)")
push!(cmd.exec, "$(gha.github_api)/repos/$(source_owner)/$(source_repo)/statuses/$(sha)")
# Run the command (silently)
io = IOBuffer()
res = run(pipeline(cmd; stdout = io, stderr = devnull))
@debug "Response of curl POST request" response = String(take!(io))
catch
@debug "Failed to post status"
catch e
@debug "Failed to post status" exception = e
end
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 @@ -553,7 +598,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
28 changes: 9 additions & 19 deletions src/deploydocs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -194,28 +194,18 @@ GitHub.
function deploydocs(;
root = currentdir(),
target = "build",
dirname = "",

repo = error("no 'repo' keyword provided."),
dirname = "", repo = error("no 'repo' keyword provided."),
branch = "gh-pages",
deploy_repo = nothing,

repo_previews = nothing,
branch_previews = branch,

deps = nothing,
make = nothing,

cname = nothing,
deploy_repo = nothing, repo_previews = nothing,
branch_previews = branch, deps = nothing,
make = nothing, cname = nothing,
devbranch = nothing,
devurl = "dev",
versions = ["stable" => "v^", "v#.#", devurl => devurl],
forcepush::Bool = false,
deploy_config = auto_detect_deploy_system(),
push_preview::Bool = false,
tag_prefix = "",

archive = nothing, # experimental and undocumented
tag_prefix = "", archive = nothing, # experimental and undocumented
)

# Try to figure out default branch (see #1443 and #1727)
Expand Down Expand Up @@ -493,10 +483,10 @@ function git_push(
cd(() -> git_commands(sshconfig), temp)
end
end
post_status(deploy_config; repo = repo, type = "success", subfolder = subfolder)
post_status(deploy_config; type = "success", subfolder = subfolder)
catch e
@error "Failed to push:" exception = (e, catch_backtrace())
post_status(deploy_config; repo = repo, type = "error")
post_status(deploy_config; type = "error")
rethrow(e)
finally
# Remove the unencrypted private key.
Expand All @@ -507,10 +497,10 @@ function git_push(
upstream = authenticated_repo_url(deploy_config)
try
cd(() -> withenv(git_commands, NO_KEY_ENV...), temp)
post_status(deploy_config; repo = repo, type = "success", subfolder = subfolder)
post_status(deploy_config; type = "success", subfolder = subfolder)
catch e
@error "Failed to push:" exception = (e, catch_backtrace())
post_status(deploy_config; repo = repo, type = "error")
post_status(deploy_config; type = "error")
rethrow(e)
end
end
Expand Down
58 changes: 54 additions & 4 deletions src/utilities/Remotes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Types and functions for handling repository remotes.
"""
module Remotes

using URIs: URI
"""
abstract type Remote

Expand Down Expand Up @@ -103,7 +104,44 @@ function repofile(remote::Remote, ref, filename, linerange = nothing)
end

"""
GitHub(user :: AbstractString, repo :: AbstractString)
parse_url(url)

Return tuple with values `authority` and `path` fragments of the given URL string.
This function is not strictly following URI specification as it will parse "github.com/user/repo" into `("github.com", "/user/repo")`
returning authority even if scheme is missing.
"""
function parse_url(url)::Tuple{String, String}
if contains(url, "://") == false
url = "https://$(url)"
end
u = URI(url)

authority = u.host
if u.userinfo != ""
authority = "$(u.userinfo)@$(authority)"
end

if u.port != ""
authority = "$(authority):$(u.port)"
end
return (authority, u.path)
end

"""
github_host()

Returns hostname of the GitHub installation where this code is running on at the moment.
This is derived from the `ENV[GITHUB_SERVER_URL]` variable which is set in every GitHub Actions workflow.
If this variable is not set, return "github.com".
"""
function github_host()
haskey(ENV, "GITHUB_SERVER_URL") || return "github.com"
url = ENV["GITHUB_SERVER_URL"]
return parse_url(url)[1]
end

"""
GitHub(user :: AbstractString, repo :: AbstractString, [host :: 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.


Represents a remote Git repository hosted on GitHub. The repository is identified by the
Expand All @@ -117,16 +155,28 @@ makedocs(

The single-argument constructor assumes that the user and repository parts are separated by
a slash (e.g. `JuliaDocs/Documenter.jl`).

A `host` can be provided to point to the location of the self-hosted GitHub installation.
"""
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)
user, repo = split(remote, '/')
return GitHub(user, repo)
url_authority, url_path = parse_url(remote)
path = url_path[1] == '/' ? url_path[2:end] : url_path

if occursin("/", path)
user, repo = split(path, "/")
return GitHub(user, repo, url_authority)
else
return GitHub(url_authority, path)
end
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
Loading
Loading