diff --git a/.github/workflows/update-version.yml b/.github/workflows/update-version.yml index 5399ca3b..2ccfeb82 100644 --- a/.github/workflows/update-version.yml +++ b/.github/workflows/update-version.yml @@ -7,9 +7,10 @@ on: workflow_dispatch: inputs: force: - description: 'Force update even if version is the same' + description: 'Force create PR even if version unchanged (useful for re-triggering)' required: false default: 'false' + type: boolean permissions: contents: write @@ -47,23 +48,52 @@ jobs: - name: Update WhatsApp Web Version id: update_version - run: yarn update:version + run: | + # Run the update script, capture exit code + # Exit codes: 0=success, 1=error, 2=fetch failed (fallback used) + set +e + yarn update:version + EXIT_CODE=$? + set -e + + echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT + + # Only fail on fatal errors (exit code 1) + if [ $EXIT_CODE -eq 1 ]; then + echo "::error::Script failed with fatal error" + exit 1 + fi + + # Exit code 2 means fetch failed but fallback was used - this is a warning + if [ $EXIT_CODE -eq 2 ]; then + echo "::warning::Could not fetch latest version from WhatsApp servers" + fi - name: Check for changes id: check_changes run: | + FORCE="${{ inputs.force }}" + if git diff --quiet; then echo "has_changes=false" >> $GITHUB_OUTPUT + if [ "$FORCE" == "true" ]; then + echo "force_mode=true" >> $GITHUB_OUTPUT + echo "::notice::Force mode enabled - will create PR even without changes" + else + echo "force_mode=false" >> $GITHUB_OUTPUT + fi else echo "has_changes=true" >> $GITHUB_OUTPUT + echo "force_mode=false" >> $GITHUB_OUTPUT fi - name: Create Pull Request - if: steps.check_changes.outputs.has_changes == 'true' + if: steps.check_changes.outputs.has_changes == 'true' || steps.check_changes.outputs.force_mode == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | BRANCH_NAME="update-version/stable" + FORCE="${{ inputs.force }}" # Configure git git config user.name "github-actions[bot]" @@ -72,25 +102,40 @@ jobs: # Check if branch exists and delete it git push origin --delete "$BRANCH_NAME" 2>/dev/null || true - # Create new branch, commit, and push + # Create new branch git checkout -b "$BRANCH_NAME" # Only add the single source of truth file git add src/Defaults/baileys-version.json - git commit -m "chore: update WhatsApp Web version" + + # Create commit (allow empty if force mode) + if [ "$FORCE" == "true" ] && git diff --cached --quiet; then + git commit --allow-empty -m "chore: force WhatsApp Web version check" + else + git commit -m "chore: update WhatsApp Web version" + fi + git push origin "$BRANCH_NAME" # Create PR using GitHub CLI - gh pr create \ - --title "chore: update WhatsApp Web version" \ - --body "Automated WhatsApp Web version update. + PR_BODY="Automated WhatsApp Web version update. This PR updates the WhatsApp Web client revision to the latest version fetched from \`web.whatsapp.com\`. **Single source of truth:** - \`src/Defaults/baileys-version.json\` (other files import from here) - **Update frequency:** Daily at 06:00 UTC" \ + **Update frequency:** Daily at 06:00 UTC" + + if [ "$FORCE" == "true" ]; then + PR_BODY="$PR_BODY + + > **Note:** This PR was created with force mode enabled." + fi + + gh pr create \ + --title "chore: update WhatsApp Web version" \ + --body "$PR_BODY" \ --base master \ --head "$BRANCH_NAME" || echo "PR already exists or could not be created" @@ -98,8 +143,20 @@ jobs: run: | echo "### WhatsApp Version Update" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{ steps.check_changes.outputs.has_changes }}" == "true" ]; then + + EXIT_CODE="${{ steps.update_version.outputs.exit_code }}" + HAS_CHANGES="${{ steps.check_changes.outputs.has_changes }}" + FORCE_MODE="${{ steps.check_changes.outputs.force_mode }}" + + if [ "$EXIT_CODE" == "2" ]; then + echo "āš ļø Warning: Could not fetch latest version from WhatsApp servers" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$HAS_CHANGES" == "true" ]; then echo "āœ… New version detected and PR created" >> $GITHUB_STEP_SUMMARY + elif [ "$FORCE_MODE" == "true" ]; then + echo "šŸ”„ Force mode: PR created (no version change)" >> $GITHUB_STEP_SUMMARY else echo "ā„¹ļø Already up to date" >> $GITHUB_STEP_SUMMARY fi diff --git a/scripts/update-version.ts b/scripts/update-version.ts index cec30337..d0952071 100644 --- a/scripts/update-version.ts +++ b/scripts/update-version.ts @@ -9,6 +9,15 @@ * Updates: src/Defaults/baileys-version.json (SINGLE SOURCE OF TRUTH) * * Usage: yarn update:version + * + * Environment variables: + * - WA_MIN_REVISION: Minimum valid revision number (default: 1000000000) + * - WA_MAX_REVISION: Maximum valid revision number (default: 9999999999) + * + * Exit codes: + * - 0: Success (version updated or already up to date) + * - 1: Fatal error (script failure) + * - 2: Fetch failed (all sources failed, fallback version used) */ import { readFileSync, writeFileSync, appendFileSync } from 'fs' @@ -29,14 +38,26 @@ interface VersionResult { error?: unknown } -// Configuration +/** + * Configuration for the version update script. + * + * Revision bounds explanation: + * - WhatsApp Web uses a numeric "client_revision" that increments with each release + * - As of 2024, revisions are in the 10-digit range (e.g., 1032141294) + * - minRevision (1000000000) ensures we don't accept clearly invalid small numbers + * that could result from parsing errors + * - maxRevision (9999999999) is the maximum 10-digit number, providing an upper sanity check + * + * These bounds can be overridden via environment variables if WhatsApp changes their + * versioning scheme in the future. + */ const CONFIG = { maxRetries: 3, retryDelayMs: 2000, // Base delay, will be multiplied by attempt number requestTimeoutMs: 10000, - // Version sanity checks - minRevision: 1000000000, // Minimum expected revision (to catch parsing errors) - maxRevision: 9999999999, // Maximum expected revision + // Version sanity checks - can be overridden via environment variables + minRevision: parseInt(process.env.WA_MIN_REVISION || '1000000000', 10), + maxRevision: parseInt(process.env.WA_MAX_REVISION || '9999999999', 10), } /** @@ -65,7 +86,11 @@ async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: nu } /** - * Validates that a revision number looks reasonable + * Validates that a revision number looks reasonable. + * WhatsApp Web revisions are typically 10-digit numbers. + * + * @param revision - The revision number to validate + * @returns true if the revision is within expected bounds */ function isValidRevision(revision: number): boolean { return ( @@ -240,6 +265,13 @@ async function main() { console.log('ā•‘ WhatsApp Web Version Update Script ā•‘') console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n') + // Log configuration + console.log('Configuration:') + console.log(` Min revision: ${CONFIG.minRevision}`) + console.log(` Max revision: ${CONFIG.maxRevision}`) + console.log(` Max retries: ${CONFIG.maxRetries}`) + console.log(` Request timeout: ${CONFIG.requestTimeoutMs}ms\n`) + console.log('Fetching latest WhatsApp Web version...\n') const result = await fetchLatestWaWebVersion() @@ -274,10 +306,18 @@ async function main() { } } - // Exit with error if we couldn't fetch latest (so CI knows) + // Exit with code 2 if fetch failed (Fix #3) + // This allows CI to distinguish between: + // - 0: Success + // - 1: Script error + // - 2: Fetch failed but fallback used if (!result.isLatest) { console.log('\n⚠ Warning: Could not fetch latest version from WhatsApp servers') - process.exit(0) // Don't fail the workflow, just warn + console.log('Exit code 2: Fetch failed, fallback version used') + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `fetch_failed=true\n`) + } + process.exit(2) } } diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index a21c4eb8..3db088f4 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -52,7 +52,6 @@ const SIX_HOURS_MS = 6 * 60 * 60 * 1000 export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { version: version as WAVersion, - fetchLatestVersion: false, versionCheckIntervalMs: SIX_HOURS_MS, browser: Browsers.macOS('Chrome'), waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat', diff --git a/src/Socket/index.ts b/src/Socket/index.ts index 9207e390..1f825f4d 100644 --- a/src/Socket/index.ts +++ b/src/Socket/index.ts @@ -67,8 +67,22 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => } const logger = mergedConfig.logger - let currentVersion = mergedConfig.version + + // Track version separately to avoid mutating config (Fix #7) + let trackedVersion: WAVersion = [...mergedConfig.version] as WAVersion let versionCheckInterval: ReturnType | null = null + let isSocketClosed = false + + /** + * Cleans up the version check interval + */ + const cleanupInterval = () => { + if (versionCheckInterval) { + clearInterval(versionCheckInterval) + versionCheckInterval = null + logger?.debug('Stopped periodic version check') + } + } // Fetch latest version before connecting try { @@ -78,7 +92,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => if (result.isLatest) { logger?.info({ version: result.version }, 'Using latest WhatsApp Web version') mergedConfig.version = result.version - currentVersion = result.version + trackedVersion = [...result.version] as WAVersion } else { logger?.warn( { error: result.error, fallbackVersion: mergedConfig.version }, @@ -95,6 +109,17 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => // Create the socket const sock = makeWASocket(mergedConfig) + // Listen for connection close to cleanup interval (Fix #1, #6) + // This handles both explicit sock.end() and internal disconnections + sock.ev.on('connection.update', (update) => { + if (update.connection === 'close') { + isSocketClosed = true + cleanupInterval() + } else if (update.connection === 'open') { + isSocketClosed = false + } + }) + // Setup periodic version check if interval > 0 const checkIntervalMs = mergedConfig.versionCheckIntervalMs if (checkIntervalMs > 0) { @@ -104,35 +129,48 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => ) versionCheckInterval = setInterval(async () => { + // Skip if socket is closed (Fix #8 - race condition) + if (isSocketClosed) { + cleanupInterval() + return + } + try { logger?.debug('Checking for WhatsApp Web version update...') const result = await fetchLatestWaWebVersion() - if (result.isLatest && versionsAreDifferent(currentVersion, result.version)) { - const isCritical = isCriticalVersionChange(currentVersion, result.version) + // Double-check socket is still open after async operation (Fix #8) + if (isSocketClosed) { + cleanupInterval() + return + } + + if (result.isLatest && versionsAreDifferent(trackedVersion, result.version)) { + const isCritical = isCriticalVersionChange(trackedVersion, result.version) + const previousVersion = trackedVersion logger?.info( { - currentVersion, + currentVersion: previousVersion, newVersion: result.version, isCritical }, 'New WhatsApp Web version detected! Will use on next reconnection.' ) - // Emit event for user to handle - sock.ev.emit('version.update', { - currentVersion, - newVersion: result.version, - isCritical - }) + // Update tracked version for next reconnection (Fix #7) + trackedVersion = [...result.version] as WAVersion - // Update the version for next reconnection - // This is the "soft" update - it will be used when WhatsApp naturally reconnects - currentVersion = result.version - mergedConfig.version = result.version + // Emit event for user to handle (only if socket still open) + if (!isSocketClosed) { + sock.ev.emit('version.update', { + currentVersion: previousVersion, + newVersion: result.version, + isCritical + }) + } } else if (result.isLatest) { - logger?.debug({ version: currentVersion }, 'Version is up to date') + logger?.debug({ version: trackedVersion }, 'Version is up to date') } else { logger?.warn({ error: result.error }, 'Failed to check for version update') } @@ -140,17 +178,6 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => logger?.warn({ error }, 'Error checking for version update') } }, checkIntervalMs) - - // Clean up interval when socket closes - const originalEnd = sock.end.bind(sock) - sock.end = (error) => { - if (versionCheckInterval) { - clearInterval(versionCheckInterval) - versionCheckInterval = null - logger?.debug('Stopped periodic version check') - } - return originalEnd(error) - } } return sock diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index 9c7a61f0..5b0d460c 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -49,17 +49,14 @@ export type SocketConfig = { logger: ILogger /** version to connect with */ version: WAVersion - /** - * Automatically fetch the latest WhatsApp Web version on connect. - * When enabled, fetches from web.whatsapp.com before connecting. - * Falls back to bundled version if fetch fails. - * @default false - */ - fetchLatestVersion: boolean /** * Interval in milliseconds to check for new WhatsApp Web versions. * When a new version is detected, it will be used on the next reconnection. * Set to 0 to disable periodic checks. + * + * Note: This option is only used by `makeWASocketAutoVersion()`. + * The standard `makeWASocket()` does not perform automatic version checks. + * * @default 21600000 (6 hours) */ versionCheckIntervalMs: number