Conversation
## 主要内容 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>
There was a problem hiding this comment.
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.
| --access-key-id ${{ secrets.ALI_ACCESS_KEY_ID }} \ | ||
| --access-key-secret ${{ secrets.ALI_ACCESS_KEY_SECRET }} |
There was a problem hiding this comment.
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.
| --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 }} |
|
|
||
| for (const r of fileReplacements) { | ||
| if (content.includes(r.oldUrl)) { | ||
| content = content.split(r.oldUrl).join(r.newUrl); |
There was a problem hiding this comment.
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.
| content = content.split(r.oldUrl).join(r.newUrl); | |
| content = content.replaceAll(r.oldUrl, r.newUrl); |
| - 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 "ℹ️ 证书可能已存在且未到期" | ||
| } |
There was a problem hiding this comment.
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.
| ~/.acme.sh/acme.sh --issue \ | ||
| --dns dns_ali \ | ||
| -d ${{ env.DOMAIN }} \ | ||
| $FORCE_FLAG \ | ||
| --debug || { | ||
| # 如果证书已存在且未到期,--issue 会失败,这是正常的 | ||
| echo "ℹ️ 证书可能已存在且未到期" | ||
| } |
There was a problem hiding this comment.
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.
| ~/.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 |
| // 阿里云旧域名: 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, |
There was a problem hiding this comment.
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.
| relative: /(?<![a-zA-Z0-9])\/resources\/[^\s\)\"\']+/g, | |
| relative: /\/resources\/[^\s\)\"\']+/g, |
| 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"); |
There was a problem hiding this comment.
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.
| console.log(" pnpm tsx scripts/migrate-images/replacer.ts --execute"); | |
| console.log(" pnpm tsx scripts/migrate-images/replacer.ts --execute"); | |
| process.exit(1); |
| default: false | ||
|
|
||
| env: | ||
| DOMAIN: "cdn.xkcoding.com" |
There was a problem hiding this comment.
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.
| DOMAIN: "cdn.xkcoding.com" | |
| DOMAIN: "static.xkcoding.com" |
| sleep 30 | ||
|
|
||
| echo "🔍 验证 HTTPS 访问..." | ||
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ env.DOMAIN }}/2017-07-12-14998277900006.jpg" || echo "000") |
There was a problem hiding this comment.
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.
| 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") |
| * 上传单个文件到 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; | ||
| } | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
Unused function uploadFile.
| * 上传单个文件到 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; | |
| } | |
| } | |
| /** |
| img.localPath, | ||
| path.resolve(process.cwd(), defaultConfig.paths.tempDir) | ||
| ); | ||
| const ossUrl = `${ossBaseUrl}/${ossPath}`; |
There was a problem hiding this comment.
Unused variable ossUrl.
| const ossUrl = `${ossBaseUrl}/${ossPath}`; |
Summary
cdn.xkcoding.com(阿里云 CDN 加速)cdn-ssl-renew.yml自动续期 CDN 证书Test plan
🤖 Generated with Claude Code