Skip to content

feat: 统一博客图片托管到阿里云 OSS + CDN#26

Merged
xkcoding merged 2 commits intomasterfrom
feature/unify-image-hosting
Jan 19, 2026
Merged

feat: 统一博客图片托管到阿里云 OSS + CDN#26
xkcoding merged 2 commits intomasterfrom
feature/unify-image-hosting

Conversation

@xkcoding
Copy link
Owner

Summary

  • 迁移 139 张图片从七牛云/旧阿里云域名到新 OSS
  • 新域名:cdn.xkcoding.com(阿里云 CDN 加速)
  • 添加图片迁移脚本:scanner/downloader/uploader/replacer
  • 添加 SSL 工作流:cdn-ssl-renew.yml 自动续期 CDN 证书

Test plan

  • 本地构建测试通过
  • OSS 图片上传验证
  • CDN HTTPS 证书部署(合并后触发工作流)

🤖 Generated with Claude Code

xkcoding and others added 2 commits January 17, 2026 00:09
## 主要内容

1. 图片托管统一化
   - 将 129 张图片从七牛云/相对路径迁移至阿里云 OSS
   - 统一使用 static.xkcoding.com 域名
   - 配置阿里云 CDN 加速

2. SSL 证书管理优化
   - 重构 ssl-manage.yml: HTTP-01 → DNS-01 验证
   - 新增 cdn-ssl-renew.yml: CDN 证书自动续期
   - 实现零停机证书续期

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 迁移图片:139 张图片从七牛云/旧阿里云域名迁移到新 OSS
- 新域名:cdn.xkcoding.com(阿里云 CDN 加速)
- 迁移脚本:scanner/downloader/uploader/replacer
- SSL 工作流:cdn-ssl-renew.yml 自动续期 CDN 证书(DNS-01 验证)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 19, 2026 06:39
@xkcoding xkcoding merged commit 3d9eafd into master Jan 19, 2026
2 of 3 checks passed
@xkcoding xkcoding deleted the feature/unify-image-hosting branch January 19, 2026 06:39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates 139 blog images from Qiniu Cloud and legacy Aliyun domains to a unified Aliyun OSS bucket with CDN acceleration. The migration includes automation scripts for scanning, downloading, uploading, and replacing image URLs, plus a new GitHub Actions workflow for automatic CDN SSL certificate renewal.

Changes:

  • Migrated 139 images to new CDN domain cdn.xkcoding.com (per title/description) with automated scripts
  • Added SSL certificate automation workflow for CDN using acme.sh DNS-01 validation
  • Updated all blog markdown files to use the new image URLs

Reviewed changes

