diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 924175b..5ba2b8c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,6 +5,14 @@ on: - master tags: '*' pull_request: + workflow_dispatch: +# fixed CPU target, so cached precompile files load on every runner type +env: + JULIA_CPU_TARGET: x86-64-v3 +concurrency: + # Skip intermediate builds: always. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true # always cancel old runs on new push jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} @@ -31,17 +39,26 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 + # coverage only on the release version, one codecov upload is enough - uses: julia-actions/julia-runtest@v1 + with: + coverage: ${{ matrix.version == '1' }} - uses: julia-actions/julia-processcoverage@v1 + if: matrix.version == '1' - uses: codecov/codecov-action@v7 + if: matrix.version == '1' with: - file: lcov.info + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false docs: name: Documentation runs-on: ubuntu-latest - permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created - actions: write - contents: read + permissions: + actions: write # needed to allow julia-actions/cache to proactively delete old caches that it has created + contents: write + pull-requests: read + statuses: write steps: - uses: actions/checkout@v7 - uses: julia-actions/setup-julia@v3 @@ -49,6 +66,8 @@ jobs: version: '1' - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 + with: + project: docs - uses: julia-actions/julia-docdeploy@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cleanup_stale_docpreview.yml b/.github/workflows/cleanup_stale_docpreview.yml new file mode 100644 index 0000000..60d0e46 --- /dev/null +++ b/.github/workflows/cleanup_stale_docpreview.yml @@ -0,0 +1,87 @@ +name: Cleanup stale doc Previews + +on: + schedule: + - cron: '0 2 * * *' # Run daily at 2 AM UTC + workflow_dispatch: # Allow manual trigger + +concurrency: + group: doc-preview-cleanup + cancel-in-progress: false + +jobs: + cleanup-closed-pr-previews: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v7 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Clean up closed PR previews + run: | + echo "Starting preview cleanup..." + + deleted_previews="" + + # Find all preview directories + if [ -d "previews" ]; then + shopt -s nullglob + for preview_dir in previews/PR*; do + if [ -d "$preview_dir" ]; then + # Extract PR number + pr_number=$(echo "$preview_dir" | sed 's/previews\/PR//') + + # Validate PR number is numeric + if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then + echo "Skipping invalid directory: $preview_dir" + continue + fi + + # Check PR state via GitHub API + api_response=$(curl -s -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/$pr_number") + + http_code="${api_response: -3}" + json_response="${api_response%???}" + + if [ "$http_code" != "200" ] && [ "$http_code" != "404" ]; then + echo "API error for PR #$pr_number: HTTP $http_code" + echo "Response: $json_response" + exit 1 + fi + + pr_state=$(echo "$json_response" | jq -r '.state // "not_found"') + + if [ "$pr_state" = "closed" ] || [ "$pr_state" = "not_found" ]; then + echo "Delete $preview_dir (PR #$pr_number is $pr_state)" + deleted_previews="$deleted_previews $preview_dir" + + git rm -rf "$preview_dir" + else + echo "Keeping $preview_dir (PR #$pr_number is open)" + fi + + sleep 1 + fi + done + shopt -u nullglob + fi + + # Commit and clean history if any previews were deleted + if [ -n "$deleted_previews" ]; then + echo "Clean up history for:$deleted_previews" + + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git commit -m "delete previews" + git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) + echo "Force push changes to upstream repo" + git push --force origin gh-pages-new:gh-pages + else + echo "No previews to clean up" + fi diff --git a/.github/workflows/docpreviewcleanup.yml b/.github/workflows/docpreviewcleanup.yml deleted file mode 100644 index 9e8042d..0000000 --- a/.github/workflows/docpreviewcleanup.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Doc Preview Cleanup - -on: - pull_request: - types: [closed] - -# Ensure that only one "Doc Preview Cleanup" workflow is force pushing at a time -concurrency: - group: doc-preview-cleanup - cancel-in-progress: false - -jobs: - doc-preview-cleanup: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout gh-pages branch - uses: actions/checkout@v7 - with: - ref: gh-pages - - name: Delete preview and history + push changes - run: | - if [ -d "${preview_dir}" ]; then - git config user.name "Documenter.jl" - git config user.email "documenter@juliadocs.github.io" - git rm -rf "${preview_dir}" - git commit -m "delete preview" - git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) - git push --force origin gh-pages-new:gh-pages - fi - env: - preview_dir: previews/PR${{ github.event.number }} diff --git a/.gitignore b/.gitignore index 99f4d44..d55b8b4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.jl.mem Manifest.toml /docs/build/ +/docs/build_draft/ diff --git a/Project.toml b/Project.toml index b2ee26e..ce42c95 100644 --- a/Project.toml +++ b/Project.toml @@ -2,6 +2,9 @@ name = "NetworkLayout" uuid = "46757867-2c16-5918-afeb-47bfcb05e46a" version = "0.4.10" +[workspace] +projects = ["docs"] + [deps] GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" @@ -16,11 +19,16 @@ Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" NetworkLayoutGraphsExt = "Graphs" [compat] +DelimitedFiles = "1" GeometryBasics = "0.5" Graphs = "1" +LinearAlgebra = "1" +Random = "1" Requires = "1" +SparseArrays = "1" StableRNGs = "1.0.2" StaticArrays = "1" +Test = "1" julia = "1.6" [extras] diff --git a/docs/Project.toml b/docs/Project.toml index fdc03a2..9d318ba 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -7,10 +7,14 @@ NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +[sources] +NetworkLayout = {path = ".."} + [compat] CairoMakie = "0.15" Documenter = "1" GraphMakie = "0.6" Graphs = "1.9" NetworkLayout = "0.4" +Random = "1" StableRNGs = "1.0.1" diff --git a/docs/localmake.jl b/docs/localmake.jl index 736026d..be5a87d 100755 --- a/docs/localmake.jl +++ b/docs/localmake.jl @@ -1,24 +1,104 @@ #! julia --startup-file=no -using Pkg -Pkg.activate(@__DIR__) -Pkg.develop(PackageSpec(path=dirname(@__DIR__))) # adds the package this script is called from -Pkg.update() -Pkg.instantiate() +#= +julia localmake.jl [--draft] -using LiveServer -@async serve(dir=joinpath(@__DIR__, "build")) - -run = true -while run - try - include("make.jl") - catch e - @info "make.jl error" e +Runs the `make.jl` script and serves the docs on `localhost:8000`. +You have to have `Revise` and `LiveServer` in your global environment for this script to work. + +Two modes available: +- Full mode (default): Builds complete docs with examples, prompts to rebuild after each run +- Draft mode (--draft): Uses Documenter's draft mode with automatic rerendering, skips examples + +The script will prompt whether to update the docs environment (10 second timeout, defaults to No). +At the end of each full build run, the user is prompted to rerun the make process. Using Revise +this will use updated `*.md` and source files, keeping the Julia session alive for faster builds. +=# + +BUILD_DIR = joinpath(@__DIR__, "build") +mkpath(BUILD_DIR) # make sure path exists, otherwise the serve might fail + +function readline_timeout(prompt, default, timeout) + msg = Channel{String}(1) + task = Task() do + try eof(stdin); put!(msg, readline(stdin)); catch end + end + interrupter = Task() do + sleep(timeout) + istaskdone(task) || Base.throwto(task, InterruptException()) end + print(prompt) + schedule(interrupter) + schedule(task) + wait(task) + answer = if isempty(msg) + println() # close line + default + else + str = take!(msg) + isempty(str) ? default : str + end + close(msg) + return answer +end - println("Run again? Enter! Exit with 'q'.") - if readline() == "q" - global run = false +function full_serving() + @info "Start server..." + port=8000 + servetask = @async serve(;dir=BUILD_DIR, port) + errormonitor(servetask) + + run = true + while run + revise() + @info "Start building docs..." + try + include("make.jl") + catch e + @error "make.jl error" exception=(e, catch_backtrace()) + end + + printstyled("\n\nDocs are served at http://localhost:$port\n\n", color=:blue, bold=true) + println("Run again? Enter! Exit with 'q'.") + if readline() == "q" + run = false + end end end + +function draft_serving() + ENV["DOCUMENTER_DRAFT"] = "true" + servedocs( + foldername=".", + buildfoldername="build_draft", + port=8001, + ) +end + +do_update = readline_timeout("Do you want to update docs environment? [y/N] ", "N", 10) + +#### +#### Set up environment +#### +using Pkg +Pkg.activate(@__DIR__) +using Revise +using LiveServer + +if VERSION ≤ v"1.11-" + Pkg.develop(PackageSpec(path=dirname(@__DIR__))) # adds the package this script is called from +end + +if do_update[1] == 'y' + Pkg.update() +end +Pkg.instantiate() + +draft_arg = findfirst(a -> a=="--draft", ARGS) +if !isnothing(draft_arg) + @info "Found --draft, run in draft mode (no examples, automatic rerendering!)" + draft_serving() +else + @info "Start full build of docs including all examples" + full_serving() +end diff --git a/docs/make.jl b/docs/make.jl index ba9db70..1f8e5e9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -8,16 +8,27 @@ using StableRNGs NetworkLayout.DEFAULT_RNG[] = StableRNG DocMeta.setdocmeta!(NetworkLayout, :DocTestSetup, :(using NetworkLayout); recursive=true) -makedocs(; modules=[NetworkLayout], - repo=Remotes.GitHub("JuliaGraphs", "NetworkLayout.jl"), - sitename="NetworkLayout.jl", - format=Documenter.HTML(; prettyurls=get(ENV, "CI", "false") == "true", - canonical="https://juliagraphs.org/NetworkLayout.jl", assets=String[]), - pages=["Home" => "index.md", - "Interface" => "interface.md"], - warnonly=[:missing_docs]) +doc = makedocs(; modules=[NetworkLayout], + repo=Remotes.GitHub("JuliaGraphs", "NetworkLayout.jl"), + sitename="NetworkLayout.jl", + build=haskey(ENV, "DOCUMENTER_DRAFT") ? "build_draft" : "build", + format=Documenter.HTML(; prettyurls=get(ENV, "CI", "false") == "true", + canonical="https://juliagraphs.org/NetworkLayout.jl", assets=String[]), + pages=["Home" => "index.md", + "Interface" => "interface.md"], + draft=haskey(ENV, "DOCUMENTER_DRAFT"), + warnonly=true, + debug=true) # return doc object # if gh_pages branch gets to big, check out # https://juliadocs.github.io/Documenter.jl/stable/man/hosting/#gh-pages-Branch -deploydocs(;repo="github.com/JuliaGraphs/NetworkLayout.jl", - push_preview=true) + +# deploy even if makedocs had errors, so the PR preview is available, but fail CI afterwards +if haskey(ENV, "GITHUB_ACTIONS") + deploydocs(; repo="github.com/JuliaGraphs/NetworkLayout.jl", + push_preview=true) + errors = setdiff(doc.internal.errors, [:missing_docs]) + if !isempty(errors) + error("makedocs encountered errors: $(errors)") + end +end