Add Fastlane release scaffolding (#601)
* Add Fastlane release scaffolding * chore: document release flow and tag guard * fix: address release automation review comments * fix: harden release version parsing * fix: resolve release review follow-ups * fix: restore Android staging application id
This commit is contained in:
@@ -78,6 +78,15 @@ opencode.json
|
|||||||
.opencode/
|
.opencode/
|
||||||
.cursor/
|
.cursor/
|
||||||
|
|
||||||
|
# Fastlane local credentials and generated output
|
||||||
|
/fastlane/*.json
|
||||||
|
/fastlane/*.p8
|
||||||
|
/fastlane/README.md
|
||||||
|
/fastlane/report.xml
|
||||||
|
/fastlane/Preview.html
|
||||||
|
/fastlane/screenshots/
|
||||||
|
/fastlane/test_output/
|
||||||
|
|
||||||
# Flutter DevTools
|
# Flutter DevTools
|
||||||
devtools_options.yaml
|
devtools_options.yaml
|
||||||
# Widgetbook
|
# Widgetbook
|
||||||
|
|||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.8)
|
||||||
|
abbrev (0.1.2)
|
||||||
|
addressable (2.9.0)
|
||||||
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
|
artifactory (3.0.17)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.4.0)
|
||||||
|
aws-partitions (1.1241.0)
|
||||||
|
aws-sdk-core (3.246.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
logger
|
||||||
|
aws-sdk-kms (1.124.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sdk-s3 (1.220.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sigv4 (1.12.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
base64 (0.2.0)
|
||||||
|
benchmark (0.5.0)
|
||||||
|
bigdecimal (4.1.2)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
csv (3.3.5)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.7.0)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.6.20240107)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (0.112.0)
|
||||||
|
faraday (1.10.5)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.8)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (>= 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.1)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.2.0)
|
||||||
|
multipart-post (~> 2.0)
|
||||||
|
faraday-net_http (1.0.2)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.4)
|
||||||
|
faraday_middleware (1.2.1)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.4.1)
|
||||||
|
fastlane (2.233.0)
|
||||||
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
abbrev (~> 0.1.2)
|
||||||
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.197)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
base64 (~> 0.2.0)
|
||||||
|
benchmark (>= 0.1.0)
|
||||||
|
bundler (>= 1.17.3, < 5.0.0)
|
||||||
|
colored (~> 1.2)
|
||||||
|
commander (~> 4.6)
|
||||||
|
csv (~> 3.3)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
fastlane-sirp (>= 1.1.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-env (>= 1.6.0, <= 2.1.1)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
http-cookie (~> 1.0.5)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.1.0, < 3)
|
||||||
|
logger (>= 1.6, < 2.0)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
mutex_m (~> 0.3.0)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
nkf (~> 0.2.0)
|
||||||
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
|
ostruct (>= 0.1.0)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.5)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (~> 3)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.4.1)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
|
fastlane-sirp (1.1.0)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.99.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-apis-core (0.18.0)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (~> 1.9)
|
||||||
|
httpclient (>= 2.8.3, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
mutex_m
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
google-apis-iamcredentials_v1 (0.27.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.17.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.62.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-cloud-core (1.8.0)
|
||||||
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (2.1.1)
|
||||||
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-errors (1.6.0)
|
||||||
|
google-cloud-storage (1.59.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-core (>= 0.18, < 2)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
|
google-apis-storage_v1 (>= 0.42)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (~> 1.9)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
googleauth (1.11.2)
|
||||||
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.1)
|
||||||
|
jwt (>= 1.4, < 3.0)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.8)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.9.0)
|
||||||
|
mutex_m
|
||||||
|
jmespath (1.6.2)
|
||||||
|
json (2.19.4)
|
||||||
|
jwt (2.10.2)
|
||||||
|
base64
|
||||||
|
logger (1.7.0)
|
||||||
|
mini_magick (4.13.2)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
multi_json (1.20.1)
|
||||||
|
multipart-post (2.4.1)
|
||||||
|
mutex_m (0.3.0)
|
||||||
|
nanaimo (0.4.0)
|
||||||
|
naturally (2.3.0)
|
||||||
|
nkf (0.2.0)
|
||||||
|
optparse (0.8.1)
|
||||||
|
os (1.1.4)
|
||||||
|
ostruct (0.6.3)
|
||||||
|
plist (3.7.2)
|
||||||
|
public_suffix (7.0.5)
|
||||||
|
rake (13.4.2)
|
||||||
|
representable (3.2.0)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.4.1)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rouge (3.28.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.4.1)
|
||||||
|
security (0.1.5)
|
||||||
|
signet (0.21.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.a)
|
||||||
|
jwt (>= 1.5, < 4.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
simctl (1.6.10)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.2)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.27.0)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.4.0)
|
||||||
|
rexml (>= 3.3.6, < 4.0)
|
||||||
|
xcpretty (0.4.1)
|
||||||
|
rouge (~> 3.28.0)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
arm64-darwin-24
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
fastlane (~> 2.233)
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.6.5
|
||||||
+212
@@ -0,0 +1,212 @@
|
|||||||
|
# Release Automation
|
||||||
|
|
||||||
|
This repo uses Fastlane as a thin wrapper around the existing Flutter, Rust, and
|
||||||
|
shell build scripts.
|
||||||
|
|
||||||
|
## Current Scope
|
||||||
|
|
||||||
|
The first release automation layer is build-only:
|
||||||
|
|
||||||
|
- validates that `pubspec.yaml` has a release version with a build number
|
||||||
|
- optionally validates that a git tag matches the pubspec version
|
||||||
|
- builds staging Android split APKs and AAB for `org.parres.whitenoise.staging`
|
||||||
|
- builds production Android split APKs and AAB for `org.parres.whitenoise`
|
||||||
|
- builds staging iOS IPA for `dev.ipf.whitenoise.staging`
|
||||||
|
- builds production iOS IPA for `org.parres.whitenoise`
|
||||||
|
- stages artifacts under `build/releases/v<version>+<build>/<flavor>/`
|
||||||
|
|
||||||
|
Staging and production are separate apps on both Android and iOS. Current iOS
|
||||||
|
publishing target for both apps is App Store Connect/TestFlight. Store release
|
||||||
|
submission, GitHub Releases, Zap Store, and Play Store upload lanes will be added
|
||||||
|
after signing and account setup are stable.
|
||||||
|
|
||||||
|
Current app IDs:
|
||||||
|
|
||||||
|
- Production Android: `org.parres.whitenoise`
|
||||||
|
- Production iOS: `org.parres.whitenoise`
|
||||||
|
- Staging Android: `org.parres.whitenoise.staging`
|
||||||
|
- Staging iOS: `dev.ipf.whitenoise.staging`
|
||||||
|
|
||||||
|
If production moves under the IPF namespace later, the expected target would be
|
||||||
|
`dev.ipf.whitenoise` for production and `dev.ipf.whitenoise.staging` for staging
|
||||||
|
on both platforms.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Install the Fastlane bundle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Release Runbook
|
||||||
|
|
||||||
|
The current release flow is local build automation plus manual uploads.
|
||||||
|
|
||||||
|
Set the release version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RELEASE_VERSION=2026.4.28
|
||||||
|
export RELEASE_BUILD=23
|
||||||
|
export RELEASE_TAG=v2026.4.28+23
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Update `pubspec.yaml`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 2026.4.28+23
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update `CHANGELOG.md`.
|
||||||
|
|
||||||
|
Move the current `Unreleased` content into a dated release section and add a
|
||||||
|
fresh `Unreleased` section above it:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
## [2026.4.28] - 2026-04-28
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the pre-release checks.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just precommit-check
|
||||||
|
just test-release-scripts
|
||||||
|
just release-doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Commit the release prep.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pubspec.yaml CHANGELOG.md RELEASE.md
|
||||||
|
git commit -m "chore: prepare ${RELEASE_TAG}"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create the annotated tag on the release commit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG}"
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Validate the tag guard.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-doctor "${RELEASE_TAG}"
|
||||||
|
```
|
||||||
|
|
||||||
|
This fails unless `pubspec.yaml` matches the tag, the tag exists, and the tag
|
||||||
|
points at the current `HEAD`.
|
||||||
|
|
||||||
|
7. Build staging and production artifacts.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-all "${RELEASE_TAG}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Release artifacts are staged under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
build/releases/v2026.4.28+23/staging/android/
|
||||||
|
build/releases/v2026.4.28+23/staging/ios/
|
||||||
|
build/releases/v2026.4.28+23/production/android/
|
||||||
|
build/releases/v2026.4.28+23/production/ios/
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Upload artifacts manually.
|
||||||
|
|
||||||
|
Current upload targets:
|
||||||
|
|
||||||
|
- GitHub Release: production APKs
|
||||||
|
- Zap Store: production arm64 APK selected by `zapstore.yaml`
|
||||||
|
- App Store Connect/TestFlight: staging IPA and production IPA
|
||||||
|
- Play Store: production AAB after Play setup is complete
|
||||||
|
- Play Store staging app: staging AAB after Play setup is complete
|
||||||
|
|
||||||
|
9. Push the release commit and tag after the build is verified.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin HEAD
|
||||||
|
git push origin "${RELEASE_TAG}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Validate the current release version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate the current release version against a tag that exists on `HEAD`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-doctor v2026.3.23+22
|
||||||
|
```
|
||||||
|
|
||||||
|
When a tag is passed, the validator requires all of these to be true:
|
||||||
|
|
||||||
|
- `pubspec.yaml` contains `version: 2026.3.23+22`
|
||||||
|
- the tag name is `v2026.3.23+22`
|
||||||
|
- the tag exists in git
|
||||||
|
- the tag points at the current `HEAD`
|
||||||
|
|
||||||
|
Build the staging app release artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the production app release artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-production
|
||||||
|
```
|
||||||
|
|
||||||
|
Build both staging and production app release artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-all v2026.3.23+22
|
||||||
|
```
|
||||||
|
|
||||||
|
Build only staging Android artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-android-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
Build only production Android artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-android-production
|
||||||
|
```
|
||||||
|
|
||||||
|
Build only staging iOS artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-ios-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
Build only production iOS artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just release-build-ios-production
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Do not commit store credentials, App Store Connect API keys, provisioning files,
|
||||||
|
or Android keystores. Fastlane local credential files such as `.p8` and `.json`
|
||||||
|
files under `fastlane/` are ignored by git.
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
require 'fileutils'
|
||||||
|
require 'open3'
|
||||||
|
require 'shellwords'
|
||||||
|
|
||||||
|
opt_out_usage
|
||||||
|
|
||||||
|
ROOT = File.expand_path('..', __dir__)
|
||||||
|
FLAVORS = {
|
||||||
|
'staging' => {
|
||||||
|
artifact_prefix: 'whitenoise-staging',
|
||||||
|
android_package_name: 'org.parres.whitenoise.staging',
|
||||||
|
ios_app_identifier: 'dev.ipf.whitenoise.staging',
|
||||||
|
},
|
||||||
|
'production' => {
|
||||||
|
artifact_prefix: 'whitenoise',
|
||||||
|
android_package_name: 'org.parres.whitenoise',
|
||||||
|
ios_app_identifier: 'org.parres.whitenoise',
|
||||||
|
},
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def run_from_root(parts)
|
||||||
|
sh("cd #{Shellwords.escape(ROOT)} && #{parts.shelljoin}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def flavor_config(flavor)
|
||||||
|
config = FLAVORS[flavor.to_s]
|
||||||
|
UI.user_error!("Unknown release flavor: #{flavor}") unless config
|
||||||
|
config
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_tag(options = {})
|
||||||
|
tag = options[:tag].to_s.strip
|
||||||
|
return tag unless tag.empty?
|
||||||
|
|
||||||
|
tag = ENV.fetch('RELEASE_TAG', '').to_s.strip
|
||||||
|
return tag unless tag.empty?
|
||||||
|
|
||||||
|
return ENV.fetch('GITHUB_REF_NAME', '').to_s.strip if ENV['GITHUB_REF_TYPE'] == 'tag'
|
||||||
|
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_info(options = {})
|
||||||
|
tag = release_tag(options)
|
||||||
|
|
||||||
|
command = ['./scripts/validate_release_version.sh']
|
||||||
|
command += ['--tag', tag] unless tag.empty?
|
||||||
|
|
||||||
|
output, error, status = Open3.capture3(*command, chdir: ROOT)
|
||||||
|
UI.user_error!(error.strip.empty? ? output.strip : error.strip) unless status.success?
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
output.lines.each do |line|
|
||||||
|
key, value = line.strip.split('=', 2)
|
||||||
|
info[key] = value if key && value
|
||||||
|
end
|
||||||
|
|
||||||
|
UI.user_error!('Release version could not be resolved') unless info['full_version']
|
||||||
|
UI.user_error!('Release build number could not be resolved') unless info['build_number']
|
||||||
|
|
||||||
|
info
|
||||||
|
end
|
||||||
|
|
||||||
|
def flutter_build_args(info)
|
||||||
|
['--build-name', info['version_name'], '--build-number', info['build_number']]
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_output_dir(info, flavor, *parts)
|
||||||
|
File.join(ROOT, 'build', 'releases', "v#{info['full_version']}", flavor.to_s, *parts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage_artifact(source, destination_dir, destination_name)
|
||||||
|
UI.user_error!("Missing artifact: #{source}") unless File.file?(source)
|
||||||
|
|
||||||
|
FileUtils.mkdir_p(destination_dir)
|
||||||
|
destination = File.join(destination_dir, destination_name)
|
||||||
|
FileUtils.cp(source, destination)
|
||||||
|
UI.message("Staged #{destination}")
|
||||||
|
destination
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_ipa
|
||||||
|
Dir[File.join(ROOT, 'build', 'ios', 'ipa', '*.ipa')].max_by { |path| File.mtime(path) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def android_aab(flavor)
|
||||||
|
candidates = Dir[File.join(ROOT, 'build', 'app', 'outputs', 'bundle', '**', "*#{flavor}*.aab")]
|
||||||
|
candidates.max_by { |path| File.mtime(path) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_macos!
|
||||||
|
UI.user_error!('iOS release builds require macOS') unless RUBY_PLATFORM.include?('darwin')
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_android_flavor(flavor, options, build_native: true)
|
||||||
|
info = release_info(options)
|
||||||
|
config = flavor_config(flavor)
|
||||||
|
args = flutter_build_args(info)
|
||||||
|
|
||||||
|
run_from_root(['./scripts/build_android.sh']) if build_native
|
||||||
|
run_from_root(['flutter', 'build', 'apk', '--flavor', flavor, '--split-per-abi'] + args)
|
||||||
|
run_from_root(['flutter', 'build', 'appbundle', '--flavor', flavor] + args)
|
||||||
|
|
||||||
|
apk_dir = File.join(ROOT, 'build', 'app', 'outputs', 'flutter-apk')
|
||||||
|
output_dir = release_output_dir(info, flavor, 'android')
|
||||||
|
|
||||||
|
%w[arm64-v8a armeabi-v7a x86_64].each do |abi|
|
||||||
|
source = File.join(apk_dir, "app-#{abi}-#{flavor}-release.apk")
|
||||||
|
unless File.file?(source)
|
||||||
|
next UI.important("Skipping missing optional APK: #{source}") if abi == 'armeabi-v7a'
|
||||||
|
|
||||||
|
UI.user_error!("Required APK was not found: #{source}")
|
||||||
|
end
|
||||||
|
|
||||||
|
stage_artifact(source, output_dir, "#{config[:artifact_prefix]}-#{info['version_name']}-#{abi}.apk")
|
||||||
|
end
|
||||||
|
|
||||||
|
source = android_aab(flavor)
|
||||||
|
UI.user_error!("#{flavor.capitalize} AAB was not found") unless source
|
||||||
|
|
||||||
|
stage_artifact(source, output_dir, "#{config[:artifact_prefix]}-#{info['version_name']}.aab")
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_ios_flavor(flavor, options, build_native: true)
|
||||||
|
require_macos!
|
||||||
|
info = release_info(options)
|
||||||
|
config = flavor_config(flavor)
|
||||||
|
|
||||||
|
run_from_root(['./scripts/build_ios.sh']) if build_native
|
||||||
|
run_from_root(
|
||||||
|
['flutter', 'build', 'ipa', '--flavor', flavor, '--export-method', 'app-store'] +
|
||||||
|
flutter_build_args(info),
|
||||||
|
)
|
||||||
|
|
||||||
|
source = latest_ipa
|
||||||
|
UI.user_error!("#{flavor.capitalize} IPA was not found") unless source
|
||||||
|
|
||||||
|
stage_artifact(
|
||||||
|
source,
|
||||||
|
release_output_dir(info, flavor, 'ios'),
|
||||||
|
"#{config[:artifact_prefix]}-#{info['full_version']}.ipa",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_channel_release(flavor, options)
|
||||||
|
build_android_flavor(flavor, options)
|
||||||
|
|
||||||
|
if RUBY_PLATFORM.include?('darwin')
|
||||||
|
build_ios_flavor(flavor, options)
|
||||||
|
else
|
||||||
|
UI.important("Skipping #{flavor} iOS artifacts because this machine is not macOS")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Validate release version metadata'
|
||||||
|
lane :release_doctor do |options|
|
||||||
|
info = release_info(options)
|
||||||
|
UI.success("Release #{info['full_version']} is ready to build")
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build staging Android APKs and AAB'
|
||||||
|
lane :build_android_staging do |options|
|
||||||
|
build_android_flavor('staging', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build production Android APKs and AAB'
|
||||||
|
lane :build_android_production do |options|
|
||||||
|
build_android_flavor('production', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build staging iOS IPA for App Store Connect/TestFlight'
|
||||||
|
lane :build_ios_staging do |options|
|
||||||
|
build_ios_flavor('staging', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build production iOS IPA for App Store Connect/TestFlight'
|
||||||
|
lane :build_ios_production do |options|
|
||||||
|
build_ios_flavor('production', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build staging app release artifacts'
|
||||||
|
lane :build_staging_release do |options|
|
||||||
|
build_channel_release('staging', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build production app release artifacts'
|
||||||
|
lane :build_production_release do |options|
|
||||||
|
build_channel_release('production', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Build staging and production release artifacts'
|
||||||
|
lane :build_all_release_artifacts do |options|
|
||||||
|
build_android_flavor('staging', options)
|
||||||
|
build_android_flavor('production', options, build_native: false)
|
||||||
|
|
||||||
|
if RUBY_PLATFORM.include?('darwin')
|
||||||
|
build_ios_flavor('staging', options)
|
||||||
|
build_ios_flavor('production', options, build_native: false)
|
||||||
|
else
|
||||||
|
UI.important('Skipping iOS artifacts because this machine is not macOS')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -186,6 +186,10 @@ validate-locales-keys:
|
|||||||
@echo "🔍 Validating l10n keys..."
|
@echo "🔍 Validating l10n keys..."
|
||||||
./scripts/validate-locales-keys.sh
|
./scripts/validate-locales-keys.sh
|
||||||
|
|
||||||
|
test-release-scripts:
|
||||||
|
@bash test/scripts/validate_release_version_test.sh
|
||||||
|
@bash test/scripts/release_automation_config_test.sh
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CLEANING
|
# CLEANING
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -278,6 +282,74 @@ build-release-android:
|
|||||||
build-release-ios:
|
build-release-ios:
|
||||||
./scripts/build_release.sh --ios
|
./scripts/build_release.sh --ios
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# RELEASE AUTOMATION
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# Validate release version metadata, optionally against a git tag
|
||||||
|
release-doctor tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane release_doctor tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane release_doctor; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build staging Android release artifacts with Fastlane
|
||||||
|
release-build-android-staging tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane build_android_staging tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane build_android_staging; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build production Android release artifacts with Fastlane
|
||||||
|
release-build-android-production tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane build_android_production tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane build_android_production; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build staging iOS IPA for App Store Connect/TestFlight with Fastlane
|
||||||
|
release-build-ios-staging tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane build_ios_staging tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane build_ios_staging; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build production iOS IPA for App Store Connect/TestFlight with Fastlane
|
||||||
|
release-build-ios-production tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane build_ios_production tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane build_ios_production; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build staging app release artifacts with Fastlane
|
||||||
|
release-build-staging tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane build_staging_release tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane build_staging_release; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build production app release artifacts with Fastlane
|
||||||
|
release-build-production tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane build_production_release tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane build_production_release; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build staging and production release artifacts with Fastlane
|
||||||
|
release-build-all tag="":
|
||||||
|
@if [ -n "{{tag}}" ]; then \
|
||||||
|
bundle exec fastlane build_all_release_artifacts tag:"{{tag}}"; \
|
||||||
|
else \
|
||||||
|
bundle exec fastlane build_all_release_artifacts; \
|
||||||
|
fi
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# BUILDING - iOS
|
# BUILDING - iOS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
Executable
+148
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PUBSPEC="pubspec.yaml"
|
||||||
|
TAG=""
|
||||||
|
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
|
||||||
|
TAG="${GITHUB_REF_NAME:-}"
|
||||||
|
fi
|
||||||
|
GITHUB_OUTPUT_FILE="${GITHUB_OUTPUT:-}"
|
||||||
|
REPO="."
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/validate_release_version.sh [--pubspec PATH] [--repo PATH] [--tag TAG] [--github-output PATH]
|
||||||
|
|
||||||
|
Validates that the Flutter pubspec version has a build number and, when a tag
|
||||||
|
is provided, that the tag matches the pubspec version and points at HEAD.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_option_value() {
|
||||||
|
local option="$1"
|
||||||
|
local value="${2:-}"
|
||||||
|
|
||||||
|
if [[ -z "$value" || "$value" == --* ]]; then
|
||||||
|
echo "Missing value for $option" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--pubspec)
|
||||||
|
require_option_value "$1" "${2:-}"
|
||||||
|
PUBSPEC="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--repo)
|
||||||
|
require_option_value "$1" "${2:-}"
|
||||||
|
REPO="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--tag)
|
||||||
|
require_option_value "$1" "${2:-}"
|
||||||
|
TAG="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--github-output)
|
||||||
|
require_option_value "$1" "${2:-}"
|
||||||
|
GITHUB_OUTPUT_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$PUBSPEC" = /* ]]; then
|
||||||
|
RESOLVED_PUBSPEC="$PUBSPEC"
|
||||||
|
else
|
||||||
|
RESOLVED_PUBSPEC="$REPO/$PUBSPEC"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$RESOLVED_PUBSPEC" ]]; then
|
||||||
|
echo "Pubspec not found: $RESOLVED_PUBSPEC" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_LINE="$(grep -E '^[[:space:]]*version:[[:space:]]*[^[:space:]]+' "$RESOLVED_PUBSPEC" | head -n 1 || true)"
|
||||||
|
if [[ -z "$VERSION_LINE" ]]; then
|
||||||
|
echo "Could not find version in $RESOLVED_PUBSPEC" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FULL_VERSION="$(
|
||||||
|
printf '%s\n' "$VERSION_LINE" |
|
||||||
|
sed -E 's/^[[:space:]]*version:[[:space:]]*//' |
|
||||||
|
sed -E 's/[[:space:]]*#.*$//' |
|
||||||
|
sed -E 's/^[[:space:]]*["'\'']?//; s/["'\'']?[[:space:]]*$//'
|
||||||
|
)"
|
||||||
|
if [[ "$FULL_VERSION" != *+* ]]; then
|
||||||
|
echo "Release version must include a build number, e.g. 2026.4.28+23" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_NAME="${FULL_VERSION%%+*}"
|
||||||
|
BUILD_NUMBER="${FULL_VERSION##*+}"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION_NAME" || -z "$BUILD_NUMBER" || "$VERSION_NAME" == "$BUILD_NUMBER" ]]; then
|
||||||
|
echo "Invalid release version: $FULL_VERSION" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ "$BUILD_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Build number must be numeric: $BUILD_NUMBER" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$TAG" ]]; then
|
||||||
|
TAG_NAME="${TAG#refs/tags/}"
|
||||||
|
TAG_VERSION="${TAG_NAME#v}"
|
||||||
|
if [[ "$TAG_VERSION" != "$FULL_VERSION" ]]; then
|
||||||
|
echo "Tag $TAG_NAME does not match pubspec version $FULL_VERSION" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "Tag validation requires a git repository: $REPO" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! TAG_COMMIT="$(git -C "$REPO" rev-parse --verify --quiet "$TAG_NAME^{commit}" 2>/dev/null)"; then
|
||||||
|
echo "Tag $TAG_NAME does not exist in $REPO" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HEAD_COMMIT="$(git -C "$REPO" rev-parse HEAD)"
|
||||||
|
if [[ "$TAG_COMMIT" != "$HEAD_COMMIT" ]]; then
|
||||||
|
echo "Tag $TAG_NAME does not point at HEAD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
printf '%s=%s\n' "$key" "$value"
|
||||||
|
if [[ -n "$GITHUB_OUTPUT_FILE" ]]; then
|
||||||
|
printf '%s=%s\n' "$key" "$value" >>"$GITHUB_OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
emit "full_version" "$FULL_VERSION"
|
||||||
|
emit "version_name" "$VERSION_NAME"
|
||||||
|
emit "build_number" "$BUILD_NUMBER"
|
||||||
|
if [[ -n "$TAG" ]]; then
|
||||||
|
emit "tag" "$TAG_NAME"
|
||||||
|
emit "tag_commit" "$TAG_COMMIT"
|
||||||
|
fi
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local haystack="$1"
|
||||||
|
local pattern="$2"
|
||||||
|
if ! printf '%s\n' "$haystack" | grep -Eq "^${pattern}$"; then
|
||||||
|
fail "expected output to contain line matching regex '$pattern'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
just_list="$(just --list --unsorted)"
|
||||||
|
assert_contains "$just_list" '[[:space:]]*release-build-staging[[:space:]].*'
|
||||||
|
assert_contains "$just_list" '[[:space:]]*release-build-production[[:space:]].*'
|
||||||
|
assert_contains "$just_list" '[[:space:]]*release-build-android-staging[[:space:]].*'
|
||||||
|
assert_contains "$just_list" '[[:space:]]*release-build-android-production[[:space:]].*'
|
||||||
|
assert_contains "$just_list" '[[:space:]]*release-build-ios-staging[[:space:]].*'
|
||||||
|
assert_contains "$just_list" '[[:space:]]*release-build-ios-production[[:space:]].*'
|
||||||
|
|
||||||
|
fastfile="$(cat fastlane/Fastfile)"
|
||||||
|
assert_contains "$fastfile" 'lane[[:space:]]+:build_android_staging[[:space:]]+do.*'
|
||||||
|
assert_contains "$fastfile" 'lane[[:space:]]+:build_android_production[[:space:]]+do.*'
|
||||||
|
assert_contains "$fastfile" 'lane[[:space:]]+:build_ios_staging[[:space:]]+do.*'
|
||||||
|
assert_contains "$fastfile" 'lane[[:space:]]+:build_ios_production[[:space:]]+do.*'
|
||||||
|
assert_contains "$fastfile" 'lane[[:space:]]+:build_staging_release[[:space:]]+do.*'
|
||||||
|
assert_contains "$fastfile" 'lane[[:space:]]+:build_production_release[[:space:]]+do.*'
|
||||||
|
assert_contains "$fastfile" "[[:space:]]*android_package_name: 'org\\.parres\\.whitenoise\\.staging',"
|
||||||
|
assert_contains "$fastfile" "[[:space:]]*android_package_name: 'org\\.parres\\.whitenoise',"
|
||||||
|
assert_contains "$fastfile" "[[:space:]]*ios_app_identifier: 'dev\\.ipf\\.whitenoise\\.staging',"
|
||||||
|
assert_contains "$fastfile" "[[:space:]]*ios_app_identifier: 'org\\.parres\\.whitenoise',"
|
||||||
|
|
||||||
|
android_gradle="$(cat android/app/build.gradle.kts)"
|
||||||
|
assert_contains "$android_gradle" '[[:space:]]*applicationIdSuffix = "\.staging"'
|
||||||
|
assert_contains "$android_gradle" '[[:space:]]*applicationId = "org\.parres\.whitenoise"'
|
||||||
|
|
||||||
|
ruby <<'RUBY'
|
||||||
|
require 'yaml'
|
||||||
|
|
||||||
|
config = YAML.safe_load(
|
||||||
|
File.read('zapstore.yaml'),
|
||||||
|
permitted_classes: [],
|
||||||
|
permitted_symbols: [],
|
||||||
|
aliases: false,
|
||||||
|
)
|
||||||
|
pattern = config.fetch('match')
|
||||||
|
regex = Regexp.new(pattern)
|
||||||
|
|
||||||
|
production_apk = 'whitenoise-2026.3.23-arm64-v8a.apk'
|
||||||
|
staging_apk = 'whitenoise-staging-2026.3.23-arm64-v8a.apk'
|
||||||
|
|
||||||
|
raise 'Zap Store match must include production arm64 APK' unless regex.match?(production_apk)
|
||||||
|
raise 'Zap Store match must exclude staging APK' if regex.match?(staging_apk)
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
echo "release_automation_config_test passed"
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SCRIPT="$REPO_ROOT/scripts/validate_release_version.sh"
|
||||||
|
|
||||||
|
TMPDIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMPDIR"' EXIT
|
||||||
|
RELEASE_REPO="$TMPDIR/repo"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local haystack="$1"
|
||||||
|
local needle="$2"
|
||||||
|
if [[ "$haystack" != *"$needle"* ]]; then
|
||||||
|
fail "expected output to contain '$needle'; got: $haystack"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_pubspec() {
|
||||||
|
local version="$1"
|
||||||
|
cat >"$RELEASE_REPO/pubspec.yaml" <<EOF
|
||||||
|
name: whitenoise
|
||||||
|
version: $version
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
write_pubspec_line() {
|
||||||
|
local version_line="$1"
|
||||||
|
cat >"$RELEASE_REPO/pubspec.yaml" <<EOF
|
||||||
|
name: whitenoise
|
||||||
|
$version_line
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir "$RELEASE_REPO"
|
||||||
|
git -C "$RELEASE_REPO" init --quiet
|
||||||
|
git -C "$RELEASE_REPO" config user.email "release-test@example.com"
|
||||||
|
git -C "$RELEASE_REPO" config user.name "Release Test"
|
||||||
|
|
||||||
|
write_pubspec "2026.4.28+23"
|
||||||
|
git -C "$RELEASE_REPO" add pubspec.yaml
|
||||||
|
git -C "$RELEASE_REPO" commit --quiet -m "prepare release"
|
||||||
|
git -C "$RELEASE_REPO" tag v2026.4.28+23
|
||||||
|
|
||||||
|
output="$("$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO" --tag "v2026.4.28+23")"
|
||||||
|
assert_contains "$output" "full_version=2026.4.28+23"
|
||||||
|
assert_contains "$output" "version_name=2026.4.28"
|
||||||
|
assert_contains "$output" "build_number=23"
|
||||||
|
assert_contains "$output" "tag=v2026.4.28+23"
|
||||||
|
assert_contains "$output" "tag_commit=$(git -C "$RELEASE_REPO" rev-parse HEAD)"
|
||||||
|
|
||||||
|
if "$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO" --tag "v2026.4.28+24" >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected mismatched tag to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "does not match pubspec version"
|
||||||
|
|
||||||
|
git -C "$RELEASE_REPO" tag -d v2026.4.28+23 >/dev/null
|
||||||
|
if "$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO" --tag "v2026.4.28+23" >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected missing tag to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "does not exist"
|
||||||
|
|
||||||
|
git -C "$RELEASE_REPO" tag v2026.4.28+23
|
||||||
|
echo "change after tag" >"$RELEASE_REPO/after-tag.txt"
|
||||||
|
git -C "$RELEASE_REPO" add after-tag.txt
|
||||||
|
git -C "$RELEASE_REPO" commit --quiet -m "move head"
|
||||||
|
if "$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO" --tag "v2026.4.28+23" >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected tag not at HEAD to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "does not point at HEAD"
|
||||||
|
|
||||||
|
write_pubspec "2026.4.28"
|
||||||
|
if "$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO" >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected missing build number to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "must include a build number"
|
||||||
|
|
||||||
|
write_pubspec_line 'version: "2026.4.28+23" # release candidate'
|
||||||
|
output="$("$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO")"
|
||||||
|
assert_contains "$output" "full_version=2026.4.28+23"
|
||||||
|
|
||||||
|
write_pubspec_line ' version: "2026.4.28+23" # release candidate'
|
||||||
|
output="$("$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO")"
|
||||||
|
assert_contains "$output" "full_version=2026.4.28+23"
|
||||||
|
|
||||||
|
mkdir "$TMPDIR/outside"
|
||||||
|
output="$(cd "$TMPDIR/outside" && "$SCRIPT" --pubspec pubspec.yaml --repo "$RELEASE_REPO")"
|
||||||
|
assert_contains "$output" "full_version=2026.4.28+23"
|
||||||
|
|
||||||
|
rm -f "$TMPDIR/out"
|
||||||
|
"$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo "$RELEASE_REPO" --github-output "$TMPDIR/out" >/dev/null
|
||||||
|
assert_contains "$(cat "$TMPDIR/out")" "full_version=2026.4.28+23"
|
||||||
|
assert_contains "$(cat "$TMPDIR/out")" "version_name=2026.4.28"
|
||||||
|
assert_contains "$(cat "$TMPDIR/out")" "build_number=23"
|
||||||
|
|
||||||
|
if "$SCRIPT" --pubspec >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected missing --pubspec value to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "Missing value for --pubspec"
|
||||||
|
|
||||||
|
if "$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --repo --tag "v2026.4.28+23" >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected missing --repo value to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "Missing value for --repo"
|
||||||
|
|
||||||
|
if "$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --tag --github-output "$TMPDIR/github-output" >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected missing --tag value to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "Missing value for --tag"
|
||||||
|
|
||||||
|
if "$SCRIPT" --pubspec "$RELEASE_REPO/pubspec.yaml" --github-output >"$TMPDIR/out" 2>"$TMPDIR/err"; then
|
||||||
|
fail "expected missing --github-output value to fail"
|
||||||
|
fi
|
||||||
|
assert_contains "$(cat "$TMPDIR/err")" "Missing value for --github-output"
|
||||||
|
|
||||||
|
echo "validate_release_version_test passed"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
repository: https://github.com/marmot-protocol/whitenoise
|
repository: https://github.com/marmot-protocol/whitenoise
|
||||||
|
match: "^whitenoise-[0-9].*-arm64-v8a\\.apk$"
|
||||||
description: |
|
description: |
|
||||||
**White Noise: The Identity-Free Messenger for Private, Censorship-Resistant Communication**
|
**White Noise: The Identity-Free Messenger for Private, Censorship-Resistant Communication**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user