From 22e79538855620019d14dfe629efb6a1a71be15c Mon Sep 17 00:00:00 2001 From: Joey Carpinelli Date: Sat, 12 Oct 2024 23:24:19 -0400 Subject: [PATCH] Minimum viable demonstration --- .JuliaFormatter.toml | 2 + .github/workflows/CI.yml | 37 ++++ .github/workflows/CompatHelper.yml | 44 +++++ .github/workflows/Documentation.yml | 32 +++ .github/workflows/Register.yml | 16 ++ .github/workflows/TagBot.yml | 28 +++ .gitignore | 33 ++++ LICENSE | 21 ++ Manifest.toml | 289 ---------------------------- Project.toml | 13 +- README.md | 43 +++++ docs/Project.toml | 6 + docs/make.jl | 6 + docs/src/_quarto.yml | 29 +++ docs/src/api/index.qmd | 20 ++ docs/src/index.md | 5 + docs/src/references.bib | 9 + src/ExperimentalTagbot.jl | 279 ++++++++++++++++++++++++--- test/runtests.jl | 4 + 19 files changed, 603 insertions(+), 313 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 .github/workflows/CI.yml create mode 100644 .github/workflows/CompatHelper.yml create mode 100644 .github/workflows/Documentation.yml create mode 100644 .github/workflows/Register.yml create mode 100644 .github/workflows/TagBot.yml create mode 100644 .gitignore create mode 100644 LICENSE delete mode 100644 Manifest.toml create mode 100644 README.md create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/_quarto.yml create mode 100644 docs/src/api/index.qmd create mode 100644 docs/src/index.md create mode 100644 docs/src/references.bib create mode 100644 test/runtests.jl diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..3feb4a1 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,2 @@ +style = "sciml" +remove_extra_newlines = true \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..9ca923d --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,37 @@ +name: CI +on: + push: + branches: + - main +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + version: + - "1" + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ + hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-runtest@latest diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 0000000..06d4648 --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,44 @@ +name: CompatHelper +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: +permissions: + contents: write + pull-requests: write +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: Check if Julia is already available in the PATH + id: julia_in_path + run: which julia + continue-on-error: true + - name: Install Julia, but only if it is not already available in the PATH + uses: julia-actions/setup-julia@v2 + with: + version: "1" + arch: ${{ runner.arch }} + if: steps.julia_in_path.outcome != 'success' + - name: "Add the General registry via Git" + run: | + import Pkg + ENV["JULIA_PKG_SERVER"] = "" + Pkg.Registry.add("General") + shell: julia --color=yes {0} + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "3" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} + - name: "Run CompatHelper" + run: | + import CompatHelper + CompatHelper.main() + shell: julia --color=yes {0} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml new file mode 100644 index 0000000..96f01db --- /dev/null +++ b/.github/workflows/Documentation.yml @@ -0,0 +1,32 @@ +name: Documentation + +on: + push: + branches: + - main # update to match your development branch (master, main, dev, trunk, ...) + tags: "*" + pull_request: + +jobs: + build: + permissions: + actions: write + contents: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: quarto-dev/quarto-actions/setup@v2 + - name: Install Jupyter + run: python -m pip install jupyter + - uses: julia-actions/cache@v2 + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.build()' + - name: Build and deploy + env: + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key + run: julia --project=docs/ docs/make.jl diff --git a/.github/workflows/Register.yml b/.github/workflows/Register.yml new file mode 100644 index 0000000..97bf97a --- /dev/null +++ b/.github/workflows/Register.yml @@ -0,0 +1,16 @@ +name: Register + +on: + workflow_dispatch: + +jobs: + release: + name: Register Package + runs-on: ubuntu-latest + steps: + - uses: peter-evans/commit-comment@v3 + with: + body: | + @JuliaRegistrator register" + + This comment was generated with `commit-comment` and [`Register.yml`](/.github/workflows/Register.yml). diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..85ad9c4 --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,28 @@ +name: TagBot + +on: + schedule: + - cron: 0 12 * * * + workflow_dispatch: + +jobs: + build: + permissions: + actions: write + contents: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + - name: Install dependencies + run: julia -e 'using Pkg; Pkg.add(url="https://github.com/cadojo/ExperimentalTagBot.jl")' + - name: Build and deploy + env: + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key + PACKAGE: ${{ github.event.repository.name }} + run: julia -e 'import GitHub as GH; auth = GH.authenticate(readchomp(`gh auth token`)); import ExperimentalTagBot; ExperimentalTagBot.create_releases(replace(ENV["PACKAGE"], ".jl"=>""); auth = auth)' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..091c3d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Files generated by invoking Julia with --code-coverage +*.jl.cov +*.jl.*.cov + +# Files generated by invoking Julia with --track-allocation +*.jl.mem + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ + +# File generated by Pkg, the package manager, based on a corresponding Project.toml +# It records a fixed state of all packages used by the project. As such, it should not be +# committed for packages, but should be committed for applications that require a static +# environment. +Manifest.toml + +# MacOs file explorer artifacts should be ignored! +.DS_Store + +# As should .vscode! +.vscode + +# Python virtual environments +venv diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f11a03b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Joseph D. Carpinelli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Manifest.toml b/Manifest.toml deleted file mode 100644 index 3322b8e..0000000 --- a/Manifest.toml +++ /dev/null @@ -1,289 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -julia_version = "1.10.5" -manifest_format = "2.0" -project_hash = "6bc853643c26c20a70dce1b89cff43283fc97c5e" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.1" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[deps.BitFlags]] -git-tree-sha1 = "0691e34b3bb8be9307330f88d1a3c3f25466c24d" -uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" -version = "0.1.9" - -[[deps.CodecZlib]] -deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "bce6804e5e6044c6daab27bb533d1295e4a2e759" -uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.6" - -[[deps.ConcurrentUtilities]] -deps = ["Serialization", "Sockets"] -git-tree-sha1 = "ea32b83ca4fefa1768dc84e504cc0a94fb1ab8d1" -uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" -version = "2.4.2" - -[[deps.Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" - -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" - -[[deps.ExceptionUnwrapping]] -deps = ["Test"] -git-tree-sha1 = "dcb08a0d93ec0b1cdc4af184b26b591e9695423a" -uuid = "460bff9d-24e4-43bc-9d9f-a8973cb893f4" -version = "0.1.10" - -[[deps.Expat_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "1c6317308b9dc757616f0b5cb379db10494443a7" -uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.6.2+0" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" - -[[deps.Git]] -deps = ["Git_jll"] -git-tree-sha1 = "04eff47b1354d702c3a85e8ab23d539bb7d5957e" -uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" -version = "1.3.1" - -[[deps.GitHub]] -deps = ["Base64", "Dates", "HTTP", "JSON", "MbedTLS", "Sockets", "SodiumSeal", "URIs"] -git-tree-sha1 = "7ee730a8484d673a8ce21d8536acfe6494475994" -uuid = "bc5e4493-9b4d-5f90-b8aa-2b2bcaad7a26" -version = "5.9.0" - -[[deps.Git_jll]] -deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "ea372033d09e4552a04fd38361cd019f9003f4f4" -uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" -version = "2.46.2+0" - -[[deps.HTTP]] -deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] -git-tree-sha1 = "d1d712be3164d61d1fb98e7ce9bcbc6cc06b45ed" -uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "1.10.8" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[deps.JLLWrappers]] -deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "be3dc50a92e5a386872a493a10050136d4703f9b" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.6.1" - -[[deps.JSON]] -deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" -uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.4" - -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.4" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.4.0+0" - -[[deps.LibGit2]] -deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" - -[[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] -uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.6.4+0" - -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.0+1" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" - -[[deps.Libiconv_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "f9557a255370125b405568f9767d6d195822a175" -uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" -version = "1.17.0+0" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[deps.LoggingExtras]] -deps = ["Dates", "Logging"] -git-tree-sha1 = "c1dd6d7978c12545b4179fb6153b9250c96b0075" -uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" -version = "1.0.3" - -[[deps.Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[deps.MbedTLS]] -deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] -git-tree-sha1 = "c067a280ddc25f196b5e7df3877c6b226d390aaf" -uuid = "739be429-bea8-5141-9913-cc70e7f3736d" -version = "1.1.9" - -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.2+1" - -[[deps.Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" - -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.1.10" - -[[deps.NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" - -[[deps.OpenSSL]] -deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] -git-tree-sha1 = "38cb508d080d21dc1128f7fb04f20387ed4c0af4" -uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" -version = "1.4.3" - -[[deps.OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "7493f61f55a6cce7325f197443aa80d32554ba10" -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.15+1" - -[[deps.PCRE2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" -version = "10.42.0+1" - -[[deps.Parsers]] -deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.1" - -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.10.0" - -[[deps.PrecompileTools]] -deps = ["Preferences"] -git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" -uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.2.1" - -[[deps.Preferences]] -deps = ["TOML"] -git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.4.3" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" - -[[deps.Random]] -deps = ["SHA"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[deps.SimpleBufferStream]] -git-tree-sha1 = "f305871d2f381d21527c770d4788c06c097c9bc1" -uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" -version = "1.2.0" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[deps.SodiumSeal]] -deps = ["Base64", "Libdl", "libsodium_jll"] -git-tree-sha1 = "80cef67d2953e33935b41c6ab0a178b9987b1c99" -uuid = "2133526b-2bfb-4018-ac12-889fb3908a75" -version = "0.1.1" - -[[deps.TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.3" - -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" - -[[deps.Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[deps.TranscodingStreams]] -git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" -uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.11.3" - -[[deps.URIs]] -git-tree-sha1 = "67db6cc7b3821e19ebe75791a9dd19c9b1188f2b" -uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -version = "1.5.1" - -[[deps.UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" - -[[deps.Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.13+1" - -[[deps.libsodium_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "f76d682d87eefadd3f165d8d9fda436464213142" -uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8" -version = "1.0.20+1" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.52.0+1" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+2" diff --git a/Project.toml b/Project.toml index 7567120..493e2a7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,12 +1,23 @@ -name = "ExperimentalTagbot" +name = "ExperimentalTagBot" uuid = "930da1f0-8ee6-4872-b248-bb5528d4fa7c" authors = ["Joey Carpinelli "] version = "0.1.0" [deps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" GitHub = "bc5e4493-9b4d-5f90-b8aa-2b2bcaad7a26" +IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [compat] +Dates = "1.11.0" GitHub = "5.9.0" +IOCapture = "0.2.5" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..61fe4c9 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# 🏷️🤖 Automated Package Releases in Julia + +_A minimum-viable [TagBot](https://github.com/JuliaRegistries/TagBot) implementation in Julia!_ + +**This project is being actively developed!** It works in local testing, and will be made more robust in 2025. + +## Installation + +_Choose one of the following lines below._ + +```julia +julia> import Pkg; Pkg.add("https://github.com/cadojo/ExperimentalTagBot.jl") +``` + +```julia +pkg> add https://github.com/cadojo/ExperimentalTagBot.jl +``` + +## Usage + +_A minimum working example._ + +This package can use the GitHub API to query Julia's [General Registry](https://github.com/JuliaRegistries/General) for a package version's PR, and then create a corresponding GitHub release. +To do this, you'll need to authenticate with the GitHub API. +The simplest way is to install the [GitHub CLI](https://cli.github.com) and authenticate with the following command: `gh auth login`. +The GitHub CLI prompts will guide you through the one-time authentication process. +Once you've authenticated the CLI, you can execute the code below to authenticate with the API. + +```julia +import GitHub +auth = GitHub.authenticate(readchomp(`gh auth token`)) +``` + +With this authentication, you can use `ExperimentalTagBot` to query Git commits for each registered package version, find registered versions without tags, and create tags for all un-tagged registered package versions. + +```julia +import ExperimentalTagBot + +package = "" # some package you own +ExperimentalTagBot.create_releases(package; auth = auth) +``` + + diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..0ca0b09 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,6 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterQuarto = "73f83fcb-c367-40db-89b6-8fd94701aaf2" +ExperimentalTagBot = "930da1f0-8ee6-4872-b248-bb5528d4fa7c" +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" +Quarto = "d7167be5-f61b-4dc9-b75c-ab62374668c5" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..98c3772 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,6 @@ +using Documenter +using Quarto + +Quarto.render(joinpath(@__DIR__, "src")) + +Documenter.deploydocs(repo = "github.com/cadojo/ExperimentalTagbot.jl") diff --git a/docs/src/_quarto.yml b/docs/src/_quarto.yml new file mode 100644 index 0000000..59db1a0 --- /dev/null +++ b/docs/src/_quarto.yml @@ -0,0 +1,29 @@ +project: + type: book + output-dir: ../build + +book: + title: "ExperimentalTagBot" + author: + name: "Joey Carpinelli" + email: "joey@loopy.codes" + date: "2025-01-02" + chapters: + - index.md + - api/index.qmd + +toc-title: "Table of Contents" + +execute: + echo: false + output: true + cache: false + freeze: false + +bibliography: references.bib + +format: + html: + theme: + light: flatly + dark: darkly diff --git a/docs/src/api/index.qmd b/docs/src/api/index.qmd new file mode 100644 index 0000000..f0718ce --- /dev/null +++ b/docs/src/api/index.qmd @@ -0,0 +1,20 @@ +--- +number-depth: 2 +--- + +# Reference + +_Docstrings for ExperimentalTagBot._ + +```{julia} +#| echo: false +#| output: false +using DocumenterQuarto +using ExperimentalTagBot +``` + +```{julia} +#| echo: false +#| output: asis +DocumenterQuarto.autodoc(ExperimentalTagBot) +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..ebadc85 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,5 @@ +--- +title: Overview +--- + +{{< include ../../README.md >}} diff --git a/docs/src/references.bib b/docs/src/references.bib new file mode 100644 index 0000000..6f2033b --- /dev/null +++ b/docs/src/references.bib @@ -0,0 +1,9 @@ +@software{Allaire_Quarto_2024, + author = {Allaire, J.J. and Teague, Charles and Scheidegger, Carlos and Xie, Yihui and Dervieux, Christophe}, + doi = {10.5281/zenodo.5960048}, + month = feb, + title = {{Quarto}}, + url = {https://github.com/quarto-dev/quarto-cli}, + version = {1.4}, + year = {2024} +} diff --git a/src/ExperimentalTagbot.jl b/src/ExperimentalTagbot.jl index 8ccf433..821ceb3 100644 --- a/src/ExperimentalTagbot.jl +++ b/src/ExperimentalTagbot.jl @@ -1,10 +1,11 @@ -module ExperimentalTagbot +module ExperimentalTagBot import Pkg -using Git -using GitHub - -export versions, repository, clone, commits_between +using Git: git +import GitHub +using Markdown +import IOCapture +using Dates """ Given a package name, return map from all versions released in the general @@ -15,50 +16,282 @@ registry to the Git SHA1 hashes in the project location. This code was originally written by user @yakir12 on Julia's Discourse in the following post: https://discourse.julialang.org/t/pkg-version-list/1257/10. """ -function versions(pkgname::AbstractString) - registry = only(filter(r -> r.name == "General", Pkg.Registry.reachable_registries())) +function registered_versions_map(package::AbstractString; registry = "General") + registry = only(filter(r -> r.name == registry, Pkg.Registry.reachable_registries())) local pkg try - pkg = only(filter(pkg -> pkg.name == pkgname, collect(values(registry.pkgs)))) + pkg = only(filter(pkg -> pkg.name == package, collect(values(registry.pkgs)))) catch e if e isa ArgumentError - throw(ErrorException("$pkgname is not registered in the General package registry")) + throw( + ErrorException( + "$package is not registered in the General package registry", + ), + ) else rethrow(e) end end - vs = [pair.first => pair.second.git_tree_sha1 for pair in Pkg.Registry.registry_info(pkg).version_info] - sort!(vs, by=x -> x.first) + vs = [pair.first => pair.second.git_tree_sha1 + for + pair in Pkg.Registry.registry_info(pkg).version_info] + sort!(vs, by = x -> x.first) + return Dict(["v" * string(pair.first) => pair.second for pair in vs]) +end + +""" +Given a package name, return all versions released in the general registry. +""" +function registered_versions(package::AbstractString; registry = "General") + vs = collect(keys(registered_versions_map(package))) + sort!(vs, by = VersionNumber) return vs end +""" +Given a registry name, return the repository. +""" +function registry_url(registry) + reg = only(filter(r -> r.name == registry, Pkg.Registry.reachable_registries())) + return reg.repo +end + """ Given a package name, return the project repository registered in the General registry. """ -function url(pkgname::AbstractString) - registry = only(filter(r -> r.name == "General", Pkg.Registry.reachable_registries())) - pkg = only(filter(pkg -> pkg.name == pkgname, collect(values(registry.pkgs)))) +function package_url(package::AbstractString; registry = "General") + reg = only(filter(r -> r.name == registry, Pkg.Registry.reachable_registries())) + pkg = only(filter(pkg -> pkg.name == package, collect(values(reg.pkgs)))) return Pkg.Registry.registry_info(pkg).repo end -function clone(url) - tmp = joinpath(tempdir(), tempname()) - run(git(["clone", convert(String, url), tmp, "--bare"])) - return tmp +""" +Given a repository URL, return the unique "{owner}/{project}" string. +""" +function repository_name(url::AbstractString) + return replace( + url, + "http://" => "", + "https://" => "", + "www." => "", + "github.com/" => "", + ".git" => "" + ) end +""" +Given a package name, return all registered versions which are not yet released. +""" +function untagged_versions(package::AbstractString; registry = "General", kwargs...) + registered = registered_versions(package; registry = registry) + tags, metadata = GitHub.tags(repository_name(package_url(package)); kwargs...) + tags = String[tag.tag for tag in tags if !isnothing(tag.tag)] -function commits_between(repo, base, tip) - local hashes - cd(repo) do - hashes = readlines(git(["rev-list", "$base..$tip", "--exclude", tip])) + for tag in tags + version = replace(tag, (package * "-") => "") + deleteat!(registered, findall(v -> v == version, registered)) end - return map(hash -> commit(repo, hash), hashes) + tags = registered + sort!(tags, by = VersionNumber) + + return tags +end + +function parent_hash(version, versions) + v = VersionNumber(version) + vs = map(VersionNumber, collect(versions)) + + filter!(n -> n < v, vs) + + v = maximum(vs) + return "v" * string(v) +end + +""" +Given a package name and version, return release PRs from in the provided registry. +""" +function release_pull_requests(package, version; registry = "General", kwargs...) + v = "v" * string(VersionNumber(version)) + results = GitHub.gh_get_json( + GitHub.DEFAULT_API, + "/search/issues"; + kwargs..., + params = "q=$package%20$v%20in%3Atitle%20is%3Apr%20repo%3A$(repository_name(registry_url(registry)))", + kwargs... + ) + return [GitHub.PullRequest(result) for result in results["items"]] +end + +""" +Given a package name and version, return all pull requests from the package +repository between the version and its parent. +""" +function find_pull_requests(package, version; kwargs...) + repo = repository_name(package_url(package)) + base = GitHub.commit( + repo, parent_hash(version, registered_versions(package)); kwargs...) + head = GitHub.commit(repo, registered_version_hash(package, version); kwargs...) + + base_date = string(Date(base.commit.author.date)) + base_date = replace(base_date, ":" => "%3A") + + head_date = string(Date(head.commit.author.date)) + head_date = replace(head_date, ":" => "%3A") + + results = GitHub.gh_get_json( + GitHub.DEFAULT_API, + "/search/issues"; + kwargs..., + params = "q=merged%3A>=$base_date%20is%3Apr%20repo%3A$(repository_name(package_url(package)))", + kwargs... + ) + + prs = [GitHub.PullRequest(result) + for result in results["items"]] + + return [pr + for pr in prs + if pr.closed_at <= head.commit.author.date] +end +""" +Given a package name and version, return all pull requests from the package +repository between the version and its parent. +""" +function find_issues(package, version; kwargs...) + repo = repository_name(package_url(package)) + base = GitHub.commit( + repo, parent_hash(version, registered_versions(package)); kwargs...) + head = GitHub.commit(repo, registered_version_hash(package, version); kwargs...) + + base_date = string(Date(base.commit.author.date)) + base_date = replace(base_date, ":" => "%3A") + + head_date = string(Date(head.commit.author.date)) + head_date = replace(head_date, ":" => "%3A") + + results = GitHub.gh_get_json( + GitHub.DEFAULT_API, + "/search/issues"; + kwargs..., + params = "q=closed%3A>=$base_date%20is%3Aissue%20repo%3A$(repository_name(package_url(package)))", + kwargs... + ) + + issues = [GitHub.Issue(result) + for result in results["items"]] + + return [issue + for issue in issues + if issue.closed_at <= head.commit.author.date] +end + +""" +Given the package name and version, return the latest release PR commit which +has been merged. +""" +function registered_version_hash( + package::AbstractString, version; registry = "General", kwargs... +) + prs = [GitHub.pull_request( + repository_name(registry_url(registry)), pr.number; kwargs...) + for + pr in release_pull_requests(package, version; registry = registry, kwargs...)] + + filter!(pr -> pr.merged, prs) # remove PRs which did not merge + sort!(prs; by = pr -> pr.closed_at) # sort PRs by merge timestamp + + pr = last(prs) # take the most recent merged PR + + lines = readlines(IOBuffer(pr.body)) + for line in lines + if startswith(line, "- Commit: ") + prefix, hash = rsplit(line, ":"; limit = 2) + return strip(hash) + end + end + error("commit not found for $package $version") +end + +function release_message(package::AbstractString, version; prefix = nothing, kwargs...) + version = string(VersionNumber(version)) + base = parent_hash(version, registered_versions(package)) + head = registered_version_hash(package, version) + + prefix = isnothing(prefix) ? package * "-" : prefix + diff = GitHub.compare( + repository_name(package_url(package)), base, head; kwargs...) + + messages = ["*" * replace(commit.commit.message, "\n\n" => "\n") + for commit in diff.commits] + pull_requests = ["* $(pr.title) ([#$(pr.number)]($(pr.html_url)))" + for pr in find_pull_requests(package, version; kwargs...)] + issues = ["* $(issue.title) ([#$(issue.number)]($(issue.html_url)))" + for issue in find_issues(package, version; kwargs...)] + + if isempty(pull_requests) + pull_requests = ["None"] + end + + if isempty(issues) + issues = ["None"] + end + + lines = ( + "[Diff since $parent]($(package_url(package))/compare/$base...$head)", + "\n## Closed Issues", + join(issues, "\n"), + "\n## Merged Pull Requests", + join(pull_requests, "\n"), + "\n## Changelog", + join(messages, "\n") + ) + + return join(lines, "\n") +end + +function create_release( + package::AbstractString, version; prefix = nothing, kwargs...) + prefix = isnothing(prefix) ? package * "-" : prefix + + repo = repository_name(package_url(package)) + version = string(VersionNumber(version)) + hash = registered_version_hash(package, version) + + tag = "$(prefix)v$(version)" + + default = Dict( + "tag_name" => tag, + "target_commitish" => hash, + "name" => "Release v$version for $package.jl", + "body" => release_message(package, version; kwargs...), + "draft" => false, + "prerelease" => false, + "generate_release_notes" => false + ) + + options = merge((; params = default), kwargs) + + @debug """creating release with the following options: + $(collect(options)) + """ + + @info "Creating Release v$(version) for $package.jl" + + GitHub.create_release(repo; options...) +end + +function create_releases( + package::AbstractString; prefix = nothing, registry = "General", kwargs...) + unreleased = untagged_versions(package; registry = registry, kwargs...) + + for version in unreleased + create_release(package, version; prefix = prefix, kwargs...) + end end end # module ExperimentalTagbot diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..49c47d0 --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,4 @@ +using Test +using ExperimentalTagBot + +@test isempty(ExperimentalTagBot.untagged_versions("ExperimentalTagBot")) \ No newline at end of file