2020年5月27日

[資安] 跨站偽造請求(Cross Site Request Forger, CSRF, XSRF) 的說明與預防

Photo by Clint Patterson on Unsplash Photo by Clint Patterson on Unsplash
keywords: csrf, internet security, one-click-attack, session-riding, XSRF

Cross Site Request Forgery 是什麼

跨站偽造請求(cross-site request forgery)也稱為 one-click attacksession riding,通常縮寫為 CSRF(有時發音為 sea-surf) 或 XSRF,這是一種利用伺服器所信任的網站來發送惡意請求的攻擊;和 cross-site scripting (XSS) 不同,XSS 是透過在網站上輸入惡意程式碼的方式來進行攻擊,通常利用的是「使用者對目標網站」的信任;而 CSRF 則是攻擊者利用「目標網站對該信用者」的信任。透過 CSRF 攻擊有機會讓使用者在無意間修改受害者的帳號密碼,或將帳戶內的金額轉帳給攻擊者。
CSRF 通常有以下流程:
  • 使用者使用正常流程登入「目標網站」
  • 「惡意網站」利用目標網站對使用者的信任(credentials),例如 Cookies
  • 欺騙使用者到「惡意網站」後,誘使使用者點擊某個按鈕,但這個按鈕可能會送出表單,而該表單的請求對象是對到「目標網站」
  • 瀏覽器預設會把使用者在「目標網站」的 Cookie 連帶送出,因此雖然該請求是在「惡意網站」發出,但「目標網站」收到請求時因為帶有 credentials,所以會誤以為是合法的請求。

攻擊可能的樣子

使用者一旦點擊下方的連結,即會向「目標網站」送出 Post 請求:
<!--
 - Code from Rails Guides
 - https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
-->

<a
  href="http://www.harmless.com/"
  onclick="
  var f = document.createElement('form');
  f.style.display = 'none';
  this.parentNode.appendChild(f);
  f.method = 'POST';
  f.action = 'http://www.example.com/account/destroy';
  f.submit();
  return false;"
  >To the harmless survey</a
>
或者使用者只要將滑鼠移過某一張圖片時,即透過 JavaScript 執行指令:
<!--
 - Code from Rails Guides
 - https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
-->
<img
  src="http://www.harmless.com/img"
  width="400"
  height="400"
  onmouseover="..."
/>

解決方式

現在多數的框架都支援去防範 CSRF 的攻擊。舉例來說,在 Ruby on Rails 的網頁應用程式中,由本站的表單或透過 AJAX 向伺服器發送的請求中都加上 security token(X-CSRF-Token),並於伺服器端驗證此 Token。由於這個 Token 只能在瀏覽本站時取得,因此攻擊者透過惡意網站試圖向伺服器發送請求時,雖然這個請求帶有認證過的 Cookie,但因為它並沒有帶有合法的 X-CSRF-Token,Rails 在處理此請求時會拋出錯誤。

參考

2020年5月26日

[資料結構] Binary Search Tree

keywords: data structure, recursive function, 遞迴函式, queue, 佇列
此系列筆記主要依照 [Udemy] Learning Data Structures in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。
Imgur
  • Binary Search Tree 有一個很重要的概念是,它是有「階層」的概念在內的,並不是把所有數字放在同一層排序
  • 在建立 BST instance 時會把 root 的 value 一併帶入
  • 新增值到 BST 時,會判斷這個值比 root 大或小,再以同樣的邏輯分派到 BST 的左下側或右下側
  • 檢驗 BST 中包含是否包含某一值時,也是使用同樣的邏輯,先判斷這個值比 root 大還是小,接著依同樣的邏輯往左下側或右下側尋找
  • 找出最小值的方式是從 root 開始一路往左下找;找出最大值的方式則是從 root 開始一路往右下找
  • 要疊代整個 BST 元素的方式可以分成 Depth First TraversalBreadth First Traversal
  • 其中 Depth First Traversal 又可以分成:
    • 由小到大依序迭代(in-order)
    • 由上往下,由左至右(pre-order)
    • 由下往上,由左至右(post-order)
  • Breadth First Traversal 則是以水平的方式由上往下依序疊代所有內容

Depth First Traversal

在撰寫這個方法的時候,需要特別留意 call stack 中執行的順序:
Imgur

In-Order

從最底部開始,先把左邊的輸出,在輸出右邊的,如此所有的值就會由小到大依序排列。輸出的內容會是:10, 20, 30, 35, 45, 50, 59, 60, 70, 85, 100, 105。
Imgur

Pre-Order

  • 適合使用在想要 Copy Tree 時使用
  • 由上往下,由左至右,依序輸出所有內容。以下圖為例,會輸出 50, 30, 20, 10, 45, 35, 70, 60, 59, 100, 85, 105:
Imgur

Post-Order

  • 適合使用在刪除節點時
  • 由下往上,由左至右。以下圖為例,會輸出 10, 20, 35, 45, 30, 59, 60, 85, 105, 100, 70, 50:
Imgur

Breadth First Traversal

適合用在有階層性的資料,例如公司的組織架構,如此可以快速地找出管理職有誰,員工有誰等等:
Imgur
實作的方式會使用到 queue 作法:
Imgur

Big O Notation

二元搜尋樹的優點在於:
  • 可以快速的找到項目
  • 可以快速的新增和移除項目
  • 它是屬於 O (log n) 的演算法
但使用它有一個很重要的前提是資料必須盡量平均分散在左右兩邊,讓它長成一個樹狀結構,如果資料都是集中在某一側的話,則不適合使用 Binary Search Tree。這種資料結構常用在「字典」、「電話簿」、「使用者資料」等等。

Sample Code

Started Template

/* eslint-disable */
function BST(value) {
  this.value = value;
  this.left = null;
  this.right = null;
}

// 在 BST 中插入值
BST.prototype.insert = function insert(value) {
  /* ... */
};

// 判斷 BST 中是否包含此值,回傳 true/false
BST.prototype.contains = function contains(value) {
  /* ... */
};

// depthFirstTraversal 可以用來疊代 Binary Search Tree 中的所有元素
const ORDER_TYPE = {
  IN_ORDER: 'IN_ORDER', // 將所有元素打平後由左至右排列
  PRE_ORDER: 'PRE_ORDER', // 由上往下,由左至右
  POST_ORDER: 'POST_ORDER', // 由下往上,由左至右
};

BST.prototype.depthFirstTraversal = function depthFirstTraversal(
  iteratorFunc,
  order = ORDER_TYPE.IN_ORDER
) {
  /* ... */
};

// 疊代 BST 中的所有元素
BST.prototype.breadthFirstTraversal = function breadthFirstTraversal(
  iteratorFunc
) {
  /* ... */
};

// 取得 BST 中的最小值
BST.prototype.getMinVal = function getMinVal() {
  /* ... */
};

// 取得 BST 中的最大值
BST.prototype.getMaxVal = function getMaxVal() {
  /* ... */
};

module.exports = {
  BST,
  ORDER_TYPE,
};

參考

[資料結構] Hash Table

keywords: data structure
此系列筆記主要依照 [Udemy] Learning Data Structures in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。
Imgur

觀念

  • Hash Table 本身是一個陣列,裡面的每一個元素都是帶有 key-value 的物件,稱作 Buckets
  • 透過自訂的 hash 函式,可以決定新增的資料要放在 Hash Table 中的哪個位置。
  • 當有不同的內容放到相同編號的 Buckets 時,會發生所謂的 collision,這時候相同 Bucket 內的資料,會透過 Linked List 的方式串接在一起。
  • 當 insert 了相同 key 的資料時,會有更新的效果。
  • 其中會包含的方法有:
    • hash
    • insert
    • get, retrieveAll

Big O Notation

  • Hash Table 常用在儲存使用者的 Email、使用者資料。
  • 缺點:除非在同一個 bucket 內,否則資料(Node)之間不會彼此參照。

Constant Time - O(1)

  • 插入資料
  • 搜尋資料

Sample Code

Started Template

// size 會決定在 hashTable 中有多少 buckets
function HashTable(size) {
  this.buckets = Array(size);
  this.numberBuckets = this.buckets.length;
}

// 每一個 key-value pair 稱作 Node
function HashNode({ key, value, next = null }) {
  this.key = key;
  this.value = value;
  this.next = next;
}

// 用來產生一個數值,此數值不能超過 buckets 的 size
HashTable.prototype.hash = function hash(key) {/* ... */};

// 用來新增 Node 到 HashTable 中
// 若該 bucket 已經有資料,則透過 Linked List 的概念,放到 next 中
// 若該 key 已經存在,則進行更新
HashTable.prototype.insert = function insert({ key, value }) {/* ... */};

// 根據 key 取得 value
HashTable.prototype.get = function get(key) {/* ... */};

// 取得所有 Buckets 中的 Node
HashTable.prototype.retrieveAll = function retrieveAll() {/* ... */};

module.exports = HashTable;

參考

[資料結構] Linked List

此系列筆記主要依照 [Udemy] Learning Data Structures in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

資料結構(Data Structure)是什麼

  • 不同的資料結構有不同的強弱項,有的在儲存(storing)和紀錄(recording)資料的速度較快,有的則是在搜尋(searching)和提取(retrieving)上比較快。
  • 資料結構的使用會影響到效能表現(performance)和程式運行的效率

Linked List

Imgur
  • Linked List 中,會紀錄該 Linked 的 headtail
  • 每一個 Node 則會紀錄它的 value, prevnext
  • 其中會包含的方法為:
    • addToHead, addToTail
    • removeHead, removeTail
    • search, indexOf

Big O Notation

Constant Time - O (1)

  • 新增/移除 Head
  • 新增/移除 Tail

Linear Time - O (n)

  • 搜尋元素是否存在
  • 搜尋元素的 index 位置

Memory Manage Benefits

使用 Linked List 的資料結構可以更有效率的使用記憶體,因為每一個元素都是獨立,彼此之間是透過 next 和 prev 來關聯在一起:
Imgur

Sample Code

Started Template

/* eslint-disable */
function LinkedList() {
  this.head = null;
  this.tail = null;
}

function Node({ value, next, prev }) {
  this.value = value;
  this.next = next;
  this.prev = prev;
}

// 輸入 value 後,把該值添加到 Linked List 的最前方
LinkedList.prototype.addToHead = function addToHead(value) {
  /* ... */
};

