From ea5b73ddc3e99ca715ffab6d2bea80ba9cd9af0b Mon Sep 17 00:00:00 2001 From: Freaks Date: Sun, 12 Jul 2026 19:43:14 +0200 Subject: [PATCH 1/4] feat(chatops): add /command for build test pr in PR --- .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/workflows/build-image-pr.yml | 65 +++++++++++++++++ .github/workflows/cleanup-pr-image.yml | 99 ++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 .github/workflows/build-image-pr.yml create mode 100644 .github/workflows/cleanup-pr-image.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a39fb2509..77a07a88f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,6 +3,8 @@ Fixes # + + ## What changed diff --git a/.github/workflows/build-image-pr.yml b/.github/workflows/build-image-pr.yml new file mode 100644 index 000000000..60f0c323b --- /dev/null +++ b/.github/workflows/build-image-pr.yml @@ -0,0 +1,65 @@ +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') && + github.event.comment.author_association == 'OWNER' + + 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 + + echo "Building image version: $VERSION" + + make docker-build-publish \ + VERSION=$VERSION \ + REPO_OWNER=${{ steps.vars.outputs.owner }} \ No newline at end of file diff --git a/.github/workflows/cleanup-pr-image.yml b/.github/workflows/cleanup-pr-image.yml new file mode 100644 index 000000000..6fc601387 --- /dev/null +++ b/.github/workflows/cleanup-pr-image.yml @@ -0,0 +1,99 @@ +name: Cleanup PR test images + +on: + pull_request: + types: [closed] + + schedule: + - cron: "0 3 1 * *" + +permissions: + packages: write + +jobs: + cleanup-closed-pr: + if: github.event_name == 'pull_request' + 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, + }); + } \ No newline at end of file From e96fc517ad1498f1d49ef7ee78be72b75d8d36a2 Mon Sep 17 00:00:00 2001 From: Patrick Erber Date: Wed, 23 Sep 2026 11:47:26 +0200 Subject: [PATCH 2/4] Update PR template to remove test image instructions Removed instructions for building a test image from the PR template. --- .github/PULL_REQUEST_TEMPLATE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 77a07a88f..a39fb2509 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,8 +3,6 @@ Fixes # - - ## What changed From 85385d89c0da2a75196bbf008234a98dfcdcb2c0 Mon Sep 17 00:00:00 2001 From: Patrick Erber Date: Fri, 25 Sep 2026 15:48:26 +0200 Subject: [PATCH 3/4] review: prevent latest-tag clobber, tighten trigger perms, avoid running PR-controlled Makefile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /create-test-image no longer tags :latest on ghcr.io — it inlines the docker buildx build/push command instead of calling `make docker-build-publish`, so the PR's own Makefile is never executed on the runner with a packages:write token (pwn-request pattern: the checked-out PR code could otherwise redefine that Make target to exfiltrate the token that docker/login-action already wrote to disk). - Broaden the trigger gate from author_association == 'OWNER' to also allow COLLABORATOR, matching "owner or someone with write access to the project" rather than literally only the single OWNER association. - Add trailing newlines to both new workflow files. --- .github/workflows/build-image-pr.yml | 17 +++++++++++++---- .github/workflows/cleanup-pr-image.yml | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-image-pr.yml b/.github/workflows/build-image-pr.yml index 60f0c323b..d1168b767 100644 --- a/.github/workflows/build-image-pr.yml +++ b/.github/workflows/build-image-pr.yml @@ -9,7 +9,7 @@ jobs: if: >- github.event.issue.pull_request && contains(github.event.comment.body, '/create-test-image') && - github.event.comment.author_association == 'OWNER' + contains(fromJSON('["OWNER", "COLLABORATOR"]'), github.event.comment.author_association) runs-on: ubuntu-latest @@ -57,9 +57,18 @@ jobs: 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" - make docker-build-publish \ - VERSION=$VERSION \ - REPO_OWNER=${{ steps.vars.outputs.owner }} \ No newline at end of file + 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 index 6fc601387..3532c9fa6 100644 --- a/.github/workflows/cleanup-pr-image.yml +++ b/.github/workflows/cleanup-pr-image.yml @@ -96,4 +96,4 @@ jobs: package_name: packageName, package_version_id: version.id, }); - } \ No newline at end of file + } From db3d9c3cdfba464baecfde895bb6a14a0de5e7fa Mon Sep 17 00:00:00 2001 From: Patrick Erber Date: Fri, 25 Sep 2026 16:00:45 +0200 Subject: [PATCH 4/4] review: use pull_request_target so cleanup-on-close actually gets a write token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pull_request (non-target) events from a fork PR get a read-only GITHUB_TOKEN regardless of the permissions: block, unless the repo has opted in to write tokens for fork PRs (off by default). Since this job never checks out or executes PR code — it only reads the PR number from the trusted event payload to delete matching ghcr.io package versions — pull_request_target is the safe way to get a write token here without the checkout-and-run risk that pattern normally carries. --- .github/workflows/cleanup-pr-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cleanup-pr-image.yml b/.github/workflows/cleanup-pr-image.yml index 3532c9fa6..3e459f8bd 100644 --- a/.github/workflows/cleanup-pr-image.yml +++ b/.github/workflows/cleanup-pr-image.yml @@ -1,7 +1,7 @@ name: Cleanup PR test images on: - pull_request: + pull_request_target: types: [closed] schedule: @@ -12,7 +12,7 @@ permissions: jobs: cleanup-closed-pr: - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: