Skip to content

PR Release Publish #1851

PR Release Publish

PR Release Publish #1851

name: PR Release Publish
on:
workflow_run:
workflows:
- PR CI
types:
- completed
concurrency:
# Serialize publishes of the same PR branch; per-run-id groups let them race each other.
group: pr-release-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
jobs:
publish:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
permissions:
actions: read
contents: none
pull-requests: read
env:
SOURCE_REPO: ${{ github.repository }}
TARGET_REPO: WinNative-Emu/WinNative-CI
RUN_ID: ${{ github.event.workflow_run.id }}
HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
steps:
- name: Resolve PR metadata and scope
id: scope
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
head_owner=$(printf '%s' "$HEAD_REPO" | cut -d/ -f1)
pr_number=$(
gh pr list \
--repo "$SOURCE_REPO" \
--state all \
--head "$HEAD_BRANCH" \
--json number,headRefOid,headRepositoryOwner,baseRefName \
--jq '
map(
select(.headRepositoryOwner.login == "'"$head_owner"'")
| select(.headRefOid == "'"$HEAD_SHA"'")
)
| first
| .number // ""
'
)
if [ -z "$pr_number" ]; then
echo "allowed=false" >> "$GITHUB_OUTPUT"
echo "Unable to resolve PR for head repo $HEAD_REPO branch $HEAD_BRANCH sha $HEAD_SHA"
exit 0
fi
gh api "repos/$SOURCE_REPO/pulls/$pr_number" > pr.json
head_repo=$(jq -r '.head.repo.full_name // ""' pr.json)
base_repo=$(jq -r '.base.repo.full_name // ""' pr.json)
if [ "$base_repo" = "$SOURCE_REPO" ]; then
echo "allowed=true" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
else
echo "allowed=false" >> "$GITHUB_OUTPUT"
echo "Skipping publish for PR targeting a different repository: head=$head_repo base=$base_repo"
fi
- name: Compute expected APK names
if: ${{ steps.scope.outputs.allowed == 'true' }}
id: apk
env:
PR_NUMBER: ${{ steps.scope.outputs.pr_number }}
run: |
short_sha=$(jq -r '.head.sha' pr.json | cut -c1-5)
label="PR#${PR_NUMBER}-${short_sha}"
release_label=$(printf '%s' "$label" | tr '#' '.')
{
echo "label=$label"
echo "standard=WinNative-Debug-Standard-${label}.apk"
echo "ludashi=WinNative-Debug-Ludashi-${label}.apk"
echo "pubg=WinNative-Debug-Pubg-${label}.apk"
echo "antutu=WinNative-Debug-Antutu-${label}.apk"
echo "release_standard=WinNative-Debug-Standard-${release_label}.apk"
echo "release_ludashi=WinNative-Debug-Ludashi-${release_label}.apk"
echo "release_pubg=WinNative-Debug-Pubg-${release_label}.apk"
echo "release_antutu=WinNative-Debug-Antutu-${release_label}.apk"
} >> "$GITHUB_OUTPUT"
- name: Download PR build artifacts
if: ${{ steps.scope.outputs.allowed == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p artifacts
# Download all flavors in parallel; the sequential form left the runner's network idle per transfer.
pids=()
for flavor in standard ludashi pubg antutu; do
gh run download "$RUN_ID" --repo "$SOURCE_REPO" --name "apk-$flavor" --dir "artifacts/$flavor" &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid"
done
- name: Build release notes from PR metadata
if: ${{ steps.scope.outputs.allowed == 'true' }}
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.scope.outputs.pr_number }}
run: |
pr_title=$(jq -r '.title' pr.json)
pr_author=$(jq -r '.user.login // "unknown"' pr.json)
pr_url=$(jq -r '.html_url' pr.json)
pr_body=$(jq -r '.body // ""' pr.json)
head_ref=$(jq -r '.head.ref' pr.json)
short_sha=$(jq -r '.head.sha' pr.json | cut -c1-5)
safe_title=$(printf '%s' "$pr_title" | tr '\r\n' ' ' | sed 's/[[:space:]]\+/ /g')
thread_name=$(printf '[#%s] %s' "$PR_NUMBER" "$safe_title" | cut -c1-100)
run_url="https://github.com/$SOURCE_REPO/actions/runs/$RUN_ID"
{
echo "title=$safe_title"
echo "author=$pr_author"
echo "url=$pr_url"
echo "tag=pr-$PR_NUMBER"
echo "thread_name=$thread_name"
} >> "$GITHUB_OUTPUT"
{
printf '# PR #%s\n\n' "$PR_NUMBER"
printf 'Title: %s\n\n' "$safe_title"
printf 'Author: @%s\n\n' "$pr_author"
printf 'Source PR: %s\n' "$pr_url"
printf 'Source branch: %s\n' "$head_ref"
printf 'Source commit: `%s`\n' "$HEAD_SHA"
printf 'Build workflow run: %s\n\n' "$run_url"
printf '## What This PR Is About\n\n'
if [ -n "$pr_body" ]; then
printf '%s\n' "$pr_body"
else
printf 'No PR description was provided.\n'
fi
printf '\n## Attached Artifacts\n\n'
printf -- '%s\n' "- ${{ steps.apk.outputs.release_standard }}"
printf -- '%s\n' "- ${{ steps.apk.outputs.release_ludashi }}"
printf -- '%s\n' "- ${{ steps.apk.outputs.release_pubg }}"
printf -- '%s\n' "- ${{ steps.apk.outputs.release_antutu }}"
printf '\nBuilt from source commit `%s`.\n' "$short_sha"
} > release-notes.md
- name: Create or update release in WinNative-CI
if: ${{ steps.scope.outputs.allowed == 'true' }}
id: release
env:
# Use a fine-grained PAT scoped only to WinNative-Emu/WinNative-CI
# with "Contents" repository permission set to write.
GH_TOKEN: ${{ secrets.WINNATIVE_CI_TOKEN }}
PR_NUMBER: ${{ steps.scope.outputs.pr_number }}
run: |
tag="${{ steps.pr.outputs.tag }}"
title="PR #$PR_NUMBER: ${{ steps.pr.outputs.title }}"
discord_marker='<!-- discord-posted -->'
discord_posted=false
# A transient API error must not be read as "release doesn't exist" — that ends in
# a create over an existing tag (HTTP 422).
if view_out=$(gh api "repos/$TARGET_REPO/releases/tags/$tag" 2>&1); then
release_exists=true
elif printf '%s' "$view_out" | grep -qi 'Not Found'; then
release_exists=false
else
printf '%s\n' "$view_out"
exit 1
fi
if [ "$release_exists" = true ]; then
created=false
existing_body=$(printf '%s' "$view_out" | jq -r '.body // ""')
if printf '%s' "$existing_body" | grep -Fq "$discord_marker"; then
discord_posted=true
if ! grep -Fq "$discord_marker" release-notes.md; then
printf '\n%s\n' "$discord_marker" >> release-notes.md
fi
fi
gh release edit "$tag" \
--repo "$TARGET_REPO" \
--title "$title" \
--notes-file release-notes.md \
--prerelease
else
created=true
if ! gh release create "$tag" \
--repo "$TARGET_REPO" \
--title "$title" \
--notes-file release-notes.md \
--prerelease; then
# Another publish run created it between our check and the create — update instead.
created=false
gh release edit "$tag" \
--repo "$TARGET_REPO" \
--title "$title" \
--notes-file release-notes.md \
--prerelease
fi
fi
standard_path="artifacts/standard/${{ steps.apk.outputs.standard }}"
ludashi_path="artifacts/ludashi/${{ steps.apk.outputs.ludashi }}"
pubg_path="artifacts/pubg/${{ steps.apk.outputs.pubg }}"
antutu_path="artifacts/antutu/${{ steps.apk.outputs.antutu }}"
release_standard_path="release-assets/${{ steps.apk.outputs.release_standard }}"
release_ludashi_path="release-assets/${{ steps.apk.outputs.release_ludashi }}"
release_pubg_path="release-assets/${{ steps.apk.outputs.release_pubg }}"
release_antutu_path="release-assets/${{ steps.apk.outputs.release_antutu }}"
test -f "$standard_path"
test -f "$ludashi_path"
test -f "$pubg_path"
test -f "$antutu_path"
mkdir -p release-assets
cp "$standard_path" "$release_standard_path"
cp "$ludashi_path" "$release_ludashi_path"
cp "$pubg_path" "$release_pubg_path"
cp "$antutu_path" "$release_antutu_path"
release_json=$(gh api "repos/$TARGET_REPO/releases/tags/$tag")
existing_apk_assets=$(
printf '%s' "$release_json" |
jq '[.assets[]? | select(.name | endswith(".apk"))] | length'
)
upload_asset() {
local file_path="$1"
local asset_name
asset_name=$(basename "$file_path")
local asset_prefix
asset_prefix=$(printf '%s' "$asset_name" | sed -E 's/PR\.[0-9]+-[A-Za-z0-9]+\.apk$//')
local existing_asset_ids
existing_asset_ids=$(
printf '%s' "$release_json" |
jq -r --arg prefix "$asset_prefix" '
.assets[]?
| select(.name | startswith($prefix))
| select(.name | endswith(".apk"))
| .id
'
)
if [ -n "$existing_asset_ids" ]; then
printf '%s\n' "$existing_asset_ids" | while IFS= read -r existing_asset_id; do
[ -n "$existing_asset_id" ] || continue
gh api --method DELETE "repos/$TARGET_REPO/releases/assets/$existing_asset_id" >/dev/null
done
fi
gh release upload "$tag" "$file_path" --repo "$TARGET_REPO"
}
upload_asset "$release_standard_path"
upload_asset "$release_ludashi_path"
upload_asset "$release_pubg_path"
upload_asset "$release_antutu_path"
release_url=$(gh release view "$tag" --repo "$TARGET_REPO" --json url --jq '.url')
{
echo "created=$created"
echo "discord_posted=$discord_posted"
echo "url=$release_url"
} >> "$GITHUB_OUTPUT"
- name: Create Discord forum post
if: ${{ steps.scope.outputs.allowed == 'true' && steps.release.outputs.discord_posted != 'true' }}
env:
DISCORD_FORUM_WEBHOOK: ${{ secrets.DISCORD_FORUM_WEBHOOK }}
PR_TITLE: ${{ steps.pr.outputs.title }}
PR_AUTHOR: ${{ steps.pr.outputs.author }}
RELEASE_URL: ${{ steps.release.outputs.url }}
THREAD_NAME: ${{ steps.pr.outputs.thread_name }}
run: |
payload=$(jq -nc \
--arg thread_name "$THREAD_NAME" \
--arg title "$PR_TITLE" \
--arg author "$PR_AUTHOR" \
--arg release_url "$RELEASE_URL" \
'{
thread_name: $thread_name,
content: (["**\($title)**", "", "**Author:** \($author)", "", $release_url] | join("\n"))
}')
curl --fail --silent --show-error \
-H "Content-Type: application/json" \
-d "$payload" \
"${DISCORD_FORUM_WEBHOOK}?wait=true"
- name: Mark Discord post on release
if: ${{ steps.scope.outputs.allowed == 'true' && steps.release.outputs.discord_posted != 'true' }}
env:
GH_TOKEN: ${{ secrets.WINNATIVE_CI_TOKEN }}
PR_NUMBER: ${{ steps.scope.outputs.pr_number }}
RELEASE_TITLE: ${{ steps.pr.outputs.title }}
RELEASE_TAG: ${{ steps.pr.outputs.tag }}
run: |
discord_marker='<!-- discord-posted -->'
if ! grep -Fq "$discord_marker" release-notes.md; then
printf '\n%s\n' "$discord_marker" >> release-notes.md
fi
gh release edit "$RELEASE_TAG" \
--repo "$TARGET_REPO" \
--title "PR #$PR_NUMBER: $RELEASE_TITLE" \
--notes-file release-notes.md \
--prerelease