mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
282 lines
13 KiB
YAML
282 lines
13 KiB
YAML
name: PR Release Publish
|
|
run-name: "sign PR #${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || inputs.pr_number }} with the release key"
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
pr_number:
|
|
description: Open pull request number
|
|
required: true
|
|
type: string
|
|
pull_request_target:
|
|
types: [labeled]
|
|
branches: [main]
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
issues: write
|
|
pull-requests: read
|
|
|
|
concurrency:
|
|
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.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 }}
|
|
base_sha: ${{ steps.gate.outputs.base_sha }}
|
|
version_code: ${{ steps.gate.outputs.version_code }}
|
|
label: ${{ steps.gate.outputs.label }}
|
|
variants: ${{ steps.variants.outputs.list }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
persist-credentials: false
|
|
# Every variant a main build ships, so a PR build can be tried under any of them.
|
|
- 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 requester, PR, and build
|
|
id: gate
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
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")
|
|
base=$(jq -r '.base.sha' <<<"$pr")
|
|
|
|
run_id=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
|
|
--event pull_request --commit "$head" \
|
|
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
|
|
jq -r --arg head "$head" '[.[] | select(.headSha == $head)] |
|
|
sort_by([.createdAt, .attempt]) | last |
|
|
select(.status == "completed" and .conclusion == "success") | .databaseId // empty')
|
|
[[ -n "$run_id" ]] || { echo '::error::The latest PR build is not green'; exit 1; }
|
|
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
|
|
jq -e --arg head "$head" --arg base "$base" --argjson number "$PR_NUMBER" '
|
|
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
|
|
any(.pull_requests[]?; .number == $number and .head.sha == $head and .base.sha == $base)
|
|
' <<<"$run" >/dev/null
|
|
|
|
sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}
|
|
tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1)
|
|
stable_tag=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name)
|
|
mkdir -p "$RUNNER_TEMP/stable" "$RUNNER_TEMP/pr"
|
|
gh release download "$stable_tag" -R "$GITHUB_REPOSITORY" \
|
|
-p "DroidDeck-$stable_tag.apk" -D "$RUNNER_TEMP/stable"
|
|
gh run download "$run_id" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D "$RUNNER_TEMP/pr"
|
|
mapfile -t apks < <(find "$RUNNER_TEMP/pr" -type f -name '*.apk')
|
|
[[ ${#apks[@]} == 1 ]] || { echo '::error::Expected one PR APK'; exit 1; }
|
|
package_line() {
|
|
"$tools/aapt" dump badging "$1" | sed -n "s/^package: name='\\([^']*\\)' versionCode='\\([^']*\\)'.*/\\1 \\2/p"
|
|
}
|
|
stable=$(package_line "$RUNNER_TEMP/stable/DroidDeck-$stable_tag.apk")
|
|
candidate=$(package_line "${apks[0]}")
|
|
[[ "$stable" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && "$candidate" =~ ^com\.droiddeck\.launcher\ [0-9]+$ &&
|
|
${candidate##* } -ge ${stable##* } ]] || {
|
|
echo "::error::PR package/versionCode '$candidate' cannot install over stable '$stable'"; exit 1;
|
|
}
|
|
|
|
current=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
|
jq -e --arg head "$head" --arg base "$base" '
|
|
.state == "open" and .draft == false and .base.ref == "main" and
|
|
.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 "base_sha=$base"
|
|
echo "version_code=${candidate##* }"
|
|
echo "label=PR.$PR_NUMBER-${head:0:7}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
sign:
|
|
needs: authorize
|
|
uses: ./.github/workflows/sign.yml
|
|
with:
|
|
environment: signing-main
|
|
variants: ${{ needs.authorize.outputs.variants }}
|
|
run_id: ${{ needs.authorize.outputs.run_id }}
|
|
label: ${{ needs.authorize.outputs.label }}
|
|
artifact: droiddeck-pr-signed
|
|
secrets: inherit
|
|
|
|
publish:
|
|
needs: [authorize, sign]
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
issues: write
|
|
pull-requests: read
|
|
steps:
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: droiddeck-pr-signed-*
|
|
merge-multiple: true
|
|
path: signed
|
|
# What this build is, for the top of the release notes: the PR's title and the first
|
|
# paragraph of its description.
|
|
- name: Describe the change
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
|
run: |
|
|
set -euo pipefail
|
|
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
|
title=$(jq -r .title <<<"$pr")
|
|
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
|
|
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
|
|
{
|
|
echo "**Not merged** - a test build of open [PR #$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
|
|
if [ -n "$summary" ]; then
|
|
echo
|
|
echo "$summary"
|
|
fi
|
|
} > notes-head.md
|
|
cat notes-head.md
|
|
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
|
|
- uses: actions/create-github-app-token@v2
|
|
id: ci-token
|
|
with:
|
|
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
|
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
|
owner: Droid-Deck
|
|
repositories: DroidDeck-CI
|
|
- name: Publish to DroidDeck-CI
|
|
id: publish
|
|
env:
|
|
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
|
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
|
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
|
|
BASE_SHA: ${{ needs.authorize.outputs.base_sha }}
|
|
VERSION_CODE: ${{ needs.authorize.outputs.version_code }}
|
|
run: |
|
|
set -euo pipefail
|
|
repo=Droid-Deck/DroidDeck-CI
|
|
tag="pr-$PR_NUMBER"
|
|
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
|
|
rm signed/SHA256SUMS-*.txt
|
|
notes="$(cat notes-head.md)
|
|
|
|
Source: https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER
|
|
Commit: $HEAD_SHA
|
|
Base: $BASE_SHA
|
|
VersionCode: $VERSION_CODE
|
|
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
|
|
|
|
SHA-256:
|
|
\`\`\`
|
|
$(cat signed/SHA256SUMS.txt)
|
|
\`\`\`"
|
|
if ! gh release view "$tag" -R "$repo" >/dev/null 2>&1; then
|
|
gh release create "$tag" -R "$repo" --target main --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
|
|
fi
|
|
gh release upload "$tag" signed/* -R "$repo" --clobber
|
|
gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
|
|
# Drop the previous build's files: anything this build did not just upload.
|
|
keep=$(cd signed && ls | jq -R . | jq -sc .)
|
|
gh api "repos/$repo/releases/tags/$tag" |
|
|
jq -r --argjson keep "$keep" '.assets[] | select(.name as $n | $keep | index($n) | not) | .id' |
|
|
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 Preview 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"
|
|
|
|
catalog:
|
|
needs: publish
|
|
if: ${{ always() && needs.publish.result != 'skipped' }}
|
|
uses: ./.github/workflows/update-catalog.yml
|
|
secrets: inherit
|