mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
Allow normal post-tag proxy propagation before dispatching downstream releases, while preserving the check that prevents them from pinning the previous commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
279 lines
11 KiB
YAML
279 lines
11 KiB
YAML
name: "release: bump version and cut the release"
|
|
|
|
# One entry point for a SeaweedFS release:
|
|
# 1. bump MAJOR/MINOR in constants.go and the Helm Chart.yaml, commit to master
|
|
# 2. push the <appVersion> tag, which fans out to the workflows that trigger on
|
|
# `push: tags` (binaries_release*, container_release_unified, helm_manual_release)
|
|
# 3. create the GitHub release, with GitHub's generated notes
|
|
# 4. dispatch "Prepare release" in seaweedfs-csi-driver and seaweedfs-operator,
|
|
# which pick up the new master through `go get -u`, and wait for both
|
|
#
|
|
# Events raised by the default GITHUB_TOKEN do not start other workflows, and it
|
|
# cannot reach the other two repositories at all. Add a repo secret RELEASE_PAT
|
|
# with `contents: write` here and `actions: write` on the csi-driver and operator
|
|
# repos. Without it the tag is still pushed, but nothing downstream of it runs.
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
bump:
|
|
description: "Which part to increment (ignored when 'version' is set)"
|
|
type: choice
|
|
options:
|
|
- minor
|
|
- major
|
|
default: minor
|
|
version:
|
|
description: "Explicit MAJOR.MINOR to set, e.g. 4.36 (overrides 'bump')"
|
|
type: string
|
|
required: false
|
|
downstream:
|
|
description: "Also release the CSI driver and the operator"
|
|
type: boolean
|
|
default: true
|
|
dry_run:
|
|
description: "Show the version bump, but change nothing"
|
|
type: boolean
|
|
default: false
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
app_version: ${{ steps.compute.outputs.app_version }}
|
|
sha: ${{ steps.tag.outputs.sha }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: master
|
|
fetch-depth: 0
|
|
token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Check the release token
|
|
env:
|
|
HAS_PAT: ${{ secrets.RELEASE_PAT != '' }}
|
|
run: |
|
|
if [ "$HAS_PAT" != "true" ]; then
|
|
echo "::warning::RELEASE_PAT is not set. The tag will be pushed with GITHUB_TOKEN, so the binary, container and helm workflows will not start on their own."
|
|
fi
|
|
|
|
- name: Compute new version
|
|
id: compute
|
|
env:
|
|
BUMP: ${{ inputs.bump }}
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
CONST=weed/util/version/constants.go
|
|
|
|
MAJOR=$(grep -oP 'MAJOR_VERSION\s*=\s*int32\(\K[0-9]+' "$CONST")
|
|
MINOR=$(grep -oP 'MINOR_VERSION\s*=\s*int32\(\K[0-9]+' "$CONST")
|
|
echo "current: ${MAJOR}.${MINOR}"
|
|
|
|
if [ -n "$INPUT_VERSION" ]; then
|
|
if ! [[ "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
|
echo "::error::version must be MAJOR.MINOR, e.g. 5.01 (got '$INPUT_VERSION')"
|
|
exit 1
|
|
fi
|
|
# 10# forces base 10 so 08/09 are not parsed as octal.
|
|
MAJOR=$((10#${INPUT_VERSION%%.*}))
|
|
MINOR=$((10#${INPUT_VERSION##*.}))
|
|
if [ "$MINOR" -gt 99 ]; then
|
|
echo "::error::minor must be 0-99 (got $MINOR); it rolls into the next major at 99"
|
|
exit 1
|
|
fi
|
|
else
|
|
case "$BUMP" in
|
|
# Minor is a 2-digit field: 4.99 -> 5.00 -> 5.01.
|
|
major) MAJOR=$((MAJOR + 1)); MINOR=0 ;;
|
|
minor)
|
|
if [ "$MINOR" -ge 99 ]; then
|
|
MAJOR=$((MAJOR + 1)); MINOR=0
|
|
else
|
|
MINOR=$((MINOR + 1))
|
|
fi
|
|
;;
|
|
*) echo "::error::unknown bump '$BUMP'"; exit 1 ;;
|
|
esac
|
|
fi
|
|
|
|
# appVersion mirrors the Go VERSION_NUMBER (zero-padded minor);
|
|
# chart version is plain SemVer (no leading zeros).
|
|
APP_VERSION=$(printf '%d.%02d' "$MAJOR" "$MINOR")
|
|
CHART_VERSION="${MAJOR}.${MINOR}.0"
|
|
echo "new: app=${APP_VERSION} chart=${CHART_VERSION}"
|
|
|
|
{
|
|
echo "major=${MAJOR}"
|
|
echo "minor=${MINOR}"
|
|
echo "app_version=${APP_VERSION}"
|
|
echo "chart_version=${CHART_VERSION}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Apply version to constants.go
|
|
env:
|
|
MAJOR: ${{ steps.compute.outputs.major }}
|
|
MINOR: ${{ steps.compute.outputs.minor }}
|
|
run: |
|
|
set -euo pipefail
|
|
CONST=weed/util/version/constants.go
|
|
sed -i -E "s/(MAJOR_VERSION[[:space:]]*=[[:space:]]*int32\()[0-9]+(\))/\1${MAJOR}\2/" "$CONST"
|
|
sed -i -E "s/(MINOR_VERSION[[:space:]]*=[[:space:]]*int32\()[0-9]+(\))/\1${MINOR}\2/" "$CONST"
|
|
grep -E 'MAJOR_VERSION|MINOR_VERSION' "$CONST"
|
|
|
|
- name: Apply version to Chart.yaml
|
|
env:
|
|
APP_VERSION: ${{ steps.compute.outputs.app_version }}
|
|
CHART_VERSION: ${{ steps.compute.outputs.chart_version }}
|
|
run: |
|
|
set -euo pipefail
|
|
CHART=k8s/charts/seaweedfs/Chart.yaml
|
|
sed -i -E "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" "$CHART"
|
|
sed -i -E "s/^version:.*/version: ${CHART_VERSION}/" "$CHART"
|
|
cat "$CHART"
|
|
|
|
- name: Commit, and push the tag
|
|
id: tag
|
|
env:
|
|
TAG: ${{ steps.compute.outputs.app_version }}
|
|
DRY_RUN: ${{ inputs.dry_run }}
|
|
run: |
|
|
set -euo pipefail
|
|
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
|
|
echo "::error::Tag ${TAG} already exists."
|
|
exit 1
|
|
fi
|
|
if [ "$DRY_RUN" = "true" ]; then
|
|
git --no-pager diff --stat
|
|
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
if git diff --quiet; then
|
|
echo "::warning::Version files are already at ${TAG}; tagging the current HEAD."
|
|
else
|
|
git add weed/util/version/constants.go k8s/charts/seaweedfs/Chart.yaml
|
|
git commit -m "${TAG}"
|
|
git push
|
|
fi
|
|
|
|
# Push the tag with git so the `push: tags` triggers fire. Creating the
|
|
# tag through the release API alone would only emit a `create` event.
|
|
git tag "$TAG"
|
|
git push origin "$TAG"
|
|
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Create the release
|
|
if: ${{ !inputs.dry_run }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ steps.compute.outputs.app_version }}
|
|
run: gh release create "$TAG" --title "$TAG" --generate-notes --verify-tag
|
|
|
|
downstream:
|
|
needs: release
|
|
if: ${{ inputs.downstream && !inputs.dry_run }}
|
|
runs-on: ubuntu-latest
|
|
# The wait below puts no bound of its own on runner-queue time; this does.
|
|
timeout-minutes: 120
|
|
permissions: {}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- repo: seaweedfs/seaweedfs-csi-driver
|
|
workflow: prepare_release.yaml
|
|
- repo: seaweedfs/seaweedfs-operator
|
|
workflow: prepare_release.yml
|
|
steps:
|
|
- name: Release ${{ matrix.repo }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
|
REPO: ${{ matrix.repo }}
|
|
WORKFLOW: ${{ matrix.workflow }}
|
|
SHA: ${{ needs.release.outputs.sha }}
|
|
MODULE: github.com/seaweedfs/seaweedfs
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${GH_TOKEN}" ]; then
|
|
echo "::error::RELEASE_PAT with actions:write on ${REPO} is required to release it"
|
|
exit 1
|
|
fi
|
|
|
|
# The dispatched workflow pins seaweedfs with `go get -u ...@latest`, so
|
|
# wait until the proxy serves the release commit as the tip. Asking for
|
|
# the commit by name is what makes the proxy fetch it. The proxy can
|
|
# take longer than five minutes to refresh @latest after a new tag.
|
|
for _ in $(seq 120); do
|
|
curl -sf "https://proxy.golang.org/${MODULE}/@v/${SHA}.info" >/dev/null || true
|
|
TIP=$(curl -sf "https://proxy.golang.org/${MODULE}/@latest" | jq -r '.Origin.Hash // ""' || true)
|
|
[ "$TIP" = "$SHA" ] && break
|
|
sleep 10
|
|
done
|
|
if [ "$TIP" != "$SHA" ]; then
|
|
echo "::error::the module proxy still serves ${TIP} as the tip, so ${REPO} would pin a pre-release commit. Run ${WORKFLOW} there once it catches up."
|
|
exit 1
|
|
fi
|
|
|
|
# The dispatched workflow publishes the release as its last step, so
|
|
# its conclusion decides success. Wait on the run, not on a wall
|
|
# clock: time it spends queued for a runner must not count against
|
|
# the budget. A dispatch does not return its run id, so take the
|
|
# newest workflow_dispatch run created since ours; a concurrent
|
|
# dispatch would be performing this same release, and waiting on it
|
|
# is just as good.
|
|
released() { gh api "repos/${REPO}/releases?per_page=30" --jq '[.[].tag_name]'; }
|
|
|
|
BEFORE=$(released)
|
|
# A minute early, so runner clock skew cannot hide the run.
|
|
DISPATCHED_AT=$(date -u -d '1 minute ago' '+%Y-%m-%dT%H:%M:%SZ')
|
|
gh workflow run -R "$REPO" "$WORKFLOW" --ref master -f bump=patch -f update_seaweedfs=true
|
|
|
|
RUN_ID=""
|
|
for _ in $(seq 12); do
|
|
sleep 10
|
|
RUN_ID=$(gh api -X GET "repos/${REPO}/actions/workflows/${WORKFLOW}/runs" \
|
|
-f event=workflow_dispatch -f "created=>=${DISPATCHED_AT}" \
|
|
--jq '(.workflow_runs | sort_by(.created_at) | last | .id) // empty' || true)
|
|
[ -n "$RUN_ID" ] && break
|
|
done
|
|
if [ -z "$RUN_ID" ]; then
|
|
echo "::error::the dispatch created no ${WORKFLOW} run in ${REPO}; see https://github.com/${REPO}/actions/workflows/${WORKFLOW}"
|
|
exit 1
|
|
fi
|
|
RUN_URL="https://github.com/${REPO}/actions/runs/${RUN_ID}"
|
|
echo "waiting on ${RUN_URL}"
|
|
|
|
# 20 minutes of execution; polls that find the run still queued do
|
|
# not consume it.
|
|
RUNNING=0
|
|
STATE=""
|
|
while :; do
|
|
sleep 15
|
|
STATE=$(gh api "repos/${REPO}/actions/runs/${RUN_ID}" \
|
|
--jq '.status + "/" + (.conclusion // "")' || true)
|
|
case "$STATE" in
|
|
completed/*) break ;;
|
|
in_progress/*) RUNNING=$((RUNNING + 1)) ;;
|
|
esac
|
|
if [ "$RUNNING" -gt 80 ]; then
|
|
echo "::error::${RUN_URL} has been executing for over 20 minutes; giving up on it"
|
|
exit 1
|
|
fi
|
|
done
|
|
if [ "$STATE" != "completed/success" ]; then
|
|
echo "::error::${RUN_URL} concluded '${STATE#completed/}'"
|
|
exit 1
|
|
fi
|
|
|
|
NEW=$(released | jq -c --argjson before "$BEFORE" '. - $before')
|
|
if [ "$(jq length <<<"$NEW")" -eq 0 ]; then
|
|
echo "::error::${RUN_URL} succeeded but ${REPO} shows no new release"
|
|
exit 1
|
|
fi
|
|
echo "${REPO} released $(jq -r 'join(", ")' <<<"$NEW")"
|