顯示具有 Git 標籤的文章。 顯示所有文章
顯示具有 Git 標籤的文章。 顯示所有文章

2025年8月6日 星期三

分享用 Git 管理 OneDrive 的方法 (OneDrive + Git + mklink) - Windows

這裡分享一下我對 OneDrive 用 Git 配合 Windows 的 mklink 指令做版本管理的方法

環境:

  1. 我的電腦系統是 Windows11。
  2. 電腦上有登入 Microsoft 帳號的 OneDrive 同步資料夾。
  3. OneDrive 上有程式碼,且是多人共用。

雖然我個人是不太喜歡把程式放在 OneDrive 中,因為 OneDrive 沒有像 Git 一樣的管控概念,比較像是注重同步檔案功能的工具而已,雖然 OneDrive 可以去看檔案的各個版本及修改時間,但是沒有辦法像 Git 一樣很方便地看到哪一批檔案在哪個時間、被誰修改、修改了哪幾行。

OneDrive 也沒有辦法像 Git 一樣先對檔案進行修改不要同步,等修改確定後再同步,也沒辦法開 Branch 做多 feature 開發管理。

不過因為公司有特別需求 (比如檔案使用者有非RD人員不會用 Git、檔案很少修改、程式內容不多之類的) 所以在這部份採用了 OneDrive,為了我自己能夠較好的管理對 OneDrive 裡各 feature 需求的版本控管,我開始想方法用 Git 來對 OneDrive 進行管理。

我的需求是:

  1. 希望能針對不同的 feature 開發建立 branch 來控管並開發,但各 feature branch 在開發時能不修改到 OneDrive 的檔案,希望等到開發完後才將 branch 的修改 merge 至 OneDrive 中的檔案。
  2. 不希望新增目前沒有在 OneDrive 中的不必要檔案,例如 Git 的 .git, .gitnore 等檔案。
  3. OneDrive 中的檔案變動 (例如可能別人修改了檔案) 能夠即時的反映在 Git repository 中,方便我知道別人修改了哪些檔案,在 feature branch merge 時能夠被檔下來得到提醒告知之類的,也要有能處理 conflict 的能力。

最後這是我想到的,利用了 Windows mklink 指令來把 Git repository directory 跟 OneDrive 資料夾同步,並配合 Git worktree 做 feature branch 版控的方法,特此分享:

假設 OneDrive 資料夾位置在

C:\Users\<userName>\xxx-onedrive-folder

先建立資料夾,例如: D:\MyOneDriveRepository\masterBranch
用 git init 把資料夾設定成 git repository,假設一開始的 branch 叫做 master。

然後執行以下指令 (/J 代表 Directory Junction):

mklink /J D:\MyOneDriveRepository\masterBranch\repoLinkToOneDrive C:\Users\<userName>\xxx-onedrive-folder

這樣就會得到一個被建立起來的資料夾:
D:\MyOneDriveRepository\masterBranch\repoLinkToOneDrive

並且 C:\Users\<userName>\xxx-onedrive-folder 和

D:\MyOneDriveRepository\masterBranch\repoLinkToOneDrive
會連結起來成為同步狀態

把 D:\MyOneDriveRepository\masterBranch\repoLinkToOneDrive 連同裡面的檔案都進行第一次的
git commit 就可以開始進行 Git 版本控管了。

因為 OneDrive 可能會跟別人一起合作共用,所以如果在開發新 feature 前我們不希望對 OneDrive 裡的檔案做修改,
也就是說記住不要隨便切換

D:\MyOneDriveRepository\masterBranch

的 branch,讓它永遠在 master branch。

如果有要開發新 feature,我們可以用 git branch <feature branch> 建立新 branch (不要切換過去),例如新 branch 叫 featureBranch,

用 git branch featureBranch 建立新 branch 後,再利用 git worktree 的方式在另外一個資料夾 checkout featureBranch 去開發,
例如開發路徑是 D:\MyOneDriveRepository\otherBranch\repoLinkToOneDrive,
可以執行

git worktree add D:\MyOneDriveRepository\otherBranch featureBranch

這樣就會得到一個被建立的資料夾:

D:\MyOneDriveRepository\otherBranch

我們就可以在裡面開發 featureBranch 的程式了。

最後等 feature 開發完後,可以再回到

D:\MyOneDriveRepository\masterBranch

用 git merge featureBranch --no-ff

來把 featureBranch merge 至 master branch 來改變 OneDrive 的檔案。

這邊放上一張圖解示意圖,可以更好地理解資料夾 Directory Junction 和 Git worktree 等之間的關係:

