Skip to content

Scheduled Release

Scheduled Release #19

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