Set up Docker and automated publishing workflow #10
Workflow file for this run
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: Docker Build and Test | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| pull_request: | |
| branches: | |
| - main | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| env: | |
| REGISTRY: docker.io | |
| jobs: | |
| lint: | |
| name: Lint Dockerfile | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Run Hadolint (Dockerfile linter) | |
| uses: hadolint/hadolint-action@v3.1.0 | |
| with: | |
| dockerfile: Dockerfile | |
| failure-threshold: warning | |
| ignore: DL3018 | |
| - name: Lint summary | |
| run: | | |
| echo "## 🔍 Dockerfile Lint Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ Dockerfile passes linting checks" >> $GITHUB_STEP_SUMMARY | |
| build-test: | |
| name: Build and Test Docker Image | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Extract project metadata | |
| id: project | |
| run: | | |
| # Extract project name and version from package.json | |
| PROJECT_NAME=$(node -p "require('./package.json').name || 'comapeo-alerts-commander'") | |
| PROJECT_VERSION=$(node -p "require('./package.json').version || '0.0.0'") | |
| NODE_VERSION=$(node -p "require('./package.json').engines?.node || '20'") | |
| echo "name=$PROJECT_NAME" >> $GITHUB_OUTPUT | |
| echo "version=$PROJECT_VERSION" >> $GITHUB_OUTPUT | |
| echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT | |
| echo "Project: $PROJECT_NAME v$PROJECT_VERSION (Node $NODE_VERSION)" | |
| - name: Set image name | |
| id: image | |
| run: | | |
| # Use secret if available, otherwise use placeholder for testing | |
| if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]; then | |
| IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/comapeo-alerts-commander" | |
| else | |
| IMAGE_NAME="local/comapeo-alerts-commander" | |
| fi | |
| echo "name=$IMAGE_NAME" >> $GITHUB_OUTPUT | |
| echo "Using image name: $IMAGE_NAME" | |
| - name: Generate Docker tags | |
| id: tags | |
| run: | | |
| TAGS="" | |
| IMAGE_NAME="${{ steps.image.outputs.name }}" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| # For PRs: pr-<number> | |
| PR_TAG="pr-${{ github.event.pull_request.number }}" | |
| TAGS="${IMAGE_NAME}:${PR_TAG}" | |
| echo "primary_tag=${PR_TAG}" >> $GITHUB_OUTPUT | |
| else | |
| # For main branch: branch-<commit> | |
| BRANCH_TAG="${{ github.ref_name }}-$(echo ${{ github.sha }} | cut -c1-7)" | |
| TAGS="${IMAGE_NAME}:${BRANCH_TAG}" | |
| echo "primary_tag=${BRANCH_TAG}" >> $GITHUB_OUTPUT | |
| fi | |
| echo "tags=${TAGS}" >> $GITHUB_OUTPUT | |
| echo "Generated tag: ${TAGS}" | |
| - name: Build Docker image | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| load: true | |
| tags: test-image:local | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| NODE_ENV=production | |
| - name: Test Docker image structure | |
| run: | | |
| echo "## 🧪 Testing Docker Image" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Get image size | |
| IMAGE_SIZE=$(docker images test-image:local --format "{{.Size}}") | |
| echo "**Image Size:** $IMAGE_SIZE" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Check image layers | |
| echo "**Image Layers:**" >> $GITHUB_STEP_SUMMARY | |
| docker history test-image:local --human=true --format "table {{.Size}}\t{{.CreatedBy}}" | head -n 10 >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Verify nginx is present | |
| docker run --rm test-image:local nginx -v | |
| # Verify required files exist in the image | |
| echo "**Verifying required files:**" >> $GITHUB_STEP_SUMMARY | |
| docker run --rm test-image:local sh -c "ls -la /usr/share/nginx/html/" | tee -a $GITHUB_STEP_SUMMARY | |
| # Check for index.html | |
| docker run --rm test-image:local sh -c "test -f /usr/share/nginx/html/index.html" && echo "✅ index.html found" >> $GITHUB_STEP_SUMMARY || (echo "❌ index.html missing" >> $GITHUB_STEP_SUMMARY && exit 1) | |
| # Check for manifest.json (PWA) | |
| docker run --rm test-image:local sh -c "test -f /usr/share/nginx/html/manifest.json" && echo "✅ manifest.json found" >> $GITHUB_STEP_SUMMARY || echo "⚠️ manifest.json missing" >> $GITHUB_STEP_SUMMARY | |
| # Check for service worker | |
| docker run --rm test-image:local sh -c "test -f /usr/share/nginx/html/sw.js" && echo "✅ sw.js found" >> $GITHUB_STEP_SUMMARY || echo "⚠️ sw.js missing" >> $GITHUB_STEP_SUMMARY | |
| # Verify nginx config | |
| docker run --rm test-image:local sh -c "nginx -t" | |
| echo "✅ Nginx configuration valid" >> $GITHUB_STEP_SUMMARY | |
| - name: Run container for testing | |
| run: | | |
| # Start container | |
| docker run -d -p 8080:80 --name test-container test-image:local | |
| # Wait for container to be healthy | |
| echo "Waiting for container to start..." | |
| sleep 5 | |
| # Check container is running | |
| docker ps | grep test-container | |
| - name: Test HTTP endpoints | |
| run: | | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**HTTP Endpoint Tests:**" >> $GITHUB_STEP_SUMMARY | |
| # Test health endpoint | |
| echo "Testing /health endpoint..." | |
| HEALTH_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health) | |
| if [ "$HEALTH_RESPONSE" = "200" ]; then | |
| echo "✅ /health returns 200" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ /health returns $HEALTH_RESPONSE" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| fi | |
| # Test main page | |
| echo "Testing / endpoint..." | |
| ROOT_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/) | |
| if [ "$ROOT_RESPONSE" = "200" ]; then | |
| echo "✅ / returns 200" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ / returns $ROOT_RESPONSE" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| fi | |
| # Test that index.html contains expected content | |
| echo "Testing page content..." | |
| CONTENT=$(curl -s http://localhost:8080/) | |
| if echo "$CONTENT" | grep -q "CoMapeo"; then | |
| echo "✅ Page contains expected content" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ Page content may be incomplete" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Test SPA routing (should return 200 for non-existent routes) | |
| echo "Testing SPA routing..." | |
| SPA_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/some/random/path) | |
| if [ "$SPA_RESPONSE" = "200" ]; then | |
| echo "✅ SPA routing works (fallback to index.html)" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ SPA routing failed: returns $SPA_RESPONSE" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| fi | |
| # Test static asset with proper headers | |
| echo "Testing static asset caching..." | |
| CACHE_HEADER=$(curl -s -I http://localhost:8080/manifest.json | grep -i "cache-control" || echo "No cache header") | |
| echo "**Cache-Control header:** $CACHE_HEADER" >> $GITHUB_STEP_SUMMARY | |
| - name: Test security headers | |
| run: | | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Security Headers:**" >> $GITHUB_STEP_SUMMARY | |
| # Check for security headers | |
| HEADERS=$(curl -s -I http://localhost:8080/) | |
| if echo "$HEADERS" | grep -qi "X-Frame-Options"; then | |
| echo "✅ X-Frame-Options header present" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ X-Frame-Options header missing" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if echo "$HEADERS" | grep -qi "X-Content-Type-Options"; then | |
| echo "✅ X-Content-Type-Options header present" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ X-Content-Type-Options header missing" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if echo "$HEADERS" | grep -qi "X-XSS-Protection"; then | |
| echo "✅ X-XSS-Protection header present" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ X-XSS-Protection header missing" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Check container logs | |
| if: always() | |
| run: | | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Container Logs:**" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| docker logs test-container 2>&1 | tail -n 20 >> $GITHUB_STEP_SUMMARY || true | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| - name: Cleanup test container | |
| if: always() | |
| run: | | |
| docker stop test-container || true | |
| docker rm test-container || true | |
| - name: Check Docker Hub credentials | |
| id: check_creds | |
| run: | | |
| if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ] && [ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]; then | |
| echo "available=true" >> $GITHUB_OUTPUT | |
| echo "Docker Hub credentials available" | |
| else | |
| echo "available=false" >> $GITHUB_OUTPUT | |
| echo "Docker Hub credentials not configured (optional for PR testing)" | |
| fi | |
| - name: Log in to Docker Hub (for PR images) | |
| if: github.event_name == 'pull_request' && steps.check_creds.outputs.available == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| continue-on-error: true | |
| - name: Tag and push PR image | |
| if: github.event_name == 'pull_request' && steps.check_creds.outputs.available == 'true' | |
| run: | | |
| PR_TAG="pr-${{ github.event.pull_request.number }}" | |
| IMAGE_NAME="${{ steps.image.outputs.name }}" | |
| # Tag the local test image | |
| docker tag test-image:local ${IMAGE_NAME}:${PR_TAG} | |
| # Push to Docker Hub | |
| docker push ${IMAGE_NAME}:${PR_TAG} || echo "Failed to push (credentials may not be configured)" | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**PR Image Published:**" >> $GITHUB_STEP_SUMMARY | |
| echo '```bash' >> $GITHUB_STEP_SUMMARY | |
| echo "docker pull ${IMAGE_NAME}:${PR_TAG}" >> $GITHUB_STEP_SUMMARY | |
| echo "docker run -p 8080:80 ${IMAGE_NAME}:${PR_TAG}" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| continue-on-error: true | |
| - name: Comment on PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| env: | |
| IMAGE_NAME: ${{ steps.image.outputs.name }} | |
| CREDS_AVAILABLE: ${{ steps.check_creds.outputs.available }} | |
| with: | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| const prTag = `pr-${prNumber}`; | |
| const imageName = process.env.IMAGE_NAME; | |
| const credsAvailable = process.env.CREDS_AVAILABLE === 'true'; | |
| let comment = `## 🐳 Docker Build Successful | |
| Your PR has been built and tested successfully! | |
| **Test Results:** ✅ All tests passed | |
| **PR Tag:** \`${prTag}\` | |
| `; | |
| if (credsAvailable) { | |
| comment += `### Test the PR image locally: | |
| \`\`\`bash | |
| docker pull ${imageName}:${prTag} | |
| docker run -p 8080:80 ${imageName}:${prTag} | |
| \`\`\` | |
| Then open http://localhost:8080 in your browser. | |
| **Note:** PR images are available for testing but are not published to the \`latest\` tag. | |
| `; | |
| } else { | |
| comment += `### Docker Hub Publishing | |
| ⚠️ Docker Hub credentials are not configured for this repository. | |
| To enable PR image publishing: | |
| 1. Add \`DOCKERHUB_USERNAME\` and \`DOCKERHUB_TOKEN\` to repository secrets | |
| 2. PR images will be automatically published for testing | |
| You can still test locally by building the Dockerfile: | |
| \`\`\`bash | |
| docker build -t ${prTag} . | |
| docker run -p 8080:80 ${prTag} | |
| \`\`\` | |
| `; | |
| } | |
| // Find existing comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('🐳 Docker Build') | |
| ); | |
| if (botComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: comment | |
| }); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: comment | |
| }); | |
| } | |
| continue-on-error: true | |
| - name: Final summary | |
| if: always() | |
| run: | | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Build Information:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- Event: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- Branch: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- Commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- Project: ${{ steps.project.outputs.name }} v${{ steps.project.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "- PR Number: #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- PR Tag: pr-${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY | |
| fi |