From 91d6dc80c7e404b81f12c0844840e5d717699ce0 Mon Sep 17 00:00:00 2001 From: Jeremy Huntwork Date: Thu, 6 Aug 2026 18:21:57 -0400 Subject: [PATCH 1/3] Upload release assets before publishing --- .github/workflows/release.yaml | 256 +++++++++++++++++++++------------ 1 file changed, 161 insertions(+), 95 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9ca5327..e454445 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -95,7 +95,7 @@ jobs: cd "$GITHUB_WORKSPACE" zig build test --summary all - release-x86_64: + create-release: needs: [detect, test] if: needs.detect.outputs.version_changed == 'true' runs-on: ubuntu-24.04 @@ -105,8 +105,6 @@ jobs: contents: write env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - outputs: - checksum: ${{ steps.build.outputs.checksum }} steps: - name: Git checkout run: | @@ -118,23 +116,6 @@ jobs: git fetch --depth=50 --tags origin "$GITHUB_SHA" git checkout --detach FETCH_HEAD - - name: Build and package - id: build - run: | - set -eu - cd "$GITHUB_WORKSPACE" - - version="${{ needs.detect.outputs.version }}" - arch=$(uname -m) - base=$(sh .forgejo/scripts/release-ci.sh asset-base "$version" linux "$arch") - - zig build -Doptimize=ReleaseSmall - mkdir -p dist - cp zig-out/bin/mere "dist/${base}" - - checksum=$(cd dist && sha256sum "$base") - printf 'checksum=%s\n' "$checksum" >> "$GITHUB_OUTPUT" - - name: Push release tag run: | set -eu @@ -146,13 +127,12 @@ jobs: tag="${{ needs.detect.outputs.tag }}" if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then printf 'Tag %s already exists on origin.\n' "$tag" - exit 0 + else + git tag -a "$tag" -m "${{ needs.detect.outputs.release_name }}" "$GITHUB_SHA" + git push origin "refs/tags/${tag}" fi - git tag -a "$tag" -m "${{ needs.detect.outputs.release_name }}" "$GITHUB_SHA" - git push origin "refs/tags/${tag}" - - - name: Create release and upload assets + - name: Create draft release env: CHANGELOG: ${{ needs.detect.outputs.changelog }} TAG: ${{ needs.detect.outputs.tag }} @@ -165,7 +145,6 @@ jobs: api_base="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases" auth_header="Authorization: Bearer $GH_TOKEN" tag="$TAG" - version="$VERSION" release_status=$( curl -sS \ @@ -178,10 +157,24 @@ jobs: case "$release_status" in 200) - printf 'Release %s already exists; reusing it.\n' "$tag" + immutable=$(python3 -c 'import json; print(json.load(open("release.json")).get("immutable", False))') + draft=$(python3 -c 'import json; print(json.load(open("release.json"))["draft"])') + if [ "$immutable" = "True" ] || [ "$draft" != "True" ]; then + printf 'Release %s already exists and cannot be reused safely (immutable=%s draft=%s).\n' "$tag" "$immutable" "$draft" >&2 + exit 1 + fi + printf 'Draft release %s already exists; reusing it.\n' "$tag" ;; 404) python3 .forgejo/scripts/release-body.py > release-create.json + python3 - <<'PY' + import json + from pathlib import Path + path = Path("release-create.json") + body = json.loads(path.read_text()) + body["draft"] = True + path.write_text(json.dumps(body)) + PY curl -fsS \ -X POST \ -H "$auth_header" \ @@ -189,6 +182,7 @@ jobs: -H "Content-Type: application/json" \ --data-binary @release-create.json \ "$api_base" > release.json + printf 'Created draft release %s.\n' "$tag" ;; *) printf 'Failed to query release %s (HTTP %s).\n' "$tag" "$release_status" >&2 @@ -197,47 +191,87 @@ jobs: ;; esac - release_id=$(python3 -c 'import json; print(json.load(open("release.json"))["id"])') + release-x86_64: + needs: [detect, test, create-release] + if: needs.detect.outputs.version_changed == 'true' + runs-on: ubuntu-24.04 + container: + image: mere/ci:v0.15.0 + permissions: + contents: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputs: + checksum: ${{ steps.build.outputs.checksum }} + steps: + - name: Git checkout + run: | + set -eu + git init "$GITHUB_WORKSPACE" + git config --global --add safe.directory "$GITHUB_WORKSPACE" + cd "$GITHUB_WORKSPACE" + git remote add origin "https://x-access-token:${GH_TOKEN}@${GITHUB_SERVER_URL#https://}/$GITHUB_REPOSITORY.git" + git fetch --depth=50 --tags origin "$GITHUB_SHA" + git checkout --detach FETCH_HEAD + + - name: Build and package + id: build + run: | + set -eu + cd "$GITHUB_WORKSPACE" + + version="${{ needs.detect.outputs.version }}" + arch=$(uname -m) + base=$(sh .forgejo/scripts/release-ci.sh asset-base "$version" linux "$arch") + + zig build -Doptimize=ReleaseSmall + mkdir -p dist + cp zig-out/bin/mere "dist/${base}" + + checksum=$(cd dist && sha256sum "$base") + printf 'checksum=%s\n' "$checksum" >> "$GITHUB_OUTPUT" + + - name: Upload x86_64 asset to draft release + run: | + set -eu + cd "$GITHUB_WORKSPACE" + + api_base="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases" + auth_header="Authorization: Bearer $GH_TOKEN" + tag="${{ needs.detect.outputs.tag }}" + curl -fsS \ + -H "$auth_header" \ + -H "Accept: application/vnd.github+json" \ + "$api_base/tags/$tag" > release.json + upload_url=$(python3 -c 'import json; print(json.load(open("release.json"))["upload_url"].split("{")[0])') - [ -n "$release_id" ] && [ -n "$upload_url" ] || { - printf '%s\n' "Failed to parse release upload information from API response." >&2 - cat release.json >&2 - exit 1 - } - - for asset in dist/*; do - name=$(basename "$asset") - status=$( - curl -sS \ - -o upload.json \ - -w '%{http_code}' \ - -X POST \ - -H "$auth_header" \ - -H "Accept: application/vnd.github+json" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@$asset" \ - "$upload_url?name=$name&label=$name" - ) - - case "$status" in - 201) - printf 'Uploaded %s\n' "$name" - ;; - 422) - printf 'Asset %s may already exist; refusing to replace it.\n' "$name" >&2 - cat upload.json >&2 - exit 1 - ;; - *) - printf 'Failed to upload %s (HTTP %s).\n' "$name" "$status" >&2 - cat upload.json >&2 - exit 1 - ;; - esac - done + asset="dist/$(basename dist/*)" + name=$(basename "$asset") + existing=$(python3 -c 'import json,sys; print(any(a["name"] == sys.argv[1] for a in json.load(open("release.json"))["assets"]))' "$name") + if [ "$existing" = "True" ]; then + printf 'Asset %s already exists in draft release; keeping it.\n' "$name" + exit 0 + fi + + status=$( + curl -sS \ + -o upload.json \ + -w '%{http_code}' \ + -X POST \ + -H "$auth_header" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@$asset" \ + "$upload_url?name=$name&label=$name" + ) + + case "$status" in + 201) printf 'Uploaded %s\n' "$name" ;; + *) printf 'Failed to upload %s (HTTP %s).\n' "$name" "$status" >&2; cat upload.json >&2; exit 1 ;; + esac release-aarch64: - needs: [detect, test, release-x86_64] + needs: [detect, test, create-release] if: needs.detect.outputs.version_changed == 'true' runs-on: ubuntu-24.04-arm container: @@ -276,7 +310,7 @@ jobs: checksum=$(cd dist && sha256sum "$base") printf 'checksum=%s\n' "$checksum" >> "$GITHUB_OUTPUT" - - name: Upload asset + - name: Upload aarch64 asset to draft release run: | set -eu cd "$GITHUB_WORKSPACE" @@ -289,10 +323,15 @@ jobs: -H "Accept: application/vnd.github+json" \ "$api_base/tags/$tag" > release.json - release_id=$(python3 -c 'import json; print(json.load(open("release.json"))["id"])') upload_url=$(python3 -c 'import json; print(json.load(open("release.json"))["upload_url"].split("{")[0])') asset="dist/$(basename dist/*)" name=$(basename "$asset") + existing=$(python3 -c 'import json,sys; print(any(a["name"] == sys.argv[1] for a in json.load(open("release.json"))["assets"]))' "$name") + if [ "$existing" = "True" ]; then + printf 'Asset %s already exists in draft release; keeping it.\n' "$name" + exit 0 + fi + status=$( curl -sS \ -o upload.json \ @@ -306,23 +345,12 @@ jobs: ) case "$status" in - 201) - printf 'Uploaded %s\n' "$name" - ;; - 422) - printf 'Asset %s may already exist; refusing to replace it.\n' "$name" >&2 - cat upload.json >&2 - exit 1 - ;; - *) - printf 'Failed to upload %s (HTTP %s).\n' "$name" "$status" >&2 - cat upload.json >&2 - exit 1 - ;; + 201) printf 'Uploaded %s\n' "$name" ;; + *) printf 'Failed to upload %s (HTTP %s).\n' "$name" "$status" >&2; cat upload.json >&2; exit 1 ;; esac checksums: - needs: [detect, release-x86_64, release-aarch64] + needs: [detect, create-release, release-x86_64, release-aarch64] if: needs.detect.outputs.version_changed == 'true' runs-on: ubuntu-24.04-arm container: @@ -332,7 +360,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - name: Upload SHA256SUMS + - name: Upload SHA256SUMS to draft release env: X86_CHECKSUM: ${{ needs.release-x86_64.outputs.checksum }} AARCH64_CHECKSUM: ${{ needs.release-aarch64.outputs.checksum }} @@ -350,6 +378,12 @@ jobs: "$api_base/tags/$tag" > release.json upload_url=$(python3 -c 'import json; print(json.load(open("release.json"))["upload_url"].split("{")[0])') + existing=$(python3 -c 'import json; print(any(a["name"] == "SHA256SUMS" for a in json.load(open("release.json"))["assets"]))') + if [ "$existing" = "True" ]; then + printf '%s\n' 'SHA256SUMS already exists in draft release; keeping it.' + exit 0 + fi + status=$( curl -sS \ -o upload.json \ @@ -363,17 +397,49 @@ jobs: ) case "$status" in - 201) - printf 'Uploaded SHA256SUMS\n' - ;; - 422) - printf '%s\n' 'SHA256SUMS may already exist; refusing to replace it.' >&2 - cat upload.json >&2 - exit 1 - ;; - *) - printf 'Failed to upload SHA256SUMS (HTTP %s).\n' "$status" >&2 - cat upload.json >&2 - exit 1 - ;; + 201) printf '%s\n' 'Uploaded SHA256SUMS' ;; + *) printf 'Failed to upload SHA256SUMS (HTTP %s).\n' "$status" >&2; cat upload.json >&2; exit 1 ;; esac + + publish: + needs: [detect, checksums] + if: needs.detect.outputs.version_changed == 'true' + runs-on: ubuntu-24.04-arm + container: + image: mere/ci:v0.15.0 + permissions: + contents: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Publish complete release + run: | + set -eu + + api_base="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases" + auth_header="Authorization: Bearer $GH_TOKEN" + tag="${{ needs.detect.outputs.tag }}" + curl -fsS \ + -H "$auth_header" \ + -H "Accept: application/vnd.github+json" \ + "$api_base/tags/$tag" > release.json + + immutable=$(python3 -c 'import json; print(json.load(open("release.json")).get("immutable", False))') + draft=$(python3 -c 'import json; print(json.load(open("release.json"))["draft"])') + if [ "$immutable" = "True" ]; then + printf 'Release %s is already immutable; refusing to modify it.\n' "$tag" >&2 + exit 1 + fi + if [ "$draft" != "True" ]; then + printf 'Release %s is already published.\n' "$tag" + exit 0 + fi + + curl -fsS \ + -X PATCH \ + -H "$auth_header" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + --data '{"draft":false}' \ + "$api_base/$(python3 -c 'import json; print(json.load(open("release.json"))["id"])')" >/dev/null + printf 'Published complete release %s.\n' "$tag" From 77bb32d8c086bfd7641a657e2e7066d0f4d2f9ec Mon Sep 17 00:00:00 2001 From: Jeremy Huntwork Date: Thu, 6 Aug 2026 18:26:35 -0400 Subject: [PATCH 2/3] Fix draft release JSON command --- .github/workflows/release.yaml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e454445..0d7086d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -167,14 +167,7 @@ jobs: ;; 404) python3 .forgejo/scripts/release-body.py > release-create.json - python3 - <<'PY' - import json - from pathlib import Path - path = Path("release-create.json") - body = json.loads(path.read_text()) - body["draft"] = True - path.write_text(json.dumps(body)) - PY + python3 -c 'import json; from pathlib import Path; path = Path("release-create.json"); body = json.loads(path.read_text()); body["draft"] = True; path.write_text(json.dumps(body))' curl -fsS \ -X POST \ -H "$auth_header" \ From f461be0c81e50f28cd5b4994ac28031d894713eb Mon Sep 17 00:00:00 2001 From: Jeremy Huntwork Date: Thu, 6 Aug 2026 18:30:42 -0400 Subject: [PATCH 3/3] Allow manual recovery of a version release --- .github/workflows/release.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0d7086d..c980edb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,6 +7,12 @@ on: paths: - build.zig.zon workflow_dispatch: + inputs: + force_release: + description: Force the detected version to release (used for recovery) + required: false + type: boolean + default: false permissions: contents: read @@ -49,8 +55,14 @@ jobs: previous_file=previous-build.zig.zon fi + metadata=$(mktemp) sh .forgejo/scripts/release-ci.sh detect-version-change build.zig.zon "$previous_file" \ - >> "$GITHUB_OUTPUT" + > "$metadata" + cat "$metadata" >> "$GITHUB_OUTPUT" + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && \ + [ "${{ inputs.force_release }}" = "true" ]; then + printf 'version_changed=true\n' >> "$GITHUB_OUTPUT" + fi changelog=$(sh .forgejo/scripts/release-ci.sh changelog) {