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


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

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

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

複製檔案

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

// 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!");
});

重新編譯前端程式

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

yarn build

比對目錄檔案 checksum 是否一致

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

//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;
// 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 批次命令

# STEP1
cd MyProject
yarn

# STEP2
node MyProject\test\copyFiles.js

# STEP3
cd MyProject
yarn build

# STEP4
node MyProject\test\compareFiles.js

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