這樣的作法有幾點好處:

  1. 如果有別人修改了 OneDrive 中的檔案,因為有 Git 管理的關係,我們也可以很容易的發現,例如 git merge 時會因為改到同一個檔案而被擋下來。
  2. 可以把別人的修改 commit 至 master branch 做記錄,雖然不能容易地知道哪幾行是何時被誰修改的有點可惜 (還要特別去 OneDrive 網頁裡查 log 有點太麻煩了 )。
  3. git 生成出來的 .git 檔案不會被上傳到 OneDrive 上,因為我們是對跟 OneDrive 做 Directory Junction 的資料夾的外層資料夾做 git init,所以 .git 並不在跟 OneDrive 做 Directory Junction 的資料夾之中。

參考資料:

  1. 理解 Symbolic Link、Hard Link 與 Directory Junction 的差異之處 | The Will Will Web
  2. git worktree 簡單介紹與使用. 現在可以在同一個專案之中,一次開啟多個不同的 branch 了。 | by Jui Yuan Liou | Medium


2022年11月17日 星期四

Git 參考資料 - 好用指令 - 學習紀錄

用指令

----------------------- 與 Log 相關 --------------------
# 用指定的 format 印出 從 <commit id> (包含) 到 <commit id2> 的所有 commit
# <commit id>...<commit id2> 可代表 <commit id> 後一個 commit 到 <commit id2> 的所有 commit (不包括  <commit id>),所以
<commit id>^...<commit id2> 代表 <commit id> 的 parent 的後一個 commit ,也就是 <commit id> 自己,到 <commit id2> 的所有 commit
git log --pretty=format:"%H %s" <commit id>^...<commit id2>

#查詢操作的詳細記錄,包括 checkout, pull, merge 等操作,可以看到操作的 commit id,可以用 git reset <commit_id> --hard 會到某個操作時的狀態,例如可以用來回到 merge 前的狀態等
git reflog
----------------------- 與 Remote Repository 相關 --------------------
# 在空的資料夾將 <git repository> 的 git project 抓下來
git clone <git repository>

#確認 remote 設定
git remote -v

#設定 remote
git remote set-url <remote 名稱> <Git Repository 位置>

#只增加不覆蓋舊的 remote
git remote set-url --add [--push] <remote 名稱> <Git Repository 位置>
--add : 只增加不覆蓋舊的
--push : 設定給 push 用的 remote

#刪除 remote
git remote set-url --delete [--push] <remote 名稱> <Git Repository 位置>
--push : 設定給 push 用的 remote
-----------------------------------------------------------------------------------------------------
# 上傳分支,-u (或是 --set-upstream) 表示並且追蹤遠端的分支,之後就不用再加 -u
git push <remote name> <branch name> 
git push -u <remote name> <branch name> 

# 只把 remote 的版控抓下來,但沒有要 merge 到 local branch 上, --all 表示抓取所有的 remote
git fetch
git fetch --all 

# 如果有的 remote branch 已被刪除,但 local 還留有 remote 追蹤 branch ,可以使用 git fetch --prune 將之清除
git fetch --prune

#git pull 等同 git fetch 和 git merge 的結合,把 remote 的版控抓下來,並 merge 到 local branch 上
git pull
如果想再產生一個 merge commit,建議可加 --rebase 參數,行為會變成
等同 git fetch 和 git rebase 的結合
git pull --rebase

----------------------- 與 Local Repository 相關 --------------------
# 移動 HEAD 到 <commit id> 的 patch
git checkout <commit id>

#建立 Barnch (可選某個 commit id 在上建立 branch)
git branch <branch name> [commit id]

# 將 <file path> 檔案還原成 <commit id> patch 的內容 (不小心被自己刪掉的檔案也可以),
# <file path> 可以是資料夾路徑或檔案路徑,路徑是從 cmd 指令所在位置開始算
git checkout <commit id> -- <file path>
Ex.: git checkout <commit id> -- *

# 刪除工作區中所有沒有被 tracked 的檔案,參考
git clean
# -n 參數:只顯示執行 git clean 後會被刪除的檔案,不會真得刪除檔案,只是做一次演習,告訴你真得執行 git clean 後會被刪除的檔案
git clean -n
# 刪除指定 <path> 路徑下的沒有被 track 過的檔案
git clean -f <path>
# 刪除當前目錄下沒有被 track 過的檔案和資料夾
git clean -df
# 刪除當前目錄下所有沒有track過的檔案. 不管他是否是.gitignore檔案裡面指定的資料夾和檔案
git clean -xf

#檢查 <file path> 有沒有符合被 .gitignore 忽略及是符合哪一條規則
git ignore <file path> -v

#在 git checkut 於 <branch1> 時,想把 <branch2> merge 到 <branch1>
git merge [--no-ff] [--no-commit] [--merge]
#--no-ff : 不要用 fast-forward 的方式 merge
# --no-commit : 不要 commit,要改變的修改都先放在 Stage files 裡,可以用 git merge --continue 繼續執行 merge 或用 git --abort 取消 merge 
#--merge : 可退回到 merge 操作之前的狀態,並且保留 worktree 的修改。 它會取消當前分支上的合併操作,並將 worktree 和 stage 區恢复到合併之前的狀態。 與 git reset --hard 不同,--merge 不會丟棄未提交的更改。


