Skip to content

Build and Release

Build and Release #49

name: Build and Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version without leading v (e.g. 2.0.0-beta1)"
required: true
type: string
source_ref:
description: "Source ref in Java-Chains/chains (tag, branch, or commit). Defaults to v<version>"
required: false
type: string
permissions:
contents: write
jobs:
# ====================================================
# Job 0: 解析发布元信息
# ====================================================
resolve_release_meta:
runs-on: ubuntu-latest
outputs:
release_version: ${{ steps.meta.outputs.release_version }}
release_tag: ${{ steps.meta.outputs.release_tag }}
source_ref: ${{ steps.meta.outputs.source_ref }}
is_prerelease: ${{ steps.meta.outputs.is_prerelease }}
docker_tags: ${{ steps.meta.outputs.docker_tags }}
chains_dev_jars_repository: ${{ steps.manifest.outputs.chains_dev_jars_repository }}
chains_dev_jars_ref: ${{ steps.manifest.outputs.chains_dev_jars_ref }}
java_echo_generator_repository: ${{ steps.manifest.outputs.java_echo_generator_repository }}
java_echo_generator_ref: ${{ steps.manifest.outputs.java_echo_generator_ref }}
java_memshell_generator_repository: ${{ steps.manifest.outputs.java_memshell_generator_repository }}
java_memshell_generator_ref: ${{ steps.manifest.outputs.java_memshell_generator_ref }}
chains_config_repository: ${{ steps.manifest.outputs.chains_config_repository }}
chains_config_ref: ${{ steps.manifest.outputs.chains_config_ref }}
steps:
- uses: actions/checkout@v4
- name: Resolve Release Metadata
id: meta
shell: bash
run: |
VERSION_INPUT="${{ inputs.version }}"
VERSION="${VERSION_INPUT#v}"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid version: $VERSION_INPUT"
echo "Use SemVer, for example: 2.0.0 or 2.0.0-beta1"
exit 1
fi
RELEASE_TAG="v$VERSION"
SOURCE_REF="${{ inputs.source_ref }}"
if [ -z "$SOURCE_REF" ]; then
SOURCE_REF="$RELEASE_TAG"
fi
if [[ "$VERSION" == *-* ]]; then
IS_PRERELEASE="true"
else
IS_PRERELEASE="false"
fi
echo "release_version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
echo "source_ref=$SOURCE_REF" >> "$GITHUB_OUTPUT"
echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT"
{
echo "docker_tags<<EOF"
echo "javachains/javachains:${VERSION}"
if [ "$IS_PRERELEASE" != "true" ]; then
echo "javachains/javachains:latest"
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Read Release Manifest
id: manifest
shell: bash
run: |
ruby <<'RUBY'
require 'yaml'
manifest = YAML.safe_load(File.read('.github/release-manifest.yml'), aliases: false)
dependencies = manifest.fetch('dependencies')
required = %w[
chains_dev_jars
java_echo_generator
java_memshell_generator
chains_config
]
File.open(ENV.fetch('GITHUB_OUTPUT'), 'a') do |out|
required.each do |name|
entry = dependencies[name] || abort("Missing dependency entry: #{name}")
repository = entry['repository'].to_s.strip
ref = entry['ref'].to_s.strip
abort("Missing repository for #{name}") if repository.empty?
abort("Missing ref for #{name}") if ref.empty?
out.puts "#{name}_repository=#{repository}"
out.puts "#{name}_ref=#{ref}"
end
end
RUBY
# ====================================================
# Job 1: 核心构建
# ====================================================
build:
needs: resolve_release_meta
runs-on: ubuntu-latest
outputs:
source_sha: ${{ steps.source_revision.outputs.source_sha }}
env:
JAVA_VERSION: '8'
NODE_VERSION: '23.2.0'
PNPM_VERSION: '9.13.2'
GO_VERSION: '1.20.14'
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
repository: Java-Chains/chains
ref: ${{ needs.resolve_release_meta.outputs.source_ref }}
token: ${{ secrets.DEPENDENCY_REPO_TOKEN }}
fetch-depth: 0
- name: Capture Source Revision
id: source_revision
shell: bash
run: |
echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate Source Version
shell: bash
run: |
EXPECTED_VERSION="${{ needs.resolve_release_meta.outputs.release_version }}"
POM_VERSION=$(sed -n '0,/<version>/s/.*<version>\([^<]*\)<\/version>.*/\1/p' pom.xml | head -n 1)
if [ -z "$POM_VERSION" ]; then
echo "Failed to read version from pom.xml"
exit 1
fi
if [ "$POM_VERSION" != "$EXPECTED_VERSION" ]; then
echo "Version mismatch:"
echo " source_ref: ${{ needs.resolve_release_meta.outputs.source_ref }}"
echo " pom.xml: $POM_VERSION"
echo " input: $EXPECTED_VERSION"
echo "Please bump/tag the source repo first, or pass the correct source_ref."
exit 1
fi
- name: Clone Pinned Dependency Repositories
env:
DEPENDENCY_REPO_TOKEN: ${{ secrets.DEPENDENCY_REPO_TOKEN }}
CHAINS_DEV_JARS_REPOSITORY: ${{ needs.resolve_release_meta.outputs.chains_dev_jars_repository }}
CHAINS_DEV_JARS_REF: ${{ needs.resolve_release_meta.outputs.chains_dev_jars_ref }}
JAVA_ECHO_GENERATOR_REPOSITORY: ${{ needs.resolve_release_meta.outputs.java_echo_generator_repository }}
JAVA_ECHO_GENERATOR_REF: ${{ needs.resolve_release_meta.outputs.java_echo_generator_ref }}
JAVA_MEMSHELL_GENERATOR_REPOSITORY: ${{ needs.resolve_release_meta.outputs.java_memshell_generator_repository }}
JAVA_MEMSHELL_GENERATOR_REF: ${{ needs.resolve_release_meta.outputs.java_memshell_generator_ref }}
CHAINS_CONFIG_REPOSITORY: ${{ needs.resolve_release_meta.outputs.chains_config_repository }}
CHAINS_CONFIG_REF: ${{ needs.resolve_release_meta.outputs.chains_config_ref }}
run: |
mkdir -p release-deps
clone_repo() {
local repository="$1"
local directory="$2"
local ref="$3"
local url="https://x-access-token:${DEPENDENCY_REPO_TOKEN}@github.com/${repository}.git"
echo "Cloning ${repository} @ ${ref}"
git clone "$url" "$directory"
git -C "$directory" checkout "$ref"
echo "Resolved $(git -C "$directory" rev-parse HEAD) for ${repository}"
}
clone_repo "$CHAINS_DEV_JARS_REPOSITORY" "release-deps/chains-dev-jars" "$CHAINS_DEV_JARS_REF"
clone_repo "$JAVA_ECHO_GENERATOR_REPOSITORY" "release-deps/java-echo-generator" "$JAVA_ECHO_GENERATOR_REF"
clone_repo "$JAVA_MEMSHELL_GENERATOR_REPOSITORY" "release-deps/java-memshell-generator" "$JAVA_MEMSHELL_GENERATOR_REF"
clone_repo "$CHAINS_CONFIG_REPOSITORY" "release-deps/chains-config" "$CHAINS_CONFIG_REF"
- name: Set up Temurin JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
cache: 'maven'
- name: Set up Maven
uses: stCarolas/setup-maven@v5
with:
maven-version: 3.9.6
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: mcp-go/go.sum
- name: Clone and Install Dev Jars
run: |
(
cd release-deps/chains-dev-jars
bash mvn_install.sh
)
- name: Build and Install Generators
run: |
(
cd release-deps/java-echo-generator
mvn clean install -DskipTests
)
(
cd release-deps/java-memshell-generator
mvn clean install -DskipTests
)
- name: Set up Node.js & pnpm
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Build Frontend
run: |
(
cd frontend
pnpm install
pnpm build
)
mkdir -p java-chains/src/main/resources/static
rm -rf java-chains/src/main/resources/static/*
cp -r frontend/dist/* java-chains/src/main/resources/static/
- name: Build Backend (Server & CLI)
run: |
mvn clean install -DskipTests -Dmaven.compiler.source=1.8 -Dmaven.compiler.target=1.8
- name: Build MCP Binaries
run: |
mkdir -p release_base/mcp-binaries
cd mcp-go
build_mcp() {
local goos="$1"
local goarch="$2"
local output="$3"
echo "Building ${output} ..."
CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" \
go build -trimpath -ldflags="-s -w" -o "../release_base/mcp-binaries/${output}" ./cmd/mcp-go
}
build_mcp linux amd64 java-chains-mcp-linux-amd64
build_mcp linux arm64 java-chains-mcp-linux-arm64
build_mcp windows amd64 java-chains-mcp-windows-amd64.exe
build_mcp windows arm64 java-chains-mcp-windows-arm64.exe
build_mcp darwin amd64 java-chains-mcp-macos-amd64
build_mcp darwin arm64 java-chains-mcp-macos-arm64
- name: Prepare Base Artifacts
run: |
mkdir -p release_base
# 1. 复制主程序,v2.0 优先使用可执行 Spring Boot fat jar
SERVER_JAR=$(find . -maxdepth 1 -type f -name "java-chains-*-exec.jar" | sort | head -n 1)
if [ -z "$SERVER_JAR" ]; then
SERVER_JAR=$(find . -maxdepth 1 -type f -name "java-chains-*.jar" \
! -name "*-sources.jar" \
! -name "*-javadoc.jar" \
! -name "*.original" | sort | head -n 1)
fi
if [ -f "$SERVER_JAR" ]; then
echo "Found Server Jar: $(basename "$SERVER_JAR")"
cp "$SERVER_JAR" release_base/java-chains.jar
else
echo "Error: java-chains jar not found in root directory"
exit 1
fi
# 2. 复制 CLI 工具,保留独立发布
CLI_JAR=$(find cli-chains/target -type f -name "cli-chains-*-jar-with-dependencies.jar" | sort | head -n 1)
if [ -z "$CLI_JAR" ]; then
CLI_JAR=$(find . -maxdepth 1 -type f -name "cli-chains-*-jar-with-dependencies.jar" | sort | head -n 1)
fi
if [ -f "$CLI_JAR" ]; then
echo "Found CLI Jar: $(basename "$CLI_JAR")"
cp "$CLI_JAR" release_base/cli-chains.jar
else
echo "Error: cli-chains jar not found"
exit 1
fi
# 2.5 复制 SDK jar
SDK_JAR=$(find chains-sdk/target -maxdepth 1 -type f -name "chains-sdk-*.jar" \
! -name "*-sources.jar" ! -name "*-javadoc.jar" ! -name "*.original" | sort | head -n 1)
if [ -f "$SDK_JAR" ]; then
echo "Found SDK Jar: $(basename "$SDK_JAR")"
cp "$SDK_JAR" release_base/java-chains-sdk.jar
else
echo "Error: chains-sdk jar not found"
exit 1
fi
# 3. 复制配置,并覆盖 v2.0 新增的 MCP 配置目录
cp -r release-deps/chains-config release_base/chains-config
rm -rf release_base/chains-config/.git
if [ -d chains-config/mcp ]; then
mkdir -p release_base/chains-config/mcp
cp -r chains-config/mcp/. release_base/chains-config/mcp/
fi
# ── 源码防泄漏:销毁所有源码,确保 artifact 只含编译产物 ──
- name: Purge Source Code
if: always()
run: |
rm -rf release-deps
rm -rf ~/.m2/repository
# 确认 release_base 中没有源码文件
if find release_base -type f \( \
-name "*.java" -o -name "*.go" -o -name "*.ts" -o -name "*.tsx" \
-o -name "*.vue" -o -name "*.jsx" -o -name "*.py" \
-o -name "*-sources.jar" -o -name "*-javadoc.jar" \
-o -name ".git" -o -name ".gitignore" \
-o -name "pom.xml" -o -name "package.json" -o -name "go.mod" \
\) | grep -q .; then
echo "ERROR: Source code files detected in release artifacts!"
find release_base -type f \( \
-name "*.java" -o -name "*.go" -o -name "*.ts" -o -name "*.tsx" \
-o -name "*.vue" -o -name "*.jsx" -o -name "*.py" \
-o -name "*-sources.jar" -o -name "*-javadoc.jar" \
-o -name ".git" -o -name ".gitignore" \
-o -name "pom.xml" -o -name "package.json" -o -name "go.mod" \
\)
exit 1
fi
find release_base -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true
echo "Artifact contents:"
find release_base -type f | sort
- name: Upload Base Artifacts
uses: actions/upload-artifact@v4
with:
name: base-artifacts
path: release_base/
# ====================================================
# Job 2: Create Release
# ====================================================
create_release:
needs: [ resolve_release_meta, build ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Draft Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_TAG="${{ needs.resolve_release_meta.outputs.release_tag }}"
RELEASE_VERSION="${{ needs.resolve_release_meta.outputs.release_version }}"
SOURCE_REF="${{ needs.resolve_release_meta.outputs.source_ref }}"
SOURCE_SHA="${{ needs.build.outputs.source_sha }}"
IS_PRERELEASE="${{ needs.resolve_release_meta.outputs.is_prerelease }}"
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
echo "Release $RELEASE_TAG already exists, skip creation."
exit 0
fi
cat > release-notes.txt <<EOF
Auto-generated release for version ${RELEASE_VERSION}
Source repository: Java-Chains/chains
Source ref: ${SOURCE_REF}
Source commit: ${SOURCE_SHA}
EOF
ARGS=(
"$RELEASE_TAG"
--draft
--title "Release $RELEASE_TAG"
--repo "$GITHUB_REPOSITORY"
--notes-file release-notes.txt
)
if [ "$IS_PRERELEASE" = "true" ]; then
ARGS+=(--prerelease)
fi
gh release create "${ARGS[@]}"
# ====================================================
# Job 3: 矩阵打包 (主程序 + 对应平台 MCP)
# ====================================================
bundle_assets:
needs: [ resolve_release_meta, build, create_release ]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- os_api: linux
arch_api: x64
suffix: linux-amd64
ext: tar.gz
mcp_binary: java-chains-mcp-linux-amd64
mcp_target: java-chains-mcp
- os_api: linux
arch_api: aarch64
suffix: linux-aarch64
ext: tar.gz
mcp_binary: java-chains-mcp-linux-arm64
mcp_target: java-chains-mcp
- os_api: windows
arch_api: x64
suffix: windows-amd64
ext: zip
mcp_binary: java-chains-mcp-windows-amd64.exe
mcp_target: java-chains-mcp.exe
- os_api: windows
arch_api: arm64
suffix: windows-arm64
ext: zip
mcp_binary: java-chains-mcp-windows-arm64.exe
mcp_target: java-chains-mcp.exe
- os_api: mac
arch_api: x64
suffix: macos-intel
ext: tar.gz
mcp_binary: java-chains-mcp-macos-amd64
mcp_target: java-chains-mcp
- os_api: mac
arch_api: aarch64
suffix: macos-m-series
ext: tar.gz
mcp_binary: java-chains-mcp-macos-arm64
mcp_target: java-chains-mcp
steps:
- uses: actions/download-artifact@v4
with:
name: base-artifacts
path: base
- name: Download JDK 8
run: |
API_URL="https://api.adoptium.net/v3/binary/latest/8/ga/${{ matrix.os_api }}/${{ matrix.arch_api }}/jdk/hotspot/normal/eclipse"
USE_AZUL="false"
if [ "${{ matrix.os_api }}" = "mac" ] && [ "${{ matrix.arch_api }}" = "aarch64" ]; then
USE_AZUL="true"
fi
if [ "${{ matrix.os_api }}" = "windows" ] && [ "${{ matrix.arch_api }}" = "arm64" ]; then
API_URL="https://api.adoptium.net/v3/binary/latest/8/ga/windows/x64/jdk/hotspot/normal/eclipse"
fi
if [ "$USE_AZUL" = "true" ]; then
AZUL_OS="${{ matrix.os_api }}"
if [ "$AZUL_OS" = "mac" ]; then
AZUL_OS="macos"
fi
API_URL="https://api.azul.com/zulu/download/community/v1.0/bundles/latest/binary/?jdk_version=8&os=$AZUL_OS&arch=${{ matrix.arch_api }}&bundle_type=jdk&ext=${{ matrix.ext }}"
fi
if [ "${{ matrix.ext }}" = "zip" ]; then
curl -L "$API_URL" -o jdk_bundle.zip
unzip -q jdk_bundle.zip -d jdk_extracted
else
curl -L "$API_URL" -o jdk_bundle.tar.gz
mkdir jdk_extracted
tar -xzf jdk_bundle.tar.gz -C jdk_extracted
fi
SUBDIR=$(ls jdk_extracted | head -n 1)
mv "jdk_extracted/$SUBDIR" jdk_final
if [ "${{ matrix.os_api }}" = "mac" ] && [ -d "jdk_final/Contents/Home" ]; then
mv jdk_final/Contents/Home jdk_real
rm -rf jdk_final
mv jdk_real jdk_final
fi
- name: Create Distribution Structure
run: |
DIST_DIR="java-chains-${{ needs.resolve_release_meta.outputs.release_version }}-${{ matrix.suffix }}"
mkdir -p "$DIST_DIR"
cp base/java-chains.jar "$DIST_DIR/"
cp -r base/chains-config "$DIST_DIR/"
cp "base/mcp-binaries/${{ matrix.mcp_binary }}" "$DIST_DIR/${{ matrix.mcp_target }}"
mv jdk_final "$DIST_DIR/jdk"
if [ "${{ matrix.os_api }}" != "windows" ]; then
chmod +x "$DIST_DIR/${{ matrix.mcp_target }}"
fi
echo "DIST_DIR=$DIST_DIR" >> "$GITHUB_ENV"
- name: Generate Start Scripts
run: |
cd "${{ env.DIST_DIR }}"
if [ "${{ matrix.os_api }}" != "windows" ]; then
echo '#!/bin/bash' > start.sh
echo 'BASE_DIR=$(cd "$(dirname "$0")" && pwd)' >> start.sh
echo '"$BASE_DIR/jdk/bin/java" -jar "$BASE_DIR/java-chains.jar"' >> start.sh
chmod +x start.sh
echo '#!/bin/bash' > start-mcp.sh
echo 'BASE_DIR=$(cd "$(dirname "$0")" && pwd)' >> start-mcp.sh
echo 'exec "$BASE_DIR/java-chains-mcp" "$@"' >> start-mcp.sh
chmod +x start-mcp.sh
else
echo '@echo off' > start.bat
echo 'set "BASE_DIR=%~dp0"' >> start.bat
echo '"%BASE_DIR%jdk\bin\java.exe" -jar "%BASE_DIR%java-chains.jar"' >> start.bat
echo 'pause' >> start.bat
echo '@echo off' > start-mcp.bat
echo 'set "BASE_DIR=%~dp0"' >> start-mcp.bat
echo '"%BASE_DIR%java-chains-mcp.exe" %*' >> start-mcp.bat
fi
- name: Compress and Upload
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_TAG="${{ needs.resolve_release_meta.outputs.release_tag }}"
ASSET_NAME="${{ env.DIST_DIR }}.${{ matrix.ext }}"
if [ "${{ matrix.ext }}" = "zip" ]; then
zip -r -q "$ASSET_NAME" "${{ env.DIST_DIR }}"
else
tar -czf "$ASSET_NAME" "${{ env.DIST_DIR }}"
fi
gh release upload "$RELEASE_TAG" "$ASSET_NAME" --clobber --repo "$GITHUB_REPOSITORY"
# ====================================================
# Job 4: 上传通用包 / MCP 全平台包 / CLI 独立包
# ====================================================
upload_standard_assets:
needs: [ resolve_release_meta, build, create_release ]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: base-artifacts
path: base
- name: Create and Upload Assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_TAG="${{ needs.resolve_release_meta.outputs.release_tag }}"
RELEASE_VERSION="${{ needs.resolve_release_meta.outputs.release_version }}"
cd base
tar -czvf "java-chains-${RELEASE_VERSION}.tar.gz" java-chains.jar chains-config
gh release upload "$RELEASE_TAG" "java-chains-${RELEASE_VERSION}.tar.gz" --clobber --repo "$GITHUB_REPOSITORY"
tar -czvf "java-chains-mcp-${RELEASE_VERSION}-binaries.tar.gz" mcp-binaries
gh release upload "$RELEASE_TAG" "java-chains-mcp-${RELEASE_VERSION}-binaries.tar.gz" --clobber --repo "$GITHUB_REPOSITORY"
mkdir cli_dist
cp cli-chains.jar cli_dist/
cp -r chains-config cli_dist/
tar -czvf "cli-chains-${RELEASE_VERSION}.tar.gz" -C cli_dist .
gh release upload "$RELEASE_TAG" "cli-chains-${RELEASE_VERSION}.tar.gz" --clobber --repo "$GITHUB_REPOSITORY"
cp java-chains-sdk.jar "java-chains-sdk-${RELEASE_VERSION}.jar"
gh release upload "$RELEASE_TAG" "java-chains-sdk-${RELEASE_VERSION}.jar" --clobber --repo "$GITHUB_REPOSITORY"
# ====================================================
# Job 5: Docker Build
# ====================================================
docker_build:
needs: [ resolve_release_meta, build ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: base-artifacts
path: .
- name: Prepare Docker MCP binary names
run: |
rm -f cli-chains.jar java-chains-sdk.jar
- name: Verify Docker MCP Binaries
run: |
test -d mcp-binaries
test -f mcp-binaries/java-chains-mcp-linux-amd64
test -f mcp-binaries/java-chains-mcp-linux-arm64
ls -la mcp-binaries
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ needs.resolve_release_meta.outputs.docker_tags }}