diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5f189da..ae0b401c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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" diff --git a/.github/workflows/pr-release-cleanup.yml b/.github/workflows/pr-release-cleanup.yml index abe506be..a34254af 100644 --- a/.github/workflows/pr-release-cleanup.yml +++ b/.github/workflows/pr-release-cleanup.yml @@ -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" diff --git a/.github/workflows/pr-release-publish.yml b/.github/workflows/pr-release-publish.yml index afc3cfa0..09143833 100644 --- a/.github/workflows/pr-release-publish.yml +++ b/.github/workflows/pr-release-publish.yml @@ -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='' + body=$(cat </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"