From 46baf1e0188421bf2d817ee1f3fe4212a9297865 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 20 May 2026 17:36:45 +0545 Subject: [PATCH] fix: avoid immutable release races during asset upload Release asset uploads were running from parallel matrix jobs. With immutable GitHub releases, the first matrix job to publish the release locked the asset list, causing later uploads to fail. Switching uploads to draft releases by tag also created multiple draft releases for the same tag. Use semantic-release to create one draft release, build all platform assets in one binary job, upload them with gh release upload, then publish the draft after uploads complete. --- .github/workflows/release.yml | 163 ++++++++++------------------------ .gitignore | 2 + .releaserc | 25 ++++++ Makefile | 69 ++++++++++++++ 4 files changed, 141 insertions(+), 118 deletions(-) create mode 100644 .releaserc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff80d61..6d679fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,148 +2,75 @@ name: Release on: push: - branches: [main, master] + branches: [main] permissions: - contents: write - packages: write - issues: write - pull-requests: write - id-token: write + contents: read env: - GO_VERSION: "1.25" + GO_VERSION: "1.26.x" jobs: - version: - name: Determine Version + semantic-release: + name: Semantic Release + permissions: + contents: write + issues: write + pull-requests: write runs-on: ubuntu-latest outputs: - version: ${{ steps.svu.outputs.version }} - tag: ${{ steps.svu.outputs.tag }} + release-version: ${{ steps.semantic.outputs.release-version }} + new-release-published: ${{ steps.semantic.outputs.new-release-published }} steps: - name: Checkout - uses: actions/checkout@v4 - - name: Install svu - run: | - curl -sL https://github.com/caarlos0/svu/releases/download/v3.2.4/svu_3.2.4_linux_amd64.tar.gz | tar xz - sudo mv svu /usr/local/bin/ - - - name: Get next version - id: svu - run: | - git fetch --tags - NEXT_VERSION=$(svu patch --tag.pattern "v*") - echo "version=${NEXT_VERSION#v}" >> $GITHUB_OUTPUT - echo "tag=$NEXT_VERSION" >> $GITHUB_OUTPUT - echo "Next version: $NEXT_VERSION" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 - - name: Create and push tag + - name: Create draft release + uses: codfish/semantic-release-action@6abd188d2458e2fd6c99073454f6cc49196362e8 # v5.0.0 + id: semantic env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag ${{ steps.svu.outputs.tag }} - git push origin ${{ steps.svu.outputs.tag }} - build: - name: Build - needs: version + + binary: + name: Build binaries + if: needs.semantic-release.outputs.new-release-published == 'true' + needs: semantic-release + permissions: + contents: write runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - goos: linux - goarch: amd64 - - goos: linux - goarch: arm64 - - goos: darwin - goarch: amd64 - - goos: darwin - goarch: arm64 - - goos: windows - goarch: amd64 steps: - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Build binary + - name: Build release assets + run: make release env: - CGO_ENABLED: 0 - run: | - VERSION="${{ needs.version.outputs.version }}" - GOOS="${{ matrix.goos }}" - GOARCH="${{ matrix.goarch }}" - - BINARY_NAME="deps-${GOOS}-${GOARCH}" - if [ "$GOOS" = "windows" ]; then - BINARY_NAME="$BINARY_NAME.exe" - fi - - GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" go build -ldflags="-s -w -X main.version=$VERSION -X main.commit=${{ github.sha }} -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - -o "$BINARY_NAME" ./cmd/deps/main.go - - - sha256sum "$BINARY_NAME" > "$BINARY_NAME".sha256 - - - name: Create archive - run: | - VERSION="${{ needs.version.outputs.version }}" - GOOS="${{ matrix.goos }}" - GOARCH="${{ matrix.goarch }}" - + VERSION: ${{ needs.semantic-release.outputs.release-version }} - ARCHIVE_NAME="deps-${GOOS}-${GOARCH}" - - BINARY_NAME="deps-${GOOS}-${GOARCH}" - if [ "$GOOS" = "windows" ]; then - BINARY_NAME="$BINARY_NAME.exe" - fi - - if [ "$GOOS" = "windows" ]; then - cp "$BINARY_NAME" deps.exe - zip "${ARCHIVE_NAME}.zip" deps.exe - rm deps.exe - sha256sum "${ARCHIVE_NAME}.zip" > "${ARCHIVE_NAME}.sha256" - else - cp "$BINARY_NAME" deps - tar czf "${ARCHIVE_NAME}.tar.gz" deps - rm deps - sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256" - fi - - - name: Upload binaries to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file_glob: true - file: deps-*.{exe,tar.gz,zip,sha256} - tag: ${{ needs.version.outputs.tag }} - draft: true - - release: - name: Create Release - needs: [version, build] + - name: Upload binaries to draft release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: v${{ needs.semantic-release.outputs.release-version }} + run: gh release upload "$TAG" ./.release/* --clobber --repo "${{ github.repository }}" + + publish-release: + name: Publish Release + if: needs.semantic-release.outputs.new-release-published == 'true' + needs: [semantic-release, binary] + permissions: + contents: write runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Update GitHub Release Notes + - name: Publish draft release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ needs.version.outputs.tag }} - run: | - NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes -f tag_name="$TAG" --jq '.body') - gh release edit "$TAG" --title "Release $TAG" --notes "$NOTES" --draft=false + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: v${{ needs.semantic-release.outputs.release-version }} + run: gh release edit "$TAG" --draft=false --repo "${{ github.repository }}" diff --git a/.gitignore b/.gitignore index 9198f44..efca1c4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ main *.test deps-test +.bin/ +.release/ # Generated directories bin/ diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..f5934a8 --- /dev/null +++ b/.releaserc @@ -0,0 +1,25 @@ +branches: + - name: main +plugins: + - - "@semantic-release/commit-analyzer" + - releaseRules: + - { type: build, release: patch } + - { type: chore, release: patch } + - { type: ci, release: patch } + - { type: doc, release: patch } + - { type: docs, release: patch } + - { type: feat, release: patch } + - { type: fix, release: patch } + - { type: perf, release: patch } + - { type: refactor, release: patch } + - { type: revert, release: patch } + - { type: style, release: patch } + - { type: test, release: patch } + parserOpts: + noteKeywords: + - MAJOR RELEASE + - "@semantic-release/release-notes-generator" + - - "@semantic-release/github" + - draftRelease: true + successComment: false + failTitle: false diff --git a/Makefile b/Makefile index 19c33fa..7b26245 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,18 @@ # Automatically installs task if not present SHELL := /bin/bash +NAME := deps +DATE := $(shell date "+%Y-%m-%d %H:%M:%S") +ifeq ($(VERSION),) +VERSION_TAG := $(shell git describe --abbrev=0 --tags --exact-match 2>/dev/null || echo latest) +else +VERSION_TAG := $(VERSION) +endif + TASK_VERSION := v3.39.2 +UPX_VERSION := 3.96 TASK_BIN := ./bin/task +UPX := ./.bin/upx UNAME_S := $(shell uname -s) UNAME_M := $(shell uname -m) @@ -29,6 +39,7 @@ endif # Task binary URL TASK_URL := https://github.com/go-task/task/releases/download/$(TASK_VERSION)/task_$(TASK_PLATFORM)_$(TASK_ARCH).tar.gz +SHA256 := $(shell command -v sha256sum >/dev/null 2>&1 && echo "sha256sum" || echo "shasum -a 256") # Ensure task is installed $(TASK_BIN): @@ -55,6 +66,64 @@ build: $(TASK_BIN) build-all: $(TASK_BIN) @$(TASK_BIN) build-all +.PHONY: linux +linux: + mkdir -p .bin + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./.bin/$(NAME)-linux-amd64 -ldflags "-X main.version=$(VERSION_TAG)" ./cmd/deps/main.go + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ./.bin/$(NAME)-linux-arm64 -ldflags "-X main.version=$(VERSION_TAG)" ./cmd/deps/main.go + +.PHONY: darwin +darwin: + mkdir -p .bin + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o ./.bin/$(NAME)-darwin-amd64 -ldflags "-X main.version=$(VERSION_TAG)" ./cmd/deps/main.go + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o ./.bin/$(NAME)-darwin-arm64 -ldflags "-X main.version=$(VERSION_TAG)" ./cmd/deps/main.go + +.PHONY: windows +windows: + mkdir -p .bin + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ./.bin/$(NAME)-windows-amd64.exe -ldflags "-X main.version=$(VERSION_TAG)" ./cmd/deps/main.go + +.PHONY: compress +compress: +ifeq ($(TASK_PLATFORM),linux) + $(MAKE) $(UPX) + $(UPX) -5 ./.bin/$(NAME)-linux-amd64 ./.bin/$(NAME)-linux-arm64 +else + @echo "Skipping upx compression on $(TASK_PLATFORM)" +endif + +.PHONY: binaries +binaries: linux darwin windows compress + +$(UPX): .bin + wget -nv -O upx.tar.xz https://github.com/upx/upx/releases/download/v$(UPX_VERSION)/upx-$(UPX_VERSION)-$(TASK_ARCH)_$(TASK_PLATFORM).tar.xz + tar xf upx.tar.xz + mv upx-$(UPX_VERSION)-$(TASK_ARCH)_$(TASK_PLATFORM)/upx .bin + rm -rf upx.tar.xz upx-$(UPX_VERSION)-$(TASK_ARCH)_$(TASK_PLATFORM) + +.bin: + mkdir -p .bin + +.PHONY: release +release: binaries + mkdir -p .release + rm -f .release/deps-* .release/deps .release/deps.exe + @for binary in .bin/$(NAME)-*; do \ + artifact=$$(basename "$$binary"); \ + archive_base="$${artifact%.exe}"; \ + (cd .bin && $(SHA256) "$$artifact") > ".release/$$artifact.sha256"; \ + if [[ "$$artifact" == *.exe ]]; then \ + cp "$$binary" ".release/$$artifact"; \ + cp "$$binary" .release/$(NAME).exe; \ + (cd .release && zip -q "$$archive_base.zip" $(NAME).exe && $(SHA256) "$$archive_base.zip" > "$$archive_base.sha256"); \ + rm -f .release/$(NAME).exe; \ + else \ + cp "$$binary" .release/$(NAME); \ + (cd .release && tar czf "$$archive_base.tar.gz" $(NAME) && $(SHA256) "$$archive_base.tar.gz" > "$$archive_base.tar.gz.sha256"); \ + rm -f .release/$(NAME); \ + fi; \ + done + # Test targets .PHONY: test test: $(TASK_BIN)