Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
148 changes: 148 additions & 0 deletions .github/workflows/release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Release Notes — Publish GitHub Release

# Automates the release-notes half of Release Guide Step 1, which is currently
# manual and has been skipped more often than not:
#
# v1.1.3 -> Release exists
# v1.1.1 -> tag exists, NO Release object
# v1.1.3-spark4.0 / -spark4.1 / -python3.* -> no Release object
#
# The gap is not cosmetic. GitHub anchors auto-generated notes to the previous
# *Release*, not the previous tag, so v1.1.1 having no Release made v1.1.3's
# notes span all of v1.1.1 + v1.1.3. This workflow computes the previous
# primary tag itself and pins the diff base, so notes stay correct even when
# an older Release object is missing.
#
# Only the primary vX.Y.Z tag gets a Release. The -spark*/-python* tags are
# build pointers consumed by ADO pipelines, not separate products.

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"

permissions:
contents: write

jobs:
publish-release:
name: Publish GitHub Release
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

# The tag filter above already excludes suffixed tags, but a tag such as
# v1.1.3-spark4.0 can still reach a `push` event through some ref
# rewrites, and minting a full Release for a build pointer would be wrong.
- name: Resolve version
id: v
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ ! "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Not a primary release tag: $REF_NAME — nothing to do."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "tag=$REF_NAME" >> "$GITHUB_OUTPUT"

# A release tag must describe a commit that is actually on master.
# Tagging a stale or side branch vX.Y.Z would publish notes for code that
# was never reviewed into the mainline.
- name: Verify the tag is on master
if: steps.v.outputs.skip == 'false'
env:
TAG: ${{ steps.v.outputs.tag }}
run: |
set -euo pipefail
git fetch --quiet origin master
if ! git merge-base --is-ancestor "refs/tags/${TAG}" origin/master; then
echo "::error::${TAG} does not point at a commit contained in master. \
Refusing to publish a release for an off-mainline commit."
exit 1
fi

# Pick the highest primary tag strictly below this one. `sort -V` gives
# correct numeric ordering (v1.1.10 > v1.1.9), which a lexical sort does not.
- name: Determine previous release tag
if: steps.v.outputs.skip == 'false'
id: prev
env:
TAG: ${{ steps.v.outputs.tag }}
run: |
set -euo pipefail
PREV=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V \
| awk -v cur="$TAG" '$0 == cur {exit} {last=$0} END {print last}')
if [ -z "$PREV" ]; then
echo "No earlier release tag found — notes will cover full history."
else
echo "Previous release: $PREV"
fi
echo "prev=$PREV" >> "$GITHUB_OUTPUT"

- name: Skip if the Release already exists
if: steps.v.outputs.skip == 'false'
id: exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.v.outputs.tag }}
run: |
set -euo pipefail
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $TAG already exists — leaving it untouched."
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi

- name: Generate and publish release notes
if: steps.v.outputs.skip == 'false' && steps.exists.outputs.found == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.v.outputs.tag }}
PREV: ${{ steps.prev.outputs.prev }}
run: |
set -euo pipefail

ARGS=(-f tag_name="$TAG" -f target_commitish=master)
if [ -n "$PREV" ]; then
ARGS+=(-f previous_tag_name="$PREV")
fi

NOTES=$(gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
-X POST "${ARGS[@]}" --jq '.body')

VERSION="${TAG#v}"
{
echo "## Installation"
echo
echo '```bash'
echo "pip install synapseml==${VERSION}"
echo '```'
echo
echo "Maven coordinate: \`com.microsoft.azure:synapseml_2.12:${VERSION}\`"
echo
echo "| Spark | Python | Tag |"
echo "| --- | --- | --- |"
echo "| 3.5 | 3.11 | \`${TAG}-spark3.5\` |"
echo "| 4.0 | 3.12 | \`${TAG}-spark4.0\` |"
echo "| 4.1 | 3.13 | \`${TAG}-spark4.1\` |"
echo
echo "$NOTES"
} > notes.md

gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--notes-file notes.md \
--verify-tag

echo "Published release $TAG (diff base: ${PREV:-<none>})"
191 changes: 191 additions & 0 deletions .github/workflows/release-prepare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Release Prepare — Version Bump PR

# Automates Release Guide Step 1.1-1.3: bump every version string in the repo,
# regenerate the versioned docs snapshot, and open the release PR.
#
# Two of the last four version bumps (v1.1.0, v1.1.3) landed as unsigned direct
# pushes to master with no PR and no review. This workflow makes the reviewed
# PR the only path, so a release can never again be an unreviewed force-push.
#
# See also: release-tag.yml (tags master once this PR merges).

