ci: streamline signed PR test builds (#114)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-09-30 22:18:08 -04:00
1 parent 05cebd4a4f
commit b565c4a6e5
3 files changed
+113 -17

No files matched your search

+5 -1
View File
@@ -387,7 +387,11 @@ jobs:
success)
echo "### Build passed"
echo
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key. A maintainer can run **PR Release Publish** from \`main\` to create an APK that installs over the release."
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key."
echo
echo "**Maintainer:** add the \`publish-test\` label to publish a release-signed tester build that installs over Stable or Nightly and appears in DroidDeck's **Test builds** list. The label is removed automatically after publishing."
echo
echo "Fallback: [open **PR Release Publish**]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/workflows/pr-release-publish.yml) on \`main\` and enter PR \`$PR\`."
;;
failure)
echo "### ERROR: Build failed"
+9 -7
View File
@@ -30,10 +30,12 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
tag="pr-$PR_NUMBER"
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
echo "Deleted $tag"
else
echo "No test build for PR #$PR_NUMBER"
fi
found=0
for tag in "pr-$PR_NUMBER" "pr-$PR_NUMBER.0"; do
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
echo "Deleted $tag"
found=1
fi
done
[ "$found" = 1 ] || echo "No test build for PR #$PR_NUMBER"
+99 -9
View File
@@ -1,28 +1,35 @@
name: PR Release Publish
run-name: sign PR #${{ inputs.pr_number }} with the release key
run-name: ${{ github.event_name == 'pull_request_target' && format('sign PR #{0} requested by @{1}', github.event.pull_request.number, github.actor) || format('sign PR #{0} with the release key', inputs.pr_number) }}
on:
workflow_dispatch:
inputs:
pr_number:
description: Open pull request
description: Open pull request number
required: true
type: number
type: string
pull_request_target:
types: [labeled]
branches: [main]
permissions:
actions: read
contents: read
issues: write
pull-requests: read
concurrency:
group: pr-release-${{ inputs.pr_number }}
group: pr-release-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: false
jobs:
authorize:
if: github.ref == 'refs/heads/main'
if: >-
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request_target' && github.event.label.name == 'publish-test')
runs-on: ubuntu-24.04
outputs:
pr_number: ${{ steps.gate.outputs.pr_number }}
run_id: ${{ steps.gate.outputs.run_id }}
head_sha: ${{ steps.gate.outputs.head_sha }}
label: ${{ steps.gate.outputs.label }}
@@ -35,13 +42,23 @@ jobs:
- name: The packages a signed build ships as
id: variants
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
- name: Check the PR and build
- name: Check the requester, PR, and build
id: gate
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
EVENT_NAME: ${{ github.event_name }}
REQUESTER: ${{ github.actor }}
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || inputs.pr_number }}
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; }
if [ "$EVENT_NAME" = pull_request_target ]; then
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$REQUESTER/permission" --jq .permission)
case "$permission" in
admin|maintain|write) ;;
*) echo "::error::@$REQUESTER does not have permission to publish release-signed test builds"; exit 1 ;;
esac
fi
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null
head=$(jq -r '.head.sha' <<<"$pr")
@@ -85,6 +102,7 @@ jobs:
.head.sha == $head and .base.sha == $base
' <<<"$current" >/dev/null
{
echo "pr_number=$PR_NUMBER"
echo "run_id=$run_id"
echo "head_sha=$head"
echo "label=PR.$PR_NUMBER-${head:0:7}"
@@ -107,6 +125,7 @@ jobs:
permissions:
actions: read
contents: read
issues: write
pull-requests: read
steps:
- uses: actions/download-artifact@v4
@@ -119,7 +138,7 @@ jobs:
- name: Describe the change
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
set -euo pipefail
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
@@ -143,9 +162,10 @@ jobs:
owner: Droid-Deck
repositories: DroidDeck-CI
- name: Publish to DroidDeck-CI
id: publish
env:
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
PR_NUMBER: ${{ inputs.pr_number }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
run: |
set -euo pipefail
@@ -175,3 +195,73 @@ jobs:
while IFS= read -r id; do
gh api --method DELETE "repos/$repo/releases/assets/$id" >/dev/null
done
published=$(gh release view "$tag" -R "$repo" --json tagName,assets)
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<<"$published" >/dev/null
release="$GITHUB_SERVER_URL/$repo/releases/tag/$tag"
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
echo "release=$release" >> "$GITHUB_OUTPUT"
echo "apk=$GITHUB_SERVER_URL/$repo/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
- name: Report the signed build on the PR
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
run: |
set -euo pipefail
short=${HEAD_SHA:0:7}
mark='<!-- droiddeck-pr-release -->'
body=$(cat <<EOF
$mark
## Signed test build
A release-signed build of this PR at \`$short\` is ready. It installs over Stable or Nightly and keeps app data.
[Download the standard APK]($APK) · [All variants and checksums]($RELEASE)
It is also available in **DroidDeck → Updates → Test builds → PR #$PR_NUMBER**.
Pushes to this PR do not re-sign automatically. After a new green build, a maintainer can add the \`publish-test\` label again.
EOF
)
id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate \
--jq ".[] | select(.body | startswith(\"$mark\")) | .id" | head -1 || true)
if [ -n "$id" ]; then
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null
else
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null
fi
- name: Clear the publish request label
if: github.event_name == 'pull_request_target'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
gh api --method DELETE "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels/publish-test"
- name: Post to Discord
env:
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
run: |
set -euo pipefail
if [ -z "$WEBHOOK" ]; then
echo "::notice::No DISCORD_BUILDS_WEBHOOK secret; nothing posted"
exit 0
fi
short=${HEAD_SHA:0:7}
title=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .title)
what="[#$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
get=$(printf 'Download: [standard APK](<%s>) · [all variants](<%s>)' "$APK" "$RELEASE")
content=$(printf '**New signed PR test build** \`pr-%s\` · \`%s\`\n%s\n_Not merged — for testing._\n%s' "$PR_NUMBER" "$short" "$what" "$get")
jq -n --arg content "$content" '{content: $content, allowed_mentions: {parse: []}}' |
curl -fsS -H 'Content-Type: application/json' -d @- "$WEBHOOK" -o /dev/null
echo "Posted: $content"