Code chi tiết

Để tạo một workflow GitHub giúp đồng bộ ghi chú từ vault repository sang quartz repository, bạn có thể sử dụng GitHub Actions. Dưới đây là hướng dẫn chi tiết về cách thiết lập workflow này.

  • Bước 1: Tạo Personal Access Token (PAT) Trước tiên, bạn cần tạo một Personal Access Token để workflow có quyền truy cập vào repository đích:
    1. Truy cập vào Settings của tài khoản GitHub của bạn
    2. Chọn Developer settings > Personal access tokens
    3. Tạo một token mới với quyền truy cập repo đầy đủ
    4. Sao chép token này để sử dụng trong bước tiếp theo
  • Bước 2: Thiết lập Secret trong repository nguồn
    1. Truy cập vào repository vault (repository nguồn)
    2. Vào phần Settings > Secrets and variables > Actions
    3. Tạo một secret mới với tên GH_PAT và giá trị là token bạn đã tạo ở bước 1
  • Bước 3: Tạo workflow file Tạo một file mới trong repository vault của bạn tại đường dẫn .github/workflows/sync-published-notes.yml với nội dung sau:
name: Sync Published Notes to Quartz
 
on:
  push:
    branches:
      - main
  workflow_dispatch:
 
jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Repository
        uses: actions/checkout@v4
        with:
          repository: hoangphuctran93/public-notes
          path: ./source-repo
          
      - name: Checkout Target Repository
        uses: actions/checkout@v4
        with:
          repository: hoangphuctran93/kb.agentc.asia
          token: ${{ secrets.GH_PAT }}
          path: ./target-repo
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
 
      - name: Install Dependencies
        run: |
          npm install gray-matter fs-extra
 
      - name: Sync Published Notes
        run: |
          # Tạo script để đồng bộ các file có publish: true và xóa các file không còn publish
          cat > sync_notes.js << 'EOF'
          const fs = require('fs');
          const fsExtra = require('fs-extra');
          const path = require('path');
          const matter = require('gray-matter');
 
          const sourceDir = process.argv[2];
          const destDir = process.argv[3];
 
          // Đảm bảo thư mục đích tồn tại
          if (!fs.existsSync(destDir)) {
            fs.mkdirSync(destDir, { recursive: true });
          }
 
          // Lưu trữ danh sách các file đã được đồng bộ
          const syncedFiles = new Set();
 
          // Hàm để kiểm tra và sao chép file
          function processFile(filePath, relativePath) {
            try {
              const content = fs.readFileSync(filePath, 'utf8');
              const { data } = matter(content);
              
              // Chỉ sao chép file có publish: true
              if (data.publish === true) {
                const destPath = path.join(destDir, relativePath);
                
                // Tạo thư mục đích nếu cần
                const destDirPath = path.dirname(destPath);
                if (!fs.existsSync(destDirPath)) {
                  fs.mkdirSync(destDirPath, { recursive: true });
                }
                
                // Sao chép file
                fs.copyFileSync(filePath, destPath);
                console.log(`Copied: ${relativePath}`);
                
                // Thêm vào danh sách đã đồng bộ
                syncedFiles.add(relativePath);
                return true;
              }
              return false;
            } catch (error) {
              console.error(`Error processing ${filePath}: ${error.message}`);
              return false;
            }
          }
 
          // Hàm đệ quy để duyệt qua tất cả các thư mục
          function processDirectory(dirPath, baseDir) {
            const files = fs.readdirSync(dirPath);
            let copiedCount = 0;
            
            for (const file of files) {
              const filePath = path.join(dirPath, file);
              const relativePath = path.relative(baseDir, filePath);
              const stat = fs.statSync(filePath);
              
              if (stat.isDirectory()) {
                copiedCount += processDirectory(filePath, baseDir);
              } else if (file.endsWith('.md')) {
                if (processFile(filePath, relativePath)) {
                  copiedCount++;
                }
              }
            }
            
            return copiedCount;
          }
 
          // Hàm để xóa các file trong thư mục đích không còn trong danh sách đồng bộ
          function cleanupDestination(destDir) {
            function scanDirectory(dirPath, baseDir) {
              if (!fs.existsSync(dirPath)) return;
              
              const files = fs.readdirSync(dirPath);
              
              for (const file of files) {
                const filePath = path.join(dirPath, file);
                const relativePath = path.relative(baseDir, filePath);
                const stat = fs.statSync(filePath);
                
                if (stat.isDirectory()) {
                  scanDirectory(filePath, baseDir);
                  
                  // Kiểm tra và xóa thư mục rỗng
                  const dirFiles = fs.readdirSync(filePath);
                  if (dirFiles.length === 0) {
                    fs.rmdirSync(filePath);
                    console.log(`Removed empty directory: ${relativePath}`);
                  }
                } else if (file.endsWith('.md') && !syncedFiles.has(relativePath)) {
                  // Xóa file không còn trong danh sách đồng bộ
                  fs.unlinkSync(filePath);
                  console.log(`Removed: ${relativePath}`);
                }
              }
            }
            
            scanDirectory(destDir, destDir);
          }
 
          // Bắt đầu xử lý từ thư mục nguồn
          const totalCopied = processDirectory(sourceDir, sourceDir);
          console.log(`Total published notes copied: ${totalCopied}`);
 
          // Xóa các file không còn được publish
          cleanupDestination(destDir);
          EOF
 
          # Đường dẫn thư mục ghi chú trong source repo
          SOURCE_DIR="./source-repo"
          # Đường dẫn thư mục đích trong target repo
          DEST_DIR="./target-repo/content"
          
          # Chạy script để đồng bộ và xóa các file
          node sync_notes.js "$SOURCE_DIR" "$DEST_DIR"
 
      - name: Commit and Push to Target Repository
        run: |
          cd ./target-repo
          git config user.name "GitHub Action"
          git config user.email "[email protected]"
          
          # Lấy thời gian hiện tại theo múi giờ +7
          TIMESTAMP=$(TZ=Asia/Bangkok date +'%Y-%m-%d %H:%M:%S')
          
          git add .
          
          # Kiểm tra xem có thay đổi không
          if git diff --staged --quiet; then
            echo "No changes to commit"
          else
            git commit -m "🔄 Sync published notes from public-notes: $TIMESTAMP (GMT+7)"
            git push
          fi

Giải thích chi tiết Workflow

1. Cấu hình cơ bản

name: Sync Published Notes to Quartz
 
on:
  push:
    branches:
      - main
  workflow_dispatch:
  • name: Tên hiển thị của workflow trong giao diện GitHub Actions
  • on: Xác định khi nào workflow được kích hoạt:
    • push: branches: - main: Chạy khi có commit được đẩy lên nhánh main
    • workflow_dispatch: Cho phép kích hoạt thủ công từ giao diện GitHub

2. Định nghĩa Job

jobs:
  sync:
    runs-on: ubuntu-latest
  • jobs: Định nghĩa các công việc cần thực hiện
  • sync: Tên của job
  • runs-on: ubuntu-latest: Chỉ định job chạy trên hệ điều hành Ubuntu phiên bản mới nhất

3. Checkout Repositories

steps:
  - name: Checkout Source Repository
    uses: actions/checkout@v4
    with:
      repository: hoangphuctran93/public-notes
      path: ./source-repo
      
  - name: Checkout Target Repository
    uses: actions/checkout@v4
    with:
      repository: hoangphuctran93/kb.agentc.asia
      token: ${{ secrets.GH_PAT }}
      path: ./target-repo
  • steps: Danh sách các bước thực hiện trong job
  • Checkout Source Repository: Tải mã nguồn từ repository hoangphuctran93/public-notes vào thư mục ./source-repo
  • Checkout Target Repository: Tải mã nguồn từ repository hoangphuctran93/kb.agentc.asia vào thư mục ./target-repo
    • token: ${{ secrets.GH_PAT }}: Sử dụng Personal Access Token đã lưu trong secrets của repository để xác thực

4. Cài đặt Node.js và Dependencies

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '18'
 
- name: Install Dependencies
  run: |
    npm install gray-matter fs-extra
  • Setup Node.js: Cài đặt Node.js phiên bản 18
  • Install Dependencies: Cài đặt các thư viện cần thiết:
    • gray-matter: Để phân tích front matter trong file markdown
    • fs-extra: Cung cấp các hàm bổ sung cho thao tác với hệ thống file

5. Script đồng bộ ghi chú

- name: Sync Published Notes
  run: |
    # Tạo script để đồng bộ các file có publish: true và xóa các file không còn publish
    cat > sync_notes.js << 'EOF'
    // [Nội dung script JavaScript]
    EOF
 
    # Đường dẫn thư mục ghi chú trong source repo
    SOURCE_DIR="./source-repo"
    # Đường dẫn thư mục đích trong target repo
    DEST_DIR="./target-repo/content"
    
    # Chạy script để đồng bộ và xóa các file
    node sync_notes.js "$SOURCE_DIR" "$DEST_DIR"
  • Tạo script: Sử dụng cat và heredoc (<<EOF) để tạo file JavaScript
  • Định nghĩa đường dẫn: Xác định thư mục nguồn và đích
  • Chạy script: Thực thi script với Node.js, truyền đường dẫn nguồn và đích làm tham số

6. Chi tiết Script JavaScript

Script JavaScript thực hiện các chức năng chính sau:

  1. Quét thư mục nguồn:

    • Duyệt đệ quy qua tất cả các thư mục và file
    • Phân tích front matter của mỗi file markdown
    • Nếu publish: true, sao chép file sang thư mục đích và lưu đường dẫn vào danh sách syncedFiles
  2. Xóa file không còn được publish:

    • Duyệt qua tất cả các file trong thư mục đích
    • Nếu file không có trong danh sách syncedFiles, xóa file đó
    • Xóa các thư mục rỗng sau khi xóa file

7. Commit và Push thay đổi

- name: Commit and Push to Target Repository
  run: |
    cd ./target-repo
    git config user.name "GitHub Action"
    git config user.email "[email protected]"
    
    # Lấy thời gian hiện tại theo múi giờ +7
    TIMESTAMP=$(TZ=Asia/Bangkok date +'%Y-%m-%d %H:%M:%S')
    
    git add .
    
    # Kiểm tra xem có thay đổi không
    if git diff --staged --quiet; then
      echo "No changes to commit"
    else
      git commit -m "🔄 Sync published notes from public-notes: $TIMESTAMP (GMT+7)"
      git push
    fi
  • cd ./target-repo: Di chuyển vào thư mục repository đích
  • git config: Cấu hình thông tin người dùng Git cho commit
  • Lấy thời gian: Tạo timestamp theo múi giờ Việt Nam (GMT+7)
  • git add .: Thêm tất cả thay đổi vào staging area
  • Kiểm tra thay đổi: Chỉ commit nếu có thay đổi
  • git commit: Tạo commit với thông điệp bao gồm emoji và timestamp
  • git push: Đẩy thay đổi lên repository đích

Cách hoạt động tổng thể

  1. Workflow được kích hoạt khi có push lên nhánh main hoặc khi được kích hoạt thủ công
  2. Tải mã nguồn từ cả hai repository (nguồn và đích)
  3. Cài đặt Node.js và các thư viện cần thiết
  4. Chạy script để:
    • Sao chép các file có publish: true từ repository nguồn sang repository đích
    • Xóa các file trong repository đích không còn tương ứng với file có publish: true trong repository nguồn
  5. Commit và push các thay đổi lên repository đích với thông điệp bao gồm timestamp theo múi giờ GMT+7

Workflow này đảm bảo rằng chỉ những ghi chú được đánh dấu publish: true mới xuất hiện trên trang web Quartz, và khi một ghi chú bị xóa hoặc không còn được đánh dấu publish, nó cũng sẽ tự động bị xóa khỏi trang web.