基於 Probot 實現 GitHub NPM 釋出機器人?

洛竹發表於2021-06-01

這是我參與更文挑戰的第1天,活動詳情檢視: 更文挑戰

怕什麼真理無窮,進一寸有一寸的歡喜。大家好,我是@洛竹,一名熱愛程式設計、熱愛生活的終身學習實踐者。

關於 Github Apps

GitHub Apps 是 GitHub 中優秀的產品。一個 GitHub App 通過 API 直接使用自己的身份進行操作,這意味著你不需要作為一個單獨的使用者維護一個機器人或服務賬戶。

GitHub Apps 可以被直接安裝到組織或者使用者賬號上,並且可以賦予它們訪問指定倉庫的許可權。它們帶有內建的 webhook 和狹窄的特定許可權。設定 GitHub 應用程式時,可以選擇希望它訪問的倉庫。比如你可以設定一個叫 MyGitHub 的應用程式,該應用程式有且僅有 octocat 倉庫的寫入 issues 的許可權。安裝 GitHub App 需要你是組織的所有者或對倉庫擁有 admin 許可權。

關於 Probot

Probot 是一個基於 Node.js 構建 GitHub Apps 的框架。它旨在消除所有繁瑣的事情,如接收和驗證 webhooks 以及做認證動作,這樣你就可以專注於你想要實現的功能。Probot 應用程式非常容易編寫、部署和分享。大多數流行的 Probot 應用被託管,所以你不需要做任何部署和管理工作。這裡有幾個基於 Probot 構建的應用:

你可以在特色應用GitHub probot-app 話題瀏覽更多應用。

Hello Probot

一個 Probot 應用本質上是匯出一個函式的 Node.js 模組

module.exports = app => {
  // your code here
};
複製程式碼

app 引數是 Probot 類的例項,該例項可以讓你訪問所有的 GitHub 精華。

aap.on 負責監聽所有 GitHub 觸發的 webhook 事件,當 GitHub 上發生任何有趣的事情,你的應用程式想知道的時候,它會通知你。

module.exports = app => {
  app.on('issues.opened', async context => {
    // 一個新的 issue 被開啟,我們應該做些什麼呢?
    context.log.info(context.payload);
  });
};
複製程式碼

傳遞給事件處理程式的上下文包括關於被觸發的事件的一切,以及一些有用的屬性,以便對事件做出有用的回應。context.octokit 是一個經過認證的 GitHub 客戶端,可以用來進行 REST API 和 GraphQL 呼叫,並允許你以程式設計方式做幾乎任何你可以在 GitHub 上通過瀏覽器做的事情。

下面是一個當新開啟 issues 時自動評論的 App。

module.exports = app => {
  app.on('issues.opened', async context => {
    // context` 從事件中提取資訊,可以傳遞給 GitHub API 呼叫。這將返回:
    // { owner: 'yourname', repo: 'yourrepo', number: 123, body: 'Hello World !}
    const params = context.issue({ body: 'Hello World!' });

    // 在 issue 上發一條評論
    return context.octokit.issues.createComment(params);
  });
};
複製程式碼

開發一個 Probot app

為了開發一個 Probot app,你首先需要安裝 Node.js 10.0.0 或更新的版本。

生成一個新的 app

create-probot-app 是開始構建一個新的 app 的最佳方式。它將生成一個新的應用程式,其中包含你所需要的一切,以開始並在生產中執行你的應用程式。

執行下面的命令生成一個專案:

$ npx create-probot-app my-first-app
複製程式碼

該命令會問一系列關於你的 app 的問題,看起來就像:

Let's create a Probot app!
? App name: my-first-app
? Description of app: A 'Hello World' GitHub App built with Probot.
? Author's full name: Katie Horne
? Author's email address: katie@auth0.com
? GitHub user or org name: khorne3
? Repository name: my-first-app
? Which template would you like to use? (Use arrow keys)
❯ basic-js
  basic-ts (use this one for TypeScript support)
  checks-js
  git-data-js
  deploy-js

Finished scaffolding files!

Installing dependencies. This may take a few minutes...

Successfully created my-first-app.

Begin using your app with:
  cd my-first-app
  npm start

View your app's README for more usage instructions.

Visit the Probot docs:
  https://probot.github.io/docs/

Get help from the community:
  https://probot.github.io/community/

Enjoy building your Probot app!
複製程式碼

建立的最重要的檔案是 index.js(你的 app 程式碼所在的位置)和 package.json(使你的 app 成為標準 npm module)。

本地執行 app

現在你已經準備好在本地執行 app 了。執行 npm start 來開啟一個 server 吧:

注意:如果你選擇了 TypeScript 模板,請確保執行了 npm run build

$ yarn start
yarn run v1.22.10
$ probot run ./lib/index.js
INFO (server): Running Probot v11.3.0 (Node.js: v14.15.5)
INFO (server): Forwarding https://smee.io/dz7D1zur24cGNj7 to http://localhost:3000/
INFO (server): Listening on http://localhost:3000
INFO (server): Connected
複製程式碼

配置 GitHub App

下列是自動配置 GitHub App 的步驟:

  1. 在本地命令列中執行 npm start
  2. 訪問 http://localhost:3000 檢視下一步。
  3. 你會看到類似下面的頁面。

  1. 點選 Register a GitHub App 按鈕繼續。
  2. 接著,你需要給你的 App 取一個沒有被佔用的名字,注意:如果你看到類似 Name is reserved for the account @tuya 的提示,這意味著你不能使用已存在的 GitHub organization 的名字作為 app 的名字(除非你是該組織的 owner)

GitHub Release 時 npm publish

實現 GitHub CI 自動釋出 NPM 包,主要是為了合理管理對外 npm 釋出許可權。而比較通用的釋出時機是在 GitHub release 時。基於上面流程圖的分析,我們可以看出 released 狀態時執行 npm publish 最合適。

我們實現的具體邏輯是,當 Probot app 監聽到 release.released 事件時,處理髮布前的操作。重要的是我們需要根據 package.json 中的 version 欄位匹配出 tag,比如:

  • 1.0.0:tag 為 latest 的 1.0.0
  • 1.0.0-beta.0:tag 為 beta 的 1.0.0-beta.0
  • 1.0.0-alpha.0:tag 為 alpha 的 1.0.0-alpha

NPM 自動釋出實現原理

釋出之前我們需要拉取倉庫程式碼、取出版本和 tag、設定 NPM publish Token 等工作。先上核心程式碼,後面我們詳細解析。

app.on('release.released', async context => {
  if (!isTuya(context)) return;
  app.log('npm publishing');
  const { repository: repo } = context.payload;
  const downloadDefaultBranch = `${repo.full_name}#${context.payload.release.tag_name}`;
  const downLoadTempDir = `${os.tmpdir()}/${repo.full_name}`;
  await download(downloadDefaultBranch, downLoadTempDir);
  const { version, scripts } = require(`${downLoadTempDir}/package.json`);
  const tag = /^\d\.\d\.\d-(.*)\.\d$/.exec(version)
    ? /^\d\.\d\.\d-(.*)\.\d$/.exec(version)[1]
    : 'latest';
  // 如果有 build 指令碼則先執行 build 指令碼
  if (scripts.build) {
    await execSh(`cd ${downLoadTempDir} && npm install && npm run build`);
  }
  try {
    const result = await npmPublish({
      package: `${downLoadTempDir}/package.json`,
      token: process.env.NPM_AUTH_TOKEN,
      registry: 'https://registry.npmjs.org/',
      tag,
    });
    if (result.type === 'none') {
      app.log.error(
        `You can't publish duplicate version ${result.package}@${result.version}`,
      );
    }
  } catch (error) {
    app.log.error(error);
  }
});
複製程式碼

NPM Publish Token

申請 NPM Publish Token

1. 訪問 npmjs.com 進入 Access Tokens 頁面

2. 點選 Generate New Token 按鈕

3. Token 型別選擇 Publish

保證 NPM Publish Token 安全性

NPM Token 是不能被別人看到的,為了達到這個目的,首先專案需要設定為私有的,然後將 Token 放到 .env 中,通過 process.env.NPM_AUTH_TOKEN 獲取。另外謹記不要在日誌中列印環境變數。

保證 GitHub App 安全性

如果把 GitHub App 釋出為 public 的,那麼任何倉庫都可以安裝該應用,這不是我們想要的結果。解決辦法有兩個,一是將應用註冊為 private 型別的,二是在監聽回撥中判斷是否是允許的組織或者使用者。我選擇的是第二種方案,校驗函式如下:

const isTuya = context => {
  const { full_name } = context.payload.repository;
  return full_name.startsWith('youngjuning') || full_name.startsWith('tuya');
};
複製程式碼

下載原始碼

我們選擇了 download-git-repo 下載 git 倉庫,但是該倉庫不支援 Promise,我們做一下簡單的改造:

const download = require('download-git-repo');

module.exports = (repo, tempDir) => {
  return new Promise((resolve, reject) => {
    download(repo, tempDir, err => {
      if (err) {
        reject(err);
      } else {
        resolve(null);
      }
    });
  });
};
複製程式碼

npmPublish

我們選擇了 @jsdevtools/npm-publish 執行釋出動作,該倉庫除了程式設計呼叫外,還可以作為 GitHub Action 和命令列工具使用。需要注意的是,我們需要用正則取出我們要釋出的 tag:

const tag = /^\d\.\d\.\d-(.*)\.\d$/.exec(version)
  ? /^\d\.\d\.\d-(.*)\.\d$/.exec(version)[1]
  : 'latest';
複製程式碼

lerna publish

lerna 管理版本由於是一次可能釋出多個倉庫,所以無法使用上面提到的釋出流程。針對 lerna,我設計的釋出流程是監聽到 push 動作後取最新的一條 commit,匹配是否包含 chore(release): publish。具體原理如下:

  1. 判斷 push 分支是否是主分支且提交資訊包含 chore(release): publish
  2. 因為是 lerna publish,所以需要使用 simple-git 這個庫 clone 專案。
  3. 由於 lerna publish 不支援 token,我們採用將 //registry.npmjs.org/:_authToken=${process.env.NPM_AUTH_TOKEN} 寫入 .npmrc 的方式完成帶 token 的釋出。
  4. 最後,我們需要使用 from-git 的方式執行 lerna publishfrom-git 的場景便是本地執行 lerna version,在 CI 中執行 lerna publish

完整程式碼如下:

app.on('push', async context => {
  if (!isTuya(context)) return;
  if (
    context.payload.ref.indexOf(context.payload.repository.default_branch) !==
      -1 &&
    context.payload.head_commit.message.indexOf('chore(release): publish') === 0
  ) {
    app.log('push event');
    execSh(`git --version`);
    const { repository: repo } = context.payload;
    const cloneTempDir = `${os.tmpdir()}/${repo.full_name}`;
    try {
      await git.clone(repo.clone_url, cloneTempDir);
      const { devDependencies } = require(`${cloneTempDir}/package.json`);
      if (devDependencies['lerna']) {
        app.log('lerna publishing');
        await execSh(
          `cd ${cloneTempDir} && echo //registry.npmjs.org/:_authToken=${process.env.NPM_AUTH_TOKEN} > .npmrc`,
        );
        await execSh(
          `cd ${cloneTempDir} && npm install && npm run build && ./node_modules/.bin/lerna publish from-git --yes --no-verify-access`,
        );
      }
      await execSh(`rm -rf ${cloneTempDir}`);
    } catch (error) {
      await execSh(`rm -rf ${cloneTempDir}`);
    }
  }
});
複製程式碼

Glitch 部署

如果你有自己的伺服器,可以直接將機器人程式部署到自己的伺服器。我這裡使用官方推薦的 Glitch 服務部署。Glitch 可以免費託管 node 應用並且直接在瀏覽器中編輯他們。對於簡單的應用完全夠了。

  1. 註冊並在 Glitch 新建專案,選擇 Import from GitHub,彈窗寫上應用 github 地址,或者使用 github.com/behaviorbot… 作為模板匯入後再將自己的程式碼複製過來。
  2. 開啟 .env 檔案使用以下內容替代:
APP_ID=<your app id>
WEBHOOK_SECRET=<your app secret>
PRIVATE_KEY_PATH=<your private_key>
NODE_ENV=production
NPM_AUTH_TOKEN=3c2c104e-9f1f-4fc5-903e-726610b75ce1
INPUT_TOKEN=
複製程式碼
  1. 將 glitch 連結設定為 GitHub App 的 webhook 地址即可,之後更新程式碼,glitch 會自動更新部署。

許可權

Probot App 的初始許可權在 app.yml 檔案中,如果 App 已經建立了,又想要更新許可權,可以在 github.com/settings/ap… 中更新。我所用的許可權配置請點選 app.yml 檢視。

本文首發於「洛竹的官方網站」,同步於公眾號「洛竹早茶館」和「掘金專欄」。

相關文章