diff --git a/.github/workflows/release-beta-on-push.yml b/.github/workflows/release-beta-on-push.yml index 12652ab4a..dfa963f3e 100644 --- a/.github/workflows/release-beta-on-push.yml +++ b/.github/workflows/release-beta-on-push.yml @@ -37,6 +37,96 @@ jobs: echo "tag=${beta_tag}" >> "$GITHUB_OUTPUT" echo "Beta release: ${beta_tag}" + release-notes: + name: Generate Release Notes + runs-on: ubuntu-latest + outputs: + notes: ${{ steps.notes.outputs.body }} + features: ${{ steps.notes.outputs.features }} + contributors: ${{ steps.notes.outputs.contributors }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - name: Build release notes + id: notes + shell: bash + run: | + set -euo pipefail + + # Use a wider range — find the previous stable tag to capture all + # contributors across the full release cycle, not just one beta bump + PREV_TAG=$(git tag --sort=-creatordate \ + | grep -vE '\-beta\.' \ + | head -1 || echo "") + if [ -z "$PREV_TAG" ]; then + RANGE="HEAD" + else + RANGE="${PREV_TAG}..HEAD" + fi + + # Extract features only (feat commits) — skip bug fixes for clean notes + FEATURES=$(git log "$RANGE" --pretty=format:"%s" --no-merges \ + | grep -iE '^feat(\(|:)' \ + | sed 's/^feat(\([^)]*\)): /\1: /' \ + | sed 's/^feat: //' \ + | sed 's/ (#[0-9]*)$//' \ + | sort -uf \ + | while IFS= read -r line; do echo "- ${line}"; done || true) + + if [ -z "$FEATURES" ]; then + FEATURES="- Incremental improvements and polish" + fi + + # Collect ALL unique contributors: git authors + Co-Authored-By + GIT_AUTHORS=$(git log "$RANGE" --pretty=format:"%an" --no-merges | sort -uf || true) + CO_AUTHORS=$(git log "$RANGE" --pretty=format:"%b" --no-merges \ + | grep -ioE 'Co-Authored-By: *[^<]+' \ + | sed 's/Co-Authored-By: *//i' \ + | sed 's/ *$//' \ + | sort -uf || true) + + # Merge, deduplicate, and filter out bots + ALL_CONTRIBUTORS=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \ + | sort -uf \ + | grep -v '^$' \ + | grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \ + | while IFS= read -r name; do echo "- ${name}"; done || true) + + # Build release body + BODY=$(cat <> "$GITHUB_OUTPUT" + + { + echo "features<> "$GITHUB_OUTPUT" + + { + echo "contributors<> "$GITHUB_OUTPUT" + web: name: Build Web Dashboard runs-on: ubuntu-latest @@ -132,7 +222,7 @@ jobs: publish: name: Publish Beta Release - needs: [version, build] + needs: [version, release-notes, build] runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -154,7 +244,7 @@ jobs: tag_name: ${{ needs.version.outputs.tag }} name: ${{ needs.version.outputs.tag }} prerelease: true - generate_release_notes: true + body: ${{ needs.release-notes.outputs.notes }} files: | artifacts/**/* install.sh diff --git a/.github/workflows/release-stable-manual.yml b/.github/workflows/release-stable-manual.yml index 09a4a0af7..1ec5cc26d 100644 --- a/.github/workflows/release-stable-manual.yml +++ b/.github/workflows/release-stable-manual.yml @@ -74,6 +74,79 @@ jobs: path: web/dist/ retention-days: 1 + release-notes: + name: Generate Release Notes + runs-on: ubuntu-latest + outputs: + notes: ${{ steps.notes.outputs.body }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - name: Build release notes + id: notes + shell: bash + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + + # Find the previous stable tag (exclude beta tags) + PREV_TAG=$(git tag --sort=-creatordate | grep -vE '\-beta\.' | grep -v "^v${INPUT_VERSION}$" | head -1 || echo "") + if [ -z "$PREV_TAG" ]; then + RANGE="HEAD" + else + RANGE="${PREV_TAG}..HEAD" + fi + + # Extract features only — skip bug fixes for clean release notes + FEATURES=$(git log "$RANGE" --pretty=format:"%s" --no-merges \ + | grep -iE '^feat(\(|:)' \ + | sed 's/^feat(\([^)]*\)): /\1: /' \ + | sed 's/^feat: //' \ + | sed 's/ (#[0-9]*)$//' \ + | sort -uf \ + | while IFS= read -r line; do echo "- ${line}"; done || true) + + if [ -z "$FEATURES" ]; then + FEATURES="- Incremental improvements and polish" + fi + + # Collect ALL unique contributors: git authors + Co-Authored-By + GIT_AUTHORS=$(git log "$RANGE" --pretty=format:"%an" --no-merges | sort -uf || true) + CO_AUTHORS=$(git log "$RANGE" --pretty=format:"%b" --no-merges \ + | grep -ioE 'Co-Authored-By: *[^<]+' \ + | sed 's/Co-Authored-By: *//i' \ + | sed 's/ *$//' \ + | sort -uf || true) + + # Merge, deduplicate, and filter out bots + ALL_CONTRIBUTORS=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \ + | sort -uf \ + | grep -v '^$' \ + | grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \ + | while IFS= read -r name; do echo "- ${name}"; done || true) + + BODY=$(cat <> "$GITHUB_OUTPUT" + build: name: Build ${{ matrix.target }} needs: [validate, web] @@ -150,7 +223,7 @@ jobs: publish: name: Publish Stable Release - needs: [validate, build] + needs: [validate, release-notes, build] runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -172,7 +245,7 @@ jobs: tag_name: ${{ needs.validate.outputs.tag }} name: ${{ needs.validate.outputs.tag }} prerelease: false - generate_release_notes: true + body: ${{ needs.release-notes.outputs.notes }} files: | artifacts/**/* install.sh diff --git a/.github/workflows/tweet-release.yml b/.github/workflows/tweet-release.yml index 4d82376b0..072fa4c84 100644 --- a/.github/workflows/tweet-release.yml +++ b/.github/workflows/tweet-release.yml @@ -16,43 +16,79 @@ on: jobs: tweet: - # Skip beta pre-releases on auto trigger - if: >- - github.event_name == 'workflow_dispatch' || - !github.event.release.prerelease runs-on: ubuntu-latest steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - name: Build tweet text id: tweet shell: bash env: RELEASE_TAG: ${{ github.event.release.tag_name || '' }} RELEASE_URL: ${{ github.event.release.html_url || '' }} - RELEASE_BODY: ${{ github.event.release.body || '' }} MANUAL_TEXT: ${{ inputs.tweet_text || '' }} run: | + set -euo pipefail + if [ -n "$MANUAL_TEXT" ]; then - # Manual dispatch — use the custom text as-is TWEET="$MANUAL_TEXT" else - # Auto trigger — look for block in release body - # Format in your release notes: - # - # ZeroClaw v0.2.0 🐾 - # - # 🚀 feature one - # 🔧 feature two - # - # the claw strikes - # - TWEET_BLOCK=$(echo "$RELEASE_BODY" | sed -n '//,//p' | sed '1d;$d' || true) - - if [ -n "$TWEET_BLOCK" ]; then - TWEET="$TWEET_BLOCK" + # Use a wider range — find the previous stable tag to capture all + # contributors across the full release cycle, not just one beta bump + PREV_TAG=$(git tag --sort=-creatordate \ + | grep -v "^${RELEASE_TAG}$" \ + | grep -vE '\-beta\.' \ + | head -1 || echo "") + if [ -z "$PREV_TAG" ]; then + RANGE="HEAD" else - # Fallback: auto-generate a simple announcement - TWEET=$(printf "ZeroClaw %s 🐾\n\nFull release notes 👇\n%s" "$RELEASE_TAG" "$RELEASE_URL") + RANGE="${PREV_TAG}..${RELEASE_TAG}" fi + + # Extract features only — no bug fixes, keep it clean and concise + FEATURES=$(git log "$RANGE" --pretty=format:"%s" --no-merges \ + | grep -iE '^feat(\(|:)' \ + | sed 's/^feat(\([^)]*\)): /\1: /' \ + | sed 's/^feat: //' \ + | sed 's/ (#[0-9]*)$//' \ + | sort -uf \ + | head -4 \ + | while IFS= read -r line; do echo "🚀 ${line}"; done || true) + + if [ -z "$FEATURES" ]; then + FEATURES="🚀 Incremental improvements and polish" + fi + + # Collect ALL unique contributors: git authors + Co-Authored-By + # Filter out bots and service accounts + GIT_AUTHORS=$(git log "$RANGE" --pretty=format:"%an" --no-merges | sort -uf || true) + CO_AUTHORS=$(git log "$RANGE" --pretty=format:"%b" --no-merges \ + | grep -ioE 'Co-Authored-By: *[^<]+' \ + | sed 's/Co-Authored-By: *//i' \ + | sed 's/ *$//' \ + | sort -uf || true) + + ALL_NAMES=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \ + | sort -uf \ + | grep -v '^$' \ + | grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \ + || true) + + TOTAL_COUNT=$(echo "$ALL_NAMES" | grep -c . || echo "0") + # Show up to 6 names, then "+ N more" if there are extras + SHOWN=$(echo "$ALL_NAMES" | head -6 | paste -sd ', ' -) + if [ "$TOTAL_COUNT" -gt 6 ]; then + EXTRA=$((TOTAL_COUNT - 6)) + CONTRIBUTORS="${SHOWN} + ${EXTRA} more" + else + CONTRIBUTORS="$SHOWN" + fi + + # Build the tweet — punchy, features-first, all contributors credited + TWEET=$(printf "🦀 ZeroClaw %s\n\n%s\n\n🙌 Contributors: %s\n\n🔗 %s" \ + "$RELEASE_TAG" "$FEATURES" "$CONTRIBUTORS" "$RELEASE_URL") fi # Append release URL if not already present and we have one @@ -60,6 +96,11 @@ jobs: TWEET=$(printf "%s\n\n%s" "$TWEET" "$RELEASE_URL") fi + # Truncate to 280 chars if needed — trim features first, keep contributors + if [ ${#TWEET} -gt 280 ]; then + TWEET="${TWEET:0:277}..." + fi + echo "--- Tweet preview ---" echo "$TWEET" echo "--- ${#TWEET} chars ---" @@ -82,6 +123,12 @@ jobs: run: | set -euo pipefail + # Skip if Twitter secrets are not configured + if [ -z "$TWITTER_CONSUMER_KEY" ] || [ -z "$TWITTER_ACCESS_TOKEN" ]; then + echo "::warning::Twitter secrets not configured — skipping tweet" + exit 0 + fi + pip install requests requests-oauthlib --quiet python3 - <<'PYEOF' diff --git a/Cargo.lock b/Cargo.lock index 759537d5a..64cb68090 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7924,7 +7924,7 @@ dependencies = [ [[package]] name = "zeroclaw" -version = "0.1.9" +version = "0.2.0" dependencies = [ "anyhow", "async-imap", diff --git a/Cargo.toml b/Cargo.toml index e36d7138e..ff7d57eb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [package] name = "zeroclaw" -version = "0.1.9" +version = "0.2.0" edition = "2021" authors = ["theonlyhennygod"] license = "MIT OR Apache-2.0"