diff --git a/.github/workflows/build-image-pr.yml b/.github/workflows/build-image-pr.yml new file mode 100644 index 000000000..d1168b767 --- /dev/null +++ b/.github/workflows/build-image-pr.yml @@ -0,0 +1,74 @@ +name: Build a test image from a PR code + +on: + issue_comment: + types: [created] + +jobs: + build-images: + if: >- + github.event.issue.pull_request && + contains(github.event.comment.body, '/create-test-image') && + contains(fromJSON('["OWNER", "COLLABORATOR"]'), github.event.comment.author_association) + + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + packages: write + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + + - name: Get Pull Request Information + id: pr + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b + with: + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + core.setOutput('sha', pr.data.head.sha); + core.setOutput('ref', pr.data.head.ref); + + - name: Checkout PR commit + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ steps.pr.outputs.sha }} + + - name: Set lowercase image owner + id: vars + run: | + echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build & Push Docker image + run: | + SHA=$(echo "${{ steps.pr.outputs.sha }}" | cut -c1-7) + VERSION=pr-${{ github.event.issue.number }}-$SHA + OWNER=${{ steps.vars.outputs.owner }} + + echo "Building image version: $VERSION" + + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --file Dockerfile \ + --target final \ + --build-arg APP_VERSION=$VERSION \ + --tag ghcr.io/$OWNER/leafwiki:$VERSION \ + --annotation "index:org.opencontainers.image.title=LeafWiki" \ + --annotation "index:org.opencontainers.image.description=LeafWiki – A fast wiki for people who think in folders, not feeds" \ + --sbom=true \ + --provenance=mode=max \ + --push . diff --git a/.github/workflows/cleanup-pr-image.yml b/.github/workflows/cleanup-pr-image.yml new file mode 100644 index 000000000..3e459f8bd --- /dev/null +++ b/.github/workflows/cleanup-pr-image.yml @@ -0,0 +1,99 @@ +name: Cleanup PR test images + +on: + pull_request_target: + types: [closed] + + schedule: + - cron: "0 3 1 * *" + +permissions: + packages: write + +jobs: + cleanup-closed-pr: + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + + steps: + - name: Delete closed PR images + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b + with: + script: | + const owner = context.repo.owner; + const packageName = context.repo.repo.toLowerCase(); + const prefix = `pr-${context.payload.pull_request.number}-`; + + const versions = await github.paginate( + github.rest.packages.getAllPackageVersionsForPackageOwnedByUser, + { + username: owner, + package_type: "container", + package_name: packageName, + per_page: 100, + } + ); + + for (const version of versions) { + const tags = version.metadata?.container?.tags ?? []; + + if (!tags.some(tag => tag.startsWith(prefix))) { + continue; + } + + core.info(`Deleting image: ${tags.join(", ")}`); + + await github.rest.packages.deletePackageVersionForUser({ + username: owner, + package_type: "container", + package_name: packageName, + package_version_id: version.id, + }); + } + + + cleanup-old-images: + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + + steps: + - name: Delete old PR images + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b + with: + script: | + const owner = context.repo.owner; + const packageName = context.repo.repo.toLowerCase(); + const retentionDays = 30; + + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - retentionDays); + + const versions = await github.paginate( + github.rest.packages.getAllPackageVersionsForPackageOwnedByUser, + { + username: owner, + package_type: "container", + package_name: packageName, + per_page: 100, + } + ); + + for (const version of versions) { + const tags = version.metadata?.container?.tags ?? []; + + const isPrImage = tags.some(tag => tag.startsWith("pr-")); + const createdAt = new Date(version.created_at); + + if (!isPrImage || createdAt > cutoff) { + continue; + } + + core.info(`Deleting old image: ${tags.join(", ")}`); + + await github.rest.packages.deletePackageVersionForUser({ + username: owner, + package_type: "container", + package_name: packageName, + package_version_id: version.id, + }); + }