From d16650906db118cdb6342ac26737ecc451e7eb30 Mon Sep 17 00:00:00 2001 From: Dev <9416650+aoinoikaz@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:15:17 -0500 Subject: [PATCH] CI: the Hackage publish builds the docs before it uploads the package The package was uploaded before the documentation was built, so a documentation build that failed left a version on Hackage with no docs, and a version cannot be withdrawn. Both are now built first, and the two uploads run back to back. Comments that narrated past releases now say what each step does and why. --- .github/workflows/publish.yml | 64 ++++++++++++++++------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 469d3f1..ac9ba56 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,10 +24,8 @@ jobs: steps: - uses: actions/checkout@v7 - # v0.9.0.0 reached Hackage with its changelog section still headed - # "## Unreleased" (the dating commit landed 73 minutes after the - # tag), and a Hackage tarball is immutable - that heading is frozen - # into the release forever. Refuse to upload anything until the + # A Hackage tarball is immutable, so whatever heading the changelog + # carries is frozen into the release. Nothing is uploaded unless the # changelog's top section is exactly this version, dated. - name: Verify changelog is dated for this release run: | @@ -53,35 +51,13 @@ jobs: ghc-version: '9.14.1' cabal-version: 'latest' - run: cabal sdist - - name: Upload to Hackage - run: | - VERSION=${GITHUB_REF#refs/tags/v} - # A docs failure downstream used to strand the release: the - # sdist was already live, so the re-run died here on Hackage's - # duplicate-version 400 without ever reaching the docs steps - # (0.9.0.0 shipped docless exactly this way). When the version - # is already published, skip the sdist and let the docs steps - # run; the probe hits this version's own cabal file, which 404s - # for any version Hackage does not have. - if curl --fail --silent --output /dev/null \ - "https://hackage.haskell.org/package/nova-cache-${VERSION}/nova-cache.cabal"; then - echo "nova-cache-${VERSION} is already on Hackage; skipping sdist upload" - exit 0 - fi - SDIST=$(ls dist-newstyle/sdist/nova-cache-*.tar.gz) - curl --fail --silent --show-error \ - --header "Authorization: X-ApiKey ${HACKAGE_TOKEN}" \ - --form "package=@${SDIST}" \ - https://hackage.haskell.org/packages/ - env: - HACKAGE_TOKEN: ${{ secrets.HACKAGE_TOKEN }} - - # Maintainer-built documentation, permanently - not a stopgap for - # Hackage's doc builder lagging GHC (today it carries 9.8 against a - # base >= 4.22 floor, so it cannot even plan the build). Building - # here documents every library, the xz sublibrary included, with - # the same pinned toolchain the release was verified on, and the - # docs land with the release instead of on the builder's schedule. + # Hackage's documentation builder runs a GHC older than this package's + # base floor, so it cannot plan the build. The documentation is built + # here instead, for every library, the xz sublibrary included, with + # the pinned toolchain the release was verified on. It is built before + # anything is uploaded, so the two uploads run back to back: a package + # published before its documentation built would stay public without + # it, since a version cannot be withdrawn. - name: Build documentation run: | VERSION=${GITHUB_REF#refs/tags/v} @@ -89,7 +65,7 @@ jobs: # cabal writes each library's tree separately and tars only the # main one. Hackage links a module from the package page only # when its HTML sits at the docs ROOT (docs/.html); a - # per-sublibrary subdirectory renders as plain text (#60). So + # per-sublibrary subdirectory renders as plain text. So # flatten: graft every sublibrary's module and source pages # into the root of the release tree, keep one copy of the # assets, and re-tar as ustar - Hackage's tar reader 400s @@ -134,6 +110,26 @@ jobs: echo "$LISTING" | grep -q "^nova-cache-${VERSION}-docs/${MODULE}\.html$" \ || { echo "${MODULE}.html missing from the docs root"; exit 1; } done + - name: Upload to Hackage + run: | + VERSION=${GITHUB_REF#refs/tags/v} + # A version Hackage already has is skipped, so a re-run after a + # failed documentation upload gets past Hackage's duplicate-version + # 400 to the docs step. The probe hits this version's own cabal + # file, which 404s for any version Hackage does not have. + if curl --fail --silent --output /dev/null \ + "https://hackage.haskell.org/package/nova-cache-${VERSION}/nova-cache.cabal"; then + echo "nova-cache-${VERSION} is already on Hackage; skipping sdist upload" + exit 0 + fi + SDIST=$(ls dist-newstyle/sdist/nova-cache-*.tar.gz) + curl --fail --silent --show-error \ + --header "Authorization: X-ApiKey ${HACKAGE_TOKEN}" \ + --form "package=@${SDIST}" \ + https://hackage.haskell.org/packages/ + env: + HACKAGE_TOKEN: ${{ secrets.HACKAGE_TOKEN }} + - name: Upload documentation to Hackage run: | VERSION=${GITHUB_REF#refs/tags/v}