Scheduled Release #19
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: Scheduled Release | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| # GitHub Actions cron uses UTC. | |
| # 19:00 UTC == 03:00 Asia/Shanghai on the next day. | |
| - cron: "0 19 * * *" | |
| permissions: | |
| contents: write | |
| jobs: | |
| precheck: | |
| name: Compute release metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_skip: ${{ steps.meta.outputs.should_skip }} | |
| next_version: ${{ steps.meta.outputs.next_version }} | |
| release_notes: ${{ steps.meta.outputs.release_notes }} | |
| steps: | |
| - name: Compute version and release notes | |
| id: meta | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const currentSha = context.sha.toLowerCase(); | |
| const { owner, repo } = context.repo; | |
| const releases = await github.paginate(github.rest.repos.listReleases, { | |
| owner, | |
| repo, | |
| per_page: 100 | |
| }); | |
| const stableReleases = releases.filter((release) => !release.prerelease && !release.draft); | |
| const latestStable = stableReleases[0] || null; | |
| if (latestStable) { | |
| const body = latestStable.body || ""; | |
| const match = body.match(/Commit:\s*([0-9a-f]{40})/i); | |
| if (match && match[1].toLowerCase() === currentSha) { | |
| core.info("current commit already released"); | |
| core.setOutput("should_skip", "true"); | |
| core.setOutput("next_version", latestStable.tag_name || ""); | |
| core.setOutput("release_notes", ""); | |
| return; | |
| } | |
| } | |
| function parseVersion(tag) { | |
| const match = String(tag || "").match(/^(\d+)\.(\d+)\.(\d+)$/); | |
| if (!match) { | |
| return null; | |
| } | |
| return { | |
| major: Number(match[1]), | |
| minor: Number(match[2]), | |
| patch: Number(match[3]) | |
| }; | |
| } | |
| let highest = { major: 0, minor: 0, patch: 0 }; | |
| let hasSemver = false; | |
| for (const release of stableReleases) { | |
| const parsed = parseVersion(release.tag_name); | |
| if (!parsed) { | |
| continue; | |
| } | |
| hasSemver = true; | |
| const isHigher = | |
| parsed.major > highest.major || | |
| (parsed.major === highest.major && parsed.minor > highest.minor) || | |
| (parsed.major === highest.major && parsed.minor === highest.minor && parsed.patch > highest.patch); | |
| if (isHigher) { | |
| highest = parsed; | |
| } | |
| } | |
| let nextVersion = "0.0.1"; | |
| if (hasSemver) { | |
| let { major, minor, patch } = highest; | |
| patch += 1; | |
| if (patch >= 100) { | |
| patch = 0; | |
| minor += 1; | |
| } | |
| if (minor >= 100) { | |
| minor = 0; | |
| major += 1; | |
| } | |
| nextVersion = `${major}.${minor}.${patch}`; | |
| } | |
| let releaseNotes = "- Initial release"; | |
| if (latestStable) { | |
| try { | |
| const compare = await github.rest.repos.compareCommits({ | |
| owner, | |
| repo, | |
| base: latestStable.tag_name, | |
| head: context.sha | |
| }); | |
| const commits = compare.data.commits || []; | |
| if (commits.length > 0) { | |
| releaseNotes = commits | |
| .map((commit) => { | |
| const sha = commit.sha.slice(0, 7); | |
| const message = String(commit.commit.message || "").split("\n")[0].trim(); | |
| return `- ${sha} ${message || "update"}`; | |
| }) | |
| .join("\n"); | |
| } else { | |
| releaseNotes = "- No code changes found in compare range"; | |
| } | |
| } catch (error) { | |
| core.warning(`compare failed: ${error.message}`); | |
| releaseNotes = "- Release notes unavailable"; | |
| } | |
| } | |
| core.setOutput("should_skip", "false"); | |
| core.setOutput("next_version", nextVersion); | |
| core.setOutput("release_notes", releaseNotes); | |
| build: | |
| name: Build ${{ matrix.goos }}-${{ matrix.goarch }} | |
| runs-on: ubuntu-latest | |
| needs: precheck | |
| if: needs.precheck.outputs.should_skip != 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| asset_prefix: linux-amd64 | |
| binary_name: web-model | |
| - goos: darwin | |
| goarch: amd64 | |
| asset_prefix: macos-amd64 | |
| binary_name: web-model | |
| - goos: windows | |
| goarch: amd64 | |
| asset_prefix: windows-amd64 | |
| binary_name: web-model.exe | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| - name: Build package | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| stage_dir="dist/${{ matrix.asset_prefix }}" | |
| mkdir -p "${stage_dir}" | |
| CGO_ENABLED=0 GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" \ | |
| go build -trimpath -ldflags="-s -w" \ | |
| -o "${stage_dir}/${{ matrix.binary_name }}" \ | |
| ./cmd/web-model | |
| cp -R extension "${stage_dir}/extension" | |
| tar -C dist -czf "dist/${{ matrix.asset_prefix }}-all.tgz" "${{ matrix.asset_prefix }}" | |
| - name: Upload packaged artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.asset_prefix }}-all | |
| path: dist/${{ matrix.asset_prefix }}-all.tgz | |
| if-no-files-found: error | |
| release: | |
| name: Publish release | |
| runs-on: ubuntu-latest | |
| needs: | |
| - precheck | |
| - build | |
| if: needs.precheck.outputs.should_skip != 'true' | |
| steps: | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: release-assets | |
| - name: Verify packaged assets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| find release-assets -type f -name '*-all.tgz' | sort | |
| count="$(find release-assets -type f -name '*-all.tgz' | wc -l | tr -d ' ')" | |
| if [ "$count" -eq 0 ]; then | |
| echo "no packaged assets found" >&2 | |
| exit 1 | |
| fi | |
| - name: Publish release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.precheck.outputs.next_version }} | |
| name: ${{ needs.precheck.outputs.next_version }} | |
| body: | | |
| Automated scheduled release. | |
| Commit: ${{ github.sha }} | |
| Changes since previous release: | |
| ${{ needs.precheck.outputs.release_notes }} | |
| prerelease: false | |
| make_latest: true | |
| files: | | |
| release-assets/**/*-all.tgz |