name: Release on: push: tags: - 'v*.*.*' workflow_dispatch: inputs: version: description: 'Version to release (e.g., v1.0.0)' required: true type: string env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: validate-release: name: Validate Release runs-on: ubuntu-latest outputs: version: ${{ steps.get-version.outputs.version }} tag: ${{ steps.get-version.outputs.tag }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Get version from tag or input id: get-version run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then VERSION="${{ github.event.inputs.version }}" echo "version=${VERSION#v}" >> $GITHUB_OUTPUT echo "tag=${VERSION}" >> $GITHUB_OUTPUT else VERSION="${GITHUB_REF#refs/tags/}" echo "version=${VERSION#v}" >> $GITHUB_OUTPUT echo "tag=${VERSION}" >> $GITHUB_OUTPUT fi - name: Validate version format run: | VERSION="${{ steps.get-version.outputs.version }}" if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then echo "❌ Invalid version format: $VERSION" echo "Expected format: X.Y.Z or X.Y.Z-suffix" exit 1 fi echo "✅ Valid version format: $VERSION" - name: Check if version matches Cargo.toml run: | CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') INPUT_VERSION="${{ steps.get-version.outputs.version }}" if [ "$CARGO_VERSION" != "$INPUT_VERSION" ]; then echo "❌ Version mismatch:" echo " Cargo.toml: $CARGO_VERSION" echo " Tag/Input: $INPUT_VERSION" exit 1 fi echo "✅ Version matches Cargo.toml: $INPUT_VERSION" test: name: Run Tests runs-on: ${{ matrix.os }} needs: validate-release strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] rust: [stable] steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - name: Cache Cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} - name: Install system dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y \ build-essential \ pkg-config \ libssl-dev \ libfontconfig1-dev \ libfreetype6-dev \ libjpeg-dev \ libpng-dev - name: Install system dependencies (macOS) if: matrix.os == 'macos-latest' run: | brew update brew install pkg-config freetype jpeg libpng - name: Run tests (library only) run: cargo test --verbose --lib build: name: Build Release Artifacts runs-on: ${{ matrix.job.os }} needs: [validate-release, test] strategy: matrix: job: - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, use-cross: false } - { os: ubuntu-latest, target: x86_64-unknown-linux-musl, use-cross: true } - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, use-cross: true } - { os: ubuntu-latest, target: aarch64-unknown-linux-musl, use-cross: true } - { os: windows-latest, target: x86_64-pc-windows-msvc, use-cross: false } - { os: macos-latest, target: x86_64-apple-darwin, use-cross: false } - { os: macos-latest, target: aarch64-apple-darwin, use-cross: false } steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.job.target }} - name: Install cross compilation tools if: matrix.job.use-cross run: | cargo install cross --git https://github.com/cross-rs/cross - name: Cache Cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-${{ matrix.job.target }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} - name: Install system dependencies (Ubuntu) if: matrix.job.os == 'ubuntu-latest' && !matrix.job.use-cross run: | sudo apt-get update sudo apt-get install -y \ build-essential \ pkg-config \ libssl-dev \ libfontconfig1-dev \ libfreetype6-dev \ libjpeg-dev \ libpng-dev - name: Install system dependencies (macOS) if: matrix.job.os == 'macos-latest' run: | brew update brew install pkg-config freetype jpeg libpng - name: Build release binary run: | if [ "${{ matrix.job.use-cross }}" = "true" ]; then cross build --release --target ${{ matrix.job.target }} else cargo build --release --target ${{ matrix.job.target }} fi - name: Prepare release archive shell: bash run: | VERSION="${{ needs.validate-release.outputs.version }}" TARGET="${{ matrix.job.target }}" # Create staging directory mkdir -p staging # Copy binary if [[ "${{ matrix.job.os }}" == "windows-latest" ]]; then cp "target/${TARGET}/release/docx-mcp.exe" staging/ BINARY="docx-mcp.exe" else cp "target/${TARGET}/release/docx-mcp" staging/ BINARY="docx-mcp" fi # Copy additional files cp README.md staging/ cp LICENSE staging/ # Create archive name ARCHIVE="docx-mcp-${VERSION}-${TARGET}" # Create archive cd staging if [[ "${{ matrix.job.os }}" == "windows-latest" ]]; then 7z a "../${ARCHIVE}.zip" * echo "ARCHIVE=${ARCHIVE}.zip" >> $GITHUB_ENV else tar czf "../${ARCHIVE}.tar.gz" * echo "ARCHIVE=${ARCHIVE}.tar.gz" >> $GITHUB_ENV fi cd .. # Generate checksums if [[ "${{ matrix.job.os }}" == "windows-latest" ]]; then certutil -hashfile "${ARCHIVE}.zip" SHA256 > "${ARCHIVE}.zip.sha256" else shasum -a 256 "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256" fi echo "BINARY=${BINARY}" >> $GITHUB_ENV - name: Upload release artifacts uses: actions/upload-artifact@v4 with: name: release-${{ matrix.job.target }} path: | ${{ env.ARCHIVE }} ${{ env.ARCHIVE }}.sha256 create-release: name: Create GitHub Release runs-on: ubuntu-latest needs: [validate-release, build] permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Prepare release assets run: | mkdir -p release-assets find artifacts -type f -name "*.tar.gz" -o -name "*.zip" -o -name "*.sha256" | \ xargs -I {} cp {} release-assets/ ls -la release-assets/ - name: Generate changelog id: changelog run: | VERSION="${{ needs.validate-release.outputs.version }}" TAG="${{ needs.validate-release.outputs.tag }}" # Get previous tag PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${TAG}$" | head -1) echo "Generating changelog from ${PREV_TAG} to ${TAG}" # Generate changelog if [ -n "$PREV_TAG" ]; then CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges ${PREV_TAG}..HEAD) else CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) fi # Create release notes cat > release-notes.md << EOF ## What's Changed ${CHANGELOG} ## Installation ### Pre-built Binaries Download the appropriate binary for your system: - **Linux x86_64**: \`docx-mcp-${VERSION}-x86_64-unknown-linux-gnu.tar.gz\` - **Linux x86_64 (musl)**: \`docx-mcp-${VERSION}-x86_64-unknown-linux-musl.tar.gz\` - **Linux ARM64**: \`docx-mcp-${VERSION}-aarch64-unknown-linux-gnu.tar.gz\` - **macOS Intel**: \`docx-mcp-${VERSION}-x86_64-apple-darwin.tar.gz\` - **macOS Apple Silicon**: \`docx-mcp-${VERSION}-aarch64-apple-darwin.tar.gz\` - **Windows x86_64**: \`docx-mcp-${VERSION}-x86_64-pc-windows-msvc.zip\` ### From Source \`\`\`bash cargo install --git https://github.com/hongkongkiwi/docx-mcp --tag ${TAG} \`\`\` ### Verification All binaries are provided with SHA256 checksums for verification: \`\`\`bash # Linux/macOS shasum -a 256 -c docx-mcp-${VERSION}-your-target.tar.gz.sha256 # Windows certutil -hashfile docx-mcp-${VERSION}-x86_64-pc-windows-msvc.zip SHA256 \`\`\` ## Full Changelog **Full Changelog**: https://github.com/hongkongkiwi/docx-mcp/compare/${PREV_TAG}...${TAG} EOF echo "CHANGELOG_FILE=release-notes.md" >> $GITHUB_OUTPUT - name: Create Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.validate-release.outputs.tag }} name: Release ${{ needs.validate-release.outputs.tag }} body_path: ${{ steps.changelog.outputs.CHANGELOG_FILE }} files: release-assets/* draft: false prerelease: ${{ contains(needs.validate-release.outputs.version, '-') }} generate_release_notes: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-crate: name: Publish to crates.io runs-on: ubuntu-latest needs: [validate-release, create-release] if: ${{ !contains(needs.validate-release.outputs.version, '-') }} # Only publish stable releases steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ build-essential \ pkg-config \ libssl-dev \ libfontconfig1-dev \ libfreetype6-dev \ libjpeg-dev \ libpng-dev - name: Cache Cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ubuntu-cargo-publish-${{ hashFiles('**/Cargo.lock') }} - name: Verify package run: cargo package --dry-run - name: Publish to crates.io run: cargo publish env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} docker-release: name: Build and Push Docker Images runs-on: ubuntu-latest needs: [validate-release, create-release] permissions: contents: read packages: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub if: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository }} ${{ secrets.DOCKERHUB_USERNAME && format('{0}/docx-mcp', secrets.DOCKERHUB_USERNAME) || '' }} tags: | type=ref,event=tag type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max update-docs: name: Update Documentation runs-on: ubuntu-latest needs: [validate-release, create-release] permissions: contents: write pages: write id-token: write steps: - name: Checkout code uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ build-essential \ pkg-config \ libssl-dev \ libfontconfig1-dev \ libfreetype6-dev \ libjpeg-dev \ libpng-dev - name: Generate documentation run: | cargo doc --all-features --no-deps echo '' > target/doc/index.html - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc cname: docs.example.com # Replace with your custom domain if you have one notify-success: name: Notify Release Success runs-on: ubuntu-latest needs: [create-release, publish-crate, docker-release, update-docs] if: success() steps: - name: Success notification run: | echo "🎉 Release ${{ needs.validate-release.outputs.tag }} completed successfully!" echo "- ✅ GitHub release created" echo "- ✅ Binaries built for all platforms" echo "- ✅ Published to crates.io" echo "- ✅ Docker images pushed" echo "- ✅ Documentation updated" notify-failure: name: Notify Release Failure runs-on: ubuntu-latest needs: [validate-release, test, build, create-release, publish-crate, docker-release, update-docs] if: failure() steps: - name: Failure notification run: | echo "❌ Release ${{ needs.validate-release.outputs.tag }} failed!" echo "Please check the workflow logs for details." exit 1