Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/ci-publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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
pull-requests: write

env:
REGISTRY: portainerci
IMAGE_NAME: d2k

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@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.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@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.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.
# When given an oci-layout:// source, imagetools uploads all blobs and
# platform manifests from the local layout then creates the manifest
# index — correctly handling the multi-arch image index end-to-end.
# 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

- name: "[execution] post PR comment"
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
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 }}';

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `> [!NOTE]\n> PR image published: \`${registry}/${image}:${tag}\``,
});
47 changes: 36 additions & 11 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ on:

permissions:
contents: read
pull-requests: write
id-token: write

env:
REGISTRY: portainerci
IMAGE_NAME: d2k

jobs:
Expand All @@ -39,23 +37,16 @@ jobs:
- name: "[preparation] checkout the current branch"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: "[preparation] tidy modules"
- name: "[preparation] set up golang"
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: go.mod

- name: "[preparation] verify go mod tidy"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go mod tidy
if ! git diff --exit-code go.mod go.sum > /dev/null 2>&1; then
echo "::warning::go.mod or go.sum is out of date — please run 'go mod tidy' locally and commit the changes"
if [ "${{ github.event_name }}" = "pull_request" ]; then
gh pr comment ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--body $'> [!WARNING]\n> `go.mod` or `go.sum` is out of date. Please run `go mod tidy` locally and commit the changes.'
fi
fi

- name: "[preparation] set up QEMU"
Expand All @@ -64,18 +55,52 @@ jobs:
- name: "[preparation] set up Docker Buildx"
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

- name: "[execution] build multi-arch image and export as OCI tarball"
if: github.event_name == 'pull_request'
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
push: false
load: false
platforms: linux/amd64,linux/arm64
build-args: VERSION=${{ github.sha }}
tags: ${{ env.IMAGE_NAME }}:${{ needs.tag.outputs.value }}
outputs: type=oci,dest=/tmp/image.tar

- name: "[execution] save image tag metadata"
if: github.event_name == 'pull_request'
run: |
echo "${{ needs.tag.outputs.value }}" > /tmp/image-tag.txt

- name: "[execution] upload image artifact"
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-image
path: /tmp/image.tar
retention-days: 1

- name: "[execution] upload image tag artifact"
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-image-tag
path: /tmp/image-tag.txt
retention-days: 1

- name: "[preparation] log in to registry"
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: "[execution] build and push multi-arch image"
if: github.event_name != 'pull_request'
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
push: true
platforms: linux/amd64,linux/arm64
build-args: VERSION=${{ github.sha }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.tag.outputs.value }}
tags: portainerci/${{ env.IMAGE_NAME }}:${{ needs.tag.outputs.value }}
sbom: true
provenance: mode=max
Loading