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
41 changes: 41 additions & 0 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ on:
paths:
- "docs/handbook/**"
- "scripts/assemble-handbook-screenshots.sh"
- "scripts/assemble-handbook-store-listing.py"
- "scripts/templates/store-listing.html.tmpl"
- "test/goldens/**"
# Store-listing section is a derived export of the Fastlane metadata —
# changing it must re-run the generator and re-commit the handbook.
- "ios/fastlane/metadata/**"
- "ios/fastlane/screenshots/**"
- "android/fastlane/metadata/**"
- "Dockerfile.handbook"
- "handbook.nginx.conf"
- "handbook.htpasswd"
Expand Down Expand Up @@ -58,6 +65,40 @@ jobs:
exit 1
fi

- name: Verify store-listing HTML sanitizer drops disallowed markup
# full_description.txt is rendered unescaped in the handbook (Google
# Play allows a small HTML subset), so the generator runs it through an
# allowlist sanitizer. Guard that the sanitizer keeps neutralizing
# injection: a </details>/<script>/javascript: payload must not survive.
run: |
python3 - <<'PY'
import importlib.util
spec = importlib.util.spec_from_file_location("gen", "scripts/assemble-handbook-store-listing.py")
gen = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gen)
payload = '</details><script>alert(1)</script><b>ok</b><a href="javascript:alert(1)">x</a>'
out = gen.sanitize_play_html(payload)
assert "<script" not in out, out
assert "</details" not in out, out
assert "javascript:" not in out, out
assert "<b>ok</b>" in out, out
print("sanitizer OK:", out)
PY

- name: Verify handbook store-listing section is in sync with metadata
# The store-listing block in docs/handbook/de/index.html is a derived
# export of the Fastlane metadata. Re-run the generator; if the working
# tree changes, the committed handbook is stale — someone edited a
# metadata file (or a screenshot) without re-running the generator.
run: |
set -euo pipefail
python3 scripts/assemble-handbook-store-listing.py /tmp/store-out
if ! git diff --quiet docs/handbook/de/index.html; then
echo "::error::docs/handbook/de/index.html is stale — re-run scripts/assemble-handbook-store-listing.py and commit."
git diff docs/handbook/de/index.html
exit 1
fi

- name: Build handbook image (no push)
run: docker build -f Dockerfile.handbook -t realunit-handbook:pr-check .

Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/handbook-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ on:
- "docs/handbook/**"
- "test/goldens/screens/**"
- "scripts/assemble-handbook-screenshots.sh"
- "scripts/assemble-handbook-store-listing.py"
- "scripts/templates/store-listing.html.tmpl"
# Store-listing section is baked into the image from the Fastlane
# metadata; a metadata- or screenshot-only change must redeploy too.
- "ios/fastlane/metadata/**"
- "ios/fastlane/screenshots/**"
- "android/fastlane/metadata/**"
- "Dockerfile.handbook"
- "handbook.nginx.conf"
- "handbook.htpasswd"
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,24 @@ jobs:
echo "is_prerelease=true" >> $GITHUB_OUTPUT
fi

android-deploy:
store-metadata-preflight:
# The beta lanes push the store listing (metadata + screenshots) to the
# live consoles alongside the binary, so gate the whole release on the
# same FIXME/character-limit checks that store-metadata.yaml enforces —
# a tag must never ship a FIXME placeholder or oversize field.
needs: guard
if: needs.guard.outputs.proceed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Reject FIXME placeholders + enforce character limits
run: bash scripts/check-store-metadata.sh

android-deploy:
needs: [guard, store-metadata-preflight]
if: needs.guard.outputs.proceed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
Expand Down Expand Up @@ -166,7 +180,7 @@ jobs:
path: build/app/outputs/flutter-apk/realunit-*.apk