on:
workflow_dispatch:
inputs:
version:
description: "New OSS version, three components (e.g. 1.1.4)"
required: true
type: string
skip_docs:
description: "Skip the versioned-docs snapshot (faster; PR will be incomplete)"
required: false
default: false
type: boolean

permissions:
contents: write
pull-requests: write

jobs:
prepare:
name: Bump versions & open release PR
runs-on: ubuntu-latest

steps:
# A release must be cut from master. Running this from a feature branch
# would open a PR that bumps versions against unreleased code.
- name: Validate ref
env:
REF: ${{ github.ref }}
run: |
set -euo pipefail
if [ "$REF" != "refs/heads/master" ]; then
echo "::error::Release prepare must run on master, got '$REF'."
exit 1
fi

- name: Validate version format
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Version '$VERSION' must be exactly X.Y.Z. SynapseML OSS \
does not use a fourth component; the .N super-patch belongs to SynapseML-Internal."
exit 1
fi

- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

# Refuse to re-prepare a version that is already published. Without this
# the workflow would happily open a PR that "bumps" to a shipped version.
- name: Guard against an already-released version
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
echo "::error::Tag v${VERSION} already exists. That version is already released."
exit 1
fi
if git ls-remote --exit-code --heads origin "release/prepare-v${VERSION}" >/dev/null 2>&1; then
echo "::error::Branch release/prepare-v${VERSION} already exists on origin. \
Delete it or finish the existing release PR first."
exit 1
fi

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

- name: Set up JDK 11
if: ${{ !inputs.skip_docs }}
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: 11
cache: sbt

- name: Install sbt
if: ${{ !inputs.skip_docs }}
run: |
SBT_VERSION="$(sed -n 's/^sbt.version *= *//p' project/build.properties | tr -d ' ')"
mkdir -p "$HOME/.local/bin"
curl -L -o "$HOME/.local/bin/sbt-launch.jar" \
"https://repo1.maven.org/maven2/org/scala-sbt/sbt-launch/${SBT_VERSION}/sbt-launch-${SBT_VERSION}.jar"
cat > "$HOME/.local/bin/sbt" <<EOF
#!/usr/bin/env bash
exec java -Dsbt.version="${SBT_VERSION}" -jar "$HOME/.local/bin/sbt-launch.jar" "\$@"
EOF
chmod +x "$HOME/.local/bin/sbt"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Set up Node
if: ${{ !inputs.skip_docs }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: "npm"
cache-dependency-path: website/package-lock.json

- name: Install website dependencies
if: ${{ !inputs.skip_docs }}
working-directory: website
run: npm ci

# bump-version.py is context-anchored: it refuses to replace a bare version
# number that has no SynapseML-identifying text near it, and exits non-zero
# if any anchored occurrence of the old version survives the run. Both
# behaviours are load-bearing here, so the exit code is not suppressed.
- name: Bump version strings
id: bump
env:
VERSION: ${{ inputs.version }}
SKIP_DOCS: ${{ inputs.skip_docs }}
run: |
set -euo pipefail
ARGS=(--to "$VERSION")
if [ "$SKIP_DOCS" = "true" ]; then
ARGS+=(--skip-docs)
fi
python scripts/bump-version.py "${ARGS[@]}"

- name: Verify the working tree actually changed
run: |
set -euo pipefail
if git diff --quiet; then
echo "::error::bump-version.py reported success but changed nothing. \
Refusing to open an empty release PR."
exit 1
fi
echo "Files touched: $(git diff --name-only | wc -l)"

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Commit and push
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
BRANCH="release/prepare-v${VERSION}"
git checkout -b "$BRANCH"
git add -A
git commit -m "chore: Bump version to v${VERSION}"
git push origin "$BRANCH"

- name: Open release PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
SKIP_DOCS: ${{ inputs.skip_docs }}
run: |
set -euo pipefail
BODY=$(cat <<EOF
Automated version bump for the **v${VERSION}** release.

Generated by \`release-prepare.yml\` from \`scripts/bump-version.py\`.

### Reviewer checklist
- [ ] Version strings look right (\`scripts/bump-version.py\` post-condition passed in CI)
- [ ] Versioned docs snapshot present for ${VERSION}$([ "$SKIP_DOCS" = "true" ] && echo " — **SKIPPED, must be added before merge**")
- [ ] No unrelated changes

### After merge
1. \`release-tag.yml\` tags master with \`v${VERSION}\`, \`v${VERSION}-spark3.5\`, \`v${VERSION}-python3.11\` and opens the spark rebase PRs.
2. Queue ADO build 17563 for \`refs/tags/v${VERSION}\` (needs ESRP/SAW approval).
3. Continue with Release Guide Step 2 (SynapseML-Internal).
EOF
)
gh pr create \
--title "chore: Bump version to v${VERSION}" \
--body "$BODY" \
--base master \
--head "release/prepare-v${VERSION}"
Loading
Loading