Skip to content

ref: move duplicated ISO date-string detection into shared utility (#42) #53

ref: move duplicated ISO date-string detection into shared utility (#42)

ref: move duplicated ISO date-string detection into shared utility (#42) #53

Workflow file for this run

name: Publish Workflow
on:
push:
branches:
- main
env:
DOCKERHUB_USERNAME: ravelloh
GHCR_IMAGE_NAME: ravelloh/neutralpress
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
has_release: ${{ steps.check.outputs.has_release }}
is_stable_release: ${{ steps.check.outputs.is_stable_release }}
version: ${{ steps.check.outputs.version }}
semver: ${{ steps.check.outputs.semver }}
build_sha: ${{ steps.finalize.outputs.build_sha }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# 1. 判断这次 push 是否新增了 changelog/v*.md 文件
- name: Detect New Changelog
id: check
run: |
# 兼容处理:获取本次 push 前的 commit SHA
BEFORE_SHA="${{ github.event.before }}"
if [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
BEFORE_SHA="HEAD^"
fi
# 寻找在这次 push 中“新增(A)”的符合 changelog/v*.md 格式的文件
NEW_FILE=$(git diff --name-status "$BEFORE_SHA" "${{ github.sha }}" | awk '$1 == "A" {print $2}' | grep -E "^changelog/v[0-9]+\.[0-9]+\.[0-9]+.*\.md$" | head -n 1 || true)
if [ -n "$NEW_FILE" ]; then
VERSION=$(basename "$NEW_FILE" .md) # 提取完整版本号,例如 v5.0.0
SEMVER=${VERSION#v} # 剥离 v 用于 Docker,例如 5.0.0
echo "has_release=true" >> "$GITHUB_OUTPUT"
if echo "$VERSION" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "is_stable_release=true" >> "$GITHUB_OUTPUT"
else
echo "is_stable_release=false" >> "$GITHUB_OUTPUT"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
echo "file_path=$NEW_FILE" >> "$GITHUB_OUTPUT"
echo "🎉 Detected new release triggered by: $NEW_FILE"
else
echo "has_release=false" >> "$GITHUB_OUTPUT"
echo "is_stable_release=false" >> "$GITHUB_OUTPUT"
echo "version=" >> "$GITHUB_OUTPUT"
echo "semver=" >> "$GITHUB_OUTPUT"
echo "file_path=" >> "$GITHUB_OUTPUT"
echo "ℹ️ No new changelog found. Proceeding with edge build only."
fi
# 2. 生成合并版 Release Notes(手写 Changelog + 自动 Git Log)
- name: Generate Combined Release Notes
if: steps.check.outputs.has_release == 'true'
run: |
VERSION="${{ steps.check.outputs.version }}"
FILE_PATH="${{ steps.check.outputs.file_path }}"
# 首先,写入手写的更新日志(作为正文头部)
cat "$FILE_PATH" > release.md
echo "" >> release.md
echo "---" >> release.md
# 只寻找“上一个正式版”tag:严格匹配 vX.Y.Z(不包含 alpha/beta/rc)
PREV_STABLE_TAG=$(git tag -l --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -vx "$VERSION" | head -n1 || true)
if [ -z "$PREV_STABLE_TAG" ]; then
PREV_STABLE_TAG="none"
fi
if [ "$PREV_STABLE_TAG" != "none" ]; then
echo "## 详细变更(${PREV_STABLE_TAG} -> ${VERSION})" >> release.md
else
echo "## 详细变更(初始版本 -> ${VERSION})" >> release.md
fi
echo "" >> release.md
# 附加自动生成的 commit 日志
if [ "$PREV_STABLE_TAG" != "none" ]; then
git log "${PREV_STABLE_TAG}"..HEAD --pretty=format:'- [%s](https://github.com/${{ github.repository }}/commit/%H) by @%an' >> release.md
else
echo "- 初始化版本" >> release.md
fi
- name: Upload Release Notes Artifact
if: steps.check.outputs.has_release == 'true'
uses: actions/upload-artifact@v4
with:
name: release-notes
path: release.md
if-no-files-found: error
retention-days: 1
# 3. 正式版发布时,同步所有 package.json 的 version,并提交回 main
- name: Sync package versions and push
if: steps.check.outputs.is_stable_release == 'true'
run: |
TARGET_VERSION="${{ steps.check.outputs.semver }}"
# 更新仓库内所有受 git 管理的 package.json 的 version 字段
PACKAGE_FILES=$(git ls-files '**/package.json')
for file in $PACKAGE_FILES; do
node -e '
const fs = require("fs");
const path = process.argv[1];
const version = process.argv[2];
const raw = fs.readFileSync(path, "utf8");
const json = JSON.parse(raw);
if (json.version !== version) {
json.version = version;
fs.writeFileSync(path, JSON.stringify(json, null, 2) + "\n");
}
' "$file" "$TARGET_VERSION"
done
git add -- $PACKAGE_FILES
# 没有变化则直接跳过
if git diff --cached --quiet; then
echo "ℹ️ package.json version already up-to-date."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "chore(release): sync package versions to v${TARGET_VERSION} [skip ci]"
git push origin HEAD:${{ github.ref_name }}
- name: Finalize build commit SHA
id: finalize
run: |
echo "build_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
build:
name: Build (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
needs: prepare
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
platform: linux/amd64
arch: amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
arch: arm64
steps:
- name: Checkout repository at build commit
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.build_sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: ${{ matrix.platform }}
tags: |
${{ env.DOCKERHUB_USERNAME }}/neutralpress
ghcr.io/${{ env.GHCR_IMAGE_NAME }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ needs.prepare.outputs.build_sha }}
cache-from: type=gha,scope=web-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=web-${{ matrix.arch }}
- name: Export digest
run: |
mkdir -p /tmp/digests
echo "${{ steps.build.outputs.digest }}" > "/tmp/digests/${{ matrix.arch }}.txt"
- name: Upload digest artifact
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/${{ matrix.arch }}.txt
if-no-files-found: error
retention-days: 1
publish:
runs-on: ubuntu-latest
needs:
- prepare
- build
permissions:
contents: write
packages: write
steps:
- name: Download digest artifacts
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build tag arguments
id: tags
env:
HAS_RELEASE: ${{ needs.prepare.outputs.has_release }}
IS_STABLE_RELEASE: ${{ needs.prepare.outputs.is_stable_release }}
SEMVER: ${{ needs.prepare.outputs.semver }}
run: |
set -euo pipefail
dockerhub_tags="-t ${{ env.DOCKERHUB_USERNAME }}/neutralpress:edge"
ghcr_tags="-t ghcr.io/${{ env.GHCR_IMAGE_NAME }}:edge"
if [ "$IS_STABLE_RELEASE" = "true" ]; then
dockerhub_tags="$dockerhub_tags -t ${{ env.DOCKERHUB_USERNAME }}/neutralpress:latest"
ghcr_tags="$ghcr_tags -t ghcr.io/${{ env.GHCR_IMAGE_NAME }}:latest"
fi
if [ "$HAS_RELEASE" = "true" ] && [ -n "$SEMVER" ]; then
dockerhub_tags="$dockerhub_tags -t ${{ env.DOCKERHUB_USERNAME }}/neutralpress:${SEMVER}"
ghcr_tags="$ghcr_tags -t ghcr.io/${{ env.GHCR_IMAGE_NAME }}:${SEMVER}"
fi
echo "dockerhub_args=$dockerhub_tags" >> "$GITHUB_OUTPUT"
echo "ghcr_args=$ghcr_tags" >> "$GITHUB_OUTPUT"
- name: Create and push Docker Hub manifest list
run: |
set -euo pipefail
sources=""
for file in /tmp/digests/*.txt; do
digest="$(cat "$file")"
sources="$sources ${{ env.DOCKERHUB_USERNAME }}/neutralpress@${digest}"
done
docker buildx imagetools create ${{ steps.tags.outputs.dockerhub_args }} $sources
- name: Create and push GHCR manifest list
run: |
set -euo pipefail
sources=""
for file in /tmp/digests/*.txt; do
digest="$(cat "$file")"
sources="$sources ghcr.io/${{ env.GHCR_IMAGE_NAME }}@${digest}"
done
docker buildx imagetools create ${{ steps.tags.outputs.ghcr_args }} $sources
- name: Download Release Notes Artifact
if: needs.prepare.outputs.has_release == 'true'
uses: actions/download-artifact@v4
with:
name: release-notes
path: .
# 发布放在最后,确保镜像清单已经成功创建
- name: Create GitHub Release
if: needs.prepare.outputs.has_release == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: Release ${{ needs.prepare.outputs.version }}
body_path: release.md
target_commitish: ${{ needs.prepare.outputs.build_sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}