From 9eded1cb9e2956534820082b12d175a30c30055e Mon Sep 17 00:00:00 2001 From: Sam Schumacher <38103916+HerrSammyDE@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:45:39 +0200 Subject: [PATCH 1/2] ci: release automatically from develop, versioned by upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Releases needed a human to push a v* tag and a second human to click Publish on the resulting draft. Both steps are gone: a merge into develop now cuts the release by itself. The version is never chosen. `prepare` fetches pterodactyl/wings' tags into refs/upstream-tags/ (a private namespace, because our fork carries tags with the same names pointing at our own merge commits) and picks the newest stable one that is an ancestor of develop. That is precisely "which upstream version is in develop", so our releases keep carrying upstream's numbering with nothing to maintain by hand. Nothing becomes visible until everything is verified. The draft release holds only tag_name and target_commitish, so an aborted run leaves no tag behind; binary.yaml and docker.yaml attach their output to it; and only once both succeed does `publish` check the assets are all present, move :latest by digest and un-draft. A failure anywhere before that leaves the previous release and the previous :latest untouched, and /releases/latest ignores drafts throughout. docker.yaml had to become a workflow_call target. It listened on `release: published`, which only ever fired because a human clicked Publish — a release published with GITHUB_TOKEN triggers no workflow runs, so automating that click would have silently ended every image build. Also: - Empty release notes are now impossible: a missing CHANGELOG.md section fails the job instead of publishing a release with an empty body. The section match is anchored, so v1.13.2 no longer also matches v1.13.20. - Dropped the release/vX branch and its sed bump of system/const.go. It wrote a version into a branch nobody uses while the real version comes from ldflags, and a git push mid-release is exactly what breaks a second run. - Added SHA256SUMS, and a smoke test asserting the built binary reports the version it was built with rather than "develop". - Re-releasing a version is an explicit `force: true` dispatch. Deleting a release by hand deliberately does not resurrect it. --- .github/workflows/binary.yaml | 130 ++++++++++++++ .github/workflows/docker.yaml | 101 +++++++---- .github/workflows/release.yaml | 299 ++++++++++++++++++++++++++++----- FORK_CHANGES.md | 3 +- 4 files changed, 459 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/binary.yaml diff --git a/.github/workflows/binary.yaml b/.github/workflows/binary.yaml new file mode 100644 index 000000000..0ceb75011 --- /dev/null +++ b/.github/workflows/binary.yaml @@ -0,0 +1,130 @@ +name: Build Binary + +# Two ways in: +# +# 1. Called by release.yaml as a reusable workflow, right after the draft +# release was opened. This is the automatic path, and it runs in the SAME +# workflow run — which is what makes it work at all: a release published +# with GITHUB_TOKEN does NOT trigger new workflow runs, so an `on: release` +# listener would silently never fire. +# +# 2. A manual dispatch, to check that a build still works without touching any +# release. On that path `inputs` is empty, every `inputs.*` reference below +# evaluates to '' rather than erroring, and the binaries are kept as run +# artifacts instead of being attached anywhere. + +on: + workflow_call: + inputs: + ref: + description: Commit SHA (or ref) to build. Defaults to the caller's ref. + type: string + required: false + default: '' + release-tag: + description: >- + Tag of an existing — possibly draft — GitHub Release to attach the + binaries and SHA256SUMS to. Empty means build and smoke-test only. + type: string + required: false + default: '' + version: + description: >- + Version to compile in, without a leading `v` (e.g. 1.13.2). Asserted + against what the built binary reports. + type: string + required: false + default: '' + workflow_dispatch: + +permissions: + contents: write + +jobs: + binary: + name: Build + runs-on: ubuntu-24.04 + steps: + - name: Code checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # Empty on dispatch, which makes actions/checkout fall back to its own + # default (the triggering ref and github.sha). + ref: ${{ inputs.ref }} + + - name: Setup Go + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version: 1.24.11 + + - name: Resolve the version + id: resolve + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + # A dispatch build carries no version; label it by commit so a stray + # binary can never claim to be a release. + if [ -z "$VERSION" ]; then + VERSION="dev-$(git rev-parse --short HEAD)" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Build release binaries + env: + CGO_ENABLED: 0 + VERSION: ${{ steps.resolve.outputs.version }} + run: | + set -euo pipefail + for arch in amd64 arm64; do + GOARCH="$arch" go build \ + -o "dist/wings_linux_${arch}" \ + -v -trimpath \ + -ldflags="-s -w -X github.com/Rene-Roscher/wings/system.Version=${VERSION}" \ + github.com/Rene-Roscher/wings + chmod 755 "dist/wings_linux_${arch}" + done + + # Catches a broken ldflags path silently producing a binary that reports + # "develop" — which would otherwise only surface on a node, after release. + - name: Assert the binary reports the version it was built with + env: + VERSION: ${{ steps.resolve.outputs.version }} + run: | + set -euo pipefail + reported="$(./dist/wings_linux_amd64 version | head -n1)" + echo "$reported" + printf '%s' "$reported" | grep -qF "wings v${VERSION}" || { + echo "::error::binary reports '${reported}', expected 'wings v${VERSION}'" + exit 1 + } + + - name: Generate SHA256SUMS + run: | + set -euo pipefail + cd dist + sha256sum wings_linux_amd64 wings_linux_arm64 > SHA256SUMS + cat SHA256SUMS + + - name: Attach the assets to the release + if: inputs.release-tag != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ inputs.release-tag }} + run: | + set -euo pipefail + # --clobber so that re-running a failed run replaces partial uploads + # instead of failing on "asset already exists". + gh release upload "$TAG" \ + dist/wings_linux_amd64 \ + dist/wings_linux_arm64 \ + dist/SHA256SUMS \ + --clobber + + - name: Upload the binaries as run artifacts + if: inputs.release-tag == '' + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: wings-binaries + path: dist/ diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 476cdfb35..58438626a 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -1,25 +1,77 @@ name: Docker +# Called by release.yaml in the same run (a release published with +# GITHUB_TOKEN does not trigger `on: release`, so the old +# `on: release: published` listener would never fire once publishing is +# automated), and on its own for `:develop` images. +# +# Tag meanings: +# :1.13.2, :v1.13.2 — a published GitHub Release +# :latest — the newest published GitHub Release +# :develop — the newest push to develop +# +# This workflow deliberately does NOT push :latest. It is moved by release.yaml's +# publish job, by digest, only after the binaries, the image and the release +# assets have all been verified — otherwise a failure in any later job would +# leave :latest pointing at a build that never became a release. + on: + workflow_call: + inputs: + ref: + description: Commit SHA (or ref) to build. Defaults to the caller's ref. + type: string + required: false + default: '' + version: + description: >- + Release version without a leading `v` (e.g. 1.13.2). When set, the + image is tagged as a release build. + type: string + required: false + default: '' + outputs: + digest: + description: Digest of the pushed image, for retagging by the caller. + value: ${{ jobs.build.outputs.digest }} push: branches: - develop - release: - types: - - published + +permissions: + contents: read + packages: write + +concurrency: + group: docker-${{ inputs.version || github.ref }} + cancel-in-progress: false jobs: - build-and-push: + build: name: Build and Push runs-on: ubuntu-24.04 - # Always run against a tag, even if the commit into the tag has [docker skip] within the commit message. - if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))" - permissions: - contents: read - packages: write + # `skip docker` only applies to the develop path. A release build must never + # be skippable by a commit message, or publish would fail with no digest. + if: "inputs.version != '' || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))" + outputs: + digest: ${{ steps.build.outputs.digest }} steps: - name: Code checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ inputs.ref }} + + - name: Get build information + id: build_info + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + if [ -n "$VERSION" ]; then + echo "build_version=${VERSION}" >> "$GITHUB_OUTPUT" + else + echo "build_version=dev-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + fi - name: Docker metadata id: docker_meta @@ -29,9 +81,9 @@ jobs: flavor: | latest=false tags: | - type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }} - type=ref,event=tag - type=ref,event=branch + type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }} + type=raw,value=v${{ inputs.version }},enable=${{ inputs.version != '' }} + type=ref,event=branch,enable=${{ inputs.version == '' }} - name: Setup QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 @@ -46,35 +98,16 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Build Information - id: build_info - run: | - echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> "$GITHUB_OUTPUT" - echo "short_sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - - - name: Build and Push (tag) + - name: Build and push + id: build uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 - if: "github.event_name == 'release' && github.event.action == 'published'" with: context: . file: ./Dockerfile push: true platforms: linux/amd64,linux/arm64 build-args: | - VERSION=${{ steps.build_info.outputs.version_tag }} - labels: ${{ steps.docker_meta.outputs.labels }} - tags: ${{ steps.docker_meta.outputs.tags }} - - - name: Build and Push (develop) - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 - if: "github.event_name == 'push' && contains(github.ref, 'develop')" - with: - context: . - file: ./Dockerfile - push: ${{ github.event_name != 'pull_request' }} - platforms: linux/amd64,linux/arm64 - build-args: | - VERSION=dev-${{ steps.build_info.outputs.short_sha }} + VERSION=${{ steps.build_info.outputs.build_version }} labels: ${{ steps.docker_meta.outputs.labels }} tags: ${{ steps.docker_meta.outputs.tags }} cache-from: type=gha diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 94d2e95e8..a651a2d69 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,57 +1,278 @@ name: Release + +# Automatic releases. The version is never ours to choose: it is always the +# version of the upstream pterodactyl/wings release that `develop` contains. +# +# push to develop +# └─ prepare resolves the newest upstream tag that is an ancestor of develop, +# refuses to continue without a matching CHANGELOG.md section, +# and opens a DRAFT release +# ├─ binary.yaml builds linux amd64/arm64 + SHA256SUMS onto the draft +# ├─ docker.yaml builds and pushes the versioned image +# └─ publish verifies the assets, moves :latest by digest, un-drafts +# +# Why draft-until-verified: nodes resolve GET /releases/latest and the :latest +# image. GitHub excludes drafts from /releases/latest, so a failure anywhere +# before `publish` leaves both pointing at the previous release rather than at a +# half-built one. Nothing becomes visible until the final job. +# +# Why binary/docker are `uses:` jobs in THIS run rather than `on: release` +# listeners: a release published with GITHUB_TOKEN does not trigger new workflow +# runs. docker.yaml used to listen on `release: published`, which only ever +# worked because a human clicked Publish. Automating that click without this +# change would have silently stopped every image build. +# +# NOTE: pushing a `v*` tag by hand no longer releases anything. Releases are cut +# from develop, by this workflow, and nowhere else. +# +# RECOVERY: a failed run leaves a DRAFT release behind. `prepare` treats an +# existing draft as "resume", so "Re-run failed jobs" on the original run picks +# up where it failed. Only a PUBLISHED release blocks a new run, which is why a +# fresh push to develop will not quietly re-cut a release that already shipped. +# +# RE-RELEASING a version: run this workflow manually with `force: true`. It +# deletes the published release AND its tag, then cuts the version again from +# the current develop. Deleting a release by hand does NOT re-trigger anything — +# that is deliberate, so an accidental deletion cannot resurrect itself with +# whatever happens to be on develop at the time. + on: push: - tags: - - "v*" + branches: + - develop + workflow_dispatch: + inputs: + force: + description: "Re-release the version even if it is already published (deletes the existing release and its tag)" + type: boolean + default: false + +permissions: + contents: write + packages: write + +concurrency: + group: release + cancel-in-progress: false + jobs: - release: - name: Release + prepare: + name: Prepare release runs-on: ubuntu-24.04 - permissions: - contents: write + outputs: + should_release: ${{ steps.decide.outputs.should_release }} + version: ${{ steps.upstream.outputs.version }} + tag: ${{ steps.upstream.outputs.tag }} + sha: ${{ github.sha }} steps: - name: Code checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: Setup Go - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: 1.24.11 + # Ancestry checks need the whole history, not the default shallow clone. + fetch-depth: 0 - - name: Build release binaries - env: - CGO_ENABLED: 0 + - name: Resolve the upstream version contained in develop + id: upstream run: | - GOARCH=amd64 go build -o dist/wings_linux_amd64 -v -trimpath -ldflags="-s -w -X github.com/Rene-Roscher/wings/system.Version=${{ github.ref_name }}" github.com/Rene-Roscher/wings - chmod 755 dist/wings_linux_amd64 - GOARCH=arm64 go build -o dist/wings_linux_arm64 -v -trimpath -ldflags="-s -w -X github.com/Rene-Roscher/wings/system.Version=${{ github.ref_name }}" github.com/Rene-Roscher/wings - chmod 755 dist/wings_linux_arm64 + set -euo pipefail + git remote add upstream https://github.com/pterodactyl/wings.git + # Fetched into a private namespace on purpose: our fork carries tags + # with the SAME names pointing at our own merge commits (our v1.13.1 + # is f7ba42d, upstream's is e771816), so a plain `fetch --tags` is + # rejected as "would clobber existing tag". + git fetch --quiet upstream 'refs/tags/v*:refs/upstream-tags/v*' + + best="" + while read -r ref; do + version="${ref#refs/upstream-tags/v}" + # Upstream tags release candidates too (v1.11.0-rc.1). We only ever + # ship stable versions. + case "$version" in *-*) continue ;; esac + # `^{commit}` dereferences annotated tags to the commit they point at. + git merge-base --is-ancestor "$(git rev-parse "${ref}^{commit}")" HEAD || continue + if [ -z "$best" ] || [ "$(printf '%s\n%s\n' "$best" "$version" | sort -V | tail -n1)" = "$version" ]; then + best="$version" + fi + done < <(git for-each-ref --format='%(refname)' 'refs/upstream-tags/v*') - - name: Create release branch + if [ -z "$best" ]; then + echo "::error::no stable upstream tag is an ancestor of develop — cannot determine a version" + exit 1 + fi + + echo "develop contains upstream v${best}" + echo "version=${best}" >> "$GITHUB_OUTPUT" + echo "tag=v${best}" >> "$GITHUB_OUTPUT" + + - name: Decide whether to release + id: decide env: - VERSION: ${{ github.ref_name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ steps.upstream.outputs.tag }} + FORCE: ${{ inputs.force }} run: | - BRANCH=release/${{ env.VERSION }} - git config --local user.email "ci@pterodactyl.io" - git config --local user.name "Pterodactyl CI" - git checkout -b $BRANCH - git push -u origin $BRANCH - sed -i "s/var Version = \".*\"/var Version = \"${VERSION:1}\"/" system/const.go - git add system/const.go - git commit -m "ci(release): bump version" - git push - - - name: write changelog + set -euo pipefail + state="$(gh release view "$TAG" --json isDraft \ + --jq 'if .isDraft then "draft" else "published" end' 2>/dev/null || echo none)" + + if [ "$FORCE" = "true" ] && [ "$state" != "none" ]; then + echo "::warning::${TAG} already exists (${state}) — force re-release requested, deleting it" + gh release delete "$TAG" --yes --cleanup-tag + state=none + fi + + case "$state" in + none) + echo "no release for ${TAG} yet — cutting it" + ;; + draft) + echo "a draft for ${TAG} exists — resuming it" + ;; + published) + echo "${TAG} is already published — nothing to do" + echo "should_release=false" >> "$GITHUB_OUTPUT" + { + echo "### No release needed" + echo "" + echo "develop is on upstream \`${TAG}\`, which is already published." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + ;; + esac + + echo "should_release=true" >> "$GITHUB_OUTPUT" + + - name: Extract the release notes + if: steps.decide.outputs.should_release == 'true' + env: + TAG: ${{ steps.upstream.outputs.tag }} run: | - sed -n "/^## ${{ github.ref_name }}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG + set -euo pipefail + # Everything between this version's heading and the next one. The `$` + # anchor matters: without it `## v1.13.2` also matches `## v1.13.20`. + sed -n "/^## ${TAG}\$/,/^## /{/^## /b;p}" CHANGELOG.md > RELEASE_CHANGELOG + if ! grep -q '[^[:space:]]' RELEASE_CHANGELOG; then + echo "::error::CHANGELOG.md has no '## ${TAG}' section — refusing to publish a release with empty notes" + exit 1 + fi + cat RELEASE_CHANGELOG - - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + - name: Open the draft release + if: steps.decide.outputs.should_release == 'true' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ steps.upstream.outputs.tag }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + # No git tag is created here. A draft only records tag_name and + # target_commitish; GitHub creates the tag when the release is + # published. That keeps an aborted run from leaving a tag behind. + if gh release view "$TAG" >/dev/null 2>&1; then + gh release edit "$TAG" --notes-file RELEASE_CHANGELOG --target "$SHA" + else + gh release create "$TAG" --draft --title "$TAG" --notes-file RELEASE_CHANGELOG --target "$SHA" + fi + { + echo "### Draft ${TAG} opened" + echo "" + echo "Building the binaries and the image now. The release is published" + echo "once both are attached and verified." + } >> "$GITHUB_STEP_SUMMARY" + + binary: + name: Binary + needs: prepare + if: needs.prepare.outputs.should_release == 'true' + uses: ./.github/workflows/binary.yaml + permissions: + contents: write + with: + ref: ${{ needs.prepare.outputs.sha }} + release-tag: ${{ needs.prepare.outputs.tag }} + version: ${{ needs.prepare.outputs.version }} + + image: + name: Image + needs: prepare + if: needs.prepare.outputs.should_release == 'true' + uses: ./.github/workflows/docker.yaml + permissions: + contents: read + packages: write + with: + ref: ${{ needs.prepare.outputs.sha }} + version: ${{ needs.prepare.outputs.version }} + + # Runs only once BOTH the binaries and the image succeeded. This is the single + # point where anything becomes visible: :latest moves and the release leaves + # draft state. Until then a failure anywhere leaves the previous release and + # the previous :latest completely untouched. + publish: + name: Publish release + needs: [prepare, binary, image] + if: needs.prepare.outputs.should_release == 'true' + runs-on: ubuntu-24.04 + permissions: + contents: write + packages: write + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: - draft: true - prerelease: ${{ contains(github.ref_name, 'rc') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') }} - body_path: ./RELEASE_CHANGELOG - files: | - dist/wings_linux_amd64 - dist/wings_linux_arm64 + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Verify first: read-only, cheap, and independent of the retag. Doing it + # before anything moves means the only thing that can fail after :latest + # has moved is a single idempotent call, which a re-run repairs. + - name: Verify the release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ needs.prepare.outputs.tag }} + run: | + set -euo pipefail + names="$(gh release view "$TAG" --json assets --jq '.assets[].name')" + echo "attached assets:"; echo "$names" + for asset in wings_linux_amd64 wings_linux_arm64 SHA256SUMS; do + printf '%s\n' "$names" | grep -qxF "$asset" || { + echo "::error::release ${TAG} is missing asset '${asset}' — refusing to publish" + exit 1 + } + done + + # Retag by digest — no rebuild, and it cannot accidentally point at a + # different image than the one the `image` job just verified. The previous + # digest is logged so a manual rollback needs no archaeology. + - name: Move :latest to the verified release image + env: + IMAGE: ghcr.io/${{ github.repository }} + DIGEST: ${{ needs.image.outputs.digest }} + run: | + set -euo pipefail + test -n "$DIGEST" || { echo "::error::image job produced no digest"; exit 1; } + echo "previous :latest -> $(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "${IMAGE}:latest" 2>/dev/null || echo '(none)')" + docker buildx imagetools create --tag "${IMAGE}:latest" "${IMAGE}@${DIGEST}" + echo ":latest -> ${DIGEST}" + + - name: Publish the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ needs.prepare.outputs.tag }} + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + # Publishing is what finally creates the git tag, at the draft's + # target_commitish. + gh release edit "$TAG" --draft=false --latest + { + echo "### Published ${TAG}" + echo "" + echo "- Binaries: \`wings_linux_amd64\`, \`wings_linux_arm64\`, \`SHA256SUMS\`" + echo "- Image: \`ghcr.io/${{ github.repository }}:${VERSION}\` (also \`:${TAG}\` and \`:latest\`)" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/FORK_CHANGES.md b/FORK_CHANGES.md index 69190136f..70ba79069 100644 --- a/FORK_CHANGES.md +++ b/FORK_CHANGES.md @@ -96,7 +96,8 @@ our customizations are **not accidentally reverted** when pulling in upstream ch | Path | What | |------|------| | `.gitignore` | Fork-added `.claude-flow/`, `.hive-mind/`, `CLAUDE.md`. Upstream will never add these — keep on merge. | -| `.github/workflows/**`, `Makefile`, `Dockerfile` | Our build/release pipeline (with the renamed module path). | +| `Makefile`, `Dockerfile` | Our build settings (with the renamed module path). | +| `.github/workflows/{release,binary,docker}.yaml` | **Fork-specific release pipeline — always keep ours.** Upstream releases by hand: a human pushes a `v*` tag, `release.yaml` cuts a draft, a human publishes it. We release automatically from `develop` instead, and the version is derived from the newest **upstream** tag that is an ancestor of `develop` — so our releases always carry the upstream version number. Upstream's `release.yaml` has diverged beyond recognition; do not merge it. See the header comment in `release.yaml` for the full flow and recovery steps. | --- From 16ac89e4e5352e63af873cc0729e4620c472279d Mon Sep 17 00:00:00 2001 From: Sam Schumacher <38103916+HerrSammyDE@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:45:59 +0200 Subject: [PATCH 2/2] docs: add the v1.13.2 changelog entry Release notes are extracted from the matching CHANGELOG.md section, so without this the release for v1.13.2 would carry an empty body. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c781b35..e3fae999b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.13.2 +### Security +* Backup download, file download and file upload tokens are now checked against the revocation denylist. Previously only websocket tokens were, so revoking a user's access to a server left already-issued download and upload links working until they expired. +* Tokens that are missing the claims needed for that check (`iat`, `server_uuid`, `user_uuid`) are now rejected outright instead of being accepted. + +### Changed +* Backup downloads, file downloads and file uploads now require a Panel that sends the new `user_uuid` claim in those tokens. **Update the Panel before Wings** — against an older Panel these requests return `404`. + ## v1.13.1 ### Security * Backup restore downloads are now hardened against SSRF: remote restore links are validated and may not resolve to private, loopback, link-local or other blocked address ranges unless permitted via the new `restore_host_allowlist` config option.