Files
vikunja/.github/workflows/preview.yml
kolaente 1b8d52ced0 fix(ci): use actual docker meta tags for preview comment SHA links
The docker/metadata-action uses github.sha for the SHA tag, which for
pull_request_target events is the base branch commit, not the PR head.
The comment step was independently constructing SHA tags from the PR
head SHA, causing preview URLs that didn't match any actual docker
image tag. Now reads the actual tags from docker meta output instead.
2026-04-07 15:05:48 +00:00

152 lines
6.2 KiB
YAML

name: Preview
on:
# pull_request_target gives write access to GHCR even for PRs from forks.
# This is safe because:
# 1. We explicitly checkout the PR's head commit (no base branch code execution)
# 2. We ONLY build a Docker image (isolated container, no workflow scripts from PR)
# 3. The github-script step only uses safe PR metadata (number, SHA) — no PR-supplied
# text (title, body, commit messages) is interpolated, so there is no injection risk
# 4. Build happens in isolated Docker container with well-defined Dockerfile
pull_request_target:
jobs:
docker:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
pull-requests: write
steps:
- name: Free Disk Space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
large-packages: false
docker-images: false
swap-storage: false
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
# For pull_request_target, we need to explicitly fetch the PR ref from forks
# since the PR's commit SHA is not reachable in the base repository.
# This is safe because no PR code is executed in workflow context.
# Only Docker build uses the PR code (isolated in container).
ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Git describe
id: ghd
uses: proudust/gh-describe@v2
- name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
with:
version: latest
- name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
tags: |
type=ref,event=pr
type=sha,format=long
- name: Build and push PR image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
RELEASE_VERSION=${{ steps.ghd.outputs.describe }}
- name: Comment on PR
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
DOCKER_META_TAGS: ${{ steps.meta.outputs.tags }}
with:
script: |
const prNumber = context.payload.pull_request.number;
const base = 'preview.vikunja.dev';
const image = `ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}`;
const marker = '<!-- vikunja-preview-comment -->';
// Extract the SHA tag from docker meta output (the actual tag pushed to GHCR)
const metaTags = process.env.DOCKER_META_TAGS.split('\n').map(t => t.trim()).filter(Boolean);
const shaImageRef = metaTags.find(t => t.includes(':sha-'));
const shaTag = shaImageRef ? shaImageRef.split(':').pop() : null;
const shortSha = shaTag ? shaTag.replace('sha-', '').substring(0, 7) : context.payload.pull_request.head.sha.substring(0, 7);
const prTag = `pr-${prNumber}`;
const newShaRow = shaTag
? `| https://${shaTag}.${base} | \`${image}:${shaTag}\` | \`${shortSha}\` |`
: '';
// Collect previous SHA rows from existing comment
let previousShaRows = [];
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
previousShaRows = existing.body
.split('\n')
.filter(l => l.includes(`sha-`) && l.includes(`.${base}`));
}
// Remove duplicate if this SHA was already recorded
if (shaTag) {
previousShaRows = previousShaRows.filter(r => !r.includes(shaTag));
}
const allShaRows = [newShaRow, ...previousShaRows].filter(Boolean).join('\n');
const body = [
marker,
`### Preview Deployment`,
``,
`Preview deployments for this PR are available at:`,
``,
`| URL | Tag | Commit |`,
`| --- | --- | --- |`,
`| https://${prTag}.${base} | \`${image}:${prTag}\` | latest |`,
allShaRows,
``,
`The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the \`${prTag}\` image — the preview picks up the new version on restart. The per-commit URLs point to a specific version and will not change.`,
``,
`<details>`,
`<summary>Run locally with Docker</summary>`,
``,
'```bash',
`docker pull ${image}:${prTag}`,
`docker run -p 3456:3456 ${image}:${prTag}`,
'```',
`</details>`,
``,
`_Last updated for commit ${shortSha}_`,
].join('\n');
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}