Jenkins檢查前端編譯程式有無簽入版控

為了避免開發人員開發完畢之後未正確簽入版控,因此實作一個檢查機制,當版控的檔案與產生出來的檔案不一致的時候,透過通知的機制告知開發人員

透過計算檔案的 checksum,比對兩個檔案的 checksum 即可得知是否相同,為了達到這個目的,需要做到下列事項

  1. 從 Git 取得程式原始碼
  2. 將前端編譯出來的程式複製到暫存的目錄
  3. 重新編譯前端程式,輸出至原來的路徑
  4. 比對暫存目錄、輸出目錄的檔案是否一致
  5. 若比對結果不一致,則發出通知

複製檔案

利用 node.js 的 fs-extra 套件來複製檔案,好處是透過 CLI 執行該程式,不管在專案的根目錄或是網站的目錄,都可以正確執行複製目錄的行為,所以 jenkins 的 cli 指令也不需要固定寫死

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// include fs-extra package
var fs = require("fs-extra");
const path = require("path");
const sourceDir = path.join(__dirname, "../Resource/Source"); // 版控目錄
const generateDir = path.join(__dirname, "../Resource/Bundle"); // 產出目錄

// copy source folder to destination
fs.copy(generateDir, sourceDir, function(err) {
if (err) {
console.log("An error occured while copying the folder.");
return console.error(err);
}
console.log("Copy completed!");
});

重新編譯前端程式

透過已設定好的指令執行即可

1
yarn build

比對目錄檔案 checksum 是否一致

透過 fs-magic 這個 node.js 的外掛來處理檔案 hash,並比對是否一致,依據最終的結果,透過回傳 EXIT Code 來告知 jenkins 任務的執行是否成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//compare.js
const _fs = require("fs-magic");

// compare directoy contents based on sha256 hash tables
async function compareDirectories(sourceDir, generateDir) {
let result = true;
let errMsg = [];

// fetch file lists
const [sourceFiles, sourceDirs] = await _fs.scandir(sourceDir, true, true);
const [generateFiles, generateDirs] = await _fs.scandir(generateDir, true, true);

// num files, directories equal ?
if (sourceFiles.length !== generateFiles.length) {
errMsg.push(`版控:[${sourceFiles.length}] 產出:[${generateFiles.length}]:目錄內檔案數量不同 `);
result = false;
}
if (sourceDirs.length !== generateDirs.length) {
errMsg.push(`版控:[${sourceDirs.length}] 產出:[${generateDirs.length}]:子目錄數量不同`);
result = false;
}

// generate file checksums
const hashes1 = await Promise.all(sourceFiles.map(f => _fs.sha256file(f)));
const hashes2 = await Promise.all(generateFiles.map(f => _fs.sha256file(f)));

// convert arrays to objects filename=>hash
const lookup = {};
for (let i = 0; i < hashes2.length; i++) {
// normalized filenames
const f2 = generateFiles[i].substr(generateDir.length);

// assign
lookup[f2] = hashes2[i];
}

// compare dir1 to dir2
for (let i = 0; i < hashes1.length; i++) {
// normalized filenames
const f1 = sourceFiles[i].substr(sourceDir.length);
// exists ?
if (!lookup[f1]) {
errMsg.push(`[ERROR] ${generateDir} 目錄內 ${f1} 檔案不存在`);
result = false;
}
// hash valid ?
if (lookup[f1] !== hashes1[i]) {
errMsg.push(`[ERROR] [${f1}] checksum not match!`);
errMsg.push(`[產 出]:[${lookup[f1]}]`);
errMsg.push(`[版 控]:[${hashes1[i]}]`);
result = false;
}
}
return { result, errMsg };
}

module.exports = compareDirectories;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// compareFiles.js
const compareDirectories = require("./compare.js");
const path = require("path");
const sourceDir = path.join(__dirname, "../Resource/Source"); // 版控目錄
const generateDir = path.join(__dirname, "../Resource/Bundle"); // 產出目錄

async function compareFiles() {
let { result, errMsg } = await compareDirectories(sourceDir, generateDir);
console.log(`result:${result}`);
if (result) {
process.exit(0);
} else {
process.exit(1);
}
}

compareFiles();

透過 exit code 回應執行結果成功或失敗,藉此控制 Jenkins Job 任務結果,可再接續其他下游專案運作

Jenkins 設定範例

新增一個 freeStyle 專案,透過 git 下載 source 完畢後,再新增執行 Windows 批次命令

1
2
3
4
5
6
7
8
9
10
11
12
13
# STEP1
cd MyProject
yarn

# STEP2
node MyProject\test\copyFiles.js

# STEP3
cd MyProject
yarn build

# STEP4
node MyProject\test\compareFiles.js

之所以分開四個步驟,是因為放在同一個 shell script 區塊,執行 yarn 就會卡住後面的指令。