name: Publish Multi-Package Alpha to npm
on:
push:
branches:
- master
- main
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Enable Corepack for Yarn support
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: yarn install --immutable
- name: Lint code
run: yarn lint
- name: Build packages
run: yarn build
- name: Run tests
run: yarn test
discover-packages:
needs: verify
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.find-packages.outputs.packages }}
package-count: ${{ steps.find-packages.outputs.package-count }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Find publishable packages
id: find-packages
run: |
echo "π Discovering publishable packages in packages/ directory..."
# Ensure jq is available
which jq || (echo "Installing jq..." && sudo apt-get update && sudo apt-get install -y jq)
PACKAGES="[]"
PACKAGE_COUNT=0
# Find all package.json files in packages/ subdirectories
for package_dir in packages/*/; do
if [ -f "${package_dir}package.json" ]; then
# Check if package is not private
IS_PRIVATE=$(node -p "
try {
const pkg = require('./${package_dir}package.json');
pkg.private === true ? 'true' : 'false';
} catch(e) {
'true';
}
")
if [ "$IS_PRIVATE" = "false" ]; then
PACKAGE_NAME=$(node -p "require('./${package_dir}package.json').name")
PACKAGE_VERSION=$(node -p "require('./${package_dir}package.json').version")
PACKAGE_PATH=${package_dir%/} # Remove trailing slash
echo "β
Found publishable package: $PACKAGE_NAME@$PACKAGE_VERSION in $PACKAGE_PATH"
# Add to packages array with error handling
NEW_PACKAGES=$(echo "$PACKAGES" | jq --arg name "$PACKAGE_NAME" --arg version "$PACKAGE_VERSION" --arg path "$PACKAGE_PATH" '. + [{name: $name, version: $version, path: $path}]')
if [ $? -eq 0 ]; then
PACKAGES="$NEW_PACKAGES"
PACKAGE_COUNT=$((PACKAGE_COUNT + 1))
else
echo "β Error adding package $PACKAGE_NAME to JSON array"
exit 1
fi
else
PACKAGE_NAME=$(node -p "require('./${package_dir}package.json').name")
echo "βοΈ Skipping private package: $PACKAGE_NAME in ${package_dir%/}"
fi
fi
done
echo "π Total publishable packages found: $PACKAGE_COUNT"
# Display package names safely
if [ "$PACKAGE_COUNT" -gt 0 ]; then
echo "π¦ Packages to publish: $(echo "$PACKAGES" | jq -c '.[].name')"
else
echo "π¦ No publishable packages found"
fi
# Output for matrix strategy - properly escape JSON for GitHub Actions
echo "packages<<EOF" >> $GITHUB_OUTPUT
echo "$PACKAGES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "package-count=$PACKAGE_COUNT" >> $GITHUB_OUTPUT
publish-packages:
needs: [verify, discover-packages]
runs-on: ubuntu-latest
if: fromJson(needs.discover-packages.outputs.package-count) > 0
strategy:
matrix:
package: ${{ fromJson(needs.discover-packages.outputs.packages) }}
fail-fast: false # Continue publishing other packages even if one fails
outputs:
results: ${{ steps.collect-results.outputs.results }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# Enable Corepack for Yarn support
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: yarn install --immutable
- name: Lint code
run: yarn lint
- name: Build packages
run: yarn build
- name: Check and publish alpha version for ${{ matrix.package.name }}
id: publish-package
run: |
PACKAGE_NAME="${{ matrix.package.name }}"
PACKAGE_VERSION="${{ matrix.package.version }}"
PACKAGE_PATH="${{ matrix.package.path }}"
echo "π Processing package: $PACKAGE_NAME@$PACKAGE_VERSION"
echo "π Package path: $PACKAGE_PATH"
# Change to package directory
cd "$PACKAGE_PATH"
# Safety check: Ensure we never publish a non-alpha version
if [[ "$PACKAGE_VERSION" =~ -alpha\. ]]; then
echo "β ERROR: package.json version already contains '-alpha' suffix!"
echo "β This workflow should only work with clean base versions (e.g., '4.0.0')"
exit 1
fi
# Get the latest alpha version number for this base version
LATEST_ALPHA_NUMBER=0
# Check for existing alpha versions and find the highest number
echo "π Checking for existing alpha versions of $PACKAGE_VERSION for $PACKAGE_NAME..."
for i in {1..100}; do
if npm view "$PACKAGE_NAME@${PACKAGE_VERSION}-alpha.$i" version 2>/dev/null; then
LATEST_ALPHA_NUMBER=$i
echo " Found: ${PACKAGE_VERSION}-alpha.$i"
else
break
fi
done
# Increment the alpha number
NEW_ALPHA_NUMBER=$((LATEST_ALPHA_NUMBER + 1))
ALPHA_VERSION="${PACKAGE_VERSION}-alpha.${NEW_ALPHA_NUMBER}"
echo "π Latest alpha number: $LATEST_ALPHA_NUMBER"
echo "π New alpha number: $NEW_ALPHA_NUMBER"
echo "π·οΈ New alpha version: $ALPHA_VERSION"
echo "β οΈ Will publish ONLY to 'alpha' tag, NOT 'latest'"
# Setup npmrc for this package (for both npm and yarn)
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc
echo "registry=https://registry.npmjs.org/" >> .npmrc
echo "always-auth=true" >> .npmrc
# Also configure Yarn for npm registry
echo "π Configuring Yarn for npm registry..."
yarn config set npmRegistryServer "https://registry.npmjs.org"
yarn config set npmAlwaysAuth true
yarn config set npmAuthToken "${NODE_AUTH_TOKEN}"
# Debug: Show package.json dependencies to understand workspace resolution
echo "π¦ Current package.json dependencies:"
node -p "JSON.stringify(require('./package.json').dependencies || {}, null, 2)"
# Safety verification before publishing
if [[ ! "$ALPHA_VERSION" =~ -alpha\. ]]; then
echo "β CRITICAL ERROR: Version '$ALPHA_VERSION' does not contain '-alpha' suffix!"
echo "β Refusing to publish - this could pollute the main npm release!"
exit 1
fi
echo "π Safety check passed: Publishing alpha version $ALPHA_VERSION"
# Temporarily update package.json version to alpha (Yarn v4 doesn't create git tags automatically)
yarn version $ALPHA_VERSION
# Debug: Show how Yarn resolves workspace dependencies
echo "π Yarn workspace dependency resolution:"
node -p "JSON.stringify(require('./package.json').dependencies || {}, null, 2)"
# Publish with alpha tag (NOT latest - ensures no main version pollution)
echo "π Publishing $PACKAGE_NAME@$ALPHA_VERSION to npm with 'alpha' tag (NOT 'latest')..."
# Always use Yarn for workspace packages (handles workspace: dependencies correctly)
echo "π§ Using Yarn to publish (handles workspace dependencies automatically)"
yarn npm publish --access public --tag alpha
# Revert package.json version back to original
yarn version $PACKAGE_VERSION
echo "β
Successfully published $PACKAGE_NAME@$ALPHA_VERSION to npm with 'alpha' tag"
echo "β οΈ This version is NOT published to 'latest' - only available via 'alpha' tag"
echo "π₯ Users can install with: npm install $PACKAGE_NAME@alpha"
# Set outputs for notification
echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
echo "alpha_version=$ALPHA_VERSION" >> $GITHUB_OUTPUT
echo "alpha_number=$NEW_ALPHA_NUMBER" >> $GITHUB_OUTPUT
echo "published=true" >> $GITHUB_OUTPUT
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Collect results
id: collect-results
run: |
# This step helps aggregate results for the notification job
echo "results={\"name\": \"${{ steps.publish-package.outputs.package_name }}\", \"version\": \"${{ steps.publish-package.outputs.alpha_version }}\", \"published\": \"${{ steps.publish-package.outputs.published }}\"}" >> $GITHUB_OUTPUT
notify-discord:
needs: [verify, discover-packages, publish-packages]
runs-on: ubuntu-latest
if: always() # Run even if previous jobs fail
steps:
- name: Send Discord notification
continue-on-error: true
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
run: |
# Check if Discord webhook is configured
if [ -z "$DISCORD_WEBHOOK" ]; then
echo "βΉοΈ DISCORD_WEBHOOK not configured, skipping notification"
exit 0
fi
# Determine overall status and color
VERIFY_RESULT="${{ needs.verify.result }}"
DISCOVER_RESULT="${{ needs.discover-packages.result }}"
PUBLISH_RESULT="${{ needs.publish-packages.result }}"
PACKAGE_COUNT="${{ needs.discover-packages.outputs.package-count || '0' }}"
if [ "$VERIFY_RESULT" == "failure" ] || [ "$DISCOVER_RESULT" == "failure" ] || [ "$PUBLISH_RESULT" == "failure" ]; then
STATUS="β **Failed**"
COLOR=15158332 # Red
if [ "$VERIFY_RESULT" == "failure" ]; then
DESCRIPTION="Build or test verification failed"
elif [ "$DISCOVER_RESULT" == "failure" ]; then
DESCRIPTION="Package discovery failed"
else
DESCRIPTION="Package publishing failed"
fi
elif [ "$PACKAGE_COUNT" -gt "0" ]; then
STATUS="π **Multi-Package Alpha Published**"
COLOR=3066993 # Green
DESCRIPTION="Successfully published $PACKAGE_COUNT alpha packages to npm"
else
STATUS="β
**Success**"
COLOR=3066993 # Green
DESCRIPTION="Build completed successfully (no publishable packages found)"
fi
# Get commit info
COMMIT_SHA="${{ github.sha }}"
SHORT_SHA="${COMMIT_SHA:0:7}"
COMMIT_MSG="${{ github.event.head_commit.message }}"
AUTHOR="${{ github.event.head_commit.author.name }}"
# Escape JSON special characters to prevent injection
COMMIT_MSG_ESCAPED=$(echo "$COMMIT_MSG" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ' | tr '\r' ' ')
AUTHOR_ESCAPED=$(echo "$AUTHOR" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
# Create base fields
BASE_FIELDS='[
{
"name": "Status",
"value": "'$STATUS'",
"inline": true
},
{
"name": "Packages Found",
"value": "'$PACKAGE_COUNT'",
"inline": true
},
{
"name": "Author",
"value": "'$AUTHOR_ESCAPED'",
"inline": true
},
{
"name": "Commit",
"value": "[`'$SHORT_SHA'`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})",
"inline": true
},
{
"name": "Branch",
"value": "`${{ github.ref_name }}`",
"inline": true
},
{
"name": "Repository",
"value": "[View on GitHub](https://github.com/${{ github.repository }})",
"inline": true
},
{
"name": "Commit Message",
"value": "'$COMMIT_MSG_ESCAPED'",
"inline": false
}
]'
# Create JSON payload
PAYLOAD=$(cat <<EOF
{
"username": "NPM Multi-Package Alpha Publisher",
"avatar_url": "https://raw.githubusercontent.com/npm/logos/master/npm%20logo/npm-logo-red.png",
"embeds": [{
"title": "π¦ Octocode Multi-Package Alpha Release",
"description": "$DESCRIPTION",
"color": $COLOR,
"fields": $BASE_FIELDS,
"footer": {
"text": "GitHub Actions β’ $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
}
}]
}
EOF
)
# Send Discord webhook with error handling
if ! curl -H "Content-Type: application/json" -X POST -d "$PAYLOAD" "$DISCORD_WEBHOOK"; then
echo "β οΈ Failed to send Discord notification, but this won't fail the workflow"
else
echo "β
Discord notification sent successfully"
fi