feat: add middleware client tools (MySQL, PostgreSQL, Redis, Docker, … #33
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 Image | |
| on: | |
| push: | |
| branches: | |
| - master | |
| workflow_dispatch: | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| build: | |
| runs-on: ${{ matrix.runs-on }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runs-on: ubuntu-latest | |
| - platform: linux/arm64 | |
| runs-on: ubuntu-24.04-arm | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Get current date for tagging | |
| run: echo "DATE_TAG=$(date +'%Y%m%d')" >> $GITHUB_ENV | |
| - name: Prepare platform pair | |
| run: | | |
| PLATFORM_PAIR=$(echo ${{ matrix.platform }} | tr '/' '-') | |
| echo "PLATFORM_PAIR=$PLATFORM_PAIR" >> $GITHUB_ENV | |
| - name: Set lowercase image name | |
| run: | | |
| echo "IMAGE_NAME=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=sha,format=short | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=${{ env.DATE_TAG }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: type=gha,scope=${{ matrix.platform }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.platform }} | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: digests-${{ env.PLATFORM_PAIR }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| merge: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Get current date for tagging | |
| run: echo "DATE_TAG=$(date +'%Y%m%d')" >> $GITHUB_ENV | |
| - name: Set lowercase image name | |
| run: | | |
| echo "IMAGE_NAME=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download digests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: /tmp/digests | |
| pattern: digests-* | |
| merge-multiple: true | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=sha,format=short | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=${{ env.DATE_TAG }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| # Build the list of digests | |
| digests=$(find . -type f | while read f; do | |
| digest=$(basename "$f") | |
| echo "${{ env.IMAGE_NAME }}@sha256:${digest}" | |
| done) | |
| # Create and push manifest with all tags | |
| tags="${{ steps.meta.outputs.tags }}" | |
| echo "$tags" | while read -r tag; do | |
| docker buildx imagetools create -t "$tag" $digests | |
| done | |
| - name: Inspect image | |
| run: | | |
| TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n1 | tr -d '\n') | |
| docker buildx imagetools inspect "$TAG" | |
| test: | |
| name: Test Image | |
| runs-on: ${{ matrix.runs-on }} | |
| needs: merge | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| packages: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runs-on: ubuntu-latest | |
| - platform: linux/arm64 | |
| runs-on: ubuntu-24.04-arm | |
| steps: | |
| - name: Set lowercase image name | |
| run: | | |
| echo "IMAGE_NAME=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Pull image | |
| run: docker pull ${{ env.IMAGE_NAME }}:${{ github.sha }} | |
| - name: Start test container | |
| run: docker run -d --name test-container ${{ env.IMAGE_NAME }}:${{ github.sha }} sleep 600 | |
| - name: Test default user is coder | |
| timeout-minutes: 1 | |
| run: | | |
| echo "Testing default user is 'coder'..." | |
| USER=$(docker exec test-container whoami) | |
| if [ "$USER" != "coder" ]; then | |
| echo "::error::Default user is '$USER', expected 'coder'" | |
| exit 1 | |
| fi | |
| echo "✓ Default user is 'coder'" | |
| - name: Test installed tools accessibility (VS Code terminal simulation) | |
| timeout-minutes: 5 | |
| run: | | |
| echo "Testing installed tools accessibility for coder user..." | |
| echo "Using interactive shell (bash -i) to simulate VS Code terminal behavior" | |
| # Test function with timeout | |
| # Uses 'bash -i -c' to simulate VS Code terminal (interactive non-login shell) | |
| # This ensures .bashrc is loaded, matching real VS Code terminal environment | |
| test_tool() { | |
| local tool=$1 | |
| local cmd=$2 | |
| local timeout_secs=10 | |
| if timeout $timeout_secs docker exec test-container bash -i -c "$cmd" > /dev/null 2>&1; then | |
| echo " ✓ $tool is accessible" | |
| return 0 | |
| else | |
| echo " ✗ $tool is NOT accessible" | |
| return 1 | |
| fi | |
| } | |
| FAILED_TOOLS="" | |
| # Go tools | |
| test_tool "go" "go version" || FAILED_TOOLS="$FAILED_TOOLS go" | |
| test_tool "gopls" "gopls version" || FAILED_TOOLS="$FAILED_TOOLS gopls" | |
| test_tool "dlv" "dlv version" || FAILED_TOOLS="$FAILED_TOOLS dlv" | |
| test_tool "golangci-lint" "golangci-lint --version" || FAILED_TOOLS="$FAILED_TOOLS golangci-lint" | |
| # Python tools | |
| test_tool "python3" "python3 --version" || FAILED_TOOLS="$FAILED_TOOLS python3" | |
| test_tool "pip" "pip --version" || FAILED_TOOLS="$FAILED_TOOLS pip" | |
| test_tool "uv" "uv --version" || FAILED_TOOLS="$FAILED_TOOLS uv" | |
| test_tool "conda" "conda --version" || FAILED_TOOLS="$FAILED_TOOLS conda" | |
| # Node.js tools | |
| test_tool "node" "node --version" || FAILED_TOOLS="$FAILED_TOOLS node" | |
| test_tool "npm" "npm --version" || FAILED_TOOLS="$FAILED_TOOLS npm" | |
| test_tool "pnpm" "pnpm --version" || FAILED_TOOLS="$FAILED_TOOLS pnpm" | |
| test_tool "yarn" "yarn --version" || FAILED_TOOLS="$FAILED_TOOLS yarn" | |
| # Java tools | |
| test_tool "java" "java -version" || FAILED_TOOLS="$FAILED_TOOLS java" | |
| test_tool "mvn" "mvn --version" || FAILED_TOOLS="$FAILED_TOOLS mvn" | |
| # Ruby tools | |
| test_tool "ruby" "ruby --version" || FAILED_TOOLS="$FAILED_TOOLS ruby" | |
| test_tool "gem" "gem --version" || FAILED_TOOLS="$FAILED_TOOLS gem" | |
| test_tool "rails" "rails --version" || FAILED_TOOLS="$FAILED_TOOLS rails" | |
| # System tools | |
| test_tool "git" "git --version" || FAILED_TOOLS="$FAILED_TOOLS git" | |
| test_tool "curl" "curl --version" || FAILED_TOOLS="$FAILED_TOOLS curl" | |
| test_tool "wget" "wget --version" || FAILED_TOOLS="$FAILED_TOOLS wget" | |
| test_tool "vim" "vim --version | head -1" || FAILED_TOOLS="$FAILED_TOOLS vim" | |
| test_tool "kubectl" "kubectl version --client --output=json" || FAILED_TOOLS="$FAILED_TOOLS kubectl" | |
| test_tool "yq" "yq --version" || FAILED_TOOLS="$FAILED_TOOLS yq" | |
| # Database and Middleware clients | |
| test_tool "mysql" "mysql --version" || FAILED_TOOLS="$FAILED_TOOLS mysql" | |
| test_tool "psql" "psql --version" || FAILED_TOOLS="$FAILED_TOOLS psql" | |
| test_tool "redis-cli" "redis-cli --version" || FAILED_TOOLS="$FAILED_TOOLS redis-cli" | |
| test_tool "docker" "docker --version" || FAILED_TOOLS="$FAILED_TOOLS docker" | |
| test_tool "kafka-topics" "kafka-topics.sh --version" || FAILED_TOOLS="$FAILED_TOOLS kafka-topics" | |
| if [ -n "$FAILED_TOOLS" ]; then | |
| echo "::error::The following tools are not accessible:$FAILED_TOOLS" | |
| exit 1 | |
| fi | |
| echo "✓ All tools are accessible for coder user (VS Code terminal simulation)" | |
| - name: Test sudo without password | |
| timeout-minutes: 1 | |
| run: | | |
| echo "Testing sudo works without password..." | |
| docker exec test-container bash -c "sudo whoami" | grep -q "root" && echo "✓ sudo works without password" || { echo "::error::sudo requires password"; exit 1; } | |
| - name: Test su command is blocked | |
| timeout-minutes: 1 | |
| run: | | |
| echo "Testing su command is blocked..." | |
| SU_OUTPUT=$(docker exec test-container bash -c "sudo -n /usr/bin/su - root -c 'whoami'" 2>&1 || true) | |
| echo "sudo su output: $SU_OUTPUT" | |
| if echo "$SU_OUTPUT" | grep -q "^root$"; then | |
| echo "::error::su command escalated to root; policy is broken" | |
| exit 1 | |
| fi | |
| if echo "$SU_OUTPUT" | grep -Eq "not allowed to execute|command not found|a password is required|password is required|authentication is required"; then | |
| echo "✓ su command is properly blocked" | |
| else | |
| echo "::error::Unexpected sudo/su behavior: $SU_OUTPUT" | |
| exit 1 | |
| fi | |
| - name: Test mirror configurations | |
| timeout-minutes: 2 | |
| run: | | |
| echo "Testing mirror configurations..." | |
| echo "Using interactive shell (bash -i) to simulate VS Code terminal behavior" | |
| # Test Go proxy | |
| docker exec test-container bash -i -c 'echo $GOPROXY | grep -q "goproxy.cn"' && echo " ✓ Go proxy configured" || echo " ⚠ Go proxy not using goproxy.cn" | |
| # Test npm registry | |
| docker exec test-container bash -i -c 'npm config get registry | grep -q "npmmirror"' && echo " ✓ npm registry configured" || echo " ⚠ npm registry not using npmmirror" | |
| # Test gem sources | |
| docker exec test-container bash -i -c 'gem sources | grep -q "ruby-china"' && echo " ✓ gem sources configured" || echo " ⚠ gem sources not using ruby-china" | |
| echo "✓ Mirror configurations verified (VS Code terminal simulation)" | |
| - name: Cleanup test container | |
| if: always() | |
| run: docker rm -f test-container 2>/dev/null || true |