// 輸入 value 後,把該值添加到 Linked List 的最後方
LinkedList.prototype.addToTail = function addToTail(value) {
  /* ... */
};

// 移除 Linked List 的第一個 Node,若 Head 存在則回傳被移除的值,否則回傳 null
LinkedList.prototype.removeHead = function removeHead() {
  /* ... */
};

// 移除 Linked List 的最後一個 Node,若 Head 存在則回傳被移除的值,否則回傳 null
LinkedList.prototype.removeTail = function removeTail() {
  /* ... */
};

// 輸入 value 後,搜尋此 LinkedList 中是否有此值
// 找不到的話回傳 null,否則回傳找的的值
LinkedList.prototype.search = function search(searchValue) {
  /* ... */
};

// 列出所有等同於 searchValue 的 indexes
LinkedList.prototype.indexOf = function indexOf(searchValue) {
  /* ... */
};

module.exports = {
  LinkedList,
  Node,
};

參考

2020年5月25日

[掘竅] 為什麼要使用 rel="noreferrer noopener",談 target="_blank" 的安全性風險

在網頁撰寫的過程中,經常當我們要另開視窗時,很容易使用 <a target="_blank"> 這樣的寫法,但如果你有使用 ESLint 的話,它會建議你在 a 標籤中要加上 rel="noreferrer noopener",也就是:
<a href="https://www.google.com" target="_blank" rel="noreferrer noopener">
  Google
</a>
之所以要加上這行,是因為當瀏覽器使用 target="_blank" 來打開新視窗時,新的視窗所在的網頁是有辦法透過 window.opener 這個物件來操作你原本的頁面。
舉例來說,當你在 A 站點了超連結另開新視窗到 B 站時,B 站可以在它的頁面中執行:
window.opener.location = 'https://www.google.com';
這時候你會發現你在 A 站的網頁默默轉址到了 Google 的頁面。
這種做法主要是利用一般人只會注意新開的視窗(B 站),而忽略了原有的視窗(A 站),但若不進一步處理, 新開的視窗是有機會可以修改到原視窗內所瀏覽的網址的。
因此,若你使用的 target="_blank" 的話,eslint-plugin-react 都會建議你要加上 rel="noreferrer noopener",以確保使用者當前瀏覽的頁面,不會因為開新視窗後被另開的這個網站給影響。

參考

2020年5月1日

[Mobile] 給開發者用:將 Android 裝置畫面投影到 MAC / PC 上的工具(scrcpy)

keywords: screen sharing, screen mirroring, miracast, android, screen recording
💡 備註:如果你是 iOS 裝置要投放到 Mac 的話則非常簡單,透過內建的 QuickTime 就可以了,只需選擇「File -> New Movie Recording」後,將來源選到 iOS 裝置即可,可參考圖一圖二的說明。
最近因為有把手機(Android)的操作畫面投影到電腦上的需求,試了幾套不同的工具後,發現許多工具都需要在手機和電腦端同時裝 App,除了會有較明顯的時間差外,大多都是透過 Android 內建的「投放」功能來達到這個效果。
但這裡因為某些理由,我不能使用原生的投放功能來投放螢幕,而是需要直接把手機畫面顯示在電腦上,找這找著找到這套很好用的 Android 投放手機螢幕工具,稱作 scrcpy
這套工具除了可以投放螢幕外,還可以錄製螢幕、傳輸檔案、並直接由電腦操作手機,最重要的是操作流暢度非常好!
但這套工具比較是給開發者的, 因為需要透過終端機下一些指令,當然你也可以照著打就好了。關於使用的方式在 scrcpy 的 Github 上已經有蠻清楚的說明,可以使用在 Linux, Mac 或 Windows 上,有興趣的可以直接到 scrcpy 的 Github 查看文件。
這裡簡單說明一下步驟,各步驟詳細的作法都可以再額外 Google 或於文件中查看:

手機端操作

  1. 啟用開發人員選項:先進到手機的「設定 -> 關於手機」然後點擊「Build Number(版本號碼)」7 次後即可開啟「開發者」功能(應該會看到提示文字)。
  2. 進入「開發人員選項」:接著回到設定頁面,在設定頁面中應該會多出「開發人員選項(Developer options)」的項目,點擊進去。
  3. 啟用「USB 偵錯」功能:進到開發人員選項後,找到「USB 偵錯(USB Debugging)的項目後開啟它
  4. 將手機透過傳輸線與電腦連接

電腦端操作(Mac)

這裡以 Mac 為例,下面指令都是在終端機輸入:
💡 Windows 的安裝方式可以參考 Github 上的說明。
  1. 安裝 Homebrew(如果還沒裝過)
# 如果還沒安裝過 homebrew 需要先安裝
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
  1. 透過 Homebrew 下載 scrcpy
$ brew install scrcpy                         # 下載 scrcpy
$ brew cask install android-platform-tools    # 下載 android 工具
  1. 手機連接電腦後,輸入
$ scrcpy
這時候就可以看到手機的畫面出現在電腦上了,而且相當流暢!
scrcpy

使用無限(wireless)方式進行連線

  1. 先將手機和電腦連接到同一個 Wifi 網路
  2. 取得手機的 IP 位置,點選設定 -> Wifi 網路,即可看到如圖的畫面
  3. 將手機與電腦使用傳輸線連結,接著在終端機輸入
$ adb tcpip 5555
$ adb connect <DEVICE_IP>:5555  # 填入裝置連上的 WIFI IP
  1. 把手機和電腦的傳輸線拔掉
  2. 執行 scrcpy
$ scrcpy
  1. 由於是透過無線傳輸,若想要得到最好的體驗,建議可以降低解析度和 bit-rate:
$ scrcpy --bit-rate 2M --max-size 800  # scrcpy -b2M -m800 縮寫
⚠️ 若想改回使用 USB 傳輸線連接,可以輸入 adb usb

更多功能:錄製螢幕、操作手機、檔案傳輸

scrcpy 這個工具除了可以投放螢幕外,也可以錄製螢幕,更可以直接在電腦上操作你的手機,還可以把檔案直接拖到手機內,真的超級方便的,而且流暢度非常高,其他更多的功能或參數設定,都可以到 scrcpy 查看!

參考

2020年2月20日

發佈 npm 套件 - 從手動到自動(7):Coveralls 的測試覆蓋率與 README 中的標章(code test coverage badge)

keywords: deploy, publish, release, CLI, npm, package.json
這篇將會是這個系列文章的最後一篇,到目前為止已經可以在「把專案推到 github 上」後,自動在 CI 上幫執行測試、打包專案、更新版號、建立 CHANGELOG、發佈到 npm、產生 commit 和在 Github 上產生 release tags。
最後一篇則要來說明 README 中常使用到的標章(badge)。先來看一些常見的標章,最後再說明如何產生相當重要的「程式碼測試覆蓋率(code test coverage)」標章吧!

在 README 中加入常見的 badge

Travis CI 的 badge

一般 README 中的 badge 其實就是單純套用在 markdown 中的圖檔,而許多不同的服務會提供各自的 badge 讓大家使用,以 Travis CI 來說,在官網的文件 Embedding Status Images 即有說明套用的方式,只需點選 Travis CI 專案中的 badge 即會顯示嵌入 markdown 的寫法:
[![Build status](https://badgen.net/travis/pjchender/react-use-opentok)](https://travis-ci.com/pjchender/react-use-opentok)

透過 shields.io 取得各種不同 badge

除了 Travis CI 的標章之外,在 shields.io 上也提供了很多不同的 badge 可以自己套用:
其中像是 npm 上套件的版本:
只需在 README.md 中加入:
# 記得要把連結改成自己的專案
[![version](https://img.shields.io/npm/v/@pjchender/function-benchmarker.svg)](https://www.npmjs.com/package/@pjchender/function-benchmarker)

透過 Coveralls 產生測試覆蓋率標章(code test coverage badge)

由 Jest 檢視程式碼測試覆蓋率

程式碼的測試覆蓋率標章(code test coverage badge)指的是在整個專案中有多少百分比的程式碼有撰寫對應的測試,以我們在 @pjchender/function-benchmarker 中使用的 Jest 來說,只要在 command line 執行:
$ npm run test -- --coverage
就會幫我們產生對應的測試覆蓋率結果:
但如果我們想要在 README 中產生測試覆蓋率的標章就沒這麼簡單了。一般來說同樣會需要一個第三方的服務幫我們執行測試,這個服務會再把測試執行的結果保存一份下來,接著就可以從這個第三方的服務取得 badge。

透過 Coveralls 保存測試結果並產生標章

其中 Coveralls 是相當常見用來執行測試的第三方服務,並且支援許多持續整合的服務。
因為整個流程有些繁瑣,先來簡單瞭解一下 Coverall 的流程和作用的時機點:
  1. 先註冊 Coverall 的帳號,並與 Github 連動,選擇要加入 Coverall 中的 Github 專案
  2. 在專案中安裝 Coverall 的 npm 套件,並在專案的 packages.json 設定執行該套件的指令
  3. 在 Travis CI 的環境變數中需要先設定 Coverall 的 token,如此才能在 Travis CI 執行的過程中通知 Coverall 執行的結果
  4. 修改 .travis.yml 的檔案,讓它在持續整合的過程中可以去執行 coveralls 的指令
  5. 推上 Github 觸發 CI
現在就讓我們一步一步開始吧!

註冊 Coveralls 帳號

要使用這個服務一樣需要先到 Coveralls 的官方網站註冊一個帳號,因為我們要和 Github 上的 repository 連動,所以可以選擇使用 Github 登入。

與 Github 連動並加入對應的 Repository

接著在最左邊的地方可以看到「ADD REPOS」的加號:
然後選擇想要加入 Coveralls 的 Github 專案,把它「打開(ON)」:

安裝 Coveralls 的 npm 套件

設定好之後要來把 Coveralls 的 npm 套件 安裝到專案中:
$ npm i coveralls -D

在 package.json 中建立對應的指令

安裝好後,在 package.jsonscript 欄位中加入名為 coveralls 來讓它幫我們產生程式碼測試覆蓋率的資料:
{
  // ...
  "scripts": {
    // ...
    "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls",
    "semantic-release": "semantic-release"
  }
  // ...
}

將 Coveralls 的 token 設定到 Travis CI 上

最後為了要讓 Travis CI 在執行 coveralls 指令的過程中,可以把執行的結果放到 Coveralls 的網站上,需要把 Coveralls 的 Token 設定到 Travis CI 上。
在 Coveralls 網站上,點入專案後即可看到 repo token
⚠️ 備註:在這頁的說明中,會請你在專案中建立一支 .coveralls.yml,並放入對應的 service_namerepo_token,但這是當你的專案是 private(不公開)的情況下才能這麼做,否則你的 token 會被所有人看到,因此對於 open source 這類公開的專案,需要把 repo_token 直接設定在 CI 服務的環境變數中
把這個 Token 複製下來,回到 Travis CI 中專案的 「settings」 中,新增一個名為 COVERALLS_REPO_TOKEN 的環境變數,並把剛剛複製的 token 貼上:

修改 .travis.yml

最後透過 .travis.yml 讓專案在持續整合的過程中,可以去執行剛剛在 package.json 中設定好的 coveralls 指令,在設定檔中多一個名為 Produce Coverage 的階段來執行 npm run coveralls 的指令:
# .travis.yml
# ...
jobs:
  include:
    - stage: Produce Coverage
      node_js: node
      script: npm run coveralls
    # Define the release stage that runs semantic-release
    # ...
變更的部分如下圖:

推上 Github 觸發 CI 並檢視結果

如此就大功告成了!讓我們推上 Github 觸發 coveralls 看看吧!
現在,在 Travis CI 上可以看到除了原本的 Release Stage 之外,多了一個 Produce coverage Stage,這是用來執行 coveralls 指令的:
執行完後,回到 Coveralls 的網站,點選專案進入後,將會看到程式碼的測試覆蓋率,並且有嵌入(EMBED)標章的地方:
以 @pjchender/function-benchmarker 這個專案來說,測試覆蓋率所使用的 badge 如下,只需把這段貼到 README.md 中就可以了:
[![Coverage Status](https://coveralls.io/repos/github/pjchender/function-benchmarker/badge.svg?branch=master)](https://coveralls.io/github/pjchender/function-benchmarker?branch=master)

範例專案

現在回到我們的專案中,就可以看到在這個 README 中已經有上面所加入的這些 badge 了:
這是這個系列文章的最後一篇,關於整個專案的程式碼和設定檔,都可以在 Github 上的 pjchender/function-benchmarker 上查看。
最後,感謝你的閱讀,若發現內容中有任何錯誤,都歡迎留言告知或到粉絲專頁發送訊息告訴我。

參考

2020年2月19日

發佈 npm 套件 - 從手動到自動(6):semantic-release 的外掛設定與自動產生 CHANGELOG

keywords: deploy, publish, release, CLI, npm, package.json
上一篇文章中已經有透露如果要對 semantic-release 套用外掛(plugin)或進行更多的設定,可以在根目錄建立一支 .releaserc.json 的檔案,現在我們一樣可以透過這支檔案,來讓 semantic-release 自動產生 CHANGELOG.md 的檔案。

安裝 semantic-release 外掛

預設的情況下,semantic-release 就已經安裝了幾個外掛(plugin),像是 :
"@semantic-release/commit-analyzer"
"@semantic-release/github"
"@semantic-release/npm"
"@semantic-release/release-notes-generator"
為了要產生 CHANGELOG 檔案,並且保存推進 git 中,需要再安裝 @semantic-release/changelog@semantic-release/git 這兩個外掛:
$ npm install @semantic-release/changelog @semantic-release/git -D

設定 .releaserc.json

接著在根目錄底下新增一隻 .releaserc.json 的檔案(如果已經建過就不用重複新增),在這支檔案裡貼上:
// .releaserc.json
{
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    "@semantic-release/npm",
    "@semantic-release/git"
  ]
}
建立好設定檔後,commit 並推上 Github,這時候在 Travis CI 持續整合的過程中,就會新增一支 CHANGELOG.md 的檔案在根目錄了:
這裡你可以注意到:
  1. 最後的 git commit 是由 semantic-release-bot 所建立的,這是透過 @semantic-release/git 這個套件達到的。
  2. 不知道你先前有沒有發現到,之前雖然每次推上 Github 或發佈到 npm 上時的版號都有不斷更新,但 git 中 package.json 檔案裡面的版號卻都沒有改變,這同樣是因為之前沒有安裝 @semantic-release/git 的關係,所以雖然 Github 和 npm 上的版號更新了,但因為 semantic-release 沒有把更新後的 package.json 檔 commit 回我們的 repository,所以本機 package.json 中的版號才會都沒有更動。
  3. CHANGELOG.md 檔案的內容有更新了,這是透過 @semantic-release/changelog 這個套件達到的。CHANGELOG 中的內容會依據 commit 的內容進行更新:

semantic-release 的外掛設定

在了解如何產生 CHANGELOG.md 檔,並透過 @semantic-release/git 來把變更的內容推回 git 後,我們可以再來看一下 semantic-release 外掛的設定:

@semantic-release/changelog

@semantic-release/changelog 這個套件中,預設會在根目錄新增一支 CHANGELOG.md 的檔案,如果你想要修改 CHANGELOG 檔放置的路徑或標題(title),一樣可以在 .releaserc.json 中進行設定:
// .releaserc.json
{
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    // 修改 changelog 檔預設的路徑和標題
    ["@semantic-release/changelog", {
      "changelogFile": "docs/CHANGELOG.md",
      "changelogTitle": "This is Title"
    }],
    // 預設會 commit 根目錄的 CHANGELOG.md 檔,因此要告訴它說去其他路徑取得 CHANGELOG 檔
    ["@semantic-release/git", {
      "assets": ["docs/CHANGELOG.md"],
    }]
  ]
}

@semantic-release/git

透過 @semantic-release/git 這個套件,可以告訴 semantic-release 哪些檔案是它可以 commit 進我們的專案,並且可以修改 commit 的訊息。預設的情況下它可以 commit 進專案的檔案包括:
['CHANGELOG.md', 'package.json', 'package-lock.json', 'npm-shrinkwrap.json']
如果檔案的路徑有變更,或者有些檔案是打包後才產生,希望可以大家可以透過 github 取得的,一樣可以在這裡進行設定,以下面的設定為例,除了 CHANGELOG.md, package.jsonpackage-lock.json 會進 git 之外,我們把打包後會產生的 extension.zip 檔案也進 git,如此使用者便會在 Github 上看得到這隻檔案:
// .releaserc.json
{
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    ["@semantic-release/git", {
      "assets": [
        "CHANGELOG.md",
        "package.json",
        "package-lock.json",
        "extension.zip"
      ]
    }]
  ]
}

@semantic-release/npm

如我你的套件並不需要發佈到 npm 上,除了可以透過上一篇的方式,設定 npmPublish: false 外,也可以在 .releaserc.json 中把 @semantic-release/npm 移掉也可以達到一樣的效果。

錯誤處理:在 Travis CI 上使用出現 Cannot find module 的問題

如果你在 Travis CI 執行的過程中出現類似的錯誤訊息時:
Cannot find module '@semantic-release/changelog'
Cannot find module '@semantic-release/git'
可以修改 .travis.yaml 的檔案,將 script 的地方改成:
script: npx -p @semantic-release/changelog -p @semantic-release/git -p semantic-release semantic-release

下一步

現在我們已經完成了 semantic-release 的基本設定,透過 semantic-release 可以幫助我們省下不少的時間,自動更新套件版號自動發佈套件到 npm,以及自動產生 CHANGELOG 檔等等...。在下一篇文章中,將會說明如何套用大家在 README 中常見各種標章(badge):
其中最重要的就是測試覆蓋率拉!就讓我們到下一篇中說明吧!

參考

2020年2月17日

發佈 npm 套件 - 從手動到自動(5):semantic-release 自動發佈到 npm

keywords: deploy, publish, release, CLI, npm, package.json
前一篇文章中,我們已經知道預設的情況下 semantic-release 它會自動在 CI 的過程中將套件發佈到 npm 上,如果你的套件是在本地打包好,而且最終打包好的 dist 資料夾也會上 git 的話,這麼做當然沒問題。然而,多數實際的情況是,dist 資料夾因為是打包壓縮過的專案,因此一般是會寫入 gitignore 中而不會上 git 的。
因此,在上篇文章的最後,你會發現透過 npm install 把我們的套件下載後,會發現套件裡面「空空的」而沒辦法使用。在這篇文章中我們會說明怎麼樣在 Travis CI 中,執行打包的動作,在 CI 中打包完之後 dist 資料夾中就會有東西,此時發佈到 npm 上時,套件就可以正常運作了。

修改 .travis.yml 設定檔

要讓 Travis CI 可以幫我們執行 build 的指令,只需修改 .travis.yml 這支設定檔:
這裡有兩個要留意的地方:
  1. release 階段中透過 script 的項目,告訴它要執行 npm run build 的指令,如此就可以在 CI 上進行打包的動作
  2. 另一個很重要的地方是 skip_cleanup: true 這個項目,如果沒有設定的話,雖然會在 CI 上執行打包的動作,但這些打包好的東西在發佈到 npm 前就會被 CI 給清除掉了,因此要加上 skip_cleanup: true 來告訴 Travis CI 不要把剛剛打包過的東西給清除掉。
備註:使用 skip_cleanup 時可能會看到該 API 已經 deprecated 的提示,但這是在 deployment API v2 後,因此現在仍然要加上,參考 Skip_cleanup: true is now deprecated. How to go for gradle publish?
如此簡單的兩行指令,就可以讓 semantic-release 幫我們把正確的套件發佈到 npm 上了!現在透過 npm install 把套件抓下來後,可以看到我們套件的 dist 資料夾中已經有打包好的檔案了:

不要自動發佈到 npm

有些時候你不希望 semantic-release 自動把套件發佈到 npm 上時,可以怎麼做呢?
這時候我們可以在專案根目錄中新增一支名為 .releaserc.json 的檔案,裡面可以這樣寫:
// .releaserc.json
{
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    [
      "@semantic-release/npm",
      {
        "npmPublish": false
      }
    ]
  ]
}
.releaserc.json 這隻檔案是給 semantic-release 的設定檔,這裡面可以設定要使用哪些 semantic-release 相關的套件(plugin),並且給予額外的參數設定,以這裡為例,透過 npmPublish: false 就可以避免套件自動發佈到 npm 上。
在安裝 semantic-release 時,預設就已經安裝了下面這些外掛(plugin)

"@semantic-release/commit-analyzer"
"@semantic-release/github"
"@semantic-release/npm"
"@semantic-release/release-notes-generator"

參考

2020年2月12日

發佈 npm 套件 - 從手動到自動(4):semantic-release 自動更新套件版號

keywords: deploy, publish, release, CLI, npm, package.json
在這篇文章中我們會先說明「語意化版本(semver)」,接著說明如何透過 semantic-release 這個工具結合 CI 來自動更新套件的版本。這篇文章算是這整個系列文章中相對最複雜的一篇,只要完成了這篇設定,只後的動作都會輕鬆很多。

語意化版本(semver)

既然要發佈到 npm 上,很重要的一點自然就是套件的版本號,而 npm 上所有的套件都是遵循「語意化版本」的「慣例」。
「語意化版本」的全名是 Semantic Versioning,常會簡稱為 semver,它的核心概念在於讓使用該套件的開發者在看到版號時就能夠知道該版本有無重大更新或需要留意的地方。一個套件的版本共會由三個數字組成,像是 1.0.0,三個數字由左至右,分別指的是「主版號(major)」、「次版號(minor)」和「修訂號(patch)」。
每個版號的變更都有它所代表的意義:
  1. 主版號(major):表示這個套件修改了原本的 API,使其無法向下相容,這是開發者最需要留意的情況,算是重大改變(BREAKING CHANGE)。
  2. 次版號(minor):在可以向下相容的情況下為套件添加了新功能(feature)
  3. 修訂號(patch):在可以下向相容的情況下做了問題修正(fix)
 備註:有時主版號改變不代表你真的需要去修改原本寫的程式,有些時候只是不再支援某一版本前的 JavaScript / NodeJS,這時候因為無法再繼續向下相容,所以也會提升主版號。
但既然稱作是「慣例」就表示可以不遵守,而這樣不遵守的情況其實在 NodeJS 的世界還蠻常見的...。
因此若要看每次套件的更新實際上有哪些變化,可以到專案中的「CHANGELOG.md」或 Github 上的「releases」標籤中查看,在一個完整的專案中一定會有這個部分:
點進去就可以看到每次版本變更時做了哪些改變。
Releases:
CHANGELOG:

透過 semantic-release 自動更新套件版本

在瞭解了 semver 後,現在我們就要來練習修改套件的版號。在 npm CLI 的工具中,有提供 npm version 這個指令可以讓開發者手動的去修改版號,但這麽做除了每次都需要手動記得去改版號之外,也有更多人為介入的空間,例如,明明已經有重大 API 的改變,卻只手動升了次版號。
為了解決人為介入與手動的問題,很多開發者會使用 semantic-release 這套工具來達到自動更新版號、產生 CHANGELOG 檔案、並且發佈到 npm 的這一系列動作。這裡我們先來看它如何自動更新套件版號。
由於 semantic-release 執行動作的階段會是在 CI 中,因此除了需要把 semantic-release 安裝到專案中外,也必須要先在 Github 建立 token,把 token 設定到 CI 的服務上(這裡是用 Travis CI),讓 CI 有讀取和寫入 Github 專案的權限;若需要讓 semantic-release 自動將套件發佈到 npm 上,同樣需要在 npm 建立 token,然後設定回 CI 的服務上...,感覺很麻煩吧,不怕,有 semantic-release-cli

使用 semantic-release-cli 快速完成設定

這個步驟很繁瑣,好在 semantic-release 有推出了 cli 可以快速幫我們完成上面這個繁瑣的過程。先把 semantic-release-cli 安裝到電腦中,然後進到專案執行 setup 的動作:
$ npm install -g semantic-release-cli

$ cd <your-module>
$ semantic-release-cli setup

$ npm install
執行 setup 的時候,它為了要幫我們在 Github 和 npm 產生對應的 token 並放到 CI 上,因此會需要輸入 Github, npm 的帳號密碼,並選擇所用的 CI 服務。這個設定的流程會像這樣:
這裡需要注意的是,在選擇 CI 服務時,如果你的專案放在 travis-ci.org 的話,請選擇 Travis CI;如果你的專案放在 travis-ci.com 的話,則選擇 Travis CI Pro;如果你使用的是其他 CI 服務的話,就選擇其他的選項。
我們來看一下剛剛這個 semantic-release-cli 幫我們做了哪些設定。
  1. 產生 Github 上的 Personal Access Token:它會到 Github > Settings > Developer Settings > Personal access token 中產生一組 token,這組 token 是要給 Travis CI 使用,你可以到 Personal access tokens 上查看:
  1. 產生 npm 上的 Authentication Token:它會到 npm > Auth Tokens 中產生一組 Authentication Token,一樣可以登入 npm 後查看:
  1. 把 Github Token 和 npm token 設定到 Travis CI 的專案中:它會到 Travis CI 中該專案的 settings 中設定 GH_TOKENNPM_TOKEN 這兩個環境變數,這是要給 semantic-release 使用的:
除了完成這些設定外,它也幫我們在專案中安裝了 semantic-release、把版號歸零,並且產生了一道 semantic-release 的 scripts,記得要執行 npm install
⚠️ 因為我們曾經把套件發佈到 npm 上過,因此這裡會把版號改回已經在 npm 上的 1.0.0,否則之後會發生版本衝突而無法發佈上 npm 的情況。
 備註:如果你不想透過 semantic-release-cli 完成這些設定的話,也可以選擇手動一步一步完成,這裡有列出步驟可以參考;在 github 和 npm 上手動建立 token,並設定到 CI 的環境變數上則可以參考這裡

修改 travis.yml

完成 semantic-release 的安裝以及對應的設定後,接下來要來修改 travis.yml 這支檔案,讓專案推上 Github 並進入 CI 的階段後可以去執行 semantic-release:
# travis.yml

language: node_js
node_js:
  - 12

jobs:
  include:
    # Define the release stage that runs semantic-release
    - stage: release
      node_js: lts/*
      # Advanced: optionally overwrite your default `script` step to skip the tests
      # script: skip
      deploy:
        provider: script
        script:
          - npx semantic-release
這份設定的說明如下:
  • 在 Travis CI 中新增一個 Job
  • 這個 Job 會在 Travis CI 的 release 階段執行
  • 執行 deploy 的動作,deploy 的方式是去執行 script
  • 要執行的 script 即是 npx semantic-release

自動變更版號的邏輯 - conventional commit

現在完成這些設定之後,我們就可以把最新的專案推到 Github 上觸發 Travis CI 執行。但在這之前,我們要來說明 semantic-release 到底是用什麼樣的依據來幫我們自動變更版號!
首先 semantic-release 變更版號的方式一樣遵循最上面所提的 semver,而它會依據每一次的 commit message 來決定要如何修改版號,並後續產生對應的 releases tag 和 CHANGELOG(這個會在後面的文章中提到)。
既然要根據 commit message 來決定如何變更版號,勢必得要有個規範,而這個 commit message 的規範主要是依據 Angular Commit Message Conventions。聽起來很複雜,但簡單來說就是他會根據 commit message 中的「關鍵字」來判斷要改變版號中的哪個部分。
以 semantic-release 提供的下表來看:
這種慣例的 commit message 都會長得像這樣:
<type>(optional scope): <description>

[optional body]

[optional footer]
其中 scope 的部分則是可以選擇性填寫的,而 type 是最主要用來判斷版號變更的依據,對應到 semver:
  • fix:表示在 API 可向下相容的情況下修改套件問題,屬於 PATCH 的版號變更
  • feat:表示在 API 可向下相容的情況下添加套件功能(feature),屬於 MINOR 的版號變更
其他常見的 type 還包括 chore, build, ci, docs, perf 等等,可以參考 Github 上的這個說明:
/* github 在 Commits 上的說明 */
type:
 • build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 • ci: Changes to our CI configuration files and scripts (example scopes: Circle, BrowserStack, SauceLabs)
 • docs: Documentation only changes
 • feat: A new feature
 • fix: A bug fix
 • perf: A code change that improves performance
 • refactor: A code change that neither fixes a bug nor adds a feature
 • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 • test: Adding missing tests or correcting existing tests
那麼要怎麼更新主版號呢?只要在 commit message 中的「Footer」區塊以 BREAKING CHANGE: 開頭的話,就會被視為有無法向下相容的重大變更,這時候就屬於 MAJOR 的版號變更。

試著透過 commit message 修改版號

讓我們直接來試試看吧!
因為剛剛在專案中的檔案已經有了變更,我們就直接寫下 commit,像這這樣:
接著一樣將推到 Github 上。成功推上去並觸發完成 CI 後,會發現我們在 Github 的 release 頁籤中多了一個tag:
點進去看後會發現 semantic-release 已經幫我們建立了一個 release tag:
現在,你可以試著在專案中新增修改一些內容,內容透過 conventional commit 的格式建立新的 commit 在推上 Github 看看版號會有什麼樣不同的變化。例如這裡我用了:
$ git commit -m "fix: remove redundant comments"
於是 releases 中又多了以下部分,修訂號也增加了:
除了 Github 多了 releases 外,semantic-release 預設也會幫我們將套件發佈到 npm 上:
但這時候如果你透過 npm install 下載專案下來看的話,會發現原本在 node_modules/@pjchender/function-benchmarker 中的 dist 資料夾不見了,等於套件中最重要的程式碼都沒了:
之所以會這樣,是因為現在是透過 semantic-release 於 CI 過程中把套件發佈到 npm 上。但在一開始把專案推上 Github 時,dist 資料夾預設是被我們 ignore 掉的(設定在 .gitignore 中),所以一開始推上 github 的檔案中就沒有打包好的 dist,自然在 CI 過程中也不會發佈到 npm 上。
要解決這個問題,我們就必須在 CI 的過程中,執行 build 的指令來打包專案,如此才會有打包後的 dist 資料夾。這個部分將會在下一篇文章中做說明。
現在,我們完成最複雜也最麻煩的一部分了,已經可以透過 semantic-release 自動更新版號,在 Github 上產生 releases 的標籤,並且自動發佈到 npm 上(雖然專案內容還不完整)。接下來,會輕鬆一些,我們將修正 npm 上的檔案沒有內容的情況,並透過 semantic-release 來產生 CHANGELOG 檔案。

參考