From f2e153b2308fb2dc812860d22a9722fcf02ae1fe Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Sat, 18 Oct 2025 13:07:15 -0300 Subject: [PATCH] Add GitHub Container Registry (GHCR) support (#163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GHCR support with conditional Docker Hub publishing This commit enhances the CI/CD pipeline to support both GitHub Container Registry (GHCR) and Docker Hub, with Docker Hub being optional based on the presence of credentials. Changes: - Add GHCR as the primary container registry - Make Docker Hub publishing conditional on DOCKER_USERNAME and DOCKER_PASSWORD secrets - Dynamically determine image names from repository owner/name (e.g., aperim/open-notebook) - Images are pushed to: * GHCR: ghcr.io/{owner}/{repo}:{version|latest} * Docker Hub (if credentials available): {owner}/{repo}:{version|latest} - Update build summary to show which registries were used Benefits: - Forks can build and publish to GHCR without Docker Hub credentials - Original repo can continue publishing to both registries - Image names automatically match the repository structure - More flexible deployment options for contributors Technical Details: - Added extract-version job outputs: ghcr_image, dockerhub_image, has_dockerhub_secrets - Added GHCR login step using GITHUB_TOKEN (always runs) - Made Docker Hub login conditional on has_dockerhub_secrets flag - Updated image tags to use dynamic repository-based names - Enhanced build summary to show registry usage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Add GITHUB_TOKEN permissions for GHCR publishing The workflow needs 'packages: write' permission to push images to GitHub Container Registry (GHCR). Permissions added: - contents: read (required for checkout) - packages: write (required for GHCR push) Without these permissions, the docker login and push to ghcr.io would fail with a 403 Forbidden error. --------- Co-authored-by: Troy Kelly Co-authored-by: Claude --- .github/workflows/build-and-release.yml | 102 +++++++++++++++++++++--- 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 82b1161..fd5e362 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -20,19 +20,27 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: - REGISTRY: docker.io - IMAGE_NAME: lfnovo/open_notebook + # Dynamic registry and image configuration based on repository + GHCR_REGISTRY: ghcr.io + DOCKER_HUB_REGISTRY: docker.io jobs: extract-version: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} + ghcr_image: ${{ steps.image.outputs.ghcr_image }} + dockerhub_image: ${{ steps.image.outputs.dockerhub_image }} + has_dockerhub_secrets: ${{ steps.check.outputs.has_dockerhub_secrets }} steps: - name: Checkout uses: actions/checkout@v4 - + - name: Extract version from pyproject.toml id: version run: | @@ -40,6 +48,34 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Extracted version: $VERSION" + - name: Determine image names + id: image + run: | + # Extract owner and repo name from GITHUB_REPOSITORY (format: owner/repo) + REPO_OWNER=$(echo "${{ github.repository }}" | cut -d'/' -f1) + REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) + + # GHCR image: ghcr.io/owner/repo + GHCR_IMAGE="${{ env.GHCR_REGISTRY }}/${REPO_OWNER}/${REPO_NAME}" + echo "ghcr_image=${GHCR_IMAGE}" >> $GITHUB_OUTPUT + echo "GHCR Image: ${GHCR_IMAGE}" + + # Docker Hub image: owner/repo (use lowercase for Docker Hub compatibility) + DOCKERHUB_IMAGE="${REPO_OWNER}/${REPO_NAME}" + echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT + echo "Docker Hub Image: ${DOCKERHUB_IMAGE}" + + - name: Check for Docker Hub credentials + id: check + run: | + if [[ -n "${{ secrets.DOCKER_USERNAME }}" && -n "${{ secrets.DOCKER_PASSWORD }}" ]]; then + echo "has_dockerhub_secrets=true" >> $GITHUB_OUTPUT + echo "Docker Hub credentials available" + else + echo "has_dockerhub_secrets=false" >> $GITHUB_OUTPUT + echo "Docker Hub credentials not available - will only push to GHCR" + fi + build-regular: needs: extract-version runs-on: ubuntu-latest @@ -51,7 +87,15 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Docker Hub + if: needs.extract-version.outputs.has_dockerhub_secrets == 'true' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -73,8 +117,10 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: | - ${{ env.IMAGE_NAME }}:${{ needs.extract-version.outputs.version }} - ${{ (github.event.inputs.push_latest == 'true' || (github.event_name == 'release' && !github.event.release.prerelease)) && format('{0}:v1-latest', env.IMAGE_NAME) || '' }} + ${{ needs.extract-version.outputs.ghcr_image }}:${{ needs.extract-version.outputs.version }} + ${{ (github.event.inputs.push_latest == 'true' || (github.event_name == 'release' && !github.event.release.prerelease)) && format('{0}:v1-latest', needs.extract-version.outputs.ghcr_image) || '' }} + ${{ needs.extract-version.outputs.has_dockerhub_secrets == 'true' && format('{0}:{1}', needs.extract-version.outputs.dockerhub_image, needs.extract-version.outputs.version) || '' }} + ${{ (needs.extract-version.outputs.has_dockerhub_secrets == 'true' && (github.event.inputs.push_latest == 'true' || (github.event_name == 'release' && !github.event.release.prerelease))) && format('{0}:v1-latest', needs.extract-version.outputs.dockerhub_image) || '' }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max @@ -94,7 +140,15 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Docker Hub + if: needs.extract-version.outputs.has_dockerhub_secrets == 'true' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -116,8 +170,10 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: | - ${{ env.IMAGE_NAME }}:${{ needs.extract-version.outputs.version }}-single - ${{ (github.event.inputs.push_latest == 'true' || (github.event_name == 'release' && !github.event.release.prerelease)) && format('{0}:v1-latest-single', env.IMAGE_NAME) || '' }} + ${{ needs.extract-version.outputs.ghcr_image }}:${{ needs.extract-version.outputs.version }}-single + ${{ (github.event.inputs.push_latest == 'true' || (github.event_name == 'release' && !github.event.release.prerelease)) && format('{0}:v1-latest-single', needs.extract-version.outputs.ghcr_image) || '' }} + ${{ needs.extract-version.outputs.has_dockerhub_secrets == 'true' && format('{0}:{1}-single', needs.extract-version.outputs.dockerhub_image, needs.extract-version.outputs.version) || '' }} + ${{ (needs.extract-version.outputs.has_dockerhub_secrets == 'true' && (github.event.inputs.push_latest == 'true' || (github.event_name == 'release' && !github.event.release.prerelease))) && format('{0}:v1-latest-single', needs.extract-version.outputs.dockerhub_image) || '' }} cache-from: type=local,src=/tmp/.buildx-cache-single cache-to: type=local,dest=/tmp/.buildx-cache-single-new,mode=max @@ -138,12 +194,26 @@ jobs: echo "**Build Type:** ${{ github.event.inputs.build_type || 'both' }}" >> $GITHUB_STEP_SUMMARY echo "**Push Latest:** ${{ github.event.inputs.push_latest || 'true' }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "### Registries:" >> $GITHUB_STEP_SUMMARY + echo "✅ **GHCR:** \`${{ needs.extract-version.outputs.ghcr_image }}\`" >> $GITHUB_STEP_SUMMARY + if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then + echo "✅ **Docker Hub:** \`${{ needs.extract-version.outputs.dockerhub_image }}\`" >> $GITHUB_STEP_SUMMARY + else + echo "⏭️ **Docker Hub:** Skipped (credentials not configured)" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY echo "### Images Built:" >> $GITHUB_STEP_SUMMARY - + if [[ "${{ needs.build-regular.result }}" == "success" ]]; then - echo "✅ **Regular:** \`${{ env.IMAGE_NAME }}:${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "✅ **Regular (GHCR):** \`${{ needs.extract-version.outputs.ghcr_image }}:${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY if [[ "${{ github.event.inputs.push_latest }}" == "true" ]]; then - echo "✅ **Regular v1-Latest:** \`${{ env.IMAGE_NAME }}:v1-latest\`" >> $GITHUB_STEP_SUMMARY + echo "✅ **Regular v1-Latest (GHCR):** \`${{ needs.extract-version.outputs.ghcr_image }}:v1-latest\`" >> $GITHUB_STEP_SUMMARY + fi + if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then + echo "✅ **Regular (Docker Hub):** \`${{ needs.extract-version.outputs.dockerhub_image }}:${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + if [[ "${{ github.event.inputs.push_latest }}" == "true" ]]; then + echo "✅ **Regular v1-Latest (Docker Hub):** \`${{ needs.extract-version.outputs.dockerhub_image }}:v1-latest\`" >> $GITHUB_STEP_SUMMARY + fi fi elif [[ "${{ needs.build-regular.result }}" == "skipped" ]]; then echo "⏭️ **Regular:** Skipped" >> $GITHUB_STEP_SUMMARY @@ -152,16 +222,22 @@ jobs: fi if [[ "${{ needs.build-single.result }}" == "success" ]]; then - echo "✅ **Single:** \`${{ env.IMAGE_NAME }}:${{ needs.extract-version.outputs.version }}-single\`" >> $GITHUB_STEP_SUMMARY + echo "✅ **Single (GHCR):** \`${{ needs.extract-version.outputs.ghcr_image }}:${{ needs.extract-version.outputs.version }}-single\`" >> $GITHUB_STEP_SUMMARY if [[ "${{ github.event.inputs.push_latest }}" == "true" ]]; then - echo "✅ **Single v1-Latest:** \`${{ env.IMAGE_NAME }}:v1-latest-single\`" >> $GITHUB_STEP_SUMMARY + echo "✅ **Single v1-Latest (GHCR):** \`${{ needs.extract-version.outputs.ghcr_image }}:v1-latest-single\`" >> $GITHUB_STEP_SUMMARY + fi + if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then + echo "✅ **Single (Docker Hub):** \`${{ needs.extract-version.outputs.dockerhub_image }}:${{ needs.extract-version.outputs.version }}-single\`" >> $GITHUB_STEP_SUMMARY + if [[ "${{ github.event.inputs.push_latest }}" == "true" ]]; then + echo "✅ **Single v1-Latest (Docker Hub):** \`${{ needs.extract-version.outputs.dockerhub_image }}:v1-latest-single\`" >> $GITHUB_STEP_SUMMARY + fi fi elif [[ "${{ needs.build-single.result }}" == "skipped" ]]; then echo "⏭️ **Single:** Skipped" >> $GITHUB_STEP_SUMMARY else echo "❌ **Single:** Failed" >> $GITHUB_STEP_SUMMARY fi - + echo "" >> $GITHUB_STEP_SUMMARY echo "### Platforms:" >> $GITHUB_STEP_SUMMARY echo "- linux/amd64" >> $GITHUB_STEP_SUMMARY