From 70d6fcf4f68e39cefb896acf58f253aafacabc32 Mon Sep 17 00:00:00 2001 From: Fernando Falci Date: Thu, 3 Jul 2025 17:42:46 +0200 Subject: [PATCH 1/4] feat: add GitHub Actions workflow to tweet on release --- .github/workflows/tweet-on-release.yml | 30 ++++++ scripts/tweet-release.js | 139 +++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 .github/workflows/tweet-on-release.yml create mode 100644 scripts/tweet-release.js diff --git a/.github/workflows/tweet-on-release.yml b/.github/workflows/tweet-on-release.yml new file mode 100644 index 000000000..f47249d57 --- /dev/null +++ b/.github/workflows/tweet-on-release.yml @@ -0,0 +1,30 @@ +name: Tweet on Release + +on: + release: + types: [published] + +jobs: + tweet: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Send tweet + if: >- + ${{ secrets.TWITTER_CONSUMER_KEY != '' && + secrets.TWITTER_CONSUMER_SECRET != '' && + secrets.TWITTER_ACCESS_TOKEN != '' && + secrets.TWITTER_ACCESS_TOKEN_SECRET != '' }} + run: node scripts/tweet-release.js + env: + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/scripts/tweet-release.js b/scripts/tweet-release.js new file mode 100644 index 000000000..d0baab1de --- /dev/null +++ b/scripts/tweet-release.js @@ -0,0 +1,139 @@ +const https = require('https'); +const crypto = require('crypto'); + +const consumerKey = process.env.TWITTER_CONSUMER_KEY; +const consumerSecret = process.env.TWITTER_CONSUMER_SECRET; +const accessToken = process.env.TWITTER_ACCESS_TOKEN; +const accessTokenSecret = process.env.TWITTER_ACCESS_TOKEN_SECRET; + + +async function sendTweet(status) { + // Twitter API v2 requires Bearer Token (OAuth 2.0) or OAuth 1.0a user context. + // We'll use OAuth 1.0a user context, constructing the Authorization header manually. + + return new Promise((resolve, reject) => { + const method = 'POST'; + const url = 'https://api.twitter.com/2/tweets'; + const urlObj = new URL(url); + + // OAuth 1.0a parameters + const oauth = { + oauth_consumer_key: consumerKey, + oauth_nonce: crypto.randomBytes(16).toString('hex'), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: Math.floor(Date.now() / 1000).toString(), + oauth_token: accessToken, + oauth_version: '1.0' + }; + + // Twitter API v2 expects JSON body + const body = JSON.stringify({ text: status }); + + // Collect parameters for signature base string + const params = { + ...oauth + // No body params for signature in v2 endpoint + }; + + // Create parameter string (sorted by key) + const paramString = Object.keys(params) + .sort() + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .join('&'); + + // Signature base string + const baseString = [ + method, + encodeURIComponent(urlObj.origin + urlObj.pathname), + encodeURIComponent(paramString) + ].join('&'); + + // Signing key + const signingKey = [ + encodeURIComponent(consumerSecret), + encodeURIComponent(accessTokenSecret) + ].join('&'); + + // Signature + const signature = crypto + .createHmac('sha1', signingKey) + .update(baseString) + .digest('base64'); + + oauth.oauth_signature = signature; + + // Build Authorization header + const authHeader = + 'OAuth ' + + Object.keys(oauth) + .sort() + .map( + key => + `${encodeURIComponent(key)}="${encodeURIComponent(oauth[key])}"` + ) + .join(', '); + + const options = { + method: 'POST', + hostname: urlObj.hostname, + path: urlObj.pathname, + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + } + }; + + const req = https.request(options, res => { + let data = ''; + res.on('data', chunk => (data += chunk)); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(JSON.parse(data)); + } else { + let errMsg = `Twitter API error: ${res.statusCode} ${res.statusMessage}`; + try { + const errJson = JSON.parse(data); + errMsg += `\n${JSON.stringify(errJson)}`; + } catch (e) { + errMsg += `\n${data}`; + } + reject(new Error(errMsg)); + } + }); + }); + + req.on('error', err => reject(err)); + req.write(body); + req.end(); + }); +} + +async function main() { + if (!consumerKey || !consumerSecret || !accessToken || !accessTokenSecret) { + console.error('Error: Twitter API credentials are not set.'); + console.error('Please set TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN, and TWITTER_ACCESS_TOKEN_SECRET environment variables.'); + process.exit(1); + } + + try { + let status; + if (process.argv[2]) { + status = process.argv[2]; + } else { + const pkg = require('../package.json'); + const version = pkg.version; + const releaseUrl = `https://github.com/handshake-org/hsd/releases/tag/v${version}`; + status = `🚀 New release! hsd v${version} is out now. Check it out: ${releaseUrl}`; + } + await sendTweet(status); + console.log('Tweet sent successfully!'); + } catch (error) { + console.error('Error sending tweet:', error); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} \ No newline at end of file From 0ae672838544d2dca464251fafc8d70f1c5c47df Mon Sep 17 00:00:00 2001 From: Fernando Falci Date: Thu, 3 Jul 2025 18:50:55 +0200 Subject: [PATCH 2/4] fix: lint --- scripts/tweet-release.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/tweet-release.js b/scripts/tweet-release.js index d0baab1de..aba098c4c 100644 --- a/scripts/tweet-release.js +++ b/scripts/tweet-release.js @@ -6,11 +6,7 @@ const consumerSecret = process.env.TWITTER_CONSUMER_SECRET; const accessToken = process.env.TWITTER_ACCESS_TOKEN; const accessTokenSecret = process.env.TWITTER_ACCESS_TOKEN_SECRET; - async function sendTweet(status) { - // Twitter API v2 requires Bearer Token (OAuth 2.0) or OAuth 1.0a user context. - // We'll use OAuth 1.0a user context, constructing the Authorization header manually. - return new Promise((resolve, reject) => { const method = 'POST'; const url = 'https://api.twitter.com/2/tweets'; @@ -38,7 +34,9 @@ async function sendTweet(status) { // Create parameter string (sorted by key) const paramString = Object.keys(params) .sort() - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .map(key => + `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}` + ) .join('&'); // Signature base string @@ -91,7 +89,8 @@ async function sendTweet(status) { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(JSON.parse(data)); } else { - let errMsg = `Twitter API error: ${res.statusCode} ${res.statusMessage}`; + let errMsg = 'Twitter API error: ' + + `${res.statusCode} ${res.statusMessage}` try { const errJson = JSON.parse(data); errMsg += `\n${JSON.stringify(errJson)}`; @@ -112,7 +111,9 @@ async function sendTweet(status) { async function main() { if (!consumerKey || !consumerSecret || !accessToken || !accessTokenSecret) { console.error('Error: Twitter API credentials are not set.'); - console.error('Please set TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN, and TWITTER_ACCESS_TOKEN_SECRET environment variables.'); + console.error('Please set TWITTER_CONSUMER_KEY, ' + + 'TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN, ' + + 'and TWITTER_ACCESS_TOKEN_SECRET environment variables.'); process.exit(1); } @@ -123,8 +124,10 @@ async function main() { } else { const pkg = require('../package.json'); const version = pkg.version; - const releaseUrl = `https://github.com/handshake-org/hsd/releases/tag/v${version}`; - status = `🚀 New release! hsd v${version} is out now. Check it out: ${releaseUrl}`; + const releaseUrl = 'https://github.com/handshake-org' + + `/hsd/releases/tag/v${version}`; + status = `🚀 New release! hsd v${version} is out now. +Check it out: ${releaseUrl}`; } await sendTweet(status); console.log('Tweet sent successfully!'); From de43acc540435468da4a082f57824269fdb83984 Mon Sep 17 00:00:00 2001 From: Fernando Falci Date: Thu, 3 Jul 2025 20:47:25 +0200 Subject: [PATCH 3/4] fix: lint --- scripts/tweet-release.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/tweet-release.js b/scripts/tweet-release.js index aba098c4c..b19e3760f 100644 --- a/scripts/tweet-release.js +++ b/scripts/tweet-release.js @@ -1,3 +1,5 @@ +'use strict'; + const https = require('https'); const crypto = require('crypto'); @@ -34,7 +36,7 @@ async function sendTweet(status) { // Create parameter string (sorted by key) const paramString = Object.keys(params) .sort() - .map(key => + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}` ) .join('&'); @@ -82,7 +84,7 @@ async function sendTweet(status) { } }; - const req = https.request(options, res => { + const req = https.request((options, res => { let data = ''; res.on('data', chunk => (data += chunk)); res.on('end', () => { @@ -90,7 +92,7 @@ async function sendTweet(status) { resolve(JSON.parse(data)); } else { let errMsg = 'Twitter API error: ' - + `${res.statusCode} ${res.statusMessage}` + + `${res.statusCode} ${res.statusMessage}`; try { const errJson = JSON.parse(data); errMsg += `\n${JSON.stringify(errJson)}`; @@ -100,7 +102,7 @@ async function sendTweet(status) { reject(new Error(errMsg)); } }); - }); + })); req.on('error', err => reject(err)); req.write(body); @@ -124,7 +126,7 @@ async function main() { } else { const pkg = require('../package.json'); const version = pkg.version; - const releaseUrl = 'https://github.com/handshake-org' + + const releaseUrl = 'https://github.com/handshake-org' + `/hsd/releases/tag/v${version}`; status = `🚀 New release! hsd v${version} is out now. Check it out: ${releaseUrl}`; @@ -139,4 +141,4 @@ Check it out: ${releaseUrl}`; if (require.main === module) { main(); -} \ No newline at end of file +} From e884298f285860079ee539fe9b1f070c748fb504 Mon Sep 17 00:00:00 2001 From: Fernando Falci Date: Thu, 3 Jul 2025 21:03:22 +0200 Subject: [PATCH 4/4] fix: lint --- scripts/tweet-release.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/tweet-release.js b/scripts/tweet-release.js index b19e3760f..2a066dc97 100644 --- a/scripts/tweet-release.js +++ b/scripts/tweet-release.js @@ -84,7 +84,7 @@ async function sendTweet(status) { } }; - const req = https.request((options, res => { + const req = https.request(options, (res) => { let data = ''; res.on('data', chunk => (data += chunk)); res.on('end', () => { @@ -102,7 +102,7 @@ async function sendTweet(status) { reject(new Error(errMsg)); } }); - })); + }); req.on('error', err => reject(err)); req.write(body);