From c45a464462da870fd83f7f42ec3ed9126ea20cd6 Mon Sep 17 00:00:00 2001 From: ThePhaseless Date: Thu, 20 Nov 2025 01:25:00 +0100 Subject: [PATCH] Add Building Feature (#276) * feat: Build Docker images for PRs with branch name labels - Remove condition preventing PR builds - Add branch name extraction and sanitization - Add branch labels to Docker images (org.opencontainers.image.branch and branch) - Enable pushing of PR Docker images - Sanitize branch names for Docker tags (replace / with -) This allows PR images to be built and tagged with their branch names, making it easier to test specific PR builds. * feat: Add path filters to Docker workflow Only build Docker images when relevant files change: - Source code (src/**, main.py, tests/**) - Docker configuration (Dockerfile, compose.yaml) - Dependencies (pyproject.toml, uv.lock) - Workflow file itself This prevents unnecessary builds when only documentation or other non-functional files are changed. * feat: Add automatic cleanup of PR Docker images Create a new workflow that automatically deletes Docker images when a PR is closed or merged. This prevents accumulation of old PR images in the container registry. Features: - Triggers on PR close/merge events - Deletes images tagged with PR number and SHA - Handles both architecture variants (amd64, arm64) - Supports both organization and user repositories - Provides detailed logging of cleanup operations * refactor: Remove redundant branch label preparation Remove duplicate branch name preparation step in merge-and-push job. Branch labels are already added during the build step, so no need to add them again when creating the manifest. * refactor: Remove redundant label configurations Remove custom label configurations from docker-publish workflow. The docker/metadata-action already sets standard OCI labels by default, so explicit label configuration is unnecessary. Also removed unused BRANCH_TAG variable preparation. * refactor: Use PR number for Docker image tags instead of SHA Changed Docker image tagging strategy for pull requests: - Use pr-{number}-{arch} for individual platform builds - Use pr-{number} for final manifest - Non-PR builds still use SHA-based tags Benefits: - Simpler, more readable tags for PRs - Easier to identify which PR an image belongs to - Cleanup script simplified to match only PR number tags Updated cleanup workflow to match new tag pattern. * docs: Add PR Docker image tags to README Document the pr-{number} tag pattern used for pull request images. These images are automatically built for PRs and cleaned up when the PR is closed. --------- Co-authored-by: Claude --- .github/workflows/docker-cleanup.yml | 107 +++++++++++++++++++++++++++ .github/workflows/docker-publish.yml | 45 +++++++++-- README.md | 1 + 3 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/docker-cleanup.yml diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml new file mode 100644 index 0000000..0e5db91 --- /dev/null +++ b/.github/workflows/docker-cleanup.yml @@ -0,0 +1,107 @@ +name: Cleanup PR Docker Images + +on: + pull_request: + types: [closed] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - name: Delete PR Docker images + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner.toLowerCase(); + const repo = context.repo.repo.toLowerCase(); + const prNumber = context.payload.pull_request.number; + + // Get the package + const packageName = `${repo}`; + + console.log(`Cleaning up images for PR #${prNumber}`); + + // Check if owner is an org or user + let isOrg = false; + try { + await github.rest.orgs.get({ org: owner }); + isOrg = true; + console.log(`Repository owner is an organization`); + } catch (error) { + console.log(`Repository owner is a user account`); + } + + try { + // Get all versions of the package + let versions; + if (isOrg) { + versions = await github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg({ + package_type: 'container', + package_name: packageName, + org: owner, + per_page: 100 + }); + } else { + versions = await github.rest.packages.getAllPackageVersionsForPackageOwnedByUser({ + package_type: 'container', + package_name: packageName, + username: owner, + per_page: 100 + }); + } + + // Filter versions that match this PR (pr-123, pr-123-amd64, pr-123-arm64) + const prVersions = versions.data.filter(version => { + const tags = version.metadata?.container?.tags || []; + // Match tags like: pr-123, pr-123-amd64, pr-123-arm64 + return tags.some(tag => + tag === `pr-${prNumber}` || + tag === `pr-${prNumber}-amd64` || + tag === `pr-${prNumber}-arm64` + ); + }); + + console.log(`Found ${prVersions.length} image version(s) to delete`); + + // Delete each version + for (const version of prVersions) { + console.log(`Deleting version ${version.id} with tags: ${version.metadata?.container?.tags?.join(', ')}`); + try { + if (isOrg) { + await github.rest.packages.deletePackageVersionForOrg({ + package_type: 'container', + package_name: packageName, + org: owner, + package_version_id: version.id + }); + } else { + await github.rest.packages.deletePackageVersionForUser({ + package_type: 'container', + package_name: packageName, + username: owner, + package_version_id: version.id + }); + } + console.log(`✓ Deleted version ${version.id}`); + } catch (error) { + console.error(`Failed to delete version ${version.id}:`, error.message); + } + } + + console.log('Cleanup completed'); + } catch (error) { + if (error.status === 404) { + console.log('No package found - nothing to clean up'); + } else { + console.error('Error during cleanup:', error.message); + throw error; + } + } diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7f07913..6035144 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -12,8 +12,26 @@ on: branches: ["*"] # Publish semver tags as releases. tags: ["v*.*.*"] + paths: + - "src/**" + - "main.py" + - "tests/**" + - "Dockerfile" + - "pyproject.toml" + - "uv.lock" + - "compose.yaml" + - ".github/workflows/docker-publish.yml" pull_request: branches: ["main"] + paths: + - "src/**" + - "main.py" + - "tests/**" + - "Dockerfile" + - "pyproject.toml" + - "uv.lock" + - "compose.yaml" + - ".github/workflows/docker-publish.yml" workflow_dispatch: env: @@ -54,7 +72,6 @@ jobs: build: needs: test - if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: read @@ -74,8 +91,13 @@ jobs: run: | SURFIX=$(echo ${{ matrix.platform }} | cut -d'/' -f2) echo "SURFIX=$SURFIX" >> $GITHUB_OUTPUT - # Generate a unique local tag for the image - echo "LOCAL_TAG=${{ github.sha }}-$SURFIX" >> $GITHUB_OUTPUT + + # Use PR number for PRs, SHA for everything else + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "LOCAL_TAG=pr-${{ github.event.pull_request.number }}-$SURFIX" >> $GITHUB_OUTPUT + else + echo "LOCAL_TAG=${{ github.sha }}-$SURFIX" >> $GITHUB_OUTPUT + fi - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -100,14 +122,14 @@ jobs: tags: type=raw,value=${{ steps.vars.outputs.LOCAL_TAG }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # Build and export Docker image for each platform (without pushing) + # Build and push Docker image for each platform - name: Build Docker image id: build uses: docker/build-push-action@v6 with: context: . pull: true - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: ${{ matrix.platform }} @@ -178,9 +200,16 @@ jobs: echo $args - docker buildx imagetools create $args \ - ${image}:${{github.sha}}-amd64 \ - ${image}:${{github.sha}}-arm64 + # Use PR-based tags for PRs, SHA-based tags for everything else + if [ "${{ github.event_name }}" == "pull_request" ]; then + docker buildx imagetools create $args \ + ${image}:pr-${{ github.event.pull_request.number }}-amd64 \ + ${image}:pr-${{ github.event.pull_request.number }}-arm64 + else + docker buildx imagetools create $args \ + ${image}:${{github.sha}}-amd64 \ + ${image}:${{github.sha}}-arm64 + fi # Sign the manifest - name: Sign the manifests diff --git a/README.md b/README.md index e83bc60..05f2453 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Recently I've partnered with a _new in town_ proxy service - ProxyBase - to offe - `v*.*.*`/`latest` - Releases considered stable - `main` - Latest release from main branch (untested) +- `pr-{number}` - Pull request images for testing (automatically cleaned up when PR closes) ## Usage