From 415760f2250812e08c4e53a9c41814bcdc72790d Mon Sep 17 00:00:00 2001 From: Mohammad Aziz Date: Sun, 25 Jan 2026 17:33:58 +0530 Subject: [PATCH] Change release process with draft first approach --- .github/workflows/release.yml | 161 +++++++++++++++++++++++++++++++--- .goreleaser.yaml | 10 ++- CONTRIBUTING.md | 26 ++++++ scripts/linux/install.sh | 4 + 4 files changed, 187 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d8a9da..5dca56a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,41 +1,180 @@ # .github/workflows/release.yml -name: goreleaser +name: release on: push: # run only against tags tags: - - "*" + - "v*" permissions: contents: write - # packages: write - # issues: write - # id-token: write jobs: goreleaser: runs-on: ubuntu-latest + outputs: + tag: ${{ steps.get_tag.outputs.tag }} steps: + - name: Get tag + id: get_tag + run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Set up Go uses: actions/setup-go@v5 with: go-version: stable - # More assembly might be required: Docker logins, GPG, etc. - # It all depends on your needs. + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: - # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser - # 'latest', 'nightly', or a semver version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution - # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + + verify-and-publish: + needs: goreleaser + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.goreleaser.outputs.tag }} + steps: + - name: Get release info + id: release + run: | + echo "Fetching draft release for tag: $TAG" + RELEASE_ID=$(gh api repos/${{ github.repository }}/releases \ + --jq ".[] | select(.tag_name == \"$TAG\" and .draft == true) | .id") + + if [ -z "$RELEASE_ID" ]; then + echo "ERROR: No draft release found for tag $TAG" + exit 1 + fi + + echo "Found draft release ID: $RELEASE_ID" + echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT + + - name: Verify asset count + run: | + echo "Verifying assets for release ID: ${{ steps.release.outputs.release_id }}" + + ASSETS=$(gh api repos/${{ github.repository }}/releases/${{ steps.release.outputs.release_id }}/assets) + ASSET_COUNT=$(echo "$ASSETS" | jq length) + + echo "Found $ASSET_COUNT assets" + echo "$ASSETS" | jq -r '.[].name' + + # Expect: hostlink_Linux_x86_64.tar.gz, hostlink_Linux_arm64.tar.gz, checksums.txt + if [ "$ASSET_COUNT" -lt 3 ]; then + echo "ERROR: Expected at least 3 assets, found $ASSET_COUNT" + exit 1 + fi + + # Verify required assets exist + for asset in "hostlink_Linux_x86_64.tar.gz" "hostlink_Linux_arm64.tar.gz" "checksums.txt"; do + if ! echo "$ASSETS" | jq -e ".[] | select(.name == \"$asset\")" > /dev/null; then + echo "ERROR: Missing required asset: $asset" + exit 1 + fi + done + + echo "All required assets present" + + - name: Download assets + run: | + mkdir -p assets + cd assets + + RELEASE_ID="${{ steps.release.outputs.release_id }}" + ASSETS=$(gh api repos/${{ github.repository }}/releases/$RELEASE_ID/assets) + + for asset in "hostlink_Linux_x86_64.tar.gz" "hostlink_Linux_arm64.tar.gz" "checksums.txt"; do + echo "Downloading $asset..." + ASSET_ID=$(echo "$ASSETS" | jq -r ".[] | select(.name == \"$asset\") | .id") + gh api repos/${{ github.repository }}/releases/assets/$ASSET_ID \ + -H "Accept: application/octet-stream" > "$asset" + echo "Downloaded $asset ($(stat -c%s "$asset") bytes)" + done + + - name: Validate gzip files + run: | + cd assets + + for tarball in hostlink_Linux_x86_64.tar.gz hostlink_Linux_arm64.tar.gz; do + echo "Validating $tarball..." + + # Check gzip magic bytes (1f8b) + MAGIC=$(xxd -p -l 2 "$tarball") + if [ "$MAGIC" != "1f8b" ]; then + echo "ERROR: $tarball is not a valid gzip file (magic: $MAGIC)" + exit 1 + fi + + # Verify it can be listed + if ! tar -tzf "$tarball" > /dev/null 2>&1; then + echo "ERROR: $tarball cannot be read by tar" + exit 1 + fi + + echo "$tarball is valid" + done + + - name: Verify checksums + run: | + cd assets + echo "Verifying SHA256 checksums..." + + # checksums.txt format: + sha256sum -c checksums.txt + + echo "All checksums verified" + + - name: Test binary execution + run: | + cd assets + echo "Testing x86_64 binary..." + + mkdir -p test + tar -xzf hostlink_Linux_x86_64.tar.gz -C test + + # Run version check + if ! ./test/hostlink --version; then + echo "ERROR: hostlink --version failed" + exit 1 + fi + + echo "Binary execution test passed" + + - name: Publish release + run: | + RELEASE_ID="${{ steps.release.outputs.release_id }}" + + # Retry logic with exponential backoff + MAX_RETRIES=3 + DELAYS=(5 15 45) + + for i in $(seq 0 $((MAX_RETRIES - 1))); do + echo "Attempt $((i + 1)) of $MAX_RETRIES: Publishing release..." + + if gh api repos/${{ github.repository }}/releases/$RELEASE_ID \ + -X PATCH -f draft=false; then + echo "Release published successfully!" + exit 0 + fi + + if [ $i -lt $((MAX_RETRIES - 1)) ]; then + DELAY=${DELAYS[$i]} + echo "Publish failed, retrying in ${DELAY}s..." + sleep $DELAY + fi + done + + echo "ERROR: Failed to publish release after $MAX_RETRIES attempts" + exit 1 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3fc04b6..6d00fc0 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -14,9 +14,7 @@ before: - go mod tidy # you may remove this if you don't need go generate - go generate ./... - # Run tests before building - - make test - - make test-it + # Note: Tests run on PR (test.yml), not during release builds: - id: hostlink @@ -48,6 +46,10 @@ archives: dst: scripts strip_parent: true +checksum: + name_template: 'checksums.txt' + algorithm: sha256 + changelog: sort: asc filters: @@ -56,6 +58,8 @@ changelog: - "^test:" release: + # Create as draft first, verify assets, then publish + draft: true footer: >- --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89cf1ed..49df41a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,6 +128,32 @@ go run main.go go test -tags=smoke ./test/smoke -run TestHlctl ``` +## Release Process + +Releases are automated via GitHub Actions and triggered by pushing a version tag. + +### Creating a Release + +1. Ensure all changes are merged to `main` +2. Create and push a version tag: + ```bash + git tag v1.2.3 + git push origin v1.2.3 + ``` +3. The CI workflow will automatically: + - Build binaries for linux/amd64 and linux/arm64 + - Create a **draft** release with all assets + - Verify all assets are valid (checksums, gzip integrity, binary execution) + - Publish the release (make it visible) + +4. Monitor the [Actions tab](https://github.com/selfhost-dev/hostlink/actions) for workflow status + +### Important Notes + +- **Do NOT create releases manually via GitHub UI.** The automated workflow ensures all release assets are verified before becoming visible to users. Manual releases bypass this verification and may cause installation failures. +- Tags must follow semver format: `v1.0.0`, `v1.2.3`, etc. +- If verification fails, the release remains as a draft for debugging. Check the workflow logs and re-trigger if needed. + ### Questions or Issues? Feel free to open an issue on GitHub or reach out to the maintainers. diff --git a/scripts/linux/install.sh b/scripts/linux/install.sh index 4ef52c7..8c5efd3 100755 --- a/scripts/linux/install.sh +++ b/scripts/linux/install.sh @@ -262,6 +262,10 @@ fi HOSTLINK_TAR=hostlink_$VERSION.tar.gz +# Download the release tarball with retry logic. +# Note: Retry logic handles transient network failures. The release process +# ensures assets exist before the version is visible to /releases/latest API, +# so 404 errors during the publish window are not expected. download_tar() { local download_url="https://github.com/selfhost-dev/hostlink/releases/download/${VERSION}/hostlink_Linux_${ARCH}.tar.gz" local tar_file="$TEMP_DIR/$HOSTLINK_TAR"