$30 off During Our Annual Pro Sale. View Details »

CiecleCIでもくもく会を支える技術

 CiecleCIでもくもく会を支える技術

2019-08-23-CiecleCIでもくもく会を支える技術

threetreeslight

August 23, 2019
Tweet

More Decks by threetreeslight

Other Decks in Technology

Transcript

  1. CiecleCI でもくもく会を ⽀える技術 CircleCI ユーザーコミュニティミートアップ on 2019-08-23 by @threetreeslight 1

    / 32
  2. こんにちわ! 2 / 32

  3. whoami NTT 系SIer (SE -> sales) ⾳楽系スタートアップ CTO メディア系スタートアップ 創業

    国内EC 創業 越境系発送代⾏サービス 1 号エンジニア Repro 創業兼CTO -> VP of Engineering -> VP of PeopleOps Akira Miki @threetreeslight コードが書けるイベントおじさん 3 / 32
  4. 今⽇話すこと・話さないこと 話さないこと Repro でのCircleCI 活⽤の詳細 話すこと Repro とCircleCI 利⽤どころ ↑スポンサートークっぽいの⼀応⼤事↑

    CircleCI でイベント開催を⾃動化している話 4 / 32
  5. 早速ですが スポンサートークいきます 5 / 32

  6. What's Repro? 6 / 32

  7. Customer Engagement Platform 7 / 32

  8. Web/App MA tool Right message To the right person At

    the right time 8 / 32
  9. T2D3 cf. Tech Crunch - The SaaS Adventure 9 /

    32
  10. Traffic 毎⽇10 億を超えるイベントデータを処理 AI でユーザーを⾃動セグメント 毎⽇2 億近いプッシュなど施策を配信 10 / 32

  11. そんなRepro での CircleCI 利⽤ 11 / 32

  12. ⾊々使っています Kick to build Image on image build server Kick

    to test on EC2 cluster and collect artifacts Attach github label about risk Run plan and apply Terraform 12 / 32
  13. そんなハイトラフィック を楽しみにたい⽅へ 13 / 32

  14. We are hiring 14 / 32

  15. 宣伝 終了 15 / 32

  16. それでは本題 参ります 16 / 32

  17. イベント 継続は⼤変 1. 企画するのが⼤変 2. 作成するのが⾯倒 3. 運営に気を使う 17 /

    32
  18. これを解決していく 18 / 32

  19. 企画するのが⼤変 もくもく会の内容を固定し、企画要素を削る 19 / 32

  20. 作成するのが⾯倒 オーガナイザーが複数いる場合は特に⾒合う。 => ⾃動化しましょう 20 / 32

  21. Setup Config 01 const moment = require('moment'); 02 const {

    EventDirCreator } = require('./event_dir_creator. 03 const { Slack } = require('./slack.js'); 04 const { ConnpassEventCreator } = require('./connpass_even 05 06 Slack.setup(process.env.SLACK_API_TOKEN); 07 08 // TODO: replase oauth api token 09 EventDirCreator.setup(process.env.GITHUB_API_TOKEN); 10 11 const nextEventNum = EventDirCreator.getNextEventNum(); 12 13 EventDirCreator.createDirWithNum(nextEventNum); 14 EventDirCreator.createPullRequestWithNum(nextEventNum); 21 / 32
  22. Setup Config 16 const eventDate = moment().day('Saturday').add(21, 'd').f 09 EventDirCreator.setup(process.env.GITHUB_API_TOKEN);

    10 11 const nextEventNum = EventDirCreator.getNextEventNum(); 12 13 EventDirCreator.createDirWithNum(nextEventNum); 14 EventDirCreator.createPullRequestWithNum(nextEventNum); 15 17 const connpassEventSettings = { 18 group: 'shinjuku-moku', 19 title: `Shinjuku Mokumoku Programming #${nextEventNum}` 20 subTitle: 'The Art of Mokumoku Programming', 21 startDate: eventDate, 22 startTime: '11:00', 23 endDate: eventDate 3W 後の開催⽇を指定 21 / 32
  23. Setup Config 18 group: 'shinjuku-moku', 19 title: `Shinjuku Mokumoku Programming

    #${nextEventNum}` 20 subTitle: 'The Art of Mokumoku Programming', 21 startDate: eventDate, 22 startTime: '11:00', 23 endDate: eventDate, 24 endTime: '18:00', 14 EventDirCreator.createPullRequestWithNum(nextEventNum); 15 16 const eventDate = moment().day('Saturday').add(21, 'd').f 17 const connpassEventSettings = { 25 participation: [ 26 { 27 attendeeType: ' ⾏きます', maxAttendees: 10, payDoor: イベントのメタ情報を突っ込む 21 / 32
  24. Setup Config 53 { 54 required: true, 55 title: '

    本もくもく会は、⾃⼰紹介と当⽇やることを記述したPul 56 answerType: 'radiobutton', 57 options: [' 問題なく遂⾏できる', ' やったことないので当⽇まで 58 }, 47 { 48 required: true, 49 title: ' 本もくもく会は、slack でcommunication を取ります。 50 answerType: 'radiobutton', 51 options: [' 問題なく遂⾏できる', ' やったことないので当⽇まで 52 }, 59 { 60 required: true, 61 title: ' もくもくして得た学びや気付き、成果をもくもく会終了時 ⺠度の担保を⽬的としたアンケートも作る 21 / 32
  25. Setup Config 66 descriptionPath: `${__dirname}/../connpass.md`, 58 }, 59 { 60

    required: true, 61 title: ' もくもくして得た学びや気付き、成果をもくもく会終了時 62 answerType: 'radiobutton', 63 options: [' ノープロブレム', 'LT 初⼼者ですががんばります'] 64 }, 65 ], 67 preview: true, 68 }; 69 70 (async () => { 71 const eventUrl = await ConnpassEventCreator.create(conn イベント本⽂の位置を指定 21 / 32
  26. Automatically Create event with Puppeteer 01 const puppeteer = require('puppeteer');

    02 const fs = require('fs'); 03 04 const logger = console; 05 06 const ConnpassEventCreator = {}; 07 08 ConnpassEventCreator.getDescription = path => fs.readFile 09 10 ConnpassEventCreator.create = async (settings) => { 11 /* eslint-disable no-await-in-loop */ 12 13 const user = process.env.CONNPASS_USER; 14 const password = process.env.CONNPASS_PASSWORD; 22 / 32
  27. Automatically Create event with Puppeteer 23 await page.goto('https://connpass.com/login/'); 24 const

    loginAreaSelector = '#login_form'; 25 await page.type(`${loginAreaSelector} input[name="usern 26 await page.type(`${loginAreaSelector} input[name="passw 27 await page.click(`${loginAreaSelector} button[type="sub 28 await page.waitForSelector('.EventCreate'); 29 19 20 // -------------------------------------------------- 21 // Login to connpass 22 // -------------------------------------------------- 30 // -------------------------------------------------- 31 // Move to group page and create event 32 // -------------------------------------------------- 33 if (typeof (settings group) === 'string') { めちゃめちゃがんばる 22 / 32
  28. Automatically Create event with Puppeteer 78 // delete exist value

    79 await page.$eval(`${timeAreaSelector} input[name="end_d 80 await page.keyboard.press('Backspace'); 81 // input data 82 await page.type(`${timeAreaSelector} input[name="end_da 83 // Unforcus from input form to close datepitcker 84 await page.click(`${timeAreaSelector} th`); 85 // Wait to close datepicker animation. Datepicker overw 86 await page.waitFor(500); 73 // wait for fill end date automatically 74 await page.waitFor(500); 75 76 // focus input form 77 await page.click(`${timeAreaSelector} input[name="end_d 87 アニメーションに殺意を覚える 22 / 32
  29. Config circleci 01 # Javascript Node CircleCI 2.0 configuration file

    02 # 03 # Check https://circleci.com/docs/2.0/language-javascript 04 # 05 version: 2.1 06 07 orbs: 08 puppeteer: threetreeslight/puppeteer@0.1.2 09 10 jobs: 11 build: 12 docker: 13 - image: circleci/node:8 14 steps: 23 / 32
  30. Config circleci 07 orbs: 08 puppeteer: threetreeslight/puppeteer@0.1.2 01 # Javascript

    Node CircleCI 2.0 configuration file 02 # 03 # Check https://circleci.com/docs/2.0/language-javascript 04 # 05 version: 2.1 06 09 10 jobs: 11 build: 12 docker: 13 - image: circleci/node:8 14 steps: puppeteer install orb 作って 23 / 32
  31. Config circleci 96 nightly: 97 triggers: 98 - schedule: 99

    # Run evey friday 100 cron: "0 0 * * 5" 101 filters: 102 branches: 103 only: 104 - master 91 requires: 92 - build 93 filters: 94 branches: 95 only: master 105 j b nightly build する 23 / 32
  32. 訪れる幸せ 24 / 32

  33. ちなみに circleci と⽇本からではconnpass server への network latency が異なるので( 致し⽅がない)sleep の微調整が必要

    25 / 32
  34. 運営に気を使う timetable にあわせ、いちいちslack のreminder 設定 したり連絡したりするの⾟い。 => firebase function +

    slash command で凌ぐ 26 / 32
  35. slack remind and command 01 // Prepare for event start

    02 // 03 // 1. Create vol-xx channel 04 // 1. Set lunch and due reminder 05 // 1. Set lunch poller 06 // 1. Set announce event channel to general 07 08 const logger = console; 09 const { Slack } = require('./slack.js'); 10 11 const Preparation = {}; 12 13 Preparation.start = async (slackToken, num) => { 14 Slack.setup(slackToken); 27 / 32
  36. slack remind and command 15 const currentChannelName = `vol-${num}`; 16

    17 logger.info(`channel name is ${currentChannelName}`); 18 19 await Slack.create_channel(currentChannelName); 10 11 const Preparation = {}; 12 13 Preparation.start = async (slackToken, num) => { 14 Slack.setup(slackToken); 20 const channelId = await Slack.get_channel_id(currentCha 21 22 // Event channel announce 23 const generalId = await Slack.get_channel_id('general') 24 開催回のchannel 作って 27 / 32
  37. slack remind and command 25 // for introduction 26 Slack.message(generalId,

    ` 今⽇のshinjuku mokumoku slack 27 Slack.message(channelId, 'wifi: \nhttps://gitpitch.com/ 28 Slack.command(channelId, '/remind', `<#${channelId}> \n 19 await Slack.create_channel(currentChannelName); 20 const channelId = await Slack.get_channel_id(currentCha 21 22 // Event channel announce 23 const generalId = await Slack.get_channel_id('general') 24 29 "@channel わからないことがあるときはまず以下を参照しましょう :poi 30 \n 31 イベントページ: https://shinjuku-moku.connpass.com/\n 32 introduction 資料: https://gitpitch.com/shinjuku-mokumoku/s 33 \ 通知を設定 27 / 32
  38. slack remind and command 34 *:warning: Attention :warning:*\n 35 -

    会場IP からのスクレイピング・クローリングコードの実⾏は⽌めてくださ 36 - 本イベントは[ アンチハラスメントポリシー](http://25.ruby.or.jp 37 - どなたでもblog などにあげられるよう写真撮影を許可していますので、そ 38 - 途中退出される場合は、**PR に** 今⽇の成果をお出しください\n 28 Slack.command(channelId, /remind , <#${channelId}> \n 29 "@channel わからないことがあるときはまず以下を参照しましょう :poi 30 \n 31 イベントページ: https://shinjuku-moku.connpass.com/\n 32 introduction 資料: https://gitpitch.com/shinjuku-mokumoku/s 33 \n 39 " at 11:30`); 40 41 // Lunch 42 Sl k d( h lId '/ ll' '" 昼⾷どこらへんが好き? 諸注意も再連絡しつつ 27 / 32
  39. slack remind and command 41 // Lunch 42 Slack.command(channelId, '/poll',

    '" 昼⾷どこらへんが好き? 34 *:warning: Attention :warning:*\n 35 - 会場IP からのスクレイピング・クローリングコードの実⾏は⽌めてくださ 36 - 本イベントは[ アンチハラスメントポリシー](http://25.ruby.or.jp 37 - どなたでもblog などにあげられるよう写真撮影を許可していますので、そ 38 - 途中退出される場合は、**PR に** 今⽇の成果をお出しください\n 39 " at 11:30`); 40 43 Slack.message(channelId, ' ランチリスト: \nhttps://github. 44 Slack.command(channelId, '/remind', `<#${channelId}> " 45 @channel もうすぐlunch です。ランチアンケートへの回答しましょう!\ 46 ランチリスト: \n 47 https://github.com/shinjuku-mokumoku/shinjuku-mokumoku/bl 公開されていないSlack API を使ってコマンドを叩き 27 / 32
  40. slack remind and command 54 // checkout 55 Slack.command(channelId, '/remind',

    `<#${channelId}> " 56 @channel checkout まであと1h です!成果のまとめなどしていきましょう 57 発表は *1.5-3 min + 質問 0-2min / person* です! 58 " at 16:00`); 59 Slack.command(channelId, '/remind', `<#${channelId}> " 60 @channel checkout の10min 前です!\n 49 Slack.command(channelId, '/remind', `<#${channelId}> "@ 50 51 // check templature 52 Slack.command(channelId, '/remind', `<#${channelId}> " 暑 53 61 今⽇の成果項を更新しshinjuku-mokumoku へPR をお願いします :muscle 62 発表ではchrome cast を使います。 終了のリマインドまで⼊れる 27 / 32
  41. deploy firebase 01 const { execSync } = require('child_process'); 02

    const Octokit = require('@octokit/rest'); 03 04 const octokit = new Octokit(); 05 const logger = console; 06 07 const owner = 'shinjuku-mokumoku'; 08 const repo = 'shinjuku-mokumoku'; 09 10 const pullRequestNum = async () => { 11 if (process.env.CIRCLE_PR_NUMBER) { 12 return process.env.CIRCLE_PR_NUMBER; 13 } 14 28 / 32
  42. deploy firebase 27 const hasFunctionDiff = async () => {

    28 const num = await pullRequestNum(); 29 logger.debug(`Pull Request is https://github.com/shinju 30 31 const res = await octokit.pulls.listFiles({ owner, repo 32 const functionFileNames = res.data.map(files => files.f 33 logger.debug(`Change Files: ${functionFileNames.join(', 34 35 return functionFileNames.length > 0; 36 }; p [ ] ; 25 }; 26 37 38 const deploy = async () => { 変更があるときだけfirebase functions にdeploy されるようにしておき 28 / 32
  43. build and deploy 01 # Javascript Node CircleCI 2.0 configuration

    file 02 # 03 # Check https://circleci.com/docs/2.0/language-javascript 04 # 05 version: 2.1 06 07 orbs: 08 puppeteer: threetreeslight/puppeteer@0.1.2 09 10 jobs: 11 build: 12 docker: 13 - image: circleci/node:8 14 steps: 29 / 32
  44. build and deploy 61 - run: 62 name: Deploy Master

    to Firebase 63 command: npm --prefix functions run deploy g 55 - functions_npm_modules- 56 - run: npm --prefix functions install 57 - save_cache: 58 paths: 59 - functions/node_modules 60 key: functions_npm_modules-{{ checksum "functio 64 65 event: 66 docker: 67 - image: circleci/node:8 68 steps: build & deploy していく 29 / 32
  45. 訪れる幸せ 30 / 32

  46. ちなみに community で使うfirebase の登録クレカ、どうす るか悩みますよね 31 / 32

  47. まとめ 誰やるにならないように常に⾃動化 puppeteer の可能性無限⼤ slack のslash command の可能性無限⼤ CI にまかせていく

    sheduled workflow 便利! orb 便利! network latency には気をつける 時間⾒つけてイベント⾃動作成が外でもできるよう package 化していこう 32 / 32