Skip to content

ci-publish

ci-publish #232

Workflow file for this run

name: ci-publish
# Triggered after the ci workflow completes on a pull_request event.
# This workflow always runs from the default branch (develop), so its code
# is never controlled by a fork contributor. Secrets are only accessed here,
# never in the ci workflow that runs fork-contributed code.
on:
workflow_run:
workflows: ['ci']
types: [completed]
permissions:
contents: read
actions: read
pull-requests: write
env:
REGISTRY: portainerci
IMAGE_NAME: portainer-run
jobs:
publish:
# Only run when the triggering ci workflow succeeded and was itself triggered
# by a pull_request event (not a push or workflow_dispatch).
if: >
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: '[preparation] download image artifact'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pr-image
path: /tmp
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: '[preparation] download image tag artifact'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pr-image-tag
path: /tmp
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: '[preparation] resolve and validate image tag'
id: tag
run: |
# Read tag from artifact written by the unprivileged ci workflow.
# Validate strictly — only allow the expected pr-<number> format to
# prevent injection if the artifact content were ever tampered with.
RAW_TAG=$(cat /tmp/image-tag.txt)
if [[ ! "$RAW_TAG" =~ ^pr-[0-9]+$ ]]; then
echo "::error::Unexpected image tag format: $RAW_TAG"
exit 1
fi
echo "value=$RAW_TAG" >> $GITHUB_OUTPUT
PR_NUMBER="${RAW_TAG#pr-}"
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
- name: '[preparation] set up Docker Buildx'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: '[preparation] log in to registry'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: '[execution] push image to registry'
# Extract the pre-built OCI tarball and push via imagetools create.
# Specify the tag explicitly in the oci-layout reference — Docker 28+ /
# BuildKit v0.29+ no longer resolves an untagged oci-layout reference
# and defaults to :latest, which won't exist in the layout.
# The org.opencontainers.image.ref.name annotation in index.json stores
# only the tag portion (e.g. pr-2), not the full image name, so the tag
# value from the artifact file is the correct reference to use here.
# The fork's code was compiled in the sandboxed ci workflow — we never
# execute it here.
run: |
mkdir /tmp/image-oci
tar -xf /tmp/image.tar -C /tmp/image-oci
docker buildx imagetools create \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.value }} \
"oci-layout:///tmp/image-oci:${{ steps.tag.outputs.value }}"
- name: '[execution] post PR comment'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const prNumber = parseInt('${{ steps.tag.outputs.pr_number }}', 10);
const tag = '${{ steps.tag.outputs.value }}';
const registry = '${{ env.REGISTRY }}';
const image = '${{ env.IMAGE_NAME }}';
const marker = '<!-- ci-publish-image-comment -->';
const body = `${marker}\n> [!NOTE]\n> PR image published: \`${registry}/${image}:${tag}\``;
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) {
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,
});
}