[實作筆記] Stryker 升級與 CI 調整

前情提要

最近換了工作,一頭栽進了前端的領域,
生活又開始忙錄了起來,過年檢視了一下自已的 Side Project ,
意外發生 CI 竟然壞了一陣子,
這是我使用的變異測試工具 Stryker 從原本的 0.17 升級到了 1.3.1 版本
這是一個蠻大幅度的升版,所以設定上相當的不一樣。

我的 OS 是 macOS Monterey 12.1
安裝了 .NET 6.0

Local 環境

首先全域安裝 dotnet-stryker

dotnet tool install -g dotnet-stryker

接下安裝 tool-manifest

dotnet new tool-manifest

這時在你專案底下應該會建立一個 .config 資料夾,
內含 dotnet-tools.json 檔,內容如下

1
2
3
4
5
6
7
8
9
10
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-stryker": {
"version": "1.3.1",
"commands": ["dotnet-stryker"]
}
}
}

下一步,在專案資料下執行以下命令

dotnet tool install dotnet-stryker

建立 stryker-config.json 檔案
最後設定你的 config 檔 stryker-config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"stryker-config": {
"project": "Marsen.NetCore.Dojo.csproj",
"test-projects": [
"./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj",
"./test/Marsen.NetCore.Dojo.Integration.Tests/Marsen.NetCore.Dojo.Integration.Tests.csproj"
],
"reporters": ["progress", "json", "dashboard"],
"project-info": {
"name": "github.com/marsen/Marsen.NetCore.Dojo",
"version": "main"
}
}
}

與之前最大的不同是,大部份的參數你都可以設定在 stryker-config 之中,
而不用在執行命令中傳入,不僅可以為其提供版本控制,更能有序的組織你的設定檔,
使其更好閱讀。
如果你的 reporters 中有設定 dashboard 需要額外加上 --dashboard-api-key
ex:

dotnet stryker –dashboard-api-key=$STRYKER_DASHBOARD_API_KEY

參考

(fin)

[實作筆記] React Practice (一) Firebase 與 React Login

前情提要

本文不會介紹 FirebaseReact.
依照這篇文章完成後,你會

  • 透過 CRA 建立一個專案
  • 透過 MUI 建立符合 Material Design 的登入畫面
  • 建立 Firsbase 專案並且透過 Firsbase 完成登入登出的功能
  • 建立登入後才能瀏覽的頁面,並且實作登入/登出功能,登出後將無法瀏覽

Firebase 設定

建立 Firebase 專案

這裡的操作非常簡單,可以直接參考下面的影片。

詳細的步驟如下

  1. 進入 firebase console
  2. 選擇建立專案(Add Project),並且為你的專案取一個名字
  3. 進入剛剛建立的專案後,選擇建立應用程式(Add App),
    在這裡我們選擇 Web 類型的應用程式
    建立完成後我們會得到一個範例檔如下, 部份機敏資料先上遮罩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "A****************",
authDomain: "******.firebaseapp.com",
projectId: "******",
storageBucket: "******.appspot.com",
messagingSenderId: "******",
appId: "1:******:web:******",
measurementId: "G-*********",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

抽出環境變數

在 React 當中的機敏資料,建議的作法是使用環境變數,
我們可以建立一個.env.local提供給開發測試用,
而 Production 環境請直接設定在環境變數中, 相關資料不會進版控.
以下幾點注意事項

  • 必需使用 REACT_APP_* 開頭才可讓 REACT APP 使用 ex:REACT_APP_APIKEY
  • 一般的建議是全大寫
  • 環境變數的更新不適用 Hot Reload,請重啟環境

啟用 Firebase 認証(Authentication)

  1. 首先要建立 Sign-in method > 原生供應商 > 電子郵件/密碼
  2. 建立第一個使用者 Users > Add User

React

Create React Application

我們透過 TypeScript 建立 React 專案

1
npx create-react-app my-app --template typescript

我們可以試著啟動專案看一下,

1
cd my-app
1
npm i
1
npm start

安裝相關的套件

emotion 相關

1
npm i @emotion/react @emotion/styled

material 相關

1
npm i @mui/material @mui/icons-material

firebase 相關

1
npm i firebase react-firebase-hooks

react-router-dom 相關

1
npm i react-router-dom@6

建立登入頁面

登入頁面與路由

在 src 中建立 pages 資料夾,
之後我們再建立 Login.tsx 檔案如下

1
2
3
4
5
import React from "react";

export const Login = () => {
return <>Login Page</>;
};

接下來我們要透過 react-router-dom 來建立路由
開啟 index.tsx 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Login } from "./pages/Login";

ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="" element={<App />} />
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

重新啟動專案,在瀏覽器輸入不同的網址將會看到不同的畫面,如此我們完成了頁面的路由。

快速建立符合 MUI 的登入頁面

我們可以透過 MUI 提供的 Template 來建立 Login 頁面
實作細節如下:

  • 參考 Source Code 建立 src/components/SigninSide 組件
  • 修改 src/pages/Login.tsx 如下:
1
2
3
4
5
import SignInSide from "../components/SignInSide";

export const Login = () => {
return <SignInSide />;
};

重新瀏覽登入畫面,就可以看到一個美觀的登入頁 .

實作登入功能

首先請花點時間看一下 Firebase 帳號密碼登入的文件
接下來我們將依照文件的解釋修改我們的 SigninSide 組件。
打開 SigninSide 組件,找到handleSubmit 函數如下:

1
2
3
4
5
6
7
8
9
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
// eslint-disable-next-line no-console
console.log({
email: data.get("email"),
password: data.get("password"),
});
};

修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import { app } from "../firebase-config";
....
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const auth = getAuth(app);
signInWithEmailAndPassword(
auth,
data.get("email")!.toString(),
data.get("password")!.toString()
)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
console.log("user", userCredential.user);
// ...
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log("error", error);
});
};

題外話,data.get(“email”)!.toString()
當中的 !. 運算子可以參考 TypeScript Non-null assertion operator 的說明

用之前在 firebase 建立的帳密登入,登入成功的話可以在 Console 看到類似以下的資訊

1
2
UserImpl {providerId: 'firebase', emailVerified: false, isAnonymous:
false, tenantId: null, providerData: Array(1), …}

實作登入後進頁面的跳轉

這裡要使用 react-dom-router 提供的 hook useNavigate,
注意 React Hook 的使用限制

CRA 建立專案時同時會幫我們安裝檢查用的 Lint 所以不用太擔心。
同時我們把 getAuth(app) 也提取到 SignInSide Component 之外

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
////getAuth 可以不用寫在 handleSubmit 中
const auth = getAuth(app);

export default function SignInSide() {
////遵循 React Hook 的規則在最上層呼叫 Hook
const navigate = useNavigate();//取得 navigate
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
signInWithEmailAndPassword(
auth,
data.get("email")!.toString(),
data.get("password")!.toString()
)
.then((userCredential) => {
// Signed in
// console.log("user", userCredential.user);
navigate("/");// navigate 到首頁

實作登出的功能

調整 App.tsx 如下,移除不相關的程式,建立一個 Button 並準備好 onClick 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Button } from "@mui/material";
import "./App.css";
function App() {
return (
<div className="App">
<Button variant="outlined" onClick={() => {}}>
LOGOUT
</Button>
</div>
);
}

export default App;

路由設定,只有登入者才能看的頁面

透過 getAuth().currentUser 取得目前的登入者資料, 如果沒有登入者轉導到 Login 頁面,

1
2
3
4
5
6
7
8
9
10
11
12
13
import { getAuth } from "firebase/auth";
....
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route
path=""
element={getAuth().currentUser ? <App /> : <Navigate to="/login" /> }
/>
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>

不過這樣的作法會有問題,
getAuth().currentUser 是非同步取得的資訊,
所以你很有可能都會拿到 null 值,
在官方的建議作法是透過 onAuthStateChanged 註冊 Observer, index.tsx 會類似下面這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const auth = getAuth();

onAuthStateChanged(auth, (user) => {
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="" element={user ? <App /> : <Navigate to="login" />} />
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
});

Todo

  • Firebase Email link(passwordless sign-in)
  • Firebase Host Project
  • PageNotFound
  • 管理 Routes
  • more…

(つづく)

[學習筆記] JavaScript function 幾種不同的寫法

回傳純值的函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//return value
function foo1(a, b) {
return a + b;
}
// arrow function
let foo2 = (a, b) => {
return a + b;
};
// skip { } and return
let foo3 = (a, b) => a + b;

console.log("foo1(1,2) is", foo1(1, 2));
console.log("foo2(1,2) is", foo2(1, 2));
console.log("foo3(1,2) is", foo3(1, 2));

回傳物件的函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//return object

function foo4(a, b) {
return { sum: a + b };
}
// arrow function
let foo5 = (a, b) => {
return { sum: a + b };
};
// add () so you can skip { } and return
let foo6 = (a, b) => ({ sum: a + b });

console.log("foo4(1,2) is", foo4(1, 2));
console.log("foo5(1,2) is", foo5(1, 2));
console.log("foo6(1,2) is", foo6(1, 2));

科里(Curry)化函數

1
2
3
4
5
6
//return function, execute foo7(1,2)()
let foo7 = (a, b) => () => a + b;
console.log("foo7(1,2) is", foo7(1, 2)());
//curry
let foo8 = (a) => (b) => a + b;
console.log("foo8(1,2) is", foo8(1)(2));

(fin)

[心得筆記] 學習圈有多厚?

前情提要

這是 2021 年 3 月的一些隨筆, 不知何故沒有整理上傳 Blog,
剛好最近翻到,作一次反芻並且記錄。

隨筆 1、學習圈有多厚?

常常聽到舒適圈、學習圈、恐慌圈, 再搭配上下圖
舒適圈、學習圈、恐慌圈

這個圖,我覺得比例不對,
所謂的 Growth Zone 應該只有簿簿的一層,
大多數的時候我們只能延著這層膜多踏出一步是一步。

舒適圈、學習膜、恐慌圈

偶有突破,才能擴大 Comfort Zone

我覺得下圖比較適合,你要找到正確的方向突破
才能從 Comfort Zone 連結到 Success

作個有梗的人

那個縫縫才是你成長區(膜)

作個有梗的人

突破是困難的,一但突破了,紅色的部份就會自動補上,這就是它神奇的地方

作個有梗的人

隨筆 2、福原愛事件

  • 甘我屁事? 除非我想當 KOL 用來蹭流量,或是一般大眾需要談資
  • 如果已離婚,她的自由
  • 如果未離婚,感情淡了,感情本來就存在灰色地帶
  • 如果未離婚,感情好,那就是假新聞

2022 王力宏事件

恰巧 2022 也有類似的事件

  • 甘我屁事 ?
  • 渣男是否違法 ? 法律是道德的底限 ?
    • 群眾觀感是否約束力,或只是群眾暴力 ?
    • 文革式審判
  • 有錢中國賺,出事回台灣(羅志祥)
  • 一個人的作品與其道德是否有關 ?
    • 好的事就是是會改變的,而且不一定會變好
    • 昨是今非(時空背景不同)

隨筆 3、錢、價格、價值

最重要的是價值,價格只是落後指標,
你應該持有資產(農田、房子、工廠、生意、股票),
錢只是流通的工具,只有被消費掉的時候會產生價值。

  • 置裝:10000
  • 孝親:104000
  • 生活:120000
  • 娛樂:40000
  • 教育:60000
    => 年度儲蓄目標: 334,000+額外家庭開支(房貸、網路、電話、水電費、第四台、瓦斯)
  • 緊急預備金:180000

邊際效應具體感受

月費 600 元的運動中心,假設每次去 3 小時,
只去一次的話,那一次就價值 600 元,
當次數變多的時候,每次的時間成本不變,現金成本卻是下降的,
運動中心能帶給我的價值,具體量化的話有

  1. 運動次數
  2. 運動時數
  3. 運動頻率*

背後的抽象目的可能包含:

  1. 健康
  2. 社交

這些「目的」與「價值」就是所謂的「效應」,
並不是越多越好,而是取得平衡*(參考系統思考)
比如說,運動過量反而會受傷變得不健康。

下表顯示,當我第一次去運動中心時,
性價比是最高的,單次的價格是 600 元,
隨著次數的提昇,單次的價格是持續下降的,
理論上每天一次的話,單次價格會下降到 20 元/次

«表一»

次數 均價 時數
1 600 3
2 300 6
3 200 9
4 150 12
5 120 15
6 100 18
10 60 30
20 30 60
30 20 90

但價值就不是這樣衡量的,
一開始的價值遞增的,但是當超過了某個奇點,
價值就會開始遞減。

長萎

那麼錢呢?
其實錢只是一個交流價值用的媒介,
以物易物本身是價值的交換,
但是會耗消很大的時間,才能找到等價的物品,
比如一頭牛可以換多少的白米呢?
更不用說是情感需求等抽象層面的東西。

所以當有了代幣、指示物(Token)、貨幣將抽象的價值具象化,
(背後要有強而有力的組織支持,如:國家,強而有力的信用才能維持貨幣價值),
不同的國家有不同的貨幣,錢則是這些貨幣的代稱。

Cat

所以真正有價值的東西並不是錢,錢只有在需要交易之時才有用,
真正有價值的是一首好聽的歌,(太好聽了,怎麼才能反覆聽呢?)
一顆蘋果、一段關係…
一直持續對社會有益的組織
ex:

  • 讓人生活有意義(專家、奴隸),
  • 提供讓人更便利生活、
  • 提供食物、衣服、交通、娛樂、
  • 提供五大需求所需事務、
  • 建立關係

隨筆 4、左右

我很左,但是極致的左是不夠務實的,
我要務實,為了實現某些左邊的理想,
我要右,極致的右才能帶來極致的左,
這樣我還左嗎?

2202 反思

就像跳舞或走路一樣,單足難行千里,
我們總要左右才能前行。
重要的是平衡與推動力。

參考

https://www.books.com.tw/products/0010653417
https://pttcareers.com/Soft_Job/1WQm1aXz

(fin)

[實作筆記] 靜態網站部署整合 GCP (一) --- Artifacts Registry & Cloud Run

目的

有機會在 GCP 上實作了兩種不同方式的靜態網站部署,
一種是透過 Artifacts Registry 與 Cloud Run,
另一種是透過 Google Cloud Storage 與 Load Balancing
特別記錄一下。

Build Web

build 建置網站, EX: flutter build webnpm rub build

  • flutter 建置的 web 靜態檔位置在 build/web
  • react-scripts build 靜態檔位置在 build 底下

問題與解決方式

react build 不同環境的靜態檔

開發上會有不同環境需
預設執行 react-scripts build 會建置出 production 的靜態檔,
故需要透過套件 env-cmd 來幫我們建立不同的靜態檔,
請參考以下的 package.json > scripts

1
2
"build": "react-scripts build"
"build:dev": "env-cmd -f .env.development react-scripts build"

ERR_OSSL_EVP_UNSUPPORTED

截至 2021 年 12,nodejs LTS 版本為 v16.13.1
Latest Features 為 v17.3.0
如果 nodejs 的版本在 v17.*.* 以上
執行 react-scripts build 會發生以下錯誤

1
2
3
4
5
6
7
Error: error:0308010C:digital envelope routines::unsupported
… …

opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
library: 'digital envelope routines',
reason: 'unsupported',
code: 'ERR_OSSL_EVP_UNSUPPORTED'

解決方法,執行以下語法

export NODE_OPTIONS=–openssl-legacy-provider

或是將 node version 換回 v16.*.* 的版本

nvm use v16.13.1

Build Image

如果選擇 Artifacts Registry 與 Cloud Run 的 solution
必須撰寫 Dockerfile 來建立 Docker Images

問題 [Dart] no active package dhttpd

dhttpd 是 Dart 的靜態網站解決方案,
不過在部署到 Cloud Run 時會發生錯誤,
更多資訊請參考 stackoverflow 的提問與 issueTracker 的後續

20220308 已有解決方案,更新

加上 dart global activate 指令,這會在執行前啟用 dhttpd 的服務,
RUN 是建立鏡像檔的語法, CMD 才是 docker image 啟動時會執行的語法,
所以應該將 activate 指令放在啟動

1
2
3
4
5
6
7
8
9
10
11
FROM docker.io/dart

WORKDIR /flutter

ENV PATH $PATH:$HOME/.pub-cache/bin

COPY build/web ./
COPY start.sh ./
EXPOSE 8082

CMD dart pub global activate dhttpd && dart pub global run dhttpd --port=8082 --host=0.0.0.0

參考聯結

暫解是使用其它的靜態網站服務或套件 ,
EX: NGINX 、 npm 的 serve 套件,
或是其它你熟悉的靜態網站服務。
請參考以下 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# pull official base image
FROM node

# set working directory
WORKDIR /flutter

# install app dependencies
RUN npm i -g serve

# add `/app/node_modules/.bin` to $PATH
ENV PATH $PATH:$HOME/.pub-cache/bin

# add app
COPY build/web ./

EXPOSE 8080

# start app and run at 0.0.0.0
CMD serve -p 8080

Artifacts Registry & Cloud Run

  1. 登入私有的 Container Registry
  2. 透過 CI 建置 image
  3. 加上版本的 Tag
  4. 推上 Container Registry

參考部份的 .gitlab-ci.yml

1
2
3
4
5
6
7
script:
- echo "Compiling the image..."
- echo -n $GCR_KEY | docker login -u _json_key_base64 --password-stdin https://asia-east1-docker.pkg.dev
- docker build -t $CI_PROJECT_NAME . --no-cache
- docker tag "$CI_PROJECT_NAME" "asia-east1-docker.pkg.dev/company_registry/docker/$CI_PROJECT_NAME:$ver"
- docker push "asia-east1-docker.pkg.dev/company_registry/docker/$CI_PROJECT_NAME:$ver"
- echo "Compile image complete."

接下手動設定 Cloud Run(這部份應該也可以自動化,未實作故無記錄)

  1. Create Service
  2. Container > General > Container Image URL > SELECT > ARTIFACT REGISTRY > 選取上面部署上去的 images
  3. Container Port 8080
  4. DEPLOY

缺點是每次 CI 跑完 Image 推上 Artifact 後,仍要手動到 Cloud Run 執行 Deploy。
這個步驟應該放在 CI Server 上(即使是需要手動觸發也是),才不會有斷點。

(fin)

[實作筆記] Gitlab Runner

前情提要

我想完成什麼 ?
如何用最佳解完成全端應用的開發 ?

  1. 前端應包含 Web、App(iOS、Android 或簡稱三螢) 與其它
  2. 後端不限語言、框架(.Net Core、nodejs)
  3. 測試部署應該自動化(CI/CD)
  4. 成本應優化
  5. 應該使用雲原生的技術與 know how

這次我們專注在第 3 點的 CI/CD 上,為了第 1 點,我建置的專案為 flutter
選用的 CI/CD Server 為 Gitlab , 我將會在這裡作較多的著墨,
最終的產出物是 flutter app image, 並推送到 image registry 上 。

概觀,每個多邊型都應該可以被置換

如上圖,每個多邊型都應該可以被置換,

  • Application 可以被換成 Nodejs/.Net Core/ …
  • Gitlab Runner 有三種,本文中會使用 specific runners ,另外有 2 種
    • Shared runners(需要信用卡認証身分,每月限制 400 分鐘)
    • Group runners (在 Gitlab 上可以)
      我將 Gitlab 設定在本機 ( MacBook Pro ) 環境上,下文會講解細節,
      也可以置換到雲端的伺服器上,搭配 K8S
      ex:
      • GKE: Google Cloud Platform Kubernetes Engine
      • EKS: Amazon Web Services’ Elastic Kubernetes Services
      • AKS: Microsoft Azure Kubernetes Service
  • Container Registry 這裡我使用最主流的 Docker Hub Registry
    以雲原生的三大平台都有對應的功能,如果有機會應該優先選用。

工具準備

首先需要 Gitlab Account ,我們選用 Gitlab 作為 CI/CD Server,
這個階段我們只會作到持續整合,而未部署,相當於只有 CI 的部份。
理論上也可以選用其它的 CI/CD 服務,像是 Azure 或是 GitHub , 這裡就不作過多的展開。

Docker , 簡單說我們的工作只有兩個步驟

  1. 在 Gitlab 你的專案 > Settings > CI/CD > Runners 註冊 Runner Executors
    • 一般來說,我們會選用 docker 或 Kubernetes,本章我會用 docker 為例
  2. 撰寫 Pipeline 與 Job 腳本,
    Pipeline
    我們預計執行以下的工作(Pipeline)
    • Test
      • Run Test
      • Run Lint
    • Build
      • Build Application
      • Build Image

開始

註冊 Gitlab Runner

我們要在 docker 建立來執行 gitlab Runner

  1. Create the Docker volume:

    1
    docker volume create gitlab-runner-config
  2. Start the GitLab Runner container using the volume we just created:

    1
    2
    3
    4
    docker run -d --name gitlab-runner --restart always \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v gitlab-runner-config:/etc/gitlab-runner \
    gitlab/gitlab-runner:latest
  3. Register Docker Runner

    依序輸入 Gitlab URL、gitlab-ci 的 token、runner 說明/名稱、Runner 的 tag、executor、docker image

    1
    2
    docker run --rm -it -v gitlab-runner-config:/etc/gitlab-runner \
    gitlab/gitlab-runner:latest register
    • Enter your GitLab instance URL (also known as the gitlab-ci coordinator URL).
    • Enter the token you obtained to register the runner.
    • Enter a description for the runner. You can change this value later in the GitLab user interface.
    • Enter the tags associated with the runner, separated by commas. You can change this value later in the GitLab user interface.
    • Provide the runner executor. For most use cases, enter docker.
    • If you entered docker as your executor, you’ll be asked for the default image to be used for projects that do not define one in .gitlab-ci.yml.
      GitLab instance URL 是 https://gitlab.com/
      你可以在專案中的 Settings > CI/CD 找到 token,
      description 會顯示在 Runner List 中,可以用易懂的描述,
      tags 可以更多的參考這本篇文章設定
      executor 選用 docker 記得需要安裝 docker daemon
      executor 為 docker 時,需要註明預設的 image,我是選用 docker:stable

執行 RUNNER

gitlab-runner run

移除 RUNNER(為了重新安裝)

  1. 驗証 runner 狀態是否 alive

    gitlab-runner verify –delete

  2. 移除 gitlab-runner

    gitlab-runner unregister –all-runners

參考

錯誤: 當使用 MacBook M1 當作 RUNNER

在 RUNNER JOB 中執行 flutter pub get 時
$ flutter pub get
發生錯誤訊息如下,當我換了一個 intel 晶片的 MacBook 當作 RUNNER 就不會有問題了。

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
Running "flutter pub get" in rettulf...
===== CRASH =====
si_signo=Trace/breakpoint trap(5), si_code=128, si_addr=(nil)
version=2.14.4 (stable) (Wed Oct 13 11:11:32 2021 +0200) on "linux_x64"
pid=57, thread=116, isolate_group=main(0x4002e9fc00), isolate=main(0x4002e7d000)
isolate_instructions=4001cf2ec0, vm_instructions=4001cf2ec0
pc 0x0000ffffab7e28ec fp 0x000000400a1af838 Unknown symbol
pc 0x0000ffffab49ed0f fp 0x000000400a1af898 Unknown symbol
pc 0x0000ffffab78114d fp 0x000000400a1af8d8 Unknown symbol
pc 0x0000ffffacaa294c fp 0x000000400a1af940 Unknown symbol
pc 0x0000ffffacaa220b fp 0x000000400a1af980 Unknown symbol
pc 0x0000ffffacd028ff fp 0x000000400a1af9f8 Unknown symbol
pc 0x0000004001e6a123 fp 0x000000400a1afaa0 dart::DartEntry::InvokeCode(dart::Code const&, unsigned long, dart::Array const&, dart::Array const&, dart::Thread*)+0x153
pc 0x0000004001e69f75 fp 0x000000400a1afb00 dart::DartEntry::InvokeFunction(dart::Function const&, dart::Array const&, dart::Array const&, unsigned long)+0x165
pc 0x0000004001e6c55d fp 0x000000400a1afb50 dart::DartLibraryCalls::HandleMessage(dart::Object const&, dart::Instance const&)+0x15d
pc 0x0000004001e93e46 fp 0x000000400a1afc30 dart::IsolateMessageHandler::HandleMessage(std::__2::unique_ptr<dart::Message, std::__2::default_delete<dart::Message> >)+0x596
pc 0x0000004001ebe7ac fp 0x000000400a1afca0 dart::MessageHandler::HandleMessages(dart::MonitorLocker*, bool, bool)+0x14c
pc 0x0000004001ebeecf fp 0x000000400a1afd00 dart::MessageHandler::TaskCallback()+0x1df
pc 0x0000004001fdde48 fp 0x000000400a1afd80 dart::ThreadPool::WorkerLoop(dart::ThreadPool::Worker*)+0x148
pc 0x0000004001fde27c fp 0x000000400a1afdb0 dart::ThreadPool::Worker::Main(unsigned long)+0x5c
pc 0x0000004001f57a48 fp 0x000000400a1afe70 /sdks/flutter/bin/cache/dart-sdk/bin/dart+0x1f57a48
-- End of DumpStackTrace
qemu: uncaught target signal 6 (Aborted) - core dumped
/sdks/flutter/bin/internal/shared.sh: line 225: 57 Aborted "$DART" --disable-dart-dev --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
Cleaning up project directory and file based variables

DIND(Docker In Docker)

我們的 Gitlab-Runner 是執行在 Docker 上,
而當我需要 build Docker Images 時,我會在 Container 中建立 docker
這時 Container 所使用的 Image 會是 Docker Image
就被稱為 DIND (Docker In Docker)

在 Gitlab CI 中 Docker Login

參考以下的 .gitlab-ci.yml

1
2
before_script:
- echo "$DOCKER_REGISTRY_PASS" | docker login -u $DOCKER_REGISTRY_USER --password-stdin

異常紀錄與解決方案

$ echo -n $LOGIN_KEY | docker login -u _json_key_base64 –password-stdin https://xxxx-docker.pkg.dev
Error: Cannot perform an interactive login from a non TTY device

主因是讀不到 $LOGIN_KEY 的環境變數,使用 Gitlab-CI 的話要注意 Protect Variable

無法對 docker hub registry

在 RUNNER JOB 中執行 docker push
錯誤訊息如下:

dial tcp: lookup docker on x.x.x.x:53: no such host error runner inside docker on armhf

這裡要修改 RUNNER 中的設定檔 config.toml , 路徑參考如下:

You can find the config.toml file in:

  • /etc/gitlab-runner/ on *nix systems when GitLab Runner is executed as root (this is also the path for service configuration)
  • ~/.gitlab-runner/ on*nix systems when GitLab Runner is executed as non-root
  • ./ on other systems

在 [[runners]] > [runner.docker] 加入 image = docker:stable 或是 privileged = true
just add image = docker:stable  and privileged = true
這可能不是一個正確的 solution , 可以更多的參考這篇討論

在 DIND 編輯文件

如上題,我們需要在 container 之中編輯文件,
這裡我會使用 vim,
不過安裝前記得先更新 apt-get
不然會出現以下錯誤

E: Unable to locate package vim on Debian jessie simplified Docker container

1
2
3
4
apt-get update
apt-get install apt-file
apt-file update
apt-get install vim # now finally this will work !!!

GCP Artifact Registry

這裡都以台灣的 region 為範例,
所以都是 ‘asia-east1-docker.pkg.dev‘ 不同的 region 請再參考 GCP 文件

推上去

記得要先登入

docker login -u _json_key_base64 –password-stdin https://asia-east1-docker.pkg.dev

推上去

docker push “asia-east1-docker.pkg.dev/{env_name}/docker/{project_name}:{version_tag}

拉下來

需要先調整 Docker configuration file 取得授權,請參考

gcloud auth configure-docker asia-east1-docker.pkg.dev

再執行

docker pull asia-east1-docker.pkg.dev/{env_name}/docker/{project_name}:{version_tag}

註冊

問題

  1. gitlab-ci-multi-runner 與 gitlab-runner 的差異為何 ?
  2. 當 docker gitlab-runner image 的 instance 執行 gitlab-runner run 會產生以下訊息, 這代表什麼意思 ?
    Configuration loaded builds=0
    listen_address not defined, metrics & debug endpoints disabled builds=0
    [session_server].listen_address not defined, session endpoints disabled builds=0

(fin)

[實作筆記] 在 terminal 中控制字串

目的

在 CI/CD 的過程需要透過 shell 處理環境變數產生版號,
以前沒有這方面的經驗,實務上也不像一般的應用程式語言那麼常操作,
避免未來忘記,特別記錄下來,如果未來有相關的語法也會在此同步更新。

宣告變數

直接給值

var=””

切割字串

只想看結果的話

echo ${HOME}|cut -d”/“ -f2
指定給變數
var=$(echo ${HOME}|cut -d”/“ -f2)

串接字串

var=$(echo ${HOME}|cut -d”/“ -f2).$USER

(fin)

[踩雷筆記] MacBook M1 silicon X Docker X MySQL Image

前情提要

新工作配發了新的 MacBook 搭載最新的 M1 晶片,
沒想到反而遇到了許多問題, 不會有太多的文字說明,
僅用來記錄我實作有效的解決方法

情境:在 Docker 跑 MySQL

當執行以下語法時,

1
docker pull mysql 

會得到錯誤訊息如下

no matching manifest for linux/arm64/v8 in the manifest list entries

解決方案

拉取映像檔

docker pull mysql/mysql-server:latest

建立 mysql container

docker run -d –name mysql -p 3306:3306 mysql/mysql-server:latest

取得原始 root 密碼

docker logs mysql 2>&1 | grep GENERATED

[Entrypoint] GENERATED ROOT PASSWORD: mysql_password_shows_here

連線至 container 的 image 執行 mysql 指令

docker exec -it mysql bash

使用原始 root 密碼登入 mysql

mysql -u root -p
Enter password:

輸入原始的 root 密碼後,
會看到畫面如下:

Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 25
Server version: 8.0.27

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective owners.

Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

重設密碼

ALTER USER USER() IDENTIFIED BY ‘password’;

授權

CREATE USER ‘root‘@’%’ IDENTIFIED BY ‘root’;
GRANT ALL ON . TO ‘root‘@’%’;
flush privileges;

修改 root 密碼

ALTER USER ‘root‘@’%’ IDENTIFIED WITH mysql_native_password BY ‘password’;
flush privileges;

(fin)

[翻譯] react-router 的三種渲染方法(component、render、children)

前情提要

本文內容大量參考此系列文章, 僅作記錄之使用。
在學習 React 的過程中, 我們需要處理瀏覽器的網址(URL)與頁面之間的關係,
目前(2021 年)主流的作法,就是使用 react-router 這個 library,
其中有三個類似的方法

Rendering Method

component

使用方法:

1
2
3
4
5
<Route path="/" component={Home} />
//Same as
<Route path="/" >
<Home />
</Route>

這個方法的缺點是並沒有提供傳遞 props 的 API

1
2
3
4
const Home = (props) => {
console.log(props);
return <div>Home</div>;
};

render

Render 這方法要求你使用一個傳入一個回傳 component 的方法,
我們可以透過方法參數傳遞 props

1
2
3
4
5
6
<Route
path="/contact"
render={(routeProps) => {
return <Contact name={name} address={address} {...routeProps} />;
}}
/>

children

基本上使用方式與 render 並無二致,
最大的差異在染渲邏輯,children 在路由不匹配的時候, 仍然會顯示,
以下例子在使用者輸入 / 會顯示 PortfolioContact

1
2
3
4
<Route path="/" exact component={Home} />
<Route path="/about" render={() => <About></About>} />
<Route path="/portfolio" children={() => <Portfolio></Portfolio>} />
<Route path="/contact" children={() => <Contact></Contact>} />

參考

(fin)

[實作筆記] 前端置多語系 i18next

前情提要

多語系是國際化的專案很重要的一部份,
這次有機會接觸到國際化的前端的案子,
記錄一下前端主流的 i18next 如何設定。

設定

  1. 安裝 i18n

    1
    2
    3
    4
    5
    # npm
    $ npm install i18next --save

    # yarn
    $ yarn add i18next
  2. 準備多語系 JSON 檔, 為 Key-Value 形式, Value 為 string
    允許巢狀, 範例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    {
    "zh": {
    "Week": {
    "Monday": "周一",
    "Tuesday": "周二",
    "Wednesday": "周三",
    "Thursday": "周四",
    "Friday": "周五",
    "Saturday": "周六",
    "Sunday": "周日"
    }
    },
    "es": {
    "Week": {
    "Monday": "Monday",
    "Tuesday": "Tuesday",
    "Wednesday": "Wednesday",
    "Thursday": "Thursday",
    "Friday": "Friday",
    "Saturday": "Saturday",
    "Sunday": "Sunday"
    }
    }
    }
  3. 起始設定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import i18next from "i18next";
    import resources from "./resources"; // 載入上一步的 JSON 檔

    i18next.init({
    lng: "zh", // 預設語言
    debug: true,
    resources: resources,
    });

    // 使用方式
    document.getElementById("output").innerHTML = i18next.t("Week.Sunday");
  4. 追加 React 設定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import React from "react";
    import i18n from "i18next";
    import { useTranslation, initReactI18next } from "react-i18next";
    import resources from "./resources"; // 載入上一步的 JSON 檔

    i18n
    .use(initReactI18next) // passes i18n down to react-i18next
    .init({
    resources: resources,
    lng: "en", // if you're using a language detector, do not define the lng option
    fallbackLng: "en",
    interpolation: {
    escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
    },
    });

    在 React 中使用

    1
    2
    3
    4
    5
    6
    7
    8
    import ReactDOM from "react-dom";
    function App() {
    const { t } = useTranslation();
    return <h2>{t("Week.Sunday")}</h2>;
    }

    // append app to dom
    ReactDOM.render(<App />, document.getElementById("root"));

其它 Framework 請參考

參考

(fin)