214 lines
7.0 KiB
JavaScript
214 lines
7.0 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Post dashboard screenshot to Slack
|
||
*
|
||
* Uploads dashboard screenshot to Slack with summary message
|
||
*
|
||
* Dry run: DRY_RUN=1, DRY_RUN=true, or --dry-run
|
||
* - Does not post. Prints payload summary.
|
||
* - If SLACK_BOT_TOKEN is set, calls auth.test only (no channel post).
|
||
*/
|
||
|
||
import 'dotenv/config';
|
||
import { WebClient } from '@slack/web-api';
|
||
import fs from 'fs/promises';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
function isDryRun() {
|
||
return (
|
||
process.env.DRY_RUN === '1' ||
|
||
process.env.DRY_RUN === 'true' ||
|
||
process.argv.includes('--dry-run')
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Build Slack post payload from local files (no network).
|
||
*/
|
||
async function buildPostPayload() {
|
||
const dataPath = path.join(__dirname, '..', 'data', 'health-data.json');
|
||
const healthData = JSON.parse(await fs.readFile(dataPath, 'utf-8'));
|
||
|
||
const now = new Date(healthData.calculatedAt).toLocaleString('ja-JP', {
|
||
timeZone: 'Asia/Tokyo',
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
|
||
let dashboardUrl = null;
|
||
try {
|
||
const urlPath = path.join(__dirname, '..', 'data', 'deployed-url.txt');
|
||
dashboardUrl = (await fs.readFile(urlPath, 'utf-8')).trim();
|
||
} catch {
|
||
// skip
|
||
}
|
||
|
||
let messageText = `📊 マガジン進捗管理ダッシュボード(${now})\n\n`;
|
||
|
||
if (dashboardUrl) {
|
||
messageText += `🔗 <${dashboardUrl}|ダッシュボードを開く>\n\n`;
|
||
}
|
||
|
||
messageText += `*全体健康度*: ${healthData.overallHealth.status} ${healthData.overallHealth.label}`;
|
||
|
||
const screenshotPaths = [
|
||
path.join(__dirname, '..', 'output', 'screenshot-1.png'),
|
||
path.join(__dirname, '..', 'output', 'screenshot-2.png'),
|
||
path.join(__dirname, '..', 'output', 'screenshot-3.png')
|
||
];
|
||
|
||
const fileUploads = [];
|
||
for (let i = 0; i < screenshotPaths.length; i++) {
|
||
try {
|
||
await fs.access(screenshotPaths[i]);
|
||
const imageBuffer = await fs.readFile(screenshotPaths[i]);
|
||
fileUploads.push({
|
||
file: imageBuffer,
|
||
filename: `magazine-dashboard-${i + 1}.png`,
|
||
path: screenshotPaths[i],
|
||
sizeBytes: imageBuffer.length
|
||
});
|
||
} catch {
|
||
// missing
|
||
}
|
||
}
|
||
|
||
return { messageText, dashboardUrl, fileUploads, healthData };
|
||
}
|
||
|
||
async function postToSlack() {
|
||
const dryRun = isDryRun();
|
||
|
||
if (dryRun) {
|
||
console.log('🧪 DRY RUN — Slack には投稿しません\n');
|
||
} else {
|
||
console.log('💬 Slackに投稿中...');
|
||
}
|
||
|
||
const channelId = process.env.MAGAZINE_SLACK_CHANNEL_ID;
|
||
if (!channelId) {
|
||
console.error('❌ MAGAZINE_SLACK_CHANNEL_ID が設定されていません');
|
||
console.log('💡 .env ファイルに MAGAZINE_SLACK_CHANNEL_ID=C0XXXXXXXXX を設定してください');
|
||
process.exit(1);
|
||
}
|
||
|
||
if (!dryRun && !process.env.MAGAZINE_SLACK_BOT_TOKEN) {
|
||
console.error('❌ MAGAZINE_SLACK_BOT_TOKEN が設定されていません');
|
||
console.log('💡 .env ファイルに MAGAZINE_SLACK_BOT_TOKEN=xoxb-xxxxx を設定してください');
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(`📮 投稿先チャンネル ID: ${channelId}`);
|
||
|
||
try {
|
||
const { messageText, dashboardUrl, fileUploads } = await buildPostPayload();
|
||
|
||
if (dryRun) {
|
||
console.log('--- 投稿予定の本文 ---');
|
||
console.log(messageText.replace(/<([^|>]+)\|([^>]+)>/g, '$2 ($1)'));
|
||
console.log('---');
|
||
console.log(`ダッシュボードURL: ${dashboardUrl ?? '(なし)'}`);
|
||
console.log(`添付画像: ${fileUploads.length}枚`);
|
||
for (const u of fileUploads) {
|
||
console.log(` - ${u.filename} (${u.sizeBytes} bytes)`);
|
||
}
|
||
if (fileUploads.length === 0) {
|
||
console.log(' (スクリーンショットなし → テキストのみ投稿の想定)');
|
||
}
|
||
|
||
if (process.env.MAGAZINE_SLACK_BOT_TOKEN) {
|
||
const slack = new WebClient(process.env.MAGAZINE_SLACK_BOT_TOKEN);
|
||
const auth = await slack.auth.test();
|
||
if (auth.ok) {
|
||
console.log('\n✅ auth.test OK(トークンは有効)');
|
||
console.log(` bot: ${auth.user ?? auth.bot_id ?? '—'} / team: ${auth.team ?? '—'}`);
|
||
} else {
|
||
console.log('\n⚠️ auth.test 失敗:', auth.error);
|
||
process.exit(1);
|
||
}
|
||
} else {
|
||
console.log('\n💡 MAGAZINE_SLACK_BOT_TOKEN 未設定のため auth.test はスキップ');
|
||
}
|
||
|
||
console.log('\n✅ ドライラン完了');
|
||
return;
|
||
}
|
||
|
||
const slack = new WebClient(process.env.MAGAZINE_SLACK_BOT_TOKEN);
|
||
|
||
const uploadsForApi = fileUploads.map(({ file, filename }) => ({ file, filename }));
|
||
|
||
console.log(`📤 メッセージを投稿中... (画像: ${uploadsForApi.length}枚)`);
|
||
|
||
if (uploadsForApi.length > 0) {
|
||
const uploadResult = await slack.files.uploadV2({
|
||
channel_id: channelId,
|
||
file_uploads: uploadsForApi,
|
||
initial_comment: messageText
|
||
});
|
||
|
||
if (uploadResult.ok) {
|
||
console.log('✅ 画像アップロード完了');
|
||
console.log('✅ Slackへの投稿が完了しました!');
|
||
console.log(`📍 投稿先チャンネルID: ${channelId}`);
|
||
|
||
if (uploadResult.files && uploadResult.files.length > 0) {
|
||
const file = uploadResult.files[0];
|
||
if (file.files && file.files.length > 0 && file.files[0].permalink) {
|
||
console.log(`📎 ファイルURL: ${file.files[0].permalink}`);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
console.error('⚠️ アップロードエラー:', uploadResult.error);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log('⚠️ スクリーンショットが見つからないため、テキストのみ投稿します');
|
||
const postResult = await slack.chat.postMessage({
|
||
channel: channelId,
|
||
text: messageText
|
||
});
|
||
|
||
if (postResult.ok) {
|
||
console.log('✅ Slackへの投稿が完了しました! (テキストのみ)');
|
||
console.log(`📍 投稿先チャンネルID: ${channelId}`);
|
||
return;
|
||
}
|
||
console.error('⚠️ 投稿エラー:', postResult.error);
|
||
process.exit(1);
|
||
} catch (error) {
|
||
console.error('❌ エラーが発生しました:', error.message);
|
||
|
||
if (error.data?.error === 'not_in_channel') {
|
||
console.log('💡 ボットをチャンネルに追加してください:');
|
||
console.log(` /invite @your-bot-name をチャンネルで実行`);
|
||
} else if (error.data?.error === 'channel_not_found') {
|
||
console.log('💡 チャンネルが見つかりません。チャンネルIDを確認してください。');
|
||
console.log(' プライベートチャンネルの場合は、ボットを招待する必要があります。');
|
||
} else if (error.data?.error === 'invalid_auth') {
|
||
console.log('💡 Slack Bot Token が無効です。.env ファイルを確認してください。');
|
||
}
|
||
|
||
if (error.stack) {
|
||
console.error(error.stack);
|
||
}
|
||
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||
postToSlack();
|
||
}
|
||
|
||
export default postToSlack;
|