Vấn đề ban đầu
Cần tạo một workflow GitHub Action để tự động thêm hoặc xóa property share_link
trong frontmatter của các file markdown, với cấu trúc URL https://domain.com/<đường-dẫn-file>
(không dấu tiếng Việt, không đuôi .md) cho những file có property publish: true
.
Tổng quan về Workflow
Workflow này sẽ:
- Được kích hoạt khi có thay đổi trong các file Markdown hoặc khi được kích hoạt thủ công
- Quét tất cả các file Markdown trong repository, không giới hạn độ sâu thư mục
- Cập nhật hoặc thêm thuộc tính
share_link
trong frontmatter của mỗi file dựa trên đường dẫn và tên file - Tự động commit các thay đổi vào repository
Chi tiết Workflow
GitHub Action workflow hoàn chỉnh gồm:
1. File workflow: .github/workflows/update-share-links.yml
name: Update Share Links
on:
push:
branches: [ main, master ]
paths:
- '**/*.md' # Chỉ kích hoạt khi có thay đổi ở file markdown
workflow_dispatch: # Cho phép kích hoạt thủ công
jobs:
update-share-links:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Lấy toàn bộ lịch sử git
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm install gray-matter fs path glob
- name: Install dependencies glob @vnodesign/slugify
run: npm install gray-matter glob @vnodesign/slugify
- name: Update share links
run: node .github/scripts/update-share-links.js
env:
DOMAIN: ${{ secrets.DOMAIN }}
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: Update share_link properties"
file_pattern: '**/*.md'
commit_user_name: "github-actions[bot]"
commit_user_email: "github-actions[bot]@users.noreply.github.com"
disable_globbing: true
push_options: "--force"
commit_author: "hoangphuctran93 <[email protected]>"
- name: Check commit status
run: |
if git log -1 --pretty=%B | grep -q "Update share_link properties"; then
echo "✅ Share links updated successfully"
else
echo "ℹ️ No changes were needed"
fi
Phân tích chi tiết
1. Cấu hình Trigger
on:
push:
branches: [ main, master ]
paths:
- '**/*.md' # Chỉ kích hoạt khi có thay đổi ở file markdown
workflow_dispatch: # Cho phép kích hoạt thủ công
- push: Workflow sẽ được kích hoạt khi có commit đẩy lên nhánh
main
hoặcmaster
- paths: Giới hạn việc kích hoạt chỉ khi có thay đổi trong các file
.md
- workflow_dispatch: Cho phép kích hoạt workflow thủ công từ giao diện GitHub Cấu hình này giúp tối ưu hiệu suất bằng cách chỉ chạy workflow khi cần thiết, tránh chạy không cần thiết khi có thay đổi ở các file không phải Markdown.
2. Cấu hình Job và Permissions
jobs:
update-share-links:
runs-on: ubuntu-latest
permissions:
contents: write
- runs-on: Chỉ định job chạy trên môi trường Ubuntu mới nhất
- permissions: Cấp quyền ghi nội dung để workflow có thể commit thay đổi vào repository
3. Checkout Repository
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Lấy toàn bộ lịch sử git
- Sử dụng
actions/checkout@v4
để clone repository vào runner fetch-depth: 0
đảm bảo toàn bộ lịch sử git được tải về, cần thiết cho việc commit và push thay đổi
4. Cài đặt Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- Cài đặt Node.js phiên bản 18 lên runner
- Node.js là cần thiết để chạy script JavaScript xử lý các file Markdown
5. Cài đặt Dependencies
- name: Install dependencies
run: npm install gray-matter fs path glob
- name: Install dependencies glob @vnodesign/slugify
run: npm install gray-matter glob @vnodesign/slugify
Cài đặt các thư viện JavaScript cần thiết:
- gray-matter: Phân tích và thao tác với frontmatter trong file Markdown
- fs và path: Thư viện chuẩn của Node.js để thao tác với hệ thống file
- glob: Tìm kiếm file theo mẫu, hỗ trợ tìm tất cả file Markdown trong repository
- @vnodesign/slugify: Chuyển đổi text thành slug URL-friendly
6. Chạy Script Cập nhật Share Links
- name: Update share links
run: node .github/scripts/update-share-links.js
env:
DOMAIN: ${{ secrets.DOMAIN }}
- Chạy script JavaScript
.github/scripts/update-share-links.js
để xử lý các file Markdown - Truyền biến môi trường
DOMAIN
từ GitHub Secrets để script có thể tạo URL đầy đủ
7. Commit Thay đổi
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: Update share_link properties"
file_pattern: '**/*.md'
commit_user_name: "github-actions[bot]"
commit_user_email: "github-actions[bot]@users.noreply.github.com"
disable_globbing: true
push_options: "--force"
commit_author: "hoangphuctran93 <[email protected]>"
- Sử dụng action
stefanzweifel/git-auto-commit-action@v5
để tự động commit thay đổi - commit_message: Thiết lập thông điệp commit
- file_pattern:
'**/*.md'
bắt tất cả file Markdown ở mọi cấp độ thư mục - disable_globbing:
true
ngăn việc mở rộng glob trước khi gửi đến git, đảm bảo mẫu đường dẫn được xử lý chính xác - push_options:
--force
đảm bảo thay đổi được đẩy lên ngay cả khi có xung đột - commit_author: Thiết lập tác giả commit
8. Kiểm tra Trạng thái Commit
- name: Check commit status
run: |
if git log -1 --pretty=%B | grep -q "Update share_link properties"; then
echo "✅ Share links updated successfully"
else
echo "ℹ️ No changes were needed"
fi
- Kiểm tra xem commit đã được tạo thành công chưa bằng cách tìm kiếm thông điệp commit
- Hiển thị thông báo phù hợp để dễ dàng theo dõi kết quả
2. File script: .github/scripts/update-share-links.js
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const glob = require('glob');
// Lấy domain từ biến môi trường hoặc sử dụng giá trị mặc định
const DOMAIN = process.env.DOMAIN || 'https://kb.agentc.asia';
// Hàm chuyển đổi tiếng Việt có dấu thành không dấu
function customSlugify(str) {
if (!str) return '';
return str
.normalize('NFD') // Tách dấu tiếng Việt
.replace(/[\u0300-\u036f]/g, '') // Xóa dấu phụ
.replace(/[đĐ]/g, match => match === 'đ' ? 'd' : 'D') // Xử lý đ/Đ
.toLowerCase() // Chuyển thành chữ thường
.replace(/&/g, '-and-') // Thay thế ký tự '&'
.replace(/%/g, '-percent') // Thay thế ký tự '%'
replace(/\+/g, '-plus-') // Thay thế ký tự '+'
.replace(/\s+/g, '-') // Thay thế khoảng trắng bằng gạch ngang
.replace(/[^\w\-\/]/g, '') // Loại bỏ ký tự không phải chữ cái, số, gạch ngang hoặc dấu /
.replace(/\-{2,}/g, '-') // Thay thế nhiều dấu gạch ngang liên tiếp
.split('/')
.map(segment => segment.replace(/^-+|-+$/g, '')) // Loại bỏ gạch ngang ở đầu và cuối mỗi phân đoạn
.filter(Boolean) // Loại bỏ phân đoạn rỗng
.join('/') // Kết hợp các phân đoạn bằng dấu /
.replace(/\/$/, ''); // Loại bỏ dấu / cuối cùng
}
const processPath = (filePath) => {
// Xử lý đường dẫn file thành URL slug
const dirPath = path.dirname(filePath);
const fileName = path.basename(filePath, path.extname(filePath));
// Xử lý từng phần của đường dẫn
const slugParts = dirPath === '.'
? [customSlugify(fileName)]
: [...dirPath.split(/[\/\\]/).map(customSlugify), customSlugify(fileName)];
return slugParts.filter(Boolean).join('/');
};
// Đếm số lượng thay đổi để báo cáo
let updatedCount = 0;
let removedCount = 0;
let errorCount = 0;
console.log('🔍 Bắt đầu quét các file markdown...');
// Quét tất cả file .md từ thư mục gốc
glob.sync('**/*.md', {
ignore: ['**/node_modules/**', '**/.github/**', '**/dist/**', '**/build/**'],
nodir: true
}).forEach(file => {
const absolutePath = path.resolve(file);
try {
const fileContent = fs.readFileSync(absolutePath, 'utf8');
const { data, content } = matter(fileContent);
let isModified = false;
if (data.publish === true) {
// Xử lý khi publish: true
const cleanPath = processPath(file);
const newShareLink = `${DOMAIN}/${cleanPath}`;
// Chỉ cập nhật nếu share_link khác
if (data.share_link !== newShareLink) {
data.share_link = newShareLink;
isModified = true;
updatedCount++;
console.log(`✅ Đã cập nhật: ${file} → ${cleanPath}`);
}
} else {
// Xử lý khi publish không phải true
if (data.hasOwnProperty('share_link')) {
delete data.share_link; // Xóa property nếu tồn tại
isModified = true;
removedCount++;
console.log(`🗑️ Đã xóa share_link từ: ${file}`);
}
}
// Chỉ ghi lại file nếu có thay đổi
if (isModified) {
fs.writeFileSync(absolutePath, matter.stringify(content, data), 'utf8');
}
} catch (error) {
console.error(`❌ Lỗi xử lý ${file}:`, error.message);
errorCount++;
}
});
console.log(`\n📊 Tổng kết:
- Đã cập nhật: ${updatedCount} file
- Đã xóa share_link: ${removedCount} file
- Lỗi: ${errorCount} file`);
if (updatedCount === 0 && removedCount === 0) {
console.log('ℹ️ Không có thay đổi nào được thực hiện.');
}
Phân tích chi tiết
1. Thiết lập và Import Dependencies
const fs = require('fs'); // Thư viện xử lý file system
const path = require('path'); // Thư viện xử lý đường dẫn
const matter = require('gray-matter'); // Thư viện xử lý frontmatter
const glob = require('glob'); // Thư viện tìm kiếm file theo mẫu
Script sử dụng các thư viện chuẩn của Node.js và các thư viện bổ sung để:
- Đọc/ghi file (
fs
) - Xử lý đường dẫn (
path
) - Phân tích và thao tác với frontmatter trong file Markdown (
gray-matter
) - Tìm kiếm file theo mẫu glob (
glob
)
2. Cấu hình Domain
const DOMAIN = process.env.DOMAIN || 'https://kb.agentc.asia';
Script lấy domain từ biến môi trường (được thiết lập trong workflow) hoặc sử dụng giá trị mặc định nếu không có. Domain này sẽ được sử dụng để tạo URL đầy đủ cho thuộc tính share_link
.
3. Hàm Slugify Tùy Chỉnh
function customSlugify(str) {
if (!str) return '';
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[đĐ]/g, match => match === 'đ' ? 'd' : 'D')
.toLowerCase()
.replace(/&/g, '-and-')
.replace(/%/g, '-percent')
.replace(/\+/g, '-plus-')
.replace(/\s+/g, '-')
.replace(/[^\w\-\/]/g, '')
.replace(/\-{2,}/g, '-')
.split('/')
.map(segment => segment.replace(/^-+|-+$/g, ''))
.filter(Boolean)
.join('/')
.replace(/\/$/, '');
}
Đây là hàm đặc biệt quan trọng để chuyển đổi text thành slug URL-friendly:
.normalize('NFD')
và.replace(/[\u0300-\u036f]/g, '')
: Chuyển đổi các ký tự tiếng Việt có dấu thành không dấu.replace(/[đĐ]/g, match => match === 'đ' ? 'd' : 'D')
: Xử lý riêng cho ký tự ‘đ/Đ’.toLowerCase()
: Chuyển tất cả thành chữ thường- Các bước tiếp theo thay thế các ký tự đặc biệt:
&
thành-and-
%
thành-percent
+
thành-plus-
- Khoảng trắng thành dấu gạch ngang
- Loại bỏ các ký tự không phải chữ cái, số, gạch ngang hoặc dấu gạch chéo
- Xử lý các trường hợp có nhiều dấu gạch ngang liên tiếp
- Xử lý từng phần của đường dẫn (nếu có dấu
/
) - Loại bỏ dấu gạch ngang ở đầu và cuối mỗi phần
- Loại bỏ dấu
/
ở cuối đường dẫn
4. Hàm Xử Lý Đường Dẫn
const processPath = (filePath) => {
// Xử lý đường dẫn file thành URL slug
const dirPath = path.dirname(filePath);
const fileName = path.basename(filePath, path.extname(filePath));
// Xử lý từng phần của đường dẫn
const slugParts = dirPath === '.'
? [customSlugify(fileName)]
: [...dirPath.split(/[\/\\]/).map(customSlugify), customSlugify(fileName)];
return slugParts.filter(Boolean).join('/');
};
Hàm này chuyển đổi đường dẫn file thành URL slug:
- Tách đường dẫn thành phần thư mục (
dirPath
) và tên file (fileName
) - Nếu file nằm ở thư mục gốc (
dirPath === '.'
), chỉ sử dụng tên file - Nếu không, tách đường dẫn thư mục thành các phần và áp dụng hàm
customSlugify
cho mỗi phần - Kết hợp các phần đã xử lý thành đường dẫn URL
5. Quét và Xử Lý File Markdown
// Đếm số lượng thay đổi để báo cáo
let updatedCount = 0;
let removedCount = 0;
let errorCount = 0;
console.log('🔍 Bắt đầu quét các file markdown...');
// Quét tất cả file .md từ thư mục gốc
glob.sync('**/*.md', {
ignore: ['**/node_modules/**', '**/.github/**', '**/dist/**', '**/build/**'],
nodir: true
}).forEach(file => {
// Xử lý từng file
});
Script sử dụng glob.sync
để tìm tất cả file .md
trong repository, với một số loại trừ quan trọng:
**/node_modules/**
: Bỏ qua thư mục node_modules**/.github/**
: Bỏ qua thư mục .github**/dist/**
,**/build/**
: Bỏ qua các thư mục build
6. Xử Lý Từng File Markdown
glob.sync('**/*.md', { /* ... */ }).forEach(file => {
const absolutePath = path.resolve(file);
try {
const fileContent = fs.readFileSync(absolutePath, 'utf8');
const { data, content } = matter(fileContent);
let isModified = false;
if (data.publish === true) {
// Xử lý khi publish: true
const cleanPath = processPath(file);
const newShareLink = `${DOMAIN}/${cleanPath}`;
// Chỉ cập nhật nếu share_link khác
if (data.share_link !== newShareLink) {
data.share_link = newShareLink;
isModified = true;
updatedCount++;
console.log(`✅ Đã cập nhật: ${file} → ${cleanPath}`);
}
} else {
// Xử lý khi publish không phải true
if (data.hasOwnProperty('share_link')) {
delete data.share_link; // Xóa property nếu tồn tại
isModified = true;
removedCount++;
console.log(`🗑️ Đã xóa share_link từ: ${file}`);
}
}
// Chỉ ghi lại file nếu có thay đổi
if (isModified) {
fs.writeFileSync(absolutePath, matter.stringify(content, data), 'utf8');
}
} catch (error) {
console.error(`❌ Lỗi xử lý ${file}:`, error.message);
errorCount++;
}
});
Đây là phần quan trọng nhất của script, xử lý từng file Markdown:
- Đọc nội dung file và phân tích frontmatter với
gray-matter
- Kiểm tra thuộc tính
publish
trong frontmatter:- Nếu
publish === true
:- Tạo đường dẫn sạch từ đường dẫn file
- Tạo share_link mới bằng cách kết hợp domain và đường dẫn sạch
- Cập nhật share_link nếu khác với giá trị hiện tại
- Nếu
publish !== true
:- Xóa thuộc tính share_link nếu tồn tại
- Nếu
- Chỉ ghi lại file nếu có thay đổi
- Xử lý lỗi và đếm số lượng file được cập nhật, xóa hoặc gặp lỗi
7. Báo Cáo Kết Quả
console.log(`\n📊 Tổng kết:
- Đã cập nhật: ${updatedCount} file
- Đã xóa share_link: ${removedCount} file
- Lỗi: ${errorCount} file`);
if (updatedCount === 0 && removedCount === 0) {
console.log('ℹ️ Không có thay đổi nào được thực hiện.');
}
Cuối cùng, script hiển thị báo cáo tổng kết về số lượng file đã được cập nhật, xóa hoặc gặp lỗi.
Các tính năng chính
- Xử lý tiếng Việt: Chuyển đổi tiếng Việt có dấu thành không dấu (ví dụ: “Hướng dẫn” → “huong-dan”)
- Xử lý đường dẫn: Thay thế khoảng trắng bằng dấu gạch ngang, loại bỏ ký tự đặc biệt
- Loại bỏ đuôi .md: Tạo URL không chứa đuôi file
- Quét file hiệu quả: Xử lý cả file ở thư mục gốc và thư mục con
- Xóa property: Tự động xóa
share_link
nếupublish
không phảitrue
Ví dụ kết quả
-
File:
Hướng dẫn/Cài đặt hệ thống.md
vớipublish: true
- Kết quả:
share_link: "https://kb.agentc.asia/huong-dan/cai-dat-he-thong"
- Kết quả:
-
File:
Đặc biệt & Khó.md
vớipublish: true
- Kết quả:
share_link: "https://kb.agentc.asia/dac-biet-and-kho"
- Kết quả:
-
File:
Bài viết.md
vớipublish: false
- Kết quả: Property
share_link
bị xóa
- Kết quả: Property
Cách sử dụng
- Thêm các file trên vào repository
- Đảm bảo các file markdown có property
publish: true
nếu muốn tạo share_link - Push lên GitHub hoặc chạy workflow thủ công
- Workflow sẽ tự động cập nhật các file và commit thay đổi
Tùy chỉnh
- Thay đổi domain bằng cách sửa biến
DOMAIN
trong script - Điều chỉnh quy tắc chuyển đổi URL bằng cách sửa hàm
customSlugify
- Thay đổi pattern file bằng cách sửa cấu hình `glob.sync
Ứng dụng cho các dự án tương tự
Workflow này có thể dễ dàng điều chỉnh cho nhiều mục đích khác nhau:
- Cập nhật các thuộc tính khác trong frontmatter:
- Thay đổi script để cập nhật các thuộc tính như
last_modified
,author
,tags
, v.v.
data.last_modified = new Date().toISOString(); data.word_count = content.split(/\s+/).length;
- Thay đổi script để cập nhật các thuộc tính như
- Tự động tạo Table of Contents:
- Điều chỉnh script để phân tích nội dung Markdown và tạo mục lục tự động
const toc = generateTableOfContents(content); data.toc = toc;
- Kiểm tra và sửa lỗi liên kết:
- Mở rộng script để kiểm tra các liên kết bị hỏng và cập nhật chúng
const updatedContent = fixBrokenLinks(content);
- Tối ưu hóa hình ảnh:
- Thêm các bước để tìm, tối ưu và thay thế đường dẫn hình ảnh trong các file Markdown
- Đồng bộ hóa metadata:
- Đảm bảo các thuộc tính như tags, categories được nhất quán trong toàn bộ repository
const keywords = extractKeywords(content); data.tags = keywords;
Tùy chỉnh theo nhu cầu cụ thể
- Thay đổi điều kiện cập nhật: Thay vì dựa vào
publish: true
, bạn có thể sử dụng điều kiện khácif (data.status === 'published' || data.visibility === 'public') { // Cập nhật share_link }
- Tùy chỉnh định dạng URL: Thay đổi cấu trúc URL theo nhu cầu
const newShareLink = `${DOMAIN}/articles/${data.category}/${cleanPath}`;
- **Thêm các tham số URL**: Thêm tham số theo dõi hoặc UTM
```javascript
const newShareLink = `${DOMAIN}/${cleanPath}?utm_source=docs`;
Lưu ý khi triển khai
- Bảo mật:
- Luôn sử dụng GitHub Secrets cho domain và các thông tin nhạy cảm
- Kiểm tra kỹ các quyền của workflow
- Hiệu suất:
- Với repository lớn, cân nhắc chỉ xử lý các file đã thay đổi
- Có thể thêm caching để tránh cài đặt lại dependencies mỗi lần chạy
- Xử lý lỗi:
- Script đã có xử lý lỗi cơ bản, nhưng có thể cần mở rộng cho các trường hợp đặc biệt
- Thêm logging chi tiết hơn để dễ dàng debug
- Tương thích:
- Đảm bảo hàm slugify xử lý đúng các ký tự đặc biệt trong ngôn ngữ của bạn
- Kiểm tra tương thích với các công cụ tạo site tĩnh (nếu có)
- Tài liệu: Đảm bảo script và workflow được tài liệu hóa đầy đủ để dễ dàng bảo trì Với workflow này, bạn có thể tự động hóa việc duy trì các thuộc tính metadata trong file Markdown, giúp tiết kiệm thời gian và đảm bảo tính nhất quán trong toàn bộ dự án.
Kết luận
Workflow “Update Share Links” là một ví dụ tuyệt vời về cách tự động hóa quy trình bảo trì tài liệu với GitHub Actions. Bằng cách tự động cập nhật thuộc tính share_link
cho tất cả file Markdown, bạn đảm bảo tính nhất quán và tiết kiệm thời gian quản lý thủ công.
Với những điều chỉnh phù hợp, bạn có thể áp dụng mẫu workflow này cho nhiều tác vụ tự động hóa khác trong quy trình phát triển và bảo trì tài liệu của mình.