Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/scripts/app-store-submit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@
* BUNDLE_ID optional, default es.deamap.mobile
* APP_VERSION 1.2.3 (CFBundleShortVersionString)
* BUILD_NUMBER 10203 (CFBundleVersion)
* RELEASE_NOTES_FILE optional, text for "What's New" (same text on every locale)
* RELEASE_NOTES_DIR optional, folder with whatsnew-<locale> files for "What's New";
* a locale without its own file gets the WHATSNEW_LOCALE one
* WHATSNEW_LOCALE optional, default es-ES (localization created if missing)
* BUILD_TIMEOUT_MINUTES optional, default 45
*
* Logs progress and Apple's error details, never the credentials.
*/
import { createPrivateKey, sign } from "node:crypto";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { join } from "node:path";

const API = "https://api.appstoreconnect.apple.com";
const EDITABLE_STATES = new Set([
Expand Down Expand Up @@ -101,8 +103,15 @@ const versionString = env("APP_VERSION");
const buildNumber = env("BUILD_NUMBER");
const locale = env("WHATSNEW_LOCALE", "es-ES");
const timeoutMinutes = Number(env("BUILD_TIMEOUT_MINUTES", "45"));
const notesFile = process.env.RELEASE_NOTES_FILE;
const whatsNew = notesFile && existsSync(notesFile) ? readFileSync(notesFile, "utf8").trim() : "";
const notesDir = process.env.RELEASE_NOTES_DIR;
const notesFor = (where) => {
for (const l of [where, locale]) {
const file = notesDir && join(notesDir, `whatsnew-${l}`);
if (file && existsSync(file)) return readFileSync(file, "utf8").trim();
}
return "";
};
const whatsNew = notesFor(locale);

// ---------------------------------------------------------------- API client

Expand Down Expand Up @@ -383,7 +392,7 @@ async function setWhatsNew(version) {
data: {
type: "appStoreVersionLocalizations",
id: localization.id,
attributes: { whatsNew },
attributes: { whatsNew: notesFor(where) },
},
});
log(`What's New (${where}): set`);
Expand Down
67 changes: 0 additions & 67 deletions .github/scripts/generate-whatsnew.sh

This file was deleted.

35 changes: 32 additions & 3 deletions .github/scripts/test/app-store-submit.mock.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
// must exit 1.
// newer-editable only a later version (1.0.14) is editable: it must not be renamed;
// the script tries to create 1.0.13, Apple refuses and the script exits 1.
// When the version is submitted, only the unanswered declared age rating question (socialMedia) is written.
// When the version is submitted, only the unanswered declared age rating question (socialMedia) is written,
// and each localization gets the What's New of its own whatsnew-<locale> file.
//
// KEY=$(node -e "const {generateKeyPairSync}=require('crypto');console.log(Buffer.from(generateKeyPairSync('ec',{namedCurve:'P-256'}).privateKey.export({type:'pkcs8',format:'pem'})).toString('base64'))")
// APP_STORE_CONNECT_API_KEY_ID=k APP_STORE_CONNECT_API_ISSUER_ID=i APP_STORE_CONNECT_API_KEY_BASE64=$KEY \
Expand All @@ -16,12 +17,22 @@
//
// Prints "MOCK CHECK OK" and exits 0 when the scenario behaves as expected.

import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const scenario = process.env.MOCK_CASE ?? "stale";
const refusalDetail =
scenario === "stale"
? "Version is not ready to be submitted yet, please try again later."
: "You must provide a value for the attribute copyright.";

// One notes file per locale: a locale must never get another locale's text.
const notesDir = mkdtempSync(join(tmpdir(), "whatsnew-"));
writeFileSync(join(notesDir, "whatsnew-es-ES"), "Novedades");
writeFileSync(join(notesDir, "whatsnew-en-US"), "News");
process.env.RELEASE_NOTES_DIR = notesDir;

// The script polls with sleep(): make every wait instant.
const realSetTimeout = globalThis.setTimeout;
globalThis.setTimeout = (fn, _ms, ...args) => realSetTimeout(fn, 0, ...args);
Expand Down Expand Up @@ -72,6 +83,16 @@ globalThis.fetch = async (url, { method = "GET", body } = {}) => {
if (route === "/v1/appStoreVersions/version/relationships/build")
return new globalThis.Response(null, { status: 204 });

if (route.startsWith("/v1/appStoreVersions/version/appStoreVersionLocalizations"))
return data([
{ id: "es-ES", attributes: { locale: "es-ES" } },
{ id: "en-US", attributes: { locale: "en-US" } },
]);
if (method === "PATCH" && route.startsWith("/v1/appStoreVersionLocalizations/")) {
events.push(`whatsNew ${route.split("/").pop()}=${attributes.whatsNew}`);
return data({ id: route.split("/").pop(), attributes });
}

if (route.startsWith("/v1/apps/app/appInfos"))
return data([
{ id: "live", attributes: { state: "READY_FOR_DISTRIBUTION" } },
Expand Down Expand Up @@ -134,12 +155,20 @@ globalThis.fetch = async (url, { method = "GET", body } = {}) => {
};

const ageRating = 'age rating {"socialMedia":false}';
const whatsNew = ["whatsNew es-ES=Novedades", "whatsNew en-US=News"];
const expected = {
stale: {
events: [ageRating, "submit old (refused)", "cancel old", "create new", "submit new"],
events: [
ageRating,
...whatsNew,
"submit old (refused)",
"cancel old",
"create new",
"submit new",
],
exitCode: 0,
},
"other-409": { events: [ageRating, "submit old (refused)"], exitCode: 1 },
"other-409": { events: [ageRating, ...whatsNew, "submit old (refused)"], exitCode: 1 },
"newer-editable": { events: ["create version (refused)"], exitCode: 1 },
}[scenario];

Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ jobs:
- name: Format check
run: npm run format:check

# Play rejects release notes over 500 characters, and only at upload time
- name: Store release notes length
run: |
for f in mobile/whatsnew/whatsnew-*; do
n=$(wc -m < "$f")
if [ "$n" -gt 500 ]; then echo "::error file=$f::$n caracteres, Play admite 500"; exit 1; fi
done

- name: App Store submit script (mock)
run: |
KEY=$(node -e "const {generateKeyPairSync}=require('crypto');console.log(Buffer.from(generateKeyPairSync('ec',{namedCurve:'P-256'}).privateKey.export({type:'pkcs8',format:'pem'})).toString('base64'))")
for c in stale other-409 newer-editable; do
APP_STORE_CONNECT_API_KEY_ID=k APP_STORE_CONNECT_API_ISSUER_ID=i APP_STORE_CONNECT_API_KEY_BASE64="$KEY" APP_VERSION=1.0.13 BUILD_NUMBER=10013 MOCK_CASE=$c node --import ./.github/scripts/test/app-store-submit.mock.mjs .github/scripts/app-store-submit.mjs
done

test:
name: Tests
runs-on: ubuntu-latest
Expand Down
20 changes: 1 addition & 19 deletions .github/workflows/release-mobile-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,6 @@ jobs:
mobile/android/app/build/outputs/bundle/release/*.aab
retention-days: 30

- name: Generate release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_BODY: ${{ github.event.release.body || '' }}
run: bash .github/scripts/generate-whatsnew.sh ./mobile/whatsnew

- name: Upload to Google Play Store
if: success()
uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1
Expand Down Expand Up @@ -247,12 +241,6 @@ jobs:
- name: Install googleapis
run: npm install --no-save --ignore-scripts googleapis

- name: Generate release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_BODY: ${{ github.event.release.body || '' }}
run: bash .github/scripts/generate-whatsnew.sh ./mobile/whatsnew

- name: Promote version on Play Store
env:
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
Expand Down Expand Up @@ -543,20 +531,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}

- name: Generate release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_BODY: ${{ github.event.release.body || '' }}
run: bash .github/scripts/generate-whatsnew.sh ./mobile/whatsnew

- name: Submit version for review
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
APP_VERSION: ${{ needs.prepare.outputs.version }}
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
RELEASE_NOTES_FILE: mobile/whatsnew/whatsnew-es-ES
RELEASE_NOTES_DIR: mobile/whatsnew
run: node .github/scripts/app-store-submit.mjs

# ──────────────────────────────────────────────────────────────
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ PR / Push a main|develop

1. Editar `mobile/version.json` → incrementar versión (semver)
2. Ejecutar `node scripts/propagate-version.js` → sincroniza a package.json, build.gradle
3. Commit + push a main
4. `ensure-tags.yml` crea tag `mobile@x.y.z` automáticamente
5. `build-android.yml` se dispara con el tag → build + deploy a Play Store
3. Escribir las novedades de la versión, para usuarios, en `mobile/whatsnew/whatsnew-es-ES` y `whatsnew-en-US` (máx. 500 caracteres). Es lo que publican Play y App Store tal cual
4. Commit + push a main
5. `ensure-tags.yml` crea tag `mobile@x.y.z` y lanza `release-mobile-app.yml` (producción en Play y revisión en App Store)

---

Expand Down
Empty file removed mobile/whatsnew/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions mobile/whatsnew/whatsnew-en-US
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stability and performance improvements.
1 change: 1 addition & 0 deletions mobile/whatsnew/whatsnew-es-ES
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Mejoras de estabilidad y rendimiento.
Loading