# 捨棄 <commit id> 之後的 patch (即等於放棄之前的 commit),並且將 HEAD 移至 <commit id> patch
git reset <commit id>
(工作區檔案內容不被影響)
git reset --hard <commit id>
(工作區檔案內容會被影響而被更新)

# 直接 reset 成指定的 patch,注意: 會捨棄 <commit id> 之後的 commit,
# 不要在 <commit id> 之後的 commit 已 push 至 remote 的情況下使用!!避免影響到同仁,除非此 branch 只有你在用
git reset --hard <commit id> 

# 把 <commit id> 的 patch 複製一份 (原來的 patch 還是會留著) 到目前位置的後面 (一樣會產生新的 commit id),
加 --edit 參數可以用指定的編輯器輸入 message,
加 -x 參數可以自動地加入 (cherry-pick from <xxxCommitId>) 的 message
git cherry-pick <commit id> 
# cherry pick commitB 之前但不包括 commitA 的所有 commit
git cherry-pick <commitA id>...<commitB id>
# cherry pick commitB 之前但不包括 commitA的parent 的所有 commit,
等同於 cherry pick commitA 到 commitB的所有 commit
git cherry-pick <commitA id>^...<commitB id>

#將現在的 patch 接到 <branch> (或是某個 commit)的後面
git rebase <branch>

#將 <commit from> 之後 (不包括 <commit from>) 到 <commit to> (或是 <branch name> 的最後一個 commit) 移到 <new base commit> 上,
#例如可以將一個 branch (<branch name> 或你現在所在的 branch) 的從 <commit from> 之後的 commits 都換到 <new base commit> 上,就像是改成從 <new base commit> 上長出 branch 一樣
git rebase --onto <new base commit> <commit from> [<commit to> | branch name>]

#互動式 git rebase
git rebase -i <after this commit>
更改 <after this commit> 之後(不包含 <after this commit>)的 patch,
會使用互動介面(可能是文字編輯器或 console,看你設定),
可改變 patch 順序、合併 patch (squash)、刪除 patch、修改 patch 註解、修改 patch 檔案內容等

git rm <file> --cached
移除 <file> 檔案將之不再被 git 控管 (在本機上不會被刪除),相當於在 repository 中移除檔案,
可以用 .gitignore 檔配合 cmd 指令來從 patch 中清除不想被 git 控管的檔案
for /F "tokens=*" %a in ('git ls-files -ci --exclude-standard') do @git rm --cached "%a"
可用以下指令查看會清除哪些檔案
Linux/MacOS:
git ls-files -ci --exclude-standard -z | xargs -0 git rm --cached

Windows (PowerShell):
git ls-files -ci --exclude-standard | % { git rm --cached "$_" }

# stash 暫存操作,將在工作區的檔案暫存
git stash
# git stash pop 從 stash 還原檔案修改但不刪除 stash,n 是第幾個 stash
git stash apply stash@{n}
# git stash pop 從 stash 還原檔案修改且刪除 stash,n 是第幾個 stash
git stash pop stash@{n}
# 刪除指定 stash,n 是第幾個 stash
git stash drop stash@{n}

#worktree 功能,可以同時以不同資料夾做為工作區,同時開發,同步共享 branch, commit。
#Note: 不能兩個 worktree 同時 checkout 在同一個 branch,例如 worktree1 正在 checkout branch1 時,如果 worktree2 要 checkout branch1 會被擋下來,直到 worktree1 checkout 到其他 branch 才行
#查詢 worktree
git worktree list

#建立新的 worktree 工作區
git worktree add <要建立新工作區的資料夾路徑> <新工作區一開始要 checkout 到哪個 branch>
例:
git worktree add D:\some-other-worktree-dir xxxBranchName

#移除不必要的 worktree 工作區,例如把不用的 worktree 工作區資料夾刪掉以後,可以用 git worktree prune 移掉 worktree 設定,才不會有 checkout branch 被擋的問題
git worktree prune

----------------------- 與 Git 設定 相關 --------------------
git config --global core.editor "'D:\notePad++\notepad++.exe' -multiInst -nosession"
指定 notePad 為 git 互動模式時用的編輯器

----------------------- 關於 commit, merge 等 message 中同時要輸入雙引號或單引號的方法  -----------
如果 git commit 或 merge 的 message 同時想要輸入雙引號或單引號的話,
可以用單引號圍住 message,然後
用三個雙引號表示一個雙引號,例如: """
用兩個單引號表示一個單引號,例如: ''
範例:
如果 message 是
this is double quote: " and this is single quote: '
可以如下輸入:
git merge xxxBranch -m 'this is double quote: """ and this is single quote: '''

----------------------- 關於 commit, merge 等 message 中要輸入換行的方法  -----------
如果 message 是(aaa 換行再 bbb):
aaa
bbb
可以如下輸入:
git commit -m 'aaa
bbb'
git commit -m "aaa
bbb"

----------------------- git-svn 的指令 (git 跟 svn 共同合做)  -----------
# 從 svn 上把 commit 抓下來,跟 git pull --rebase 功能一樣,只是是從 svn 上抓
git svn rebase
# 將 commit push 至 svn,跟 git push 功能一樣,只是是 push 至 svn
git svn dcommit

參考資料:

  1. SVN Migrate to Git

2022年8月19日 星期五

Git - 如何取消歷史 commit 中對某個檔案的修改紀錄

今天遇到一個情況是,
在 Git 的 commit 歷史紀錄中,
希望把其中有一次的 commit 中的某個檔案修改取消掉。
例如假設我現在狀態最新 commit 是 commit_0,
之後修改了檔案 file_1 和檔案 file_2 並 commit 成 commit_1,
接著又修改其他東西 commit 了 commit_2, commit_3 之類的,
但後來我反悔了,想把 commit_1 中對檔案 file_1 的修改取消掉,
也就是希望當作之前在 commit_1 中沒有修改過 file_1。

在這篇文章中特別紀錄一下上述情境的解法:
首先先列出一下 commit 的紀錄如下:

commit_3 ........
commit_2 ........
commit_1 修改了 file_1, file_2
commit_0 ........

首先要執行以下指令進入 git rebase interaction 互動模式

git rebase -i {commit_0 的 commit id}

接著會跳出編輯器供修改,例如可能是以下內容:

pick {commit_0 的 commitId} ......
pick {commit_1 的 commitId} ......
pick {commit_2 的 commitId} ......
pick {commit_3 的 commitId} ......

因為要修改的是 commit_1,所以我們要把 commit_1 的 "pick" 改成 "edit",
改完後儲存關掉,回到指令視窗,
執行以下指令將 file_1 的內容變回在 commit_1 之前,也就是在 commit_0 時的狀態:

git checkout {commit_0 的 commit id} -- {file_1 的路徑}

再來把已回復內容的 file_1 加進暫存區以進行 commit:

git add {file_1 的路徑}

接著再執行如下的 commit amend 指令,重新 commit 以取代原來的 commit_1 ,
這樣因為對於 commit_0 來說 file_1 在這次的 commit 其內容並沒有任何被修改的地方,
所以新的這個 commit_1 將不會有修改 file_1 的紀錄:

git commit --amend

最後我們再執行如下的 rebase continue 指令讓 rebase 的程序繼續進行下去至結束就行了。

git rebase --continue

參考資料:

  1. While editing a commit in `git rebase -i`, have to revert changes in a single file

2022年2月28日 星期一

GitLab CICD - 使用 GitLab Runner

這裡紀錄下使用
GitLab 做 CICD (Continuous Integration/Continuous Deployment, 持續整合、持續佈置)
時的一些佈驟及事項

GitLab 使用了 Job, Gitlab-Runner 及 executor 的設計。

在 Git 專案根目錄上會需要建立一個描述 Job 的 .gitlab-ci.yml 檔案,
其中記載了 CICD 要做的事項流程即要執行的 script 語句。

Gitlab-Runner 會跟 Gitlab repository 連線、檢查有無分派 Job 、並接受 Job , Gitlab-Runner 可能是被按裝在在 Windows 系統上、或是被按裝在 Linux 上、或甚至是在某台 server 跑起來的 Docker 中都有可能。

在 Gitlab-Runner 中,可以在其設定檔 config.toml 中設定多個 Gitlab-executor,當 Gitlab-Runner 接到需要執行的 Job 時,就會啟動相應 (例如 .gitlab-ci.yml 中擁有 tag1, tag2 的 Job 對應到有 tag1, tag2 的 Gitlab-executor) 的 Gitlab-executor。

因為 Gitlab-executor 本身需要跟 Giblab repository 連線,所以需要指定 Gitlab repository 的 SSL 憑證,例如如果專案位於 Gitlab 的 https://xxx.git/... 上,那就要取得 https://xxx.git 的憑證 (例如可以用 Chrome 瀏覽器去下載憑證)。

Gitlab-executor 是主要執行 .gitlab-ci.yml 中 Job 的 script 語句的環境,有分成幾種不同環境的 executor,
例如官方列出的 shell, docker, ssh 等等,可參考 Executors | GitLab
當使用 shell 環境的 Gitlab-executor 時,相當於直接在 Git-Runner 安裝環境上直接執行 script,
例如如果 Git-Runner 裝在 Windows 系統上,相當於直接執行 Windows 的 command line 語句。
或是例如如果 Git-Runner 裝在 Linux 系統 (如果裝在 docker 運行的 Linux 上也是一樣) 上,則相當於直接執行 Linux bash 語句。

當使用 docker 環境的 Gitlab-executor 時,Gitlab-Runner 會在所處環境下以指定的 docker image (可在 .gitab-ci.yml 中指定,或是在 config.toml 中指定預設的 image) 啟動一個指定的 docker container (這時 Gitlab-Runner 所被安裝的環境下必須要事先安裝好 docker),
Job 的 script 會以在這個被啟動的 docker container 下被執行。
例如如果啟動的 docker container 是含有安裝 php 的 container,那就能執行 Job script 的 php 語句。


這裡我以一個例子做示範,
情境是用 docker 啟動兩個 container,
一個運行 Gitlab-Runner,
另一個運行 Linux ubuntu 系統來模擬正式環境程式要佈署到的那台 server,
在這裡 Git project 假設是一個 Maven 專案,我想要利用 GitLab CICD 來對專案進行建構 war 檔 (mvn clean install),並以 SSH 的方式傳送到佈署 server,並以 SSH 連線佈署 server 執行解包 (jar -xvf xxx.war)

以下開始示範:

首先先安裝 Gitlab-Runner,
詳請可參考官網 Install GitLab Runner | GitLab ,
在這邊我以用 docker 來安裝 Gitlab-Runner 示範,
docker 可以使用 gitlab/gitlab-runner:latest 這個 docker image 來安裝 Gitlab-Runner,

安裝完好後,接下來要來進行 Gitlab-executor 的設定,
詳請請參閱官網 Registering runners | GitLab
設定主要以/etc/gitlab-runner/config.toml 設定檔來設定,
如果 Gitlab-Runner 不是用 root 身份啟動的話,檔案就會改放在登入 user 的位置,
例如 ~/.gitlab-runner/config.toml,可參考 Advanced configuration | GitLab 。
如果本來就有已設定好的 config.toml 檔的話,放在 Gitlab-Runner container 中的正確位置就可以了,基本上每次對 config.toml 檔的修改 Gitlab-Runner 都會偵測到並再次載入修改的設定,不太需要重開 Gitlab-Runner,

如果第一次沒有 config.toml 檔存在,想要一個範本的話,可以使用交互模示來進行 register Gitlab-executor,啟動 Gitlab-Runner 後,執行 container 的 gitlab-runner register 指令:
(假設 container 名稱取叫 my-gitlab-runner-container)
docker exec -it my-gitlab-runner-container gitlab-runner register

如果 Gitlab repository 是 https 的話,如之前所述需要設定憑證位置,指令要改成:
docker exec -it my-gitlab-runner-container gitlab-runner register --tls-ca-file=/path/to/tlsCaFile
請把 /path/to/tlsCaFile 改成憑證在 container 中的位置,

設定好以後就會多出 /etc/gitlab-runner/config.toml,
裡面的內容之後可以依自己需求需改
(保持可例如用 docker cp 取出後設定到 docker-compose volume 裡面做持久化之類),
而註冊好的 Gitlab-Executor 也會在 GibLab 網站上各 Project (如果是 Project runner 的話) 的
CICD --> Runner 設定裡面,
需要注意的是,在 GitLab 網站介面中,有時也會把 Gitlab-executor 稱呼成 Runner,
其實可以把 Gitlab-executor 當成是 Gitlab-Runner 在不同環境下的 Job script 執行就可以比較好理解了。

我的模擬目錄如下:

/docker-compose.yml

/gitlab-runner/Dockerfile
/gitlab-runner/config/config.toml
/gitlab-runner/ssh/id_rsa
/gitlab-runner/ssh/id_rsa.pub
/gitlab-runner/ssl/gitLabCA.cer

/online-server/Dockerfile
/online-server/ssh/authorized_keys
/online-server/project/

這邊我就直接貼上 docker-compose 的設定:

docker-compose.yml:

version: '2'
services:
    docker-runner:
        build: ./gitlab-runner
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
            - ./gitlab-runner/ssl:/data/ssl/
            - ./gitlab-runner/config:/etc/gitlab-runner
    online-server:
        build: ./online-server
        volumes:
            - ./online-server/ssh:/root/.ssh
            - ./online-server/project:/data/project
        ports:
            - "2222:22"


先看 volumes 中的設定,
/var/run/docker.sock:/var/run/docker.sock 設定了 docker socket 的用法,
來讓 Gitlab-Runner 可以像宿主機 (host) 那樣使用 docker 去啟動一個 Gitlab-executor。

./gitlab-runner/ssl:/data/ssl/ 則是把 Gitlab repository 的 SSL 憑證放到 Gitlab-Runner container 中。

而 ./gitlab-runner/config:/etc/gitlab-runner 的 設定則是讓 Gitlab-Runner 的設定檔 config.toml 持久化,不要 container 關閉了以後修改就不見了。

在 /gitlab-runner/ssh 中的 id_rsa 和 id_rsa.pub 是用 ssh-keygen 指令產生出來的 SSH 私鑰 (private key) 和公鑰 (public key),可以用來設定 Gitlab-Executor 在使用 SSH 連線時,不用密碼登入 (這邊其實公鑰用不到,公鑰主要是要放在被 SSH 連線的對方 server 上),
詳細可參考:
在這裡我們沒有對 /gitlab-runner/ssh 設定 volumes 的原因是,
因為 Gitlab-Executor 在執行時會以 "gitlab-runner" 這樣的 user 身份做登入 (不是 root),
所以 id_rsa 必須放在 /home/gitlab-runner/.ssh 資料夾之下,
並且 id_rsa 及它所在的資料夾都必須能為 "gitlab-runner" user 來存取,
所以之後我們會用 Dockerfile 的 ADD 指令把 id_rsa 加進 Gitlab-Runner container 中並設定擁有者和權限。
注意的是,把 id_rsa 放到 Gitlab-Runner container 中這件事只需對 shell 的 executor 做,
因為如果是 docker 的 executor,官方有說可以在 Gitlab 中設定變數 (Variables) 並在 Job 中完成 container 的 SSH private key 設定,可參考 SSH keys when using the Docker executor

現在讓我們來看 /gitlab-runner/Dockerfile 的內容:
/gitlab-runner/Dockerfile :
FROM gitlab/gitlab-runner:latest

RUN mkdir /home/gitlab-runner/.ssh
ADD ["ssh", "/home/gitlab-runner/.ssh"]
RUN chown -R gitlab-runner:gitlab-runner /home/gitlab-runner/.ssh
RUN chmod -R 700 /home/gitlab-runner/.ssh

可以看到在 /gitlab-runner-Dockerfile 中,設定了要啟動的 docker image 為 gitlab/gitlab-runner,
建立了 gitlab-runner 身份的 .ssh 資料夾,
用 ADD 指定放進了 id_rsa,並且用 chown 更改了檔案擁有者、
用了 chmod 改變了檔案權限,700 代表只允設檔案擁有者有 read (讀), write (寫), execute (執行) 的權限,須注意的是不能把 id_rsa 權限設定的太大,例如 777,太大的權限在 SSH 連線時也有可能會被禁止。

在我們看 /gitlab-runner/config/config.toml 之前,
先來看一下 onlin-server 的設定,online-server 是用來模擬一個有開放 SSH 連接並已有安裝好 jdk 的線上佈署環境。 

/online-server/Dockerfile :

FROM ubuntu:latest

RUN apt-get update
RUN apt-get install ssh -y
RUN apt-get install openjdk-11-jdk -y

# RUN echo "root:12345"|chpasswd # we don't need to set password for root because we want to use ssh-key-only-login

RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config # let root user can login
#RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin without-password/g' /etc/ssh/sshd_config # let root user can login but only use way without password, like ssh key

RUN sed -i 's/#AuthorizedKeysFile/AuthorizedKeysFile/g' /etc/ssh/sshd_config # let "authorized_keys" file can be used for ssh login
RUN mkdir ~/.ssh
RUN chmod -R 700 ~/.ssh

RUN service ssh start

EXPOSE 22

CMD ["/usr/sbin/sshd","-D"]
在 online-server 的 dockerfile 中,使用了 ubuntu 的 docker image,
並用 apt-get 安裝了 ssh 及  openjdk-11-jdk,
在這邊為了方便是先假設 Gitlab-Executor 會用 root 身份來登入 SSH,
不過在實際上為了安全,通常應會再設定一個非 root 的身份來讓其登入,
因為在這裡不用 root 用密碼登入 (會用 SSH key 登入),所以沒設定密碼,如果要設定 root 的密碼的話,可以用以下指令,請把 12345 換成要的密碼,如果是要其他身份請把 root 換掉:
RUN echo "root:12345"|chpasswd

接著要修改 /etc/ssh/sshd_config 檔的內容,
因為這裡需要讓 root 身份登入,所以需要把內容裡的
#PermitRootLogin prohibit-password 換成
PermitRootLogin yes 或
PermitRootLogin without-password (可以登入,但不能用密碼登入),
可以看到我們使用
sed -i
的指令來做修改。

然後
#AuthorizedKeysFile 也要把註解拿掉,換成
AuthorizedKeysFile

接著建立 .ssh 資料夾並設定權限,要注意的是
RUN mkdir ~/.ssh 
RUN chmod -R 700 ~/.ssh
只適用於讓 root 身份登入的情況,如果需要讓其他身份登入,請照類似
/gitlab-runner/Dockerfile 中的方式,為非 root 身份建立正確位置的 .ssh 資料夾、設定擁有者和權限。

最後用以下命令啟動 SSH service,往外打開 22 port,
並把 container 的生命週期綁在 "/usr/sbin/sshd" 上:
RUN service ssh start 
EXPOSE 22 
CMD ["/usr/sbin/sshd","-D"]

在來我們來看下 Gitlab-Executor 的設定,即 /gitlab-runner/config/config.toml,
/gitlab-runner/config/config.toml :
concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "xxx description"
  url = "https://xxx-git"
  token = "xxxxxxxxxxxxxxxxxxxx"
  tls-ca-file = "/data/ssl/gitLabCA.cer"
  executor = "docker"
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
  [runners.docker]
    tls_verify = false
    image = "ubuntu:latest"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    network_mode = "gitlab-cicd-test_default"
    volumes = ["/c/Users/xxx/.m2/repository:/.m2/repository"]
    shm_size = 0

[[runners]]
  name = "yyy description"
  url = "https://xxx-git/"
  token = "yyyyyyyyyyyyyyyyyyyy"
  tls-ca-file = "/data/ssl/gitLabCA.cer"
  executor = "shell"
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]

在此例 config.toml 中,
有兩個 Gitlab-Executor 被設定,分別是環境為 docker 的 docker executor 和環境為 shell 的 shell executor,環境是用 executor 這個參數來設定,例如 executor = "docker"。
可以看到兩個 executor 都設定了 Gitlab reposotiry SSL 憑證的位置,即 "tls-ca-file" 這個參數設定。

在 docker executor 中,[runners.docker] 裡可以設定一些關於會被啟動的 executor 的 container 的設定,其中例如:
image 參數可設定預設 docker image,如果 GitLab CICD Job 設定檔 .gitlab-ci.yml 的 Job 沒有指定 docker image 的話,就會拿預設 docker image 來用。

volumes 可以設定 Gitlab-executor 的 volumes,在此例中因為我的宿主機是 Windows (我使用了 Windows 系充可以安裝的 Docker Desktop),並且如剛才所說我們是使用了 Docker socket 而非 dind (Docker in Docker) 的方式,Gitlab-Executor 和 Gitlab-Runner 是平行的關係 (即不是在 Gitlab-Runner container 裡又開了一個 Gitlab-Executor container),
所以 volumes 設定的宿主機是 Windows,
在這裡我設定了 Maven repository 的位置做 volumes 給 Docker-executor 用以避免每次執行 CICD 時,mvn clean install 都要再上網抓一次 library ,因為每次 CICD Job 執行完後,啟動起來的 Docker Gitlab-Executor 都會被銷毀掉,裡面的資料沒做特別設定的話也都會消失掉。
要注意的是,Windows 系統的路徑,例如 D:\\xxx 要寫成 /d/xxx,且路徑上不可有空白符號。

network_mode 可以設定 Docker Gitlab-Executor container 使用的 network_mode,
效果等同於 docker run 指令的 --network_mode 參數及 docker-compose.yml 裡的 network_mode 參數。
因為我的 docker-compose.yml 建立起來的 gitlab-runner 和 online-server 這兩個 container 所處的網路名稱為 gitlab-cicd-test_default,
為了讓不是被 docker-compose.yml 建立起來的 Gitlab-Executor container 能夠與 online-server 這個 container 溝通 (之後要用 SSH 去連),所以我用了
network_mode = "gitlab-cicd-test_default"
把 executor container 加進 "gitlab-cicd-test_default" 網路中。

最後來看看 .gitlab-ci.yml 裡面寫了什麼,
.gitlab-ci.yml:
variables:
  MAVEN_OPTS: "-Dmaven.repo.local=/.m2/repository"

cache:
  paths:
    - .m2/repository/

stages:
  - build
  # - test
  - deploy

build-job-docker:
  stage: build
  image: maven:3.6.3-jdk-11
  script:
    - mvn clean install
  tags:
    - docker
  artifacts:
    paths:
      - target/*.war
    expire_in: 1 day
  when: manual

build-job-shell:
  stage: build
  script:
    - mvn clean install
  tags:
    - shell
  artifacts:
    paths:
      - target/*.war
    expire_in: 1 day 
  when: manual

deploy-job-docker-executor:
  stage: deploy
  before_script:
    ##
    ## Install ssh-agent if not already installed, it is required by Docker.
    ## (change apt-get to yum if you use an RPM-based image)
    ##
    - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'

    ##
    ## Run ssh-agent (inside the build environment)
    ##
    - eval $(ssh-agent -s)

    ##
    ## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
    ## We're using tr to fix line endings which makes ed25519 keys work
    ## without extra base64 encoding.
    ## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
    ##
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -

    ##
    ## Create the SSH directory and give it the right permissions
    ##
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh

    ##
    ## Optionally, if you will be using any Git commands, set the user name and
    ## and email.
    ##
    # - git config --global user.email "user@example.com"
    # - git config --global user.name "User name"
  script:
    - cd target
    - apt-get update && apt-get install ssh -y
    - scp -o StrictHostKeyChecking=no xxx.war root@online-server:/data/project
    - ssh -o StrictHostKeyChecking=no root@online-server "cd /data/project && jar -xvf /data/project/xxx.war"
  tags:
    - docker  
  when: manual

deploy-job-ssh-shell-executor:
  stage: deploy
  script:
    - cd target
    - scp xxx.war root@online-server:/data/project
    - ssh -o StrictHostKeyChecking=no root@online-server "cd /data/project && jar -xvf /data/project/xxx.war"
  tags:
    - shell  
  when: manual
說明:
  1. 在這裡,我已經為注冊好的兩個 Gitlab executor 分別設定了 "docker" 和 "shell" 的 tag。
  2. MAVEN_OPTS: "-Dmaven.repo.local=/.m2/repository" 是設定給 Maven 的環境變量,指定 Maven local repository 的存放位置,並利用 cache paths 的設定將期設成 cache,:
    cache:
      paths:
        - .m2/repository/
    
    這樣在一個 Gitlab CICD Pipeline中的 Job 可以共享 Maven 下載的 Library,不用每個 Job 都再下載一次,雖然效果跟 artifacts 很像,不過用途不太一樣,如果要在 Job 之間共享檔案並且希望能在 Gitlab 網頁界面上存取,例如被 CICD 建立 (build) or 佈署 (deploy) 的檔案,通常會使用 artifacts 而非 cache,詳情可參考 How cache is different from artifacts
    需注意的是,以上的 Maven repository 設定只對 Docker Gitlab Executor 有效,代表的是被啟動的 Docker Gitlab Executor Container 的 Maven local repository 位置,
    如果是 Shell Gitlab Executor 的話, Gitlab Runner 本身所處的環境應該會已經先設定好 Maven 的相關配置才對。
  3. 在這裡我只用到了 build, deploy 兩個 stage,所以 test stage 被注解掉了。
    artifacts 設定了要和其他 Job 共享的檔案位置,是對 project 根目錄的相對位置,在這裡 target/*.war 即是 Maven 的 clean install 命令產出的 war 檔位置,expire_in 可設置 artifact 檔留存在 Gitlab 上的時間,在留存期間我們都可以到 Gitlab 上下載。
      artifacts:
        paths:
          - target/*.war
        expire_in: 1 day
    
  4. deploy-job-docker-executor 是一個 Docker Gitlab Executor,before_script 裡設定的語句可以在 script 語句執行之前被執行,在這邊執行的語句是參考了官方的範例:SSH keys when using the Docker executor ,把我們在 Gitlab 中設定 SSH_PRIVATE_KEY 參數設定到了 Docker Gitlab Executor Container 中的 SSH Private Key 應存放位置 (SSH_PRIVATE_KEY 的值即為 id_rsa 裡的 private key 內容)。
  5. deploy-job-docker-executor 的 script 內容為,安裝 SSH 連線用軟體,
    進到 target 資料夾 (裡面有之前設定到 artifact 的 war 檔),
    使用 scp 指令將 war 檔傳到要被布署的 server 上,
    再使用 ssh 指令登入布署 server,用 jar -xvf 指令去解開 war 檔完成佈署。
  6. deploy-job-ssh-shell-executor 是一個 Shell Gitlab Executor,因為已經事先在其所在環境上 (即 Gitlab Runner 所安裝處的環境,此例為 Linux 環境) 設置好配置,所以只要直接執行 script 指令就好,script 指令基本跟 deploy-job-docker-executor 一樣,只差在不用再安裝 SSH 連線軟體。
    需要注意的是,如果 Gitllab Runner 是裝在 Windows 系統上的話,script 裡的語句就會是 Windows cmd 的語法,可能會與 Linux bash 語法稍有不同。
  7. scp 及 ssh 指令的 -o StrictHostKeyChecking=no 參數是告訴 scp, ssh 指令使用"非交互方式" 執行,因為例如在第一次使用 scp, ssh 連線時,會有提示訊息詢問,例如:
    The authenticity of host 'xxx (xxx)' can't be established.
    RSA key fingerprint is yyyyyyy.
    Are you sure you want to continue connecting (yes/no)?
    
    但因為我們沒有辦法在 script 模式下用 yes 或 no 的交互模式,所以這時就可以用
    -o StrictHostKeyChecking=no
    來取消交互方式。
  8. 因為我們的 Gitlab Executor 都是設定手動執行 (when: manual),所以需要到 Gitlab 上自行啟動 CICD Pipeline ,如果一切都順利的話,應就可在被佈署 server 上 (即 online-server) 看到被解包的專案了。
參考資料: