diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 54ea2d2..59c6d90 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -4,6 +4,8 @@ on: push: branches: - main + paths: + - "!Artifacts.toml" # Impending package release. Will trigger via `tags` tags: ["*"] concurrency: # Skip intermediate builds: on all builds except on the "main" branch. @@ -11,8 +13,38 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.run_number || 0 }} cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: + unpublished: + name: Artifact Check + runs-on: ubuntu-latest + outputs: + key: ${{ steps.key.outputs.key }} + tarball_filename: ${{ steps.key.outputs.tarball_filename }} + content_hash: ${{ steps.key.outputs.content_hash }} + steps: + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + modified_artifacts_toml: + - Artifacts.toml + - uses: actions/checkout@v4 + if: ${{ steps.filter.outputs.modified_artifacts_toml == 'true' }} + - name: Check for unpublished artifact + if: ${{ steps.filter.outputs.modified_artifacts_toml == 'true' }} + id: key + shell: julia --color=yes {0} + run: | + include(joinpath(pwd(), "gen", "artifacts.jl")) + (; key, tarball_filename, content_hash) = gh_artifact() + open(ENV["GITHUB_OUTPUT"], "a") do io + println(io, "key=$key") + println(io, "tarball_filename=$tarball_filename") + println(io, "content_hash=$content_hash") + end + test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + needs: unpublished runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.version == 'nightly' }} strategy: @@ -28,6 +60,51 @@ jobs: - x64 steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Allows checkout of Artifacts.toml from base ref + - uses: dawidd6/action-download-artifact@v3 + if: ${{ needs.unpublished.outputs.key }} + id: action-artifact + with: + workflow: Update.yaml + name: ${{ needs.unpublished.outputs.key }} + - name: Clear cached Overrides.toml + if: ${{ !needs.unpublished.outputs.key }} + run: rm -f ~/.julia/artifacts/Overrides.toml + - name: Create artifacts Overrides.toml + if: ${{ needs.unpublished.outputs.key }} + run: | + set -ex + tarball_path="$PWD/${{ needs.unpublished.outputs.tarball_filename }}" + [ -f "$tarball_path" ] || exit 1 + + # A valid Artifacts.toml file is required: Pkg.jl issue #2662 + mv Artifacts.toml NewArtifacts.toml + git checkout origin/${{ github.base_ref }} -- Artifacts.toml + + artifact_dir="$HOME/.julia/artifacts/${{ needs.unpublished.outputs.content_hash }}" + mkdir -p "$artifact_dir" + tar xvf "$tarball_path" -C "$artifact_dir" + + # Why doesn't this work? + # cat > ~/.julia/artifacts/Overrides.toml < ~/.julia/artifacts/Overrides.toml <- + An automated PR generated by the ${{ github.workflow }} workflow. + add-paths: | + Project.toml + Artifacts.toml + commit-message: ${{ steps.build.outputs.commit_message }} + branch: gh/update-tzdata + delete-branch: ${{ github.event_name == 'pull_request' }} + token: ${{ secrets.TZJDATA_UPDATE_TOKEN }} # TODO: Fine-grained token expires diff --git a/Project.toml b/Project.toml index 4547c79..688d77f 100644 --- a/Project.toml +++ b/Project.toml @@ -7,12 +7,20 @@ version = "1.0.0+2023c" Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" [compat] +CodecZlib = "0.7" +SHA = "1.6" +Tar = "1" TimeZones = "1" julia = "1.6" [extras] +CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +Tar = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" [targets] -test = ["TimeZones", "Test"] +test = ["CodecZlib", "Pkg", "SHA", "TOML", "Tar", "Test", "TimeZones"] diff --git a/gen/artifacts.jl b/gen/artifacts.jl new file mode 100644 index 0000000..0ee8c0c --- /dev/null +++ b/gen/artifacts.jl @@ -0,0 +1,19 @@ +using TOML: TOML + +const PKG_ROOT = joinpath(@__DIR__(), "..") + +function gh_artifact(artifact_toml=joinpath(PKG_ROOT, "Artifacts.toml")) + toml = TOML.parsefile(artifact_toml) + tarball_artifact = only(toml["tzjdata"]["download"]) + tarball_filename = basename(tarball_artifact["url"]) + tarball_sha256 = tarball_artifact["sha256"] + content_hash = toml["tzjdata"]["git-tree-sha1"] + + key = "$(tarball_filename)-$(tarball_sha256)" + return (; + tarball_filename, + tarball_sha256, + content_hash, + key, + ) +end diff --git a/gen/make.jl b/gen/make.jl index 6993dce..75ecf06 100644 --- a/gen/make.jl +++ b/gen/make.jl @@ -109,19 +109,7 @@ function upload_to_github_release(owner, repo_name, commit, tag, path; token=ENV run(cmd) end -# TODO: Re-running always bumps version -if abspath(PROGRAM_FILE) == @__FILE__ - tzdata_version = tzdata_latest_version() - tarball_name = "tzjfile-v1-tzdata$(tzdata_version).tar.gz" - - # Build tzjfile artifact content - # TZData.cleanup(tzdata_version, _scratch_dir()) - compiled_dir = TZData.build(tzdata_version, _scratch_dir()) - - @info "Creating tarball $tarball_name" - tarball_path = joinpath(tempdir(), tarball_name) - create_tarball(compiled_dir, tarball_path) - +function update_tzdata() repo_path = joinpath(@__DIR__, "..") pkg_url = remote_url(repo_path) @@ -129,7 +117,28 @@ if abspath(PROGRAM_FILE) == @__FILE__ project_toml = joinpath(repo_path, "Project.toml") project = read_project(project_toml) old_version = project.version - new_version = Base.nextpatch(project.version) + old_tzdata_version = only(old_version.build) + + # Always fetch the current list of tzdata versions (ignoring any caching). + tzdata_versions = TZData.tzdata_versions() + i = findfirst(==(old_tzdata_version), tzdata_versions) + if i == length(tzdata_versions) + new_tzdata_version = tzdata_versions[i] + new_version = Base.nextpatch(old_version) + else + new_tzdata_version = tzdata_versions[i + 1] + new_version = Base.nextminor(old_version) + end + + tarball_name = "tzjfile-v1-tzdata$(new_tzdata_version).tar.gz" + + # Build tzjfile artifact content + # TZData.cleanup(new_tzdata_version, _scratch_dir()) + compiled_dir = TZData.build(new_tzdata_version, _scratch_dir()) + + @info "Creating tarball $tarball_name" + tarball_path = joinpath(tempdir(), tarball_name) + create_tarball(compiled_dir, tarball_path) # Include tzdata version in build number new_version = VersionNumber( @@ -137,7 +146,7 @@ if abspath(PROGRAM_FILE) == @__FILE__ new_version.minor, new_version.patch, (), - (tzdata_version,), + (new_tzdata_version,), ) @info "Bumping package $(project.name) from $old_version -> $new_version" @@ -148,18 +157,49 @@ if abspath(PROGRAM_FILE) == @__FILE__ artifact_url = "$(pkg_url)/releases/download/$(tag)/$(basename(tarball_path))" artifacts_toml = joinpath(repo_path, "Artifacts.toml") + content_hash = tree_hash_sha1(tarball_path) + tarball_sha256 = sha256sum(tarball_path) bind_artifact!( artifacts_toml, "tzjdata", - tree_hash_sha1(tarball_path); - download_info=[(artifact_url, sha256sum(tarball_path))], + content_hash; + download_info=[(artifact_url, tarball_sha256)], force=true, ) + commit_message = "Set artifact to tzdata$(new_tzdata_version) and project to $(new_version)" + + return (; + repo_path, + project_toml, + artifacts_toml, + artifact_url, + tarball_path, + tarball_sha256, + old_tzdata_version, + new_tzdata_version, + old_version, + new_version, + commit_message, + ) +end + +# TODO: Re-running always bumps version +if abspath(PROGRAM_FILE) == @__FILE__ + (; + repo_path, + new_tzdata_version, + new_version, + project_toml, + artifacts_toml, + artifact_url, + tarball_path, + commit_message, + ) = update_tzdata() + # TODO: Ensure no other files are staged before committing @info "Committing and pushing Project.toml and Artifacts.toml" branch = "main" - message = "Set artifact to tzdata$(tzdata_version) and project to $(new_version)" # TODO: ghr and LibGit2 use different credential setups. Double check # what BB does here. @@ -168,7 +208,7 @@ if abspath(PROGRAM_FILE) == @__FILE__ # TODO: This allows empty commits LibGit2.add!(repo, basename(artifacts_toml)) LibGit2.add!(repo, basename(project_toml)) - LibGit2.commit(repo, message) + LibGit2.commit(repo, commit_message) # Same as "refs/heads/$branch" but fails if branch doesn't exist locally branch_ref = LibGit2.lookup_branch(repo, branch) diff --git a/test/runtests.jl b/test/runtests.jl index 918bd68..2add4ff 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,10 @@ +using Artifacts: Artifacts +using Base: SHA1 +using CodecZlib: GzipDecompressorStream +using Pkg.Types: read_project +using SHA: sha256 using TZJData +using Tar: Tar using TimeZones: TZJFile, TimeZone, Class using Test @@ -25,10 +31,42 @@ function _reload_cache!(cache::AbstractDict, compiled_dir::AbstractString) return cache end +# Compute the Artifact.toml `git-tree-sha1`. +function tree_hash_sha1(tarball_path) + return open(GzipDecompressorStream, tarball_path, "r") do tar + SHA1(Tar.tree_hash(tar)) + end +end + +# Compute the Artifact.toml `sha256` from the compressed archive. +function sha256sum(tarball_path) + return open(tarball_path, "r") do tar + bytes2hex(sha256(tar)) + end +end + @testset "TZJData.jl" begin @test isdir(TZJData.ARTIFACT_DIR) @test occursin(r"^\d{4}[a-z]$", TZJData.TZDATA_VERSION) + @testset "validate unpublished artifact" begin + artifact_toml = get(ENV, "TZJDATA_ARTIFACT_TOML", nothing) + tarball_path = get(ENV, "TZJDATA_TARBALL_PATH", nothing) + if !isnothing(artifact_toml) && !isnothing(tarball_path) + project = read_project(joinpath(@__DIR__(), "..", "Project.toml")) + artifacts = Artifacts.parse_toml(artifact_toml) + + @test artifacts["tzjdata"]["git-tree-sha1"] == string(tree_hash_sha1(tarball_path)) + @test length(artifacts["tzjdata"]["download"]) == 1 + @test artifacts["tzjdata"]["download"][1]["sha256"] == sha256sum(tarball_path) + + url = artifacts["tzjdata"]["download"][1]["url"] + @test contains(url, "/v$(project.version)/") + @test basename(url) == basename(tarball_path) + @test contains(basename(url), r"tzdata(?\d{2}\d{2}?[a-z])") + end + end + @testset "load compiled" begin cache = Dict{String,Tuple{TimeZone,Class}}() _reload_cache!(cache, TZJData.ARTIFACT_DIR)