From 055b213057a00024a31fc54df55797463808fa01 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 3 Nov 2026 18:58:46 +0100 Subject: [PATCH] ci(mobile): novedades de las tiendas desde mobile/whatsnew, no desde la PR Las notas de Play y App Store salian del titulo y cuerpo de la PR fusionada, asi que la 1.0.12 publico texto interno de CI. Ahora salen solo de mobile/whatsnew/whatsnew-, cada idioma con su texto, y el CI comprueba el limite de 500 caracteres de Play y ejecuta el mock del envio a App Store. --- .github/scripts/app-store-submit.mjs | 17 +++-- .github/scripts/generate-whatsnew.sh | 67 ------------------- .../scripts/test/app-store-submit.mock.mjs | 35 +++++++++- .github/workflows/ci.yml | 15 +++++ .github/workflows/release-mobile-app.yml | 20 +----- AGENTS.md | 6 +- mobile/whatsnew/.gitkeep | 0 mobile/whatsnew/whatsnew-en-US | 1 + mobile/whatsnew/whatsnew-es-ES | 1 + 9 files changed, 66 insertions(+), 96 deletions(-) delete mode 100644 .github/scripts/generate-whatsnew.sh delete mode 100644 mobile/whatsnew/.gitkeep create mode 100644 mobile/whatsnew/whatsnew-en-US create mode 100644 mobile/whatsnew/whatsnew-es-ES diff --git a/.github/scripts/app-store-submit.mjs b/.github/scripts/app-store-submit.mjs index 73fda8fe..02a73201 100644 --- a/.github/scripts/app-store-submit.mjs +++ b/.github/scripts/app-store-submit.mjs @@ -32,7 +32,8 @@ * 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- 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 * @@ -40,6 +41,7 @@ */ 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([ @@ -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 @@ -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`); diff --git a/.github/scripts/generate-whatsnew.sh b/.github/scripts/generate-whatsnew.sh deleted file mode 100644 index 062f9a20..00000000 --- a/.github/scripts/generate-whatsnew.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -# Generates whatsnew files from the merged PR title/body or GitHub release body. -# Used by release workflow to provide Play Store & App Store release notes. -# -# Usage: ./generate-whatsnew.sh -# -# Environment variables: -# GITHUB_REPOSITORY - owner/repo (set automatically by GitHub Actions) -# GITHUB_SHA - commit SHA (set automatically by GitHub Actions) -# GH_TOKEN - GitHub token for API access -# RELEASE_BODY - (optional) release body text, takes priority over PR detection - -set -euo pipefail - -WHATSNEW_DIR="${1:?Usage: generate-whatsnew.sh }" -LOCALES=("en-US" "es-ES") -MAX_LENGTH=500 - -mkdir -p "$WHATSNEW_DIR" - -NOTES="" - -# Priority 1: Use release body if provided -if [ -n "${RELEASE_BODY:-}" ]; then - echo "Using release body for whatsnew" - NOTES="$RELEASE_BODY" -fi - -# Priority 2: Try to find the PR associated with this commit -if [ -z "$NOTES" ]; then - echo "Looking for PR associated with commit ${GITHUB_SHA}..." - PR_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" 2>/dev/null || echo "[]") - - if [ "$PR_DATA" != "[]" ] && [ "$(echo "$PR_DATA" | jq length)" -gt 0 ]; then - TITLE=$(echo "$PR_DATA" | jq -r '.[0].title // ""') - BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""') - - if [ -n "$TITLE" ]; then - echo "Found PR: $TITLE" - if [ -n "$BODY" ]; then - NOTES=$(printf "%s\n\n%s" "$TITLE" "$BODY") - else - NOTES="$TITLE" - fi - fi - fi -fi - -# If no notes found, keep existing whatsnew files -if [ -z "$NOTES" ]; then - echo "No PR or release found, keeping existing whatsnew files" - exit 0 -fi - -# Strip markdown formatting that doesn't render well in store listings -NOTES=$(echo "$NOTES" | sed -E 's/^#{1,6}\s+//g; s/\*\*([^*]+)\*\*/\1/g; s/\*([^*]+)\*/\1/g; s/`([^`]+)`/\1/g') - -# Truncate to Play Store limit (500 chars) -if [ ${#NOTES} -gt $MAX_LENGTH ]; then - NOTES="${NOTES:0:$((MAX_LENGTH - 3))}..." -fi - -# Write to all locales -for LOCALE in "${LOCALES[@]}"; do - printf '%s' "$NOTES" > "$WHATSNEW_DIR/whatsnew-${LOCALE}" - echo "Written whatsnew-${LOCALE} (${#NOTES} chars)" -done diff --git a/.github/scripts/test/app-store-submit.mock.mjs b/.github/scripts/test/app-store-submit.mock.mjs index 44c3e4e3..be601a66 100644 --- a/.github/scripts/test/app-store-submit.mock.mjs +++ b/.github/scripts/test/app-store-submit.mock.mjs @@ -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- 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 \ @@ -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); @@ -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" } }, @@ -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]; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f6d0806..6efdd25d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release-mobile-app.yml b/.github/workflows/release-mobile-app.yml index 7b2a11f5..bed90b22 100644 --- a/.github/workflows/release-mobile-app.yml +++ b/.github/workflows/release-mobile-app.yml @@ -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 @@ -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 }} @@ -543,12 +531,6 @@ 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 }} @@ -556,7 +538,7 @@ jobs: 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 # ────────────────────────────────────────────────────────────── diff --git a/AGENTS.md b/AGENTS.md index 869fd029..b1d95ecd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) --- diff --git a/mobile/whatsnew/.gitkeep b/mobile/whatsnew/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/mobile/whatsnew/whatsnew-en-US b/mobile/whatsnew/whatsnew-en-US new file mode 100644 index 00000000..e1946fa8 --- /dev/null +++ b/mobile/whatsnew/whatsnew-en-US @@ -0,0 +1 @@ +Stability and performance improvements. \ No newline at end of file diff --git a/mobile/whatsnew/whatsnew-es-ES b/mobile/whatsnew/whatsnew-es-ES new file mode 100644 index 00000000..2c33806c --- /dev/null +++ b/mobile/whatsnew/whatsnew-es-ES @@ -0,0 +1 @@ +Mejoras de estabilidad y rendimiento. \ No newline at end of file