ref: move duplicated ISO date-string detection into shared utility (#42) #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |