From 7ef494d9cdf7e824d3e71139010db1a16ddb9136 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Mon, 5 Jan 2026 14:58:49 -0500 Subject: [PATCH] feat(ci): add LinkedIn and refactor to reusable workflows - Add LinkedIn notification job using shared reusable workflow - Replace inline Bluesky posting with reusable workflow - Replace inline discussion creation with reusable workflow - All three notification jobs now use CodingWithCalvin/.github workflows --- .github/workflows/release.yml | 279 ++++++++++------------------------ 1 file changed, 78 insertions(+), 201 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1e3b7c..702eef6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -317,205 +317,82 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - notify: - name: Send Release Notifications - runs-on: ubuntu-latest + notify-discussion: + name: Create GitHub Discussion needs: release - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Create GitHub Discussion Announcement - id: discussion - run: | - VERSION="${{ github.event.inputs.version }}" - RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/v$VERSION" - REPO_URL="https://github.com/${{ github.repository }}" - - # Get repository ID and Announcements category ID - REPO_DATA=$(gh api graphql -f query=' - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - id - discussionCategories(first: 10) { - nodes { - id - name - } - } - } - } - ' -f owner="${{ github.repository_owner }}" -f repo="${{ github.event.repository.name }}") - - REPO_ID=$(echo "$REPO_DATA" | jq -r '.data.repository.id') - CATEGORY_ID=$(echo "$REPO_DATA" | jq -r '.data.repository.discussionCategories.nodes[] | select(.name == "Announcements") | .id') - - if [ -z "$CATEGORY_ID" ]; then - echo "Error: Could not find Announcements category" - exit 1 - fi - - echo "Repository ID: $REPO_ID" - echo "Announcements Category ID: $CATEGORY_ID" - - # Create discussion body (build it with string concatenation to avoid heredoc issues) - DISCUSSION_BODY="## Changes in this release" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"See the [full changelog](RELEASE_URL_PLACEHOLDER) for details on what's new in this release." - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"## Installation" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"### Quick Install (Recommended)" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"**macOS / Linux:**" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n''```bash' - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n'"curl -fsSL REPO_URL_PLACEHOLDER/releases/download/vVERSION_PLACEHOLDER/install.sh | bash" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n''```' - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"**Windows (PowerShell):**" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n''```powershell' - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n'"irm REPO_URL_PLACEHOLDER/releases/download/vVERSION_PLACEHOLDER/install.ps1 | iex" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n''```' - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"### Manual Installation" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"1. Download the appropriate archive for your platform from the [release page](RELEASE_URL_PLACEHOLDER)" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n'"2. Extract the archive" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n'"3. Move binaries to a directory in your PATH" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n'"4. Run \`dtvem init\` to complete setup" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"## Supported Platforms" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"- ✅ Windows (amd64, arm64)" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n'"- ✅ macOS (amd64, arm64/Apple Silicon)" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n'"- ✅ Linux (amd64)" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"---" - DISCUSSION_BODY="$DISCUSSION_BODY"$'\n\n'"📦 [View Release](RELEASE_URL_PLACEHOLDER) | 📖 [Documentation](REPO_URL_PLACEHOLDER)" - # Replace placeholders with actual values - DISCUSSION_BODY="${DISCUSSION_BODY//RELEASE_URL_PLACEHOLDER/$RELEASE_URL}" - DISCUSSION_BODY="${DISCUSSION_BODY//REPO_URL_PLACEHOLDER/$REPO_URL}" - DISCUSSION_BODY="${DISCUSSION_BODY//VERSION_PLACEHOLDER/${{ github.event.inputs.version }}}" - - # Create the discussion - DISCUSSION_RESULT=$(gh api graphql -f query=' - mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) { - createDiscussion(input: { - repositoryId: $repositoryId - categoryId: $categoryId - title: $title - body: $body - }) { - discussion { - url - } - } - } - ' -f repositoryId="$REPO_ID" -f categoryId="$CATEGORY_ID" -f title="🎉 dtvem v$VERSION has been released!" -f body="$DISCUSSION_BODY") - - DISCUSSION_URL=$(echo "$DISCUSSION_RESULT" | jq -r '.data.createDiscussion.discussion.url') - echo "✓ Created discussion: $DISCUSSION_URL" - echo "discussion_url=$DISCUSSION_URL" >> $GITHUB_OUTPUT - shell: bash - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Post to BlueSky - run: | - VERSION="${{ github.event.inputs.version }}" - RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/v$VERSION" - DISCUSSION_URL="${{ steps.discussion.outputs.discussion_url }}" - - # Discover runtimes from src/runtimes/ directory - RUNTIME_DIRS=$(find src/runtimes -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort) - - # Capitalize runtime names - RUNTIME_LIST=() - for runtime in $RUNTIME_DIRS; do - RUNTIME_LIST+=("${runtime^}") # Capitalize first letter - done - - # Format runtime names based on count (with hashtags) - RUNTIME_COUNT=${#RUNTIME_LIST[@]} - if [ $RUNTIME_COUNT -eq 1 ]; then - RUNTIME_NAMES="#${RUNTIME_LIST[0]}" - elif [ $RUNTIME_COUNT -eq 2 ]; then - RUNTIME_NAMES="#${RUNTIME_LIST[0]} and #${RUNTIME_LIST[1]}" - else - # Three or more: "#A, #B, and #C" - RUNTIME_NAMES="" - for i in "${!RUNTIME_LIST[@]}"; do - if [ $i -eq 0 ]; then - RUNTIME_NAMES="#${RUNTIME_LIST[$i]}" - elif [ $i -eq $((RUNTIME_COUNT - 1)) ]; then - RUNTIME_NAMES="${RUNTIME_NAMES}, and #${RUNTIME_LIST[$i]}" - else - RUNTIME_NAMES="${RUNTIME_NAMES}, #${RUNTIME_LIST[$i]}" - fi - done - fi - - echo "Detected runtimes: $RUNTIME_NAMES" - - # Authenticate with BlueSky - echo "Authenticating with BlueSky..." - AUTH_RESPONSE=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \ - -H "Content-Type: application/json" \ - -d "{\"identifier\": \"${{ secrets.BLUESKY_USERNAME }}\", \"password\": \"${{ secrets.BLUESKY_APP_PASSWORD }}\"}") - - ACCESS_TOKEN=$(echo "$AUTH_RESPONSE" | jq -r '.accessJwt') - DID=$(echo "$AUTH_RESPONSE" | jq -r '.did') - - if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" == "null" ]; then - echo "Error: Failed to authenticate with BlueSky" - echo "Response: $AUTH_RESPONSE" - exit 1 - fi - - echo "✓ Authenticated as $DID" - - # Create post text (must be under 300 graphemes) - POST_TEXT="🚀 #dtvem v${VERSION} is now available!" - POST_TEXT="${POST_TEXT}"$'\n\n'"Cross-platform version manager for ${RUNTIME_NAMES} - supports #Windows, #Linux, and #MacOS" - POST_TEXT="${POST_TEXT}"$'\n\n'"Release: ${RELEASE_URL}" - POST_TEXT="${POST_TEXT}"$'\n'"Discuss: ${DISCUSSION_URL}" - - echo "Post text: $POST_TEXT" - echo "Post length: $(echo -n "$POST_TEXT" | wc -c) characters" - - # Get current timestamp in ISO 8601 format - TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - - # Calculate facets (byte positions for hashtags and links) - echo "Calculating facets for links and hashtags..." - export POST_TEXT - FACETS=$(python3 -c "import json; import re; import os; text = os.environ['POST_TEXT']; facets = []; [facets.append({'index': {'byteStart': len(text[:m.start()].encode('utf-8')), 'byteEnd': len(text[:m.start()+len(m.group(0))].encode('utf-8'))}, 'features': [{'\$type': 'app.bsky.richtext.facet#tag', 'tag': m.group(1)}]}) for m in re.finditer(r'#(\w+)', text)]; [facets.append({'index': {'byteStart': len(text[:m.start()].encode('utf-8')), 'byteEnd': len(text[:m.start()+len(m.group())].encode('utf-8'))}, 'features': [{'\$type': 'app.bsky.richtext.facet#link', 'uri': m.group()}]}) for m in re.finditer(r'https?://[^\s]+', text)]; print(json.dumps(facets))") - - echo "Facets: $FACETS" - - # Create the post using jq to properly escape JSON - echo "Creating BlueSky post..." - POST_RESPONSE=$(jq -n \ - --arg did "$DID" \ - --arg text "$POST_TEXT" \ - --arg timestamp "$TIMESTAMP" \ - --argjson facets "$FACETS" \ - '{ - repo: $did, - collection: "app.bsky.feed.post", - record: { - text: $text, - facets: $facets, - createdAt: $timestamp, - "$type": "app.bsky.feed.post" - } - }' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $ACCESS_TOKEN" \ - -d @-) - - POST_URI=$(echo "$POST_RESPONSE" | jq -r '.uri') - - if [ -z "$POST_URI" ] || [ "$POST_URI" == "null" ]; then - echo "Error: Failed to create BlueSky post" - echo "Response: $POST_RESPONSE" - exit 1 - fi - - # Extract the post ID from the URI (format: at://did:plc:.../app.bsky.feed.post/POST_ID) - POST_ID=$(echo "$POST_URI" | sed 's|.*/||') - POST_URL="https://bsky.app/profile/${{ secrets.BLUESKY_USERNAME }}/post/$POST_ID" - - echo "✓ Posted to BlueSky: $POST_URL" - shell: bash + uses: CodingWithCalvin/.github/.github/workflows/github-discussion.yml@main + with: + title: "🎉 dtvem v${{ github.event.inputs.version }} has been released!" + body: | + ## Changes in this release + + See the [full changelog](https://github.com/${{ github.repository }}/releases/tag/v${{ github.event.inputs.version }}) for details on what's new in this release. + + ## Installation + + ### Quick Install (Recommended) + + **macOS / Linux:** + ```bash + curl -fsSL https://github.com/${{ github.repository }}/releases/download/v${{ github.event.inputs.version }}/install.sh | bash + ``` + + **Windows (PowerShell):** + ```powershell + irm https://github.com/${{ github.repository }}/releases/download/v${{ github.event.inputs.version }}/install.ps1 | iex + ``` + + ### Manual Installation + + 1. Download the appropriate archive for your platform from the [release page](https://github.com/${{ github.repository }}/releases/tag/v${{ github.event.inputs.version }}) + 2. Extract the archive + 3. Move binaries to a directory in your PATH + 4. Run `dtvem init` to complete setup + + ## Supported Platforms + + - ✅ Windows (amd64, arm64) + - ✅ macOS (amd64, arm64/Apple Silicon) + - ✅ Linux (amd64) + + --- + + 📦 [View Release](https://github.com/${{ github.repository }}/releases/tag/v${{ github.event.inputs.version }}) | 📖 [Documentation](https://github.com/${{ github.repository }}) + + notify-bluesky: + name: Post to Bluesky + needs: notify-discussion + uses: CodingWithCalvin/.github/.github/workflows/bluesky-post.yml@main + with: + post_text: | + 🚀 #dtvem v${{ github.event.inputs.version }} is now available! + + Cross-platform version manager for #Node, #Python, and #Ruby - supports #Windows, #Linux, and #macOS + + [Release Notes](https://github.com/${{ github.repository }}/releases/tag/v${{ github.event.inputs.version }}) + [Discussion](${{ needs.notify-discussion.outputs.discussion_url }}) + embed_url: https://github.com/${{ github.repository }}/releases/tag/v${{ github.event.inputs.version }} + embed_title: dtvem v${{ github.event.inputs.version }} + embed_description: Cross-platform runtime version manager for Node.js, Python, and Ruby + secrets: + BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }} + BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }} + + notify-linkedin: + name: Post to LinkedIn + needs: notify-discussion + uses: CodingWithCalvin/.github/.github/workflows/linkedin-post.yml@main + with: + post_text: | + 🚀 #dtvem v${{ github.event.inputs.version }} is now available! + + Cross-platform version manager for #Node, #Python, and #Ruby - supports #Windows, #Linux, and #macOS + + Release Notes: https://github.com/${{ github.repository }}/releases/tag/v${{ github.event.inputs.version }} + Discussion: ${{ needs.notify-discussion.outputs.discussion_url }} + article_url: https://github.com/${{ github.repository }}/releases/tag/v${{ github.event.inputs.version }} + article_title: dtvem v${{ github.event.inputs.version }} + article_description: Cross-platform runtime version manager for Node.js, Python, and Ruby + secrets: + LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }} + LINKEDIN_CLIENT_ID: ${{ secrets.LINKEDIN_CLIENT_ID }}