Skip to content

Commit a02449f

Browse files
committed
chore: one-off App Store ownership transfer for launchpad
The app store has no transfer button: the current owner unlocks the app and the new owner registers it again with the certificate and a signature over the app id. Mark has unlocked launchpad, so this runs that registration with the signing key that already lives in this repository's secrets. Temporary: remove this workflow and its branch once the transfer has landed.
1 parent 12fca7c commit a02449f

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: App Store ownership transfer
2+
3+
# One-off, delete this file and its branch once the transfer has landed.
4+
#
5+
# The Nextcloud app store has no "transfer to user X" button and no
6+
# co-maintainer concept. Ownership moves the other way around: the CURRENT
7+
# owner unlocks the app (Account > Transfer app ownership), and the NEW owner
8+
# registers it again with the app certificate plus a signature over the app id.
9+
# That registration is the transfer, and it is exactly the POST below.
10+
#
11+
# See https://nextcloudappstore.readthedocs.io/en/latest/developer.html
12+
# #transferring-your-app-to-a-new-owner
13+
#
14+
# The app ends up owned by whichever account owns NEXTCLOUD_APPSTORE_TOKEN.
15+
# The signing key never leaves the runner and the signature is never printed:
16+
# together with the public certificate it is enough for anyone to claim an
17+
# app that is currently unlocked for transfer.
18+
on:
19+
push:
20+
branches: [chore/appstore-ownership-transfer]
21+
22+
jobs:
23+
transfer:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Register launchpad under the App Store token's account
27+
env:
28+
APP_ID: launchpad
29+
SIGNING_KEY: ${{ secrets.NEXTCLOUD_SIGNING_KEY }}
30+
SIGNING_CERT: ${{ secrets.NEXTCLOUD_SIGNING_CERT }}
31+
APPSTORE_TOKEN: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }}
32+
run: |
33+
set -euo pipefail
34+
35+
# An org secret that is not shared with this repo arrives as an empty
36+
# string, not as an error, so the request would 401 for a reason the
37+
# response body does not explain. Fail here instead.
38+
[ -n "$APPSTORE_TOKEN" ] || { echo "::error::NEXTCLOUD_APPSTORE_TOKEN is empty in this repository's scope"; exit 1; }
39+
[ -n "$SIGNING_KEY" ] || { echo "::error::NEXTCLOUD_SIGNING_KEY is empty in this repository's scope"; exit 1; }
40+
41+
printf '%s\n' "$SIGNING_KEY" > signing-key.key
42+
printf '%s\n' "$SIGNING_CERT" > signing-cert.crt
43+
44+
SIG=$(echo -n "$APP_ID" | openssl dgst -sha512 -sign signing-key.key | openssl base64 -A)
45+
CODE=$(curl -s -o response.json -w '%{http_code}' \
46+
-X POST "https://apps.nextcloud.com/api/v1/apps" \
47+
-H "Authorization: Token $APPSTORE_TOKEN" \
48+
-H "Content-Type: application/json" \
49+
-d "{\"certificate\": $(jq -Rs . < signing-cert.crt), \"signature\": \"$SIG\"}")
50+
rm -f signing-key.key
51+
52+
echo "HTTP $CODE"
53+
cat response.json || true
54+
echo
55+
56+
case "$CODE" in
57+
200|201) echo "OK: $APP_ID is now registered to the account that owns NEXTCLOUD_APPSTORE_TOKEN" ;;
58+
403) echo "::error::403: the app is locked for transfer, or the token's account may not register it"; exit 1 ;;
59+
401) echo "::error::401: the App Store token was rejected"; exit 1 ;;
60+
*) echo "::error::unexpected HTTP $CODE"; exit 1 ;;
61+
esac

0 commit comments

Comments
 (0)