Copilot reviewed 59 out of 60 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
.github/workflows/cdn-ssl-renew.yml New workflow for automated CDN SSL certificate management using acme.sh
scripts/migrate-images/*.ts Complete migration toolchain: scanner, downloader, uploader, and URL replacer scripts
src/data/blog/**/*.md Batch URL replacements from old domains to new CDN domain
openspec/changes/unify-image-hosting/*.md Design documentation, proposal, and task tracking
.gitignore Added exclusions for migration temp files and reports

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +76 to +77
--access-key-id ${{ secrets.ALI_ACCESS_KEY_ID }} \
--access-key-secret ${{ secrets.ALI_ACCESS_KEY_SECRET }}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secret name uses ALI_ACCESS_KEY_ID and ALI_ACCESS_KEY_SECRET (lines 32-33, 76-77), but according to the proposal.md documentation, the expected secret names are ALI_KEY and ALI_SECRET. This inconsistency could cause the workflow to fail if secrets are configured using the documented names. Either update the workflow to use ALI_KEY and ALI_SECRET, or update all documentation to specify ALI_ACCESS_KEY_ID and ALI_ACCESS_KEY_SECRET.

Suggested change
--access-key-id ${{ secrets.ALI_ACCESS_KEY_ID }} \
--access-key-secret ${{ secrets.ALI_ACCESS_KEY_SECRET }}
--access-key-id ${{ secrets.ALI_KEY }} \
--access-key-secret ${{ secrets.ALI_SECRET }}

Copilot uses AI. Check for mistakes.

for (const r of fileReplacements) {
if (content.includes(r.oldUrl)) {
content = content.split(r.oldUrl).join(r.newUrl);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL replacement at line 119 uses split().join() for string replacement instead of replaceAll() or a global regex. While this works, it's less clear in intent. Since Node.js 15+ supports String.prototype.replaceAll(), consider using content = content.replaceAll(r.oldUrl, r.newUrl) for better readability and potentially better performance.

Suggested change
content = content.split(r.oldUrl).join(r.newUrl);
content = content.replaceAll(r.oldUrl, r.newUrl);

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +62
- name: Configure Aliyun DNS API
run: |
export Ali_Key="${{ secrets.ALI_ACCESS_KEY_ID }}"
export Ali_Secret="${{ secrets.ALI_ACCESS_KEY_SECRET }}"

# 保存到 acme.sh 账户配置
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt

# 写入环境变量供后续步骤使用
echo "Ali_Key=${{ secrets.ALI_ACCESS_KEY_ID }}" >> $GITHUB_ENV
echo "Ali_Secret=${{ secrets.ALI_ACCESS_KEY_SECRET }}" >> $GITHUB_ENV

- name: Issue/Renew Certificate
run: |
FORCE_RENEW="${{ github.event.inputs.force_renew }}"

# 检查是否需要强制续期
FORCE_FLAG=""
if [ "$FORCE_RENEW" = "true" ]; then
echo "🔄 强制重新申请证书..."
FORCE_FLAG="--force"
fi

# 使用 DNS-01 验证申请/续期证书
echo "🔐 申请证书: ${{ env.DOMAIN }}"
~/.acme.sh/acme.sh --issue \
--dns dns_ali \
-d ${{ env.DOMAIN }} \
$FORCE_FLAG \
--debug || {
# 如果证书已存在且未到期,--issue 会失败,这是正常的
echo "ℹ️ 证书可能已存在且未到期"
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported environment variables Ali_Key and Ali_Secret are set using echo in step "Configure Aliyun DNS API" (lines 39-40), but they will not be available in the subsequent "Issue/Renew Certificate" step because environment variables set with echo >> $GITHUB_ENV are only available in subsequent steps, not in the same step. The acme.sh command in the next step needs these variables. Consider using the env context directly in the run command for the "Issue/Renew Certificate" step.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +62
~/.acme.sh/acme.sh --issue \
--dns dns_ali \
-d ${{ env.DOMAIN }} \
$FORCE_FLAG \
--debug || {
# 如果证书已存在且未到期,--issue 会失败,这是正常的
echo "ℹ️ 证书可能已存在且未到期"
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling logic at lines 59-62 catches failures from the acme.sh --issue command and outputs an informational message. However, this will cause the workflow to appear successful even when certificate issuance actually fails (not just when it's already valid). Consider checking the actual exit code or error message to distinguish between "certificate already valid" and genuine failures.

Suggested change
~/.acme.sh/acme.sh --issue \
--dns dns_ali \
-d ${{ env.DOMAIN }} \
$FORCE_FLAG \
--debug || {
# 如果证书已存在且未到期,--issue 会失败,这是正常的
echo "ℹ️ 证书可能已存在且未到期"
}
if ! ~/.acme.sh/acme.sh --issue \
--dns dns_ali \
-d ${{ env.DOMAIN }} \
$FORCE_FLAG \
--debug; then
ACME_EXIT_CODE=$?
# acme.sh 约定:退出码 2 通常表示证书已存在且未到期,无需续期
if [ "$ACME_EXIT_CODE" -eq 2 ]; then
echo "ℹ️ 证书已存在且未到期,无需续期"
else
echo "❌ 证书申请/续期失败,退出码: $ACME_EXIT_CODE"
exit "$ACME_EXIT_CODE"
fi
fi

Copilot uses AI. Check for mistakes.
// 阿里云旧域名: http(s)://static.aliyun.xkcoding.com/xxx
aliyunOld: /https?:\/\/static\.aliyun\.xkcoding\.com\/[^\s\)\"\']+/g,
// 相对路径: /resources/xxx
relative: /(?<![a-zA-Z0-9])\/resources\/[^\s\)\"\']+/g,
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for relative paths at line 71 uses a negative lookbehind (?<![a-zA-Z0-9]) to avoid matching paths that are part of a longer word. However, this pattern could fail to match valid relative paths that appear after certain characters like parentheses or quotes without spaces, which are common in markdown. For example, ](/resources/image.png) might not match because the closing bracket is not in the negative lookbehind character class. Consider testing this regex thoroughly or simplifying it to /\/resources\/[^\s\)\"\']+/g.

Suggested change
relative: /(?<![a-zA-Z0-9])\/resources\/[^\s\)\"\']+/g,
relative: /\/resources\/[^\s\)\"\']+/g,

Copilot uses AI. Check for mistakes.
console.log(" --execute, -e 执行替换");
console.log("\n示例:");
console.log(" pnpm tsx scripts/migrate-images/replacer.ts --dry-run");
console.log(" pnpm tsx scripts/migrate-images/replacer.ts --execute");
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replacer script requires explicit command-line flags (--dry-run or --execute) but if neither is provided, it only prints usage information without exiting with an error code. This could cause issues in automated scripts that expect a non-zero exit code on failure. Consider adding process.exit(1) after the usage message at line 249 to indicate that the command was not executed.

Suggested change
console.log(" pnpm tsx scripts/migrate-images/replacer.ts --execute");
console.log(" pnpm tsx scripts/migrate-images/replacer.ts --execute");
process.exit(1);

Copilot uses AI. Check for mistakes.
default: false

env:
DOMAIN: "cdn.xkcoding.com"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new CDN domain is cdn.xkcoding.com according to the PR title and description, but in the GitHub Actions workflow, the DOMAIN environment variable is set to cdn.xkcoding.com at line 17, while the design documentation and actual markdown replacements use static.xkcoding.com. This inconsistency needs to be resolved. Either update the workflow to use static.xkcoding.com or update all markdown files and documentation to use cdn.xkcoding.com.

Suggested change
DOMAIN: "cdn.xkcoding.com"
DOMAIN: "static.xkcoding.com"

Copilot uses AI. Check for mistakes.
sleep 30

echo "🔍 验证 HTTPS 访问..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ env.DOMAIN }}/2017-07-12-14998277900006.jpg" || echo "000")
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verification step at line 126 tests HTTPS with a specific hardcoded image URL "2017-07-12-14998277900006.jpg". This creates a dependency on that specific image existing in the OSS bucket. If that image is missing or the path changes, the verification will fail even if the SSL certificate is correctly deployed. Consider using a more generic verification approach, such as checking the SSL certificate itself or testing the domain root URL.

Suggested change
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ env.DOMAIN }}/2017-07-12-14998277900006.jpg" || echo "000")
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ env.DOMAIN }}/" || echo "000")

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +70
* 上传单个文件到 OSS
*/
function uploadFile(localPath: string, ossPath: string, bucket: string): boolean {
const ossUrl = `oss://${bucket}/${ossPath}`;
try {
execSync(`"${ossutilCmd}" cp "${localPath}" "${ossUrl}" --force`, {
stdio: "pipe",
});
return true;
} catch (error) {
console.error(` ❌ 上传失败: ${ossPath}`);
console.error(` ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}

/**
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused function uploadFile.

Suggested change
* 上传单个文件到 OSS
*/
function uploadFile(localPath: string, ossPath: string, bucket: string): boolean {
const ossUrl = `oss://${bucket}/${ossPath}`;
try {
execSync(`"${ossutilCmd}" cp "${localPath}" "${ossUrl}" --force`, {
stdio: "pipe",
});
return true;
} catch (error) {
console.error(` ❌ 上传失败: ${ossPath}`);
console.error(` ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
/**

Copilot uses AI. Check for mistakes.
img.localPath,
path.resolve(process.cwd(), defaultConfig.paths.tempDir)
);
const ossUrl = `${ossBaseUrl}/${ossPath}`;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable ossUrl.

Suggested change
const ossUrl = `${ossBaseUrl}/${ossPath}`;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant