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: Build and Push Docker Images with Nix Flakes | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| schedule: | |
| - cron: '0 2 * * 1' # Weekly on Monday at 2 AM | |
| workflow_dispatch: | |
| inputs: | |
| push_images: | |
| description: 'Push images to registry' | |
| type: boolean | |
| default: true | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| security-events: write | |
| actions: read | |
| checks: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Nix 2.31.1 | |
| run: | | |
| # 安装最新稳定版 Nix 2.31.1 (单用户模式) | |
| sh <(curl -L https://nixos.org/nix/install) --no-daemon --yes | |
| # 重新加载环境 | |
| . /home/runner/.nix-profile/etc/profile.d/nix.sh | |
| # 验证安装 | |
| nix --version | |
| - name: Configure Nix | |
| run: | | |
| # 加载 Nix 环境 | |
| . /home/runner/.nix-profile/etc/profile.d/nix.sh | |
| # 配置 Nix 以支持 Flakes | |
| mkdir -p ~/.config/nix | |
| cat > ~/.config/nix/nix.conf << EOF | |
| experimental-features = nix-command flakes | |
| allow-import-from-derivation = true | |
| EOF | |
| # 验证配置 | |
| nix --version | |
| - name: Clean build environment | |
| run: | | |
| # 加载 Nix 环境 | |
| . /home/runner/.nix-profile/etc/profile.d/nix.sh | |
| # 清理 Nix 缓存 | |
| echo "🧹 Cleaning Nix cache..." | |
| nix-collect-garbage | |
| nix store gc | |
| # 清理 Docker 环境 | |
| echo "🧹 Cleaning Docker environment..." | |
| docker image prune -f | |
| docker container prune -f | |
| docker builder prune -f | |
| # 清理可能冲突的镜像 | |
| docker rmi ghcr.io/reaslab/docker-python-runner:secure-latest 2>/dev/null || echo " No existing image to remove" | |
| - name: Setup Nix Flake environment | |
| run: | | |
| # 加载 Nix 环境 | |
| . /home/runner/.nix-profile/etc/profile.d/nix.sh | |
| # 设置环境变量以允许非自由包(Gurobi) | |
| export NIXPKGS_ALLOW_UNFREE=1 | |
| # 进入 Nix 开发环境并构建 | |
| echo "🔧 Setting up Nix Flake environment..." | |
| nix develop --command bash -c " | |
| echo '🐍 Building Python Docker image with Nix Flakes...' | |
| echo 'Current directory:' | |
| pwd | |
| echo 'Available files:' | |
| ls -la | |
| # 设置更宽松的构建选项 | |
| export NIX_BUILD_CORES=0 | |
| export NIX_CONF_DIR=/tmp/nix-conf | |
| mkdir -p \$NIX_CONF_DIR | |
| # 使用与本地构建相同的方式,添加详细输出 | |
| echo 'Starting build with detailed output...' | |
| nix build .#docker-image --option sandbox false --impure --rebuild --show-trace --verbose || { | |
| echo '❌ First build attempt failed, trying with different options...' | |
| # 尝试不同的构建选项 | |
| nix build .#docker-image --option sandbox false --impure --rebuild --option max-jobs 1 --option cores 1 || { | |
| echo '❌ Second build attempt failed, trying without rebuild...' | |
| nix build .#docker-image --option sandbox false --impure --option max-jobs 1 --option cores 1 | |
| } | |
| } | |
| echo 'Build command completed, checking results...' | |
| # 检查构建结果 | |
| echo '🔍 Checking build results...' | |
| ls -la | |
| # 验证构建结果 | |
| if [ -L result ]; then | |
| echo '✅ Result symlink created successfully' | |
| ls -la result | |
| echo 'Result points to:' | |
| readlink result | |
| echo 'File exists and is readable:' | |
| test -r result && echo 'Yes' || echo 'No' | |
| else | |
| echo '❌ Result symlink not found, checking for other outputs...' | |
| # 查找可能的输出文件 | |
| find . -name '*.tar.gz' -o -name 'docker-image*' 2>/dev/null || echo 'No tar.gz files found' | |
| # 检查 Nix store 中的构建结果 | |
| echo 'Checking Nix store for build results...' | |
| nix-store --query --outputs \$(nix-instantiate .#docker-image) 2>/dev/null || echo 'No outputs found in store' | |
| exit 1 | |
| fi | |
| " | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Container Registry | |
| if: github.event.inputs.push_images != 'false' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Load Docker image | |
| run: | | |
| # 加载 Nix 环境 | |
| . /home/runner/.nix-profile/etc/profile.d/nix.sh | |
| # 设置环境变量以允许非自由包(Gurobi) | |
| export NIXPKGS_ALLOW_UNFREE=1 | |
| # 在 Nix 开发环境中加载 Docker 镜像 | |
| echo "🐳 Loading Docker image from Nix build result..." | |
| nix develop --command bash -c " | |
| if [ -L result ]; then | |
| echo '✅ Result symlink found, loading Docker image...' | |
| docker load < result | |
| echo '✅ Docker image loaded successfully' | |
| else | |
| echo '❌ Result symlink not found, rebuilding...' | |
| nix build .#docker-image --option sandbox false --impure --rebuild | |
| docker load < result | |
| echo '✅ Docker image rebuilt and loaded successfully' | |
| fi | |
| " | |
| - name: Tag Docker image | |
| if: github.event.inputs.push_images != 'false' | |
| run: | | |
| # Get the image ID from the loaded image | |
| IMAGE_ID=$(docker images --format "{{.ID}}" | head -1) | |
| echo "Image ID: $IMAGE_ID" | |
| # Create tags based on trigger type | |
| TAGS=() | |
| # Always create latest tag | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest") | |
| # Create version-specific tags based on trigger | |
| if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| # Main branch push: create timestamp and SHA tags | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-$(date +%Y%m%d-%H%M%S)") | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-${{ github.sha }}") | |
| elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| # Manual trigger: create timestamp tag only | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-$(date +%Y%m%d-%H%M%S)") | |
| elif [ "${{ github.event_name }}" = "schedule" ]; then | |
| # Scheduled: create timestamp tag only | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-$(date +%Y%m%d-%H%M%S)") | |
| fi | |
| # Tag with all tags | |
| for tag in "${TAGS[@]}"; do | |
| echo "Tagging with: $tag" | |
| docker tag "$IMAGE_ID" "$tag" | |
| done | |
| echo "All tags created successfully" | |
| - name: Push Docker image | |
| if: github.event.inputs.push_images != 'false' | |
| run: | | |
| # Create tags based on trigger type (same as tagging step) | |
| TAGS=() | |
| # Always create latest tag | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest") | |
| # Create version-specific tags based on trigger | |
| if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| # Main branch push: create timestamp and SHA tags | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-$(date +%Y%m%d-%H%M%S)") | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-${{ github.sha }}") | |
| elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| # Manual trigger: create timestamp tag only | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-$(date +%Y%m%d-%H%M%S)") | |
| elif [ "${{ github.event_name }}" = "schedule" ]; then | |
| # Scheduled: create timestamp tag only | |
| TAGS+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-$(date +%Y%m%d-%H%M%S)") | |
| fi | |
| # Push all tags | |
| for tag in "${TAGS[@]}"; do | |
| echo "Pushing: $tag" | |
| docker push "$tag" | |
| done | |
| echo "All tags pushed successfully" | |
| - name: Run security scan | |
| if: github.event.inputs.push_images != 'false' | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| continue-on-error: true | |
| - name: Install jq for SARIF parsing | |
| if: github.event.inputs.push_images != 'false' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y jq | |
| - name: Display local security scan results | |
| if: github.event.inputs.push_images != 'false' && always() | |
| run: | | |
| echo "## 🔍 Local Security Scan Results" >> $GITHUB_STEP_SUMMARY | |
| if [ -f "trivy-results.sarif" ]; then | |
| echo "✅ Trivy security scan completed successfully" >> $GITHUB_STEP_SUMMARY | |
| # Extract vulnerability count | |
| VULNERABILITIES=$(jq '.runs[0].results | length' trivy-results.sarif 2>/dev/null || echo "0") | |
| echo "- **Vulnerabilities found:** $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY | |
| # Show high/critical vulnerabilities | |
| HIGH_CRITICAL=$(jq '.runs[0].results[] | select(.level == "error" or .level == "warning") | .level' trivy-results.sarif 2>/dev/null | wc -l || echo "0") | |
| echo "- **High/Critical issues:** $HIGH_CRITICAL" >> $GITHUB_STEP_SUMMARY | |
| # Show scan summary | |
| echo "### 📊 Scan Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Image scanned:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Scan format:** SARIF" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Results file:** \`trivy-results.sarif\`" >> $GITHUB_STEP_SUMMARY | |
| # Show some sample vulnerabilities if any | |
| if [ "$VULNERABILITIES" -gt 0 ]; then | |
| echo "### 🚨 Sample Vulnerabilities" >> $GITHUB_STEP_SUMMARY | |
| jq -r '.runs[0].results[0:3][] | "- **\(.level)**: \(.message.text)"' trivy-results.sarif 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "Unable to parse vulnerability details" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "### ✅ No vulnerabilities found" >> $GITHUB_STEP_SUMMARY | |
| echo "The Docker image appears to be secure!" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "⚠️ Security scan results not available" >> $GITHUB_STEP_SUMMARY | |
| echo "The Trivy scan may have failed or the results file was not generated." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Upload Trivy scan results | |
| if: github.event.inputs.push_images != 'false' && github.ref == 'refs/heads/main' | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| continue-on-error: true | |
| - name: Display security scan results | |
| if: github.event.inputs.push_images != 'false' && always() | |
| run: | | |
| echo "## 🔒 Security Scan Results" >> $GITHUB_STEP_SUMMARY | |
| # Check if Advanced Security is available | |
| if [ -f "trivy-results.sarif" ]; then | |
| echo "✅ Security scan completed successfully" >> $GITHUB_STEP_SUMMARY | |
| echo "📊 Scan results saved to trivy-results.sarif" >> $GITHUB_STEP_SUMMARY | |
| # Display scan summary | |
| echo "### 📋 Scan Summary" >> $GITHUB_STEP_SUMMARY | |
| if command -v jq >/dev/null 2>&1; then | |
| VULNERABILITIES=$(jq '.runs[0].results | length' trivy-results.sarif 2>/dev/null || echo "0") | |
| echo "- **Vulnerabilities found:** $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "### 📄 Full Report" >> $GITHUB_STEP_SUMMARY | |
| echo "Detailed scan results are available in the SARIF file." >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ Security scan results not available" >> $GITHUB_STEP_SUMMARY | |
| echo "This may be due to:" >> $GITHUB_STEP_SUMMARY | |
| echo "- Advanced Security not enabled for this repository" >> $GITHUB_STEP_SUMMARY | |
| echo "- Scan failed to complete" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Note:** Code scanning requires GitHub Advanced Security (paid feature)." >> $GITHUB_STEP_SUMMARY | |
| echo "The Trivy scan still runs locally and results are available in the workflow logs." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| test: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: github.event.inputs.push_images != 'false' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Test Docker image functionality | |
| run: | | |
| echo "Testing Docker image functionality..." | |
| # 拉取镜像 | |
| docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest | |
| # 基本验证 | |
| echo "Image size: $(docker images --format '{{.Size}}' ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest)" | |
| echo "Image user: $(docker inspect --format='{{.Config.User}}' ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest)" | |
| # 测试容器创建 | |
| CONTAINER_ID=$(docker create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest echo "test") | |
| if [ $? -eq 0 ]; then | |
| echo "✅ Container creation successful" | |
| docker rm $CONTAINER_ID | |
| else | |
| echo "❌ Container creation failed" | |
| exit 1 | |
| fi | |
| # 测试Python和UV(使用临时文件系统挂载) | |
| echo "Testing Python:" | |
| docker run --rm --tmpfs /tmp:noexec,nosuid,size=100m --user root ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest /bin/bash -c "export PATH=\${PATH} && python --version" || echo "Python not found" | |
| echo "Testing UV:" | |
| docker run --rm --tmpfs /tmp:noexec,nosuid,size=100m --user root ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:secure-latest /bin/bash -c "export PATH=\${PATH} && uv --version" || echo "UV not found" | |
| echo "✅ All tests completed successfully" | |
| generate-summary: | |
| runs-on: ubuntu-latest | |
| needs: [build, test] | |
| if: always() | |
| steps: | |
| - name: Generate summary | |
| run: | | |
| echo "## 🐳 Docker Image Build Summary (Nix Flakes)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Tags:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- \`secure-latest\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Build System:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Nix Flakes for reproducible builds" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Declarative environment management" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Features:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Secure Python 3.12 environment" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ UV package manager" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Gurobi optimization solver" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Scientific computing packages" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Non-root user execution" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Resource limits and security restrictions" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Security:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Trivy vulnerability scanning" >> $GITHUB_STEP_SUMMARY | |
| echo "- ⚠️ Code scanning requires GitHub Advanced Security (paid feature)" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Local security scan results available in workflow logs" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Registry:** [GitHub Container Registry](https://github.com/orgs/reaslab/packages)" >> $GITHUB_STEP_SUMMARY |