From 69421944b5deb3666e6bebc5bd35258900242992 Mon Sep 17 00:00:00 2001 From: Kenneth Hough Date: Fri, 22 May 2026 06:41:19 -0500 Subject: [PATCH] ci(release): modernize release.sh + add tag-triggered GitHub Release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier 1 release-pipeline modernization for kyte-php (mirrors what we did for kyte-api-js, scoped to kyte-php's composer-only distribution). Two changes: 1) release.sh — fix + harden The old script tried to read src/Core/Version.php for hardcoded MAJOR/MINOR/PATCH constants. Those were removed in v4.3.2 when Version.php was rewritten to read from Composer's runtime data (Composer\InstalledVersions::getPrettyVersion). The script has been silently broken since then — every release after v4.3.2, including v4.4.0 just shipped, bypassed it and used git tag directly. This rewrite drops the dead Version.php check and adds pre-flight gates: - CHANGELOG.md top matches the version arg (kept from original) - Working tree clean (would otherwise tag unintended state) - Current branch is master (releases off master only) - Local master in sync with origin (no stale tags) - Tag doesn't already exist locally OR upstream set -e added so any failed git command aborts rather than continuing with a half-baked release. 2) .github/workflows/release.yml — new Tag push (v*) triggers an Actions job that: - Extracts the version from the tag ref - Verifies a matching `## ` header exists in CHANGELOG.md (fails loud — refuses to publish an empty release) - Extracts the section between that header and the next `## ` as release notes - Creates a GitHub Release with the notes No S3 / CDN upload — kyte-php is composer-distributed, Packagist auto-detects new tags from GitHub within minutes of the release. That's why this workflow is materially smaller than kyte-api-js's deploy.yml. Belt-and-suspenders: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true env var so action-gh-release runs on Node 24 ahead of GitHub's 2026-06-02 force-migration (same fix we applied to kyte-api-js). After this lands, the v4.4.1 / v4.5.0 / etc. release flow becomes: 1. Land work on master with a CHANGELOG entry 2. ./release.sh X.Y.Z (pre-flight + tag + push) 3. GitHub Release auto-created from CHANGELOG section 4. Packagist picks up the tag, customers composer update to pull release-please for kyte-php is deferred (filed as Tempo card — release cadence is slow enough that manual CHANGELOG management remains acceptable, and the no-version-field-in-composer.json quirk makes release-please integration messier than for kyte-api-js). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 72 +++++++++++++++++++ release.sh | 128 +++++++++++++++++++++------------- 2 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..92600657 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: Release + +# Tag-triggered. Extracts the matching `## ` section from +# CHANGELOG.md and creates a GitHub Release. Unlike kyte-api-js, no +# S3 / CloudFront step is needed — kyte-php is composer-distributed, +# and Packagist auto-detects the new tag from GitHub within minutes. +# +# Version comes from the tag (refs/tags/v4.4.0 → 4.4.0). CHANGELOG.md +# must have a `## ` H2 anywhere in the file — the script +# extracts everything between that header and the next `## ` as +# release notes. + +on: + push: + tags: + - 'v*' + +permissions: + contents: write # needed for softprops/action-gh-release + +# Opt into Node.js 24 ahead of the 2026-06-02 force-migration. +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + release: + name: Create GitHub Release + runs-on: ubuntu-latest + + steps: + - name: Check out source + uses: actions/checkout@v5 + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + + - name: Verify CHANGELOG.md has matching section + run: | + VERSION="${{ steps.version.outputs.VERSION }}" + if ! grep -q "^## ${VERSION}$" CHANGELOG.md; then + echo "::error::Tag v$VERSION but CHANGELOG.md has no '## $VERSION' section." + echo "::error::Add a CHANGELOG entry before tagging." + exit 1 + fi + + - name: Extract release notes from CHANGELOG.md + id: notes + run: | + VERSION="${{ steps.version.outputs.VERSION }}" + # Capture everything between `## $VERSION` and the next `## ` line. + awk -v v="^## ${VERSION}$" ' + $0 ~ v { capture=1; next } + /^## / && capture { exit } + capture { print } + ' CHANGELOG.md > release-notes.md + + if [ ! -s release-notes.md ]; then + echo "::error::Could not extract release notes for v$VERSION from CHANGELOG.md" + exit 1 + fi + + echo "Release notes preview (first 20 lines):" + head -20 release-notes.md + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: kyte-php v${{ steps.version.outputs.VERSION }} + body_path: release-notes.md + draft: false + prerelease: false diff --git a/release.sh b/release.sh index 10a28303..be27aaed 100644 --- a/release.sh +++ b/release.sh @@ -1,50 +1,82 @@ #!/bin/bash +# +# Tag and push a release. +# +# CI (.github/workflows/release.yml) handles the actual GitHub Release +# creation on tag push. This script's only job is to verify CHANGELOG +# matches the version, ensure the working tree is clean, and tag. +# +# kyte-php is distributed via Composer (packagist.org), which auto- +# discovers tags from GitHub. Once the tag exists upstream, customers +# can `composer require keyqcloud/kyte-php:^X.Y` to pull it. +# +# Usage: ./release.sh 4.4.0 -print_error() { - echo "\033[1;31m$1\033[0m" -} - -if [ "$#" -eq 1 ]; then - # Check the CHANGELOG.md - changelog_version=$(awk '/## /{print $2;exit}' CHANGELOG.md) - - if [ "$changelog_version" != "$1" ]; then - print_error "Version in CHANGELOG.md does not match the release version." - exit 1 - fi - - # Define the path to the Version.php file - php_version_file_path="src/Core/Version.php" - - # Extract the version from the PHP file - php_version=$(awk -F '=' '/const MAJOR/{major=$2} /const MINOR/{minor=$2} /const PATCH/{patch=$2} END{print major"."minor"."patch}' "$php_version_file_path") - - # Remove semicolons using tr - php_version=$(echo "$php_version" | tr -d ';') - - # Compare the desired version with the PHP version - if [ "$1" != "$php_version" ]; then - print_error "Version mismatch: Desired version $1, Version.php is $php_version" - exit 1 - fi - - echo "Creating tag for release version $1" - - git tag "v$1" - - if [ $? -eq 0 ]; then - echo "Git tag created successfully for v$1." - # Push the tag to the origin - git push origin --tags - - if [ $? -eq 0 ]; then - echo "Git push successful. New release v$1 is available" - else - print_error "Git push failed." - exit 1 - fi - else - print_error "Git tag creation failed." - exit 1 - fi -fi \ No newline at end of file +set -e + +print_error() { printf "\033[1;31m%s\033[0m\n" "$1"; } +print_success() { printf "\033[1;32m%s\033[0m\n" "$1"; } + +if [ "$#" -ne 1 ]; then + print_error "Usage: ./release.sh " + print_error "Example: ./release.sh 4.4.0" + exit 1 +fi + +VERSION="$1" + +# Sanity-check CHANGELOG has the new version at the top. +CHANGELOG_VERSION=$(awk '/^## /{print $2; exit}' CHANGELOG.md) +if [ "$CHANGELOG_VERSION" != "$VERSION" ]; then + print_error "Version in CHANGELOG.md ($CHANGELOG_VERSION) does not match $VERSION." + print_error "Update CHANGELOG.md to add a '## $VERSION' section before releasing." + exit 1 +fi + +# Refuse to tag against a dirty tree — would tag unintended state. +if ! git diff-index --quiet HEAD --; then + print_error "Working tree is not clean. Commit or stash changes first." + git status --short + exit 1 +fi + +# Refuse to tag if we're not on master. +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$CURRENT_BRANCH" != "master" ]; then + print_error "Releases must be tagged from master. Current branch: $CURRENT_BRANCH" + exit 1 +fi + +# Refuse to tag if local master is behind origin. +git fetch origin master --quiet +LOCAL=$(git rev-parse master) +REMOTE=$(git rev-parse origin/master) +if [ "$LOCAL" != "$REMOTE" ]; then + print_error "Local master is not in sync with origin/master. Push or pull first." + exit 1 +fi + +# Refuse to tag if the tag already exists locally or upstream. +if git rev-parse "v$VERSION" >/dev/null 2>&1; then + print_error "Tag v$VERSION already exists locally." + exit 1 +fi +if git ls-remote --tags origin "v$VERSION" 2>/dev/null | grep -q "v$VERSION"; then + print_error "Tag v$VERSION already exists on origin." + exit 1 +fi + +print_success "Tagging v$VERSION..." +git tag "v$VERSION" +git push origin "v$VERSION" + +print_success "" +print_success "Tagged v$VERSION and pushed." +print_success "GitHub Actions will now extract release notes from CHANGELOG.md" +print_success "and create the GitHub Release." +print_success "Watch: https://github.com/keyqcloud/kyte-php/actions" +print_success "" +print_success "Once the GitHub Release is published, Packagist will auto-detect" +print_success "the new tag within minutes. Customers pulling via Composer with" +print_success "a constraint matching v$VERSION (e.g., ^${VERSION%%.*}.${VERSION#*.}" +print_success "or ^${VERSION%%.*}.${VERSION#*.}.0) will receive it on next update."