Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Does NOT run the public release pipeline (Docker :latest, Snap stable, Latest GH release).
# Tag format must include a suffix after the semver patch, e.g. v1.149.0-beta.1
# (public release.yml only matches v[1-9].[0-9]+.[0-9]+).


name: beta-release

on:
workflow_dispatch:
inputs:
tag:
description: 'Existing annotated tag to release (e.g. v1.149.0-beta.1)'
required: true
type: string
push:
tags:
- 'v[1-9].[0-9]+.[0-9]+-*'

permissions:
contents: write

jobs:
integration-tests:
name: 'Integration Tests'
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}
fetch-depth: 0

- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.24.x

- name: Run integration tests
run: make test_integration

beta-release:
name: 'Beta Release'
needs: integration-tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}
fetch-depth: 0

- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.24.x

- name: Run GoReleaser (beta)
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: "~> v2"
args: release --clean -f .goreleaser.beta.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
with:
name: doctl-beta-${{ github.event.inputs.tag || github.ref_name }}
path: dist/*
retention-days: 90
if-no-files-found: error

# HEAD-checks every artifact at the URL pattern action-doctl uses, so
# customers pinning beta versions don't silently 404 if archive naming drifts.
verify-action-doctl-urls:
name: 'Verify action-doctl URL contract'
needs: beta-release
runs-on: ubuntu-latest
steps:
- name: HEAD-check every archive at the action-doctl URL pattern
run: |
set -euo pipefail
TAG="${{ github.event.inputs.tag || github.ref_name }}"
VER="${TAG#v}"
BASE="https://github.com/digitalocean/doctl/releases/download/${TAG}"

# Mirror of action-doctl/main.js platform/arch switches:
# process.platform: darwin | linux | win32(=>windows, ext=zip)
# process.arch: arm64 | x64(=>amd64) | ia32(=>386)
urls=(
"${BASE}/doctl-${VER}-darwin-amd64.tar.gz"
"${BASE}/doctl-${VER}-darwin-arm64.tar.gz"
"${BASE}/doctl-${VER}-linux-amd64.tar.gz"
"${BASE}/doctl-${VER}-linux-arm64.tar.gz"
"${BASE}/doctl-${VER}-linux-386.tar.gz"
"${BASE}/doctl-${VER}-windows-amd64.zip"
"${BASE}/doctl-${VER}-windows-arm64.zip"
"${BASE}/doctl-${VER}-windows-386.zip"
)

fail=0
for url in "${urls[@]}"; do
if curl --fail --silent --show-error --location --head "${url}" --output /dev/null; then
echo "ok ${url}"
else
echo "FAIL ${url}"
fail=1
fi
done

if [[ "${fail}" -ne 0 ]]; then
echo
echo "One or more artifact URLs do not match the action-doctl URL pattern."
echo "Check archives.name_template in .goreleaser.beta.yml — it must produce"
echo " doctl-<Version>-<Os>-<Arch>.{tar.gz|zip}"
echo "(no leading 'v' on the version segment of the filename)."
exit 1
fi

- name: Smoke-test linux-amd64 binary
run: |
set -euo pipefail
TAG="${{ github.event.inputs.tag || github.ref_name }}"
VER="${TAG#v}"
URL="https://github.com/digitalocean/doctl/releases/download/${TAG}/doctl-${VER}-linux-amd64.tar.gz"

curl --fail --silent --show-error --location "${URL}" --output /tmp/doctl.tgz
tar -xzf /tmp/doctl.tgz -C /tmp
chmod +x /tmp/doctl

out="$(/tmp/doctl version)"
echo "${out}"

# Binary's Version.String() is "Major.Minor.Patch-Label" with
# Label=beta (set in .goreleaser.beta.yml ldflags). The trailing
# ".N" of the tag is not embedded in the binary, so assert the
# base-and-label portion only.
base="${VER%%-*}"
expected="${base}-beta"
if ! grep -F -- "${expected}" <<<"${out}" >/dev/null; then
echo
echo "Smoke test failed: expected 'doctl version' output to contain '${expected}', got:"
echo "${out}"
exit 1
fi
50 changes: 50 additions & 0 deletions .goreleaser.beta.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Customer beta releases. Used by .github/workflows/beta-release.yml
# only — not by the public tagged-release workflow.
project_name: doctl
version: 2
builds:
- main: ./cmd/doctl/main.go
env:
- CGO_ENABLED=0
- GO111MODULE=on
flags:
- -mod=vendor
- -v
ldflags:
- -X github.com/digitalocean/doctl.Build={{ .ShortCommit }}
- -X github.com/digitalocean/doctl.Major={{ .Major }}
- -X github.com/digitalocean/doctl.Minor={{ .Minor }}
- -X github.com/digitalocean/doctl.Patch={{ .Patch }}
- -X github.com/digitalocean/doctl.Label=beta
goos:
- windows
- darwin
- linux
ignore:
- goos: darwin
goarch: 386

archives:
- name_template: "doctl-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
format_overrides:
- goos: windows
format: zip
files: [a-workaround-to-include-only-the-binary*]
wrap_in_directory: false

source:
enabled: true
name_template: 'doctl-{{ .Version }}-source'

checksum:
name_template: "doctl-{{ .Version }}-checksums.sha256"

Comment thread
anup-deka marked this conversation as resolved.
release:
github:
owner: digitalocean
name: doctl
prerelease: true
make_latest: false

changelog:
disable: false
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [Build Scripts](#build-scripts)
- [Releasing](#releasing)
- [Tagging a release](#tagging-a-release)
- [Cutting a beta release](#cutting-a-beta-release)
- [Snap](#snap)
- [Updating Homebrew](#updating-homebrew)

Expand Down Expand Up @@ -223,3 +224,29 @@ Notes on `BUMP=(bugfix|feature|breaking) make tag`:

To learn a bit more about how that all works, check out [goreleaser](https://goreleaser.com/intro)
and the config we use for it: [.goreleaser.yml](https://github.com/digitalocean/doctl/blob/main/.goreleaser.yml)

### Cutting a beta release

Beta tags publish as GitHub prereleases (not marked Latest, not pushed to Docker Hub or Snap stable), so `action-doctl`'s `version: latest` continues to fetch GA only; customers consume betas by explicit version pin.

1. Push your beta work to its branch. The repo must be clean and in sync with `origin`.

1. Synchronize tags from the remote.
```bash
git fetch --tags origin
```

1. Tag the beta using `BUMP=(bugfix|feature|breaking) make beta_tag`.
Example:

```bash
make beta_tag
```

Notes on `BUMP=(bugfix|feature|breaking) make beta_tag`:
- BUMP accepts: `bugfix`, `feature`, `breaking` as well as `patch`, `minor` and `major` values. Default is `minor` (`make tag` defaults to `patch`).
- The script computes `bump(BUMP, latest_GA_tag)` then auto-increments `-beta.N`. Do not pass `BETA=` manually.
- The command assumes you have a remote repository named `origin` pointing to this repository. If you'd prefer to specify a different remote repository, you can do so by setting `ORIGIN=(preferred remote name)`.
- The new tag triggers the [`beta-release`](.github/workflows/beta-release.yml) workflow.

the beta config we use: [.goreleaser.beta.yml](https://github.com/digitalocean/doctl/blob/main/.goreleaser.beta.yml).
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ tag:
@echo ""
@ORIGIN=${ORIGIN} scripts/bumpversion.sh

.PHONY: beta_tag
beta_tag:
@echo "==> beta_tag (BUMP defaults to minor; pass BUMP=patch|major to override)"
@echo ""
@ORIGIN=${ORIGIN} scripts/beta_tag.sh

.PHONY: _release
_release:
@echo "=> releasing"
Expand Down
89 changes: 89 additions & 0 deletions scripts/beta_tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash

set -euo pipefail

ORIGIN=${ORIGIN:-origin}

# Aliases: bugfix=patch, feature=minor, breaking=major.
# Default for beta is "minor" because beta builds usually preview a new minor.
BUMP=${BUMP:-minor}

set +e
git fetch --tags "${ORIGIN}" &>/dev/null
set -e

# Latest GA tag only (vX.Y.Z) — beta tags must not affect the bump base.
latest_ga_tag="$(git tag -l | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)"
if [[ -z "${latest_ga_tag}" ]]; then
echo "Error: no GA tag found (expected format vX.Y.Z)"
exit 1
fi
latest_ga="${latest_ga_tag#v}"

IFS='.' read -r major minor patch <<< "${latest_ga}"
case "${BUMP}" in
bugfix | patch)
patch=$((patch + 1))
;;
feature | minor)
minor=$((minor + 1))
patch=0
;;
breaking | major)
major=$((major + 1))
minor=0
patch=0
;;
*)
echo "Error: invalid BUMP='${BUMP}'."
echo "Use one of: bugfix, patch, feature, minor, breaking, major."
exit 1
;;
esac
target_base="${major}.${minor}.${patch}"

# Next beta number for this base (auto-increment; no manual BETA=).
base_escaped="${target_base//./\\.}"
max_beta=0
existing_beta_tags=()
while IFS= read -r tag; do
[[ -z "${tag}" ]] && continue
if [[ "${tag}" =~ ^v${base_escaped}-beta\.([0-9]+)$ ]]; then
num="${BASH_REMATCH[1]}"
existing_beta_tags+=("${tag}")
if (( num > max_beta )); then
max_beta="${num}"
fi
fi
done < <(git tag -l | grep -E "^v${base_escaped}-beta\\.[0-9]+$" | sort -V)

beta_num=$((max_beta + 1))
new_tag="v${target_base}-beta.${beta_num}"

if [[ $(git status --porcelain) != "" ]]; then
echo "Error: repo is dirty. Run git status, clean repo and try again."
exit 1
elif [[ $(git status --porcelain -b | grep -e "ahead" -e "behind") != "" ]]; then
echo "Error: repo has unpushed commits. Push commits to remote and try again."
exit 1
fi

if git rev-parse "${new_tag}" >/dev/null 2>&1; then
echo "Error: tag ${new_tag} already exists."
if ((${#existing_beta_tags[@]} > 0)); then
echo "Existing beta tags for v${target_base}: ${existing_beta_tags[*]}"
fi
echo "Delete the duplicate tag locally/remotely, or fetch tags from ${ORIGIN}, then retry."
exit 1
fi

echo "Latest GA: ${latest_ga_tag} → BUMP=${BUMP} → beta base: v${target_base}"
if ((${#existing_beta_tags[@]} > 0)); then
echo "Existing betas: ${existing_beta_tags[*]} → next: ${new_tag}"
else
echo "No existing beta tags for v${target_base} → creating ${new_tag}"
fi
echo "Creating beta tag ${new_tag} (pushes to ${ORIGIN})"
git tag -m "customer beta release ${new_tag}" -a "${new_tag}"
git push "${ORIGIN}" tag "${new_tag}"
echo "Pushed ${new_tag}. Watch: Actions -> beta-release"
7 changes: 5 additions & 2 deletions scripts/bumpversion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ ORIGIN=${ORIGIN:-origin}

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
version="$("$DIR"/../scripts/version.sh -s)"
if [[ -z "$version" ]]; then
echo "Error: no GA tag found (expected format vX.Y.Z)"
exit 1
fi
IFS='.' read -r major minor patch <<< "$version"

# Bump defaults to patch. We provide friendly aliases
Expand Down Expand Up @@ -39,5 +43,4 @@ new_version="v${major}.${minor}.${patch}"

git tag -m "release ${new_version}" -a "$new_version" && git push "${ORIGIN}" tag "$new_version"

echo "Bumped version to ${new_version}"

echo "Bumped version to ${new_version}"
10 changes: 9 additions & 1 deletion scripts/version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ Options:
"

semver() {
git tag -l | sort --version-sort | tail -n1 | cut -c 2-
# Prefer the latest GA tag (vX.Y.Z). Beta tags must not be reported as
# "latest" so callers like snap/snapcraft.yaml and scripts/_build.sh keep
# producing GA-based versions while a beta line is in flight.
local v
v="$(git tag -l | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort --version-sort | tail -n1 | cut -c 2-)"
if [[ -z "$v" ]]; then
v="$(git tag -l | sort --version-sort | tail -n1 | cut -c 2-)"
fi
echo "$v"
}

branch() {
Expand Down
Loading