ios-deploy:
needs: guard
needs: [guard, store-metadata-preflight]
if: needs.guard.outputs.proceed == 'true'
runs-on: macos-26
timeout-minutes: 45
Expand Down
115 changes: 115 additions & 0 deletions .github/workflows/store-metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Store metadata sync

on:
push:
branches: [main]
paths:
- 'ios/fastlane/metadata/**'
- 'ios/fastlane/screenshots/**'
- 'android/fastlane/metadata/**'
workflow_dispatch:
inputs:
platform:
description: 'Which store to push to'
required: true
default: 'both'
type: choice
options: [ios, android, both]

permissions:
contents: read

concurrency:
group: store-metadata-${{ github.ref }}
cancel-in-progress: false # never abort an in-flight upload

jobs:
preflight:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@v5
- name: Reject FIXME placeholders + enforce character limits
run: bash scripts/check-store-metadata.sh

ios:
needs: preflight
if: ${{ github.event_name == 'push' || github.event.inputs.platform == 'ios' || github.event.inputs.platform == 'both' }}
runs-on: macos-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v5

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4'
bundler-cache: true
working-directory: ios

- name: Install matching bundler
run: gem install bundler -v 2.7.2

- name: Install gems
run: bundle _2.7.2_ install
working-directory: ios

- name: Write Apple API key + export env (mirrors release.yaml pattern)
run: |
set -euo pipefail
KEY_PATH="${{ github.workspace }}/AuthKey.p8"
echo "$APP_STORE_CONNECT_KEY" > "$KEY_PATH"
echo "FASTLANE_APPLE_API_KEY_PATH=$KEY_PATH" >> $GITHUB_ENV
echo "FASTLANE_APPLE_API_KEY_ID=$APP_STORE_CONNECT_KEY_ID" >> $GITHUB_ENV
echo "FASTLANE_APPLE_API_ISSUER_ID=$APP_STORE_CONNECT_ISSUER_ID" >> $GITHUB_ENV
env:
APP_STORE_CONNECT_KEY: ${{ secrets.APP_STORE_CONNECT_KEY }}
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}

- name: Run deliver (metadata-only)
run: bundle exec fastlane store_metadata --verbose
working-directory: ios
env:
FASTLANE_HIDE_CHANGELOG: 1

- name: Cleanup
if: always()
run: rm -f "${FASTLANE_APPLE_API_KEY_PATH:-${{ github.workspace }}/AuthKey.p8}" || true

android:
needs: preflight
if: ${{ github.event_name == 'push' || github.event.inputs.platform == 'android' || github.event.inputs.platform == 'both' }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4'
bundler-cache: true
working-directory: android

- name: Install matching bundler
run: gem install bundler -v 2.7.2

- name: Install gems
run: bundle _2.7.2_ install
working-directory: android

- name: Decode Play Store service-account JSON (mirrors release.yaml pattern)
env:
PLAY_STORE_JSON: ${{ secrets.PLAY_STORE_JSON_BASE64 }}
run: |
set -euo pipefail
echo "$PLAY_STORE_JSON" | base64 --decode > android/credentials.json

- name: Run supply (metadata-only)
run: bundle exec fastlane store_metadata
working-directory: android

- name: Cleanup
if: always()
run: rm -f android/credentials.json || true
30 changes: 30 additions & 0 deletions Dockerfile.handbook
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,40 @@ COPY scripts/assemble-handbook-screenshots.sh ./scripts/
COPY test/goldens/screens/ ./test/goldens/screens/
RUN bash ./scripts/assemble-handbook-screenshots.sh /out

# Store-listing section: derived export of the Fastlane metadata
# (ios/fastlane/metadata + android/fastlane/metadata + screenshots). The
# generator copies the PNGs to /out/{ios,android}/... and rewrites the
# <!-- BEGIN/END:store-listing --> block in docs/handbook/de/index.html in
# place. Single source of truth is the Fastlane metadata; this handbook
# section just renders it (same upstream/downstream model as mails/).
#
# Note: this stage always renders from the metadata and serves its own
# rewritten index.html, so the IMAGE is self-consistent even if the
# committed docs/handbook/de/index.html were stale. Keeping the committed
# handbook in sync with the metadata is enforced separately by the sync
# gate in handbook-build-check.yaml (the generator is pure-stdlib and
# version-stable, so the runner's python3 and this Alpine python3 produce
# identical output).
FROM alpine:3.20 AS store-listing-builder
WORKDIR /work
RUN apk add --no-cache python3
COPY scripts/assemble-handbook-store-listing.py ./scripts/
COPY scripts/templates/store-listing.html.tmpl ./scripts/templates/
COPY ios/fastlane/metadata/ ./ios/fastlane/metadata/
COPY ios/fastlane/screenshots/ ./ios/fastlane/screenshots/
COPY android/fastlane/metadata/ ./android/fastlane/metadata/
COPY docs/handbook/de/index.html ./docs/handbook/de/index.html
RUN python3 ./scripts/assemble-handbook-store-listing.py /out && cp ./docs/handbook/de/index.html /out/index.html

FROM nginx:1.27.5-alpine

COPY docs/handbook/ /usr/share/nginx/html/
COPY --from=screenshots-builder /out/ /usr/share/nginx/html/screenshots/
# Store-listing PNGs + the substituted index.html (overwrites the verbatim
# docs/handbook copy above with the generated export).
COPY --from=store-listing-builder /out/ios/ /usr/share/nginx/html/store/ios/
COPY --from=store-listing-builder /out/android/ /usr/share/nginx/html/store/android/
COPY --from=store-listing-builder /out/index.html /usr/share/nginx/html/de/index.html
COPY handbook.nginx.conf /etc/nginx/conf.d/default.conf
COPY handbook.htpasswd /etc/nginx/handbook.htpasswd

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ Tier 1 specs live under `test/integration/**` and run inside the same `flutter t
| `auto-release-pr.yaml` | Push `develop` · manual | Opens Release PR `develop` → `main` |
| `auto-tag.yaml` | Push `develop` | Creates the next `vX.Y.Z` patch tag (PATCH = previous + 1, MINOR/MAJOR from pubspec floor) |
| `release.yaml` | Tag `v*` · manual | Single store-release pipeline. Guard job routes by PATCH: `vX.Y.0` → production candidate (GitHub release, prerelease: false); `vX.Y.Z` (Z >= 1) → internal release (GitHub pre-release). Both lanes deploy Android + iOS to Play Internal + TestFlight; production promotion stays manual in the store backends. |
| `store-metadata.yaml` | Push `main` under `*/fastlane/metadata/**` or `ios/fastlane/screenshots/**` · manual `workflow_dispatch` | Sync App Store + Play Store listing text + screenshots without rebuilding the app. A `preflight` gate rejects `FIXME-` placeholders and over-length text fields before either store upload runs. |
| `handbook-deploy.yaml` | Push `develop` under `docs/handbook/**`, `Dockerfile.handbook`, `handbook.nginx.conf`, `handbook.htpasswd`, or the workflow files · manual | Builds the handbook image once and rolls it out to DEV (`:beta`) then PRD (`:latest`) sequentially via the reusable `handbook.yaml` — PRD only runs after DEV is green |
| `handbook.yaml` | Called by `handbook-deploy.yaml` (`workflow_call`) | Reusable build → Docker Hub push → server pull/recreate → smoke check, parameterised per environment |

Expand All @@ -183,6 +184,8 @@ Tags follow plain SemVer: `vMAJOR.MINOR.PATCH`. There is no pre-release suffix

A single release workflow (`release.yaml`) listens on the `v*` tag pattern and uses a guard job to route based on the PATCH component: patch tags go through the internal lane (`prerelease: true` on GitHub), MAJOR/MINOR tags through the production-candidate lane (`prerelease: false`). Either way the build lands in the Test tracks first — the App Store / Play Store production track is never updated by a tag push.

The `beta` lanes push the **store listing** (Fastlane metadata + screenshots) to App Store Connect / Play Console alongside every binary, so a tag-driven release keeps the listing in sync with the build (production promotion / final submit stay manual — see `store-metadata.yaml`). Because of that, `release.yaml` runs the same `scripts/check-store-metadata.sh` preflight (FIXME placeholders + character limits) as `store-metadata.yaml` in a gating `store-metadata-preflight` job before either deploy lane runs — a tag can never ship a `FIXME-` placeholder or an oversize field to the live consoles.

The build number is derived deterministically from the tag by `tool/generate_release_info.dart` using `MAJOR * 10_000_000 + MINOR * 100_000 + PATCH * 1_000 + 999`. The fixed `+999` suffix keeps every new build strictly above the legacy beta build codes; the first new build `v1.0.15` lands at `10_015_999`, comfortably above the highest published legacy beta `v1.0.0-beta.14` at `10_000_014`.

`pubspec.yaml`'s `version:` field has two roles:
Expand Down
31 changes: 31 additions & 0 deletions android/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,36 @@ platform :android do
track: release_track,
aab: aab_path
)

# Push the Play Store listing (metadata + screenshots) alongside the AAB.
# Defaults come from Appfile (package_name + json_key_file).
upload_to_play_store(
metadata_path: "./fastlane/metadata/android",
skip_upload_apk: true,
skip_upload_aab: true,
skip_upload_changelogs: false,
skip_upload_metadata: false,
skip_upload_images: false,
skip_upload_screenshots: false,
track: release_track
)
end

desc "Upload Play Store listing (metadata + screenshots) without a binary"
lane :store_metadata do
# Hard-pin the metadata-only push to the internal track. The main store
# listing is global, but the changelog is per-track — pinning "internal"
# (not release_track) guarantees a metadata sync can never write to the
# production track even if release_track is ever changed.
upload_to_play_store(
metadata_path: "./fastlane/metadata/android",
skip_upload_apk: true,
skip_upload_aab: true,
skip_upload_changelogs: false,
skip_upload_metadata: false,
skip_upload_images: false,
skip_upload_screenshots: false,
track: "internal"
)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Erste Veröffentlichung der RealUnit Wallet App.
23 changes: 23 additions & 0 deletions android/fastlane/metadata/android/de-DE/full_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Die offizielle App der RealUnit Schweiz AG – für den einfachen, gebührenfreien Kauf und die sichere Verwahrung der RealUnit Aktientoken. Ohne Bank. Ohne Gebühren. Direkt in Ihrer Hand.

<b>Ihre Vorteile</b>

✔ Kostenloser Kauf von RealUnit Token

✔ Bankenunabhängige, sichere Verwahrung Ihrer Token

✔ Aktueller Aktienkurs und persönliche Vermögensübersicht

✔ Belege für Handel und Steuern jederzeit abrufbar

✔ Kompatibel mit der Hardware Wallet Bitbox02 Nova

<b>Für wen ist die App?</b>

Die RealUnit App richtet sich an Anlegerinnen und Anleger, die ihr Vermögen ausserhalb des Bankensystems verwalten möchten – transparent, sicher und selbstbestimmt.

<b>Über RealUnit Schweiz AG</b>

Die RealUnit Schweiz AG ist eine börsenkotierte Investmentgesellschaft, welche breit diversifiziert in Realwerte investiert. Wir verfolgen das Ziel, das uns anvertraute Vermögen bestmöglich vor Krisen und Kaufkraftverlust zu schützen und das Privateigentum zu sichern.

Jetzt herunterladen und Ihre finanzielle Souveränität zurückgewinnen.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RealUnit Token kaufen, verwahren & verwalten – sicher und bankenunabhängig.
1 change: 1 addition & 0 deletions android/fastlane/metadata/android/de-DE/title.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RealUnit Wallet
Empty file.
Loading
Loading