rosefc 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rosefc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # The Node service: the dynamic half as a container. Builds the app, then
2
+ # runs `rosefc serve` - the cluster runner (one worker per core, bounded
3
+ # request bodies, request/header/keep-alive timeouts, graceful SIGTERM
4
+ # drain, one access line per request on stdout). Pair it with
5
+ # Dockerfile.static (the Go binary in front) using docker-compose.yml.
6
+ FROM node:22-alpine AS build
7
+ WORKDIR /app
8
+ COPY package.json package-lock.json ./
9
+ RUN npm ci
10
+ COPY . .
11
+ RUN npm run build
12
+
13
+ FROM node:22-alpine
14
+ WORKDIR /app
15
+ ENV NODE_ENV=production PORT=3000
16
+ COPY --from=build /app/node_modules ./node_modules
17
+ COPY --from=build /app/dist ./dist
18
+ COPY --from=build /app/src ./src
19
+ COPY --from=build /app/package.json ./
20
+ EXPOSE 3000
21
+ # ROSEFC_WORKERS defaults to the container's CPU count.
22
+ # The demo api's SQLite file lands in the working directory: mount a volume
23
+ # at /app to keep it across deploys (a real deployment points the api at
24
+ # Postgres instead - the connection block is the only thing that changes).
25
+ CMD ["node", "node_modules/tsx/dist/cli.mjs", "src/cli/index.ts", "serve"]
@@ -0,0 +1,34 @@
1
+ # The static half of the hybrid deploy.
2
+ #
3
+ # Build the site with Node, embed it into ONE Go binary, ship an image with
4
+ # no runtime to install and nothing to configure:
5
+ #
6
+ # docker build -f deploy/Dockerfile.static -t rosefc-static .
7
+ # docker run -p 8080:8080 -e UPSTREAM=http://dynamic:3000 rosefc-static
8
+ #
9
+ # UPSTREAM is optional. Without it the binary is the pure-static deploy (it
10
+ # answers 405 for POSTs and a JSON 404 for unbaked /api routes - the
11
+ # documented boundary). With it, POST server actions and live /api handlers
12
+ # are reverse-proxied to the Node/edge backend.
13
+ FROM node:22-alpine AS site
14
+ WORKDIR /app
15
+ COPY package.json package-lock.json ./
16
+ RUN npm ci
17
+ COPY . .
18
+ RUN npm run build
19
+
20
+ # golang:alpine (not a pinned patch) tracks go.mod: the module declares the
21
+ # minimum toolchain, and a stale pin is a build failure nobody needs.
22
+ FROM golang:alpine AS bin
23
+ WORKDIR /app
24
+ COPY main.go go.mod ./
25
+ COPY --from=site /app/dist ./dist
26
+ RUN go build -trimpath -o rosefc-server .
27
+
28
+ FROM alpine:3.20
29
+ COPY --from=bin /app/rosefc-server /usr/local/bin/rosefc-server
30
+ ENV PORT=8080
31
+ EXPOSE 8080
32
+ # The access log goes to stdout and errors to stderr, so `docker logs`
33
+ # and any collector see them without mounting a file.
34
+ ENTRYPOINT ["rosefc-server"]
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vercel function entry: the edge adapter, unchanged.
3
+ *
4
+ * vercel.json rewrites every unmatched path here and ships dist/ with the
5
+ * bundle (the adapter imports dist/server.js at runtime, so the function
6
+ * cannot start without it). The handler is createEdgeHandler's returned
7
+ * (request) => Response, exposed behind Vercel's Web Standard signature.
8
+ *
9
+ * The dist path goes through fileURLToPath, NOT URL.pathname: on Windows a
10
+ * pathname carries the drive as `/C:/...` and the leading slash makes the
11
+ * dynamic import resolve against the drive root instead of this directory.
12
+ */
13
+ import { fileURLToPath } from 'node:url';
14
+ import { createEdgeHandler } from '../src/cli/edge.ts';
15
+
16
+ const dist = fileURLToPath(new URL('../dist/', import.meta.url));
17
+ const handle = await createEdgeHandler(dist);
18
+
19
+ export default { fetch: handle };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The rosefc Cloudflare Pages function - the dynamic half of a Pages
3
+ * deploy.
4
+ *
5
+ * Pages answers static assets (everything in dist/: prerendered
6
+ * documents, bundles, baked API bodies) from its own asset server and
7
+ * invokes this function only for the paths that are NOT files. So the
8
+ * split is the same as on every other target - the static half never
9
+ * pays a function invocation, and the dynamic half (dynamic routes, live
10
+ * /api routes, POST server actions) renders here, one request per page.
11
+ *
12
+ * The `[[path]]` filename is Pages' catch-all: this file handles every
13
+ * incoming URL the asset layer did not answer.
14
+ *
15
+ * workerd has NO filesystem, which is the one real difference from the
16
+ * Node targets: the adapter cannot read dist/ at boot. Two things close
17
+ * that gap, both visible below:
18
+ *
19
+ * 1. `import * as server from '../../dist/server.js'` - a STATIC import,
20
+ * so Pages' deploy-time compiler bundles the server module into the
21
+ * function. (The generated bundle has zero Node built-in imports, so
22
+ * it runs on workerd as-is.)
23
+ * 2. the client bundle, the component styles and the broken-route
24
+ * manifest are fetched from this very site once per isolate - they
25
+ * are static assets of the same deployment, and the function only
26
+ * ever sees non-asset paths, so the fetch cannot re-enter it.
27
+ *
28
+ * The CSP is then computed from the exact fetched bytes, identical to what
29
+ * the Node server hashes: same documents, same headers, every target.
30
+ *
31
+ * Install: copy this file to <project>/functions/[[path]].ts and
32
+ * deploy/cloudflare/wrangler.toml to <project>/wrangler.toml, then
33
+ * `npx wrangler pages deploy` (or push, with the Pages git integration).
34
+ * In a project that depends on the published package the import below
35
+ * becomes:
36
+ * import { createEdgeHandler } from 'rosefc/edge';
37
+ */
38
+ import { createEdgeHandler } from '../src/cli/edge.ts';
39
+ import * as server from '../dist/server.js';
40
+
41
+ let handle: ((request: Request) => Promise<Response>) | null = null;
42
+
43
+ export const onRequest = async (context: { request: Request }): Promise<Response> => {
44
+ if (!handle) {
45
+ const base = new URL(context.request.url);
46
+ const [client, styles, broken] = await Promise.all([
47
+ fetch(new URL('/client.js', base)).then((r) => r.text()),
48
+ fetch(new URL('/styles.css', base)).then((r) => (r.ok ? r.text() : '')),
49
+ fetch(new URL('/broken.json', base)).then((r) => (r.ok ? r.json() : [])),
50
+ ]);
51
+ handle = await createEdgeHandler('./dist', {
52
+ server,
53
+ client,
54
+ styles,
55
+ broken: broken as string[],
56
+ });
57
+ }
58
+ return handle(context.request);
59
+ };
@@ -0,0 +1,14 @@
1
+ # Cloudflare Pages - rosefc. `pages_build_output_dir` makes this file the
2
+ # source of truth for the project, so it lives in the repo next to the app.
3
+ name = "rosefc"
4
+ pages_build_output_dir = "./dist"
5
+
6
+ # The build writes dist/ exactly like every other target; Pages then serves
7
+ # it as static assets and deploys ./functions alongside it.
8
+ [build]
9
+ command = "npm run build"
10
+
11
+ # workerd has no Node filesystem: the function imports the adapter, which
12
+ # imports node:url / node:path. nodejs_compat provides both.
13
+ compatibility_date = "2024-09-23"
14
+ compatibility_flags = ["nodejs_compat"]
@@ -0,0 +1,45 @@
1
+ # The hybrid deploy:
2
+ # one command - `docker compose -f deploy/docker-compose.yml up --build`.
3
+ #
4
+ # static -> the immutable half: the Go single binary serves prerendered
5
+ # documents, build-precompressed brotli, ETag 304s and the SPA
6
+ # fallback, logs one access line per request to stdout and
7
+ # answers /metrics (Prometheus text format).
8
+ # dynamic -> the live half: server actions, /api handlers, middleware,
9
+ # $data - the Node server, same code, same compiler output.
10
+ #
11
+ # Every request the static half cannot answer itself (any POST, any /api
12
+ # route without a baked body) is reverse-proxied to `dynamic` via UPSTREAM.
13
+ # Drop the UPSTREAM line and the static service becomes the pure-static
14
+ # deploy: POSTs answer 405, unbaked APIs answer the JSON 404.
15
+ services:
16
+ static:
17
+ build:
18
+ context: ..
19
+ dockerfile: deploy/Dockerfile.static
20
+ environment:
21
+ PORT: "8080"
22
+ UPSTREAM: http://dynamic:3000
23
+ ports:
24
+ - "8080:8080"
25
+ depends_on:
26
+ dynamic:
27
+ condition: service_healthy
28
+ restart: unless-stopped
29
+ dynamic:
30
+ image: node:22-alpine
31
+ working_dir: /app
32
+ # Build inside the container: the host's node_modules may hold
33
+ # foreign-platform binaries (esbuild ships one per OS).
34
+ command: sh -c "npm ci && npm run build && npm run preview"
35
+ environment:
36
+ PORT: "3000"
37
+ volumes:
38
+ - ..:/app
39
+ - /app/node_modules
40
+ healthcheck:
41
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
42
+ interval: 5s
43
+ timeout: 3s
44
+ retries: 20
45
+ restart: unless-stopped
@@ -0,0 +1,28 @@
1
+ # Fly.io: the rosefc Node service as a machine.
2
+ #
3
+ # cp deploy/fly.toml ./fly.toml
4
+ # fly launch --no-deploy
5
+ # fly deploy
6
+ #
7
+ # The app builds deploy/Dockerfile.node (Node builds the site, then runs
8
+ # `rosefc serve` - the cluster runner). Swap the dockerfile line for
9
+ # "deploy/Dockerfile.static" to ship the Go single binary instead, and set
10
+ # UPSTREAM to the Node app for the dynamic half.
11
+ app = "rosefc"
12
+ primary_region = "iad"
13
+
14
+ [build]
15
+ dockerfile = "deploy/Dockerfile.node"
16
+
17
+ [http_service]
18
+ internal_port = 3000
19
+ force_https = true
20
+ # rosefc serve already clusters across every core of the machine; give it
21
+ # the cores instead of paying for more, smaller machines.
22
+ auto_stop_machines = "suspend"
23
+ auto_start_machines = true
24
+ min_machines_running = 1
25
+
26
+ [[vm]]
27
+ size = "shared-cpu-2x"
28
+ memory = "512mb"
@@ -0,0 +1,128 @@
1
+ # The hybrid deploy on Kubernetes - the same split
2
+ # `docker-compose.yml` runs, as manifests:
3
+ #
4
+ # static -> the immutable half: the Go single binary serves prerendered
5
+ # documents, build-precompressed brotli, ETag 304s and the SPA
6
+ # fallback, logs one access line per request and answers
7
+ # /metrics (Prometheus text format).
8
+ # dynamic -> the live half: server actions, /api handlers, middleware,
9
+ # $data - the Node server, same code, same compiler output.
10
+ #
11
+ # Every request the static half cannot answer itself (any POST, any /api
12
+ # route without a baked body) is reverse-proxied to `dynamic` through
13
+ # UPSTREAM, which is the in-cluster DNS name of the Service below. Delete the
14
+ # UPSTREAM env and the static Deployment becomes the pure-static deploy:
15
+ # POSTs answer 405, unbaked APIs answer the JSON 404.
16
+ #
17
+ # kubectl apply -f deploy/k8s/rosefc.yaml
18
+ #
19
+ # Build the two images first (from the repo root):
20
+ # docker build -f deploy/Dockerfile.static -t rosefc-static .
21
+ # docker build -f deploy/Dockerfile.node -t rosefc .
22
+ # ...then push them to your registry and replace the image names below.
23
+ # The Ingress is optional: delete it and port-forward the static Service.
24
+ apiVersion: apps/v1
25
+ kind: Deployment
26
+ metadata:
27
+ name: rosefc-static
28
+ labels: { app: rosefc, tier: static }
29
+ spec:
30
+ replicas: 2
31
+ selector:
32
+ matchLabels: { app: rosefc, tier: static }
33
+ template:
34
+ metadata:
35
+ labels: { app: rosefc, tier: static }
36
+ annotations:
37
+ prometheus.io/scrape: "true"
38
+ prometheus.io/path: /metrics
39
+ prometheus.io/port: "8080"
40
+ spec:
41
+ containers:
42
+ - name: static
43
+ image: rosefc-static:latest
44
+ env:
45
+ - name: PORT
46
+ value: "8080"
47
+ - name: UPSTREAM
48
+ value: http://rosefc-dynamic:3000
49
+ ports:
50
+ - name: http
51
+ containerPort: 8080
52
+ readinessProbe:
53
+ httpGet: { path: /, port: http }
54
+ resources:
55
+ requests: { cpu: 50m, memory: 32Mi }
56
+ limits: { memory: 64Mi }
57
+ ---
58
+ apiVersion: v1
59
+ kind: Service
60
+ metadata:
61
+ name: rosefc-static
62
+ spec:
63
+ selector: { app: rosefc, tier: static }
64
+ ports:
65
+ - name: http
66
+ port: 80
67
+ targetPort: http
68
+ ---
69
+ apiVersion: apps/v1
70
+ kind: Deployment
71
+ metadata:
72
+ name: rosefc-dynamic
73
+ labels: { app: rosefc, tier: dynamic }
74
+ spec:
75
+ replicas: 2
76
+ selector:
77
+ matchLabels: { app: rosefc, tier: dynamic }
78
+ template:
79
+ metadata:
80
+ labels: { app: rosefc, tier: dynamic }
81
+ spec:
82
+ containers:
83
+ - name: dynamic
84
+ image: rosefc:latest
85
+ # `rosefc serve` is the production runner: the stdlib cluster (one
86
+ # worker per core), bounded bodies, timeouts, graceful drain.
87
+ command: ["rosefc", "serve"]
88
+ env:
89
+ - name: PORT
90
+ value: "3000"
91
+ ports:
92
+ - name: http
93
+ containerPort: 3000
94
+ readinessProbe:
95
+ httpGet: { path: /, port: http }
96
+ resources:
97
+ requests: { cpu: 100m, memory: 128Mi }
98
+ limits: { memory: 512Mi }
99
+ ---
100
+ apiVersion: v1
101
+ kind: Service
102
+ metadata:
103
+ name: rosefc-dynamic
104
+ spec:
105
+ # ClusterIP on purpose: the dynamic half is reached through the static
106
+ # half's UPSTREAM proxy, never directly.
107
+ type: ClusterIP
108
+ selector: { app: rosefc, tier: dynamic }
109
+ ports:
110
+ - name: http
111
+ port: 3000
112
+ targetPort: http
113
+ ---
114
+ apiVersion: networking.k8s.io/v1
115
+ kind: Ingress
116
+ metadata:
117
+ name: rosefc
118
+ spec:
119
+ rules:
120
+ - http:
121
+ paths:
122
+ - path: /
123
+ pathType: Prefix
124
+ backend:
125
+ service:
126
+ name: rosefc-static
127
+ port:
128
+ number: 80
@@ -0,0 +1,87 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Rosefc - the shared scheme for the generated Xcode project.
4
+
5
+ `rosefc export xcode` copies this to
6
+ RosefcApp.xcodeproj/xcshareddata/xcschemes/RosefcApp.xcscheme and substitutes
7
+ __ROSEFC_TARGET_ID__ with the pbxproj's own target id. `xcodebuild archive`
8
+ requires a scheme, and Xcode only auto-creates one when the project is opened
9
+ in the IDE - which a CI runner never does.
10
+ -->
11
+ <Scheme
12
+ LastUpgradeVersion = "1500"
13
+ version = "1.7">
14
+ <BuildAction
15
+ parallelizeBuildables = "YES"
16
+ buildImplicitDependencies = "YES">
17
+ <BuildActionEntries>
18
+ <BuildActionEntry
19
+ buildForTesting = "YES"
20
+ buildForRunning = "YES"
21
+ buildForProfiling = "YES"
22
+ buildForArchiving = "YES"
23
+ buildForAnalyzing = "YES">
24
+ <BuildableReference
25
+ BuildableIdentifier = "primary"
26
+ BlueprintIdentifier = "__ROSEFC_TARGET_ID__"
27
+ BuildableName = "RosefcApp.app"
28
+ BlueprintName = "RosefcApp"
29
+ ReferencedContainer = "container:RosefcApp.xcodeproj">
30
+ </BuildableReference>
31
+ </BuildActionEntry>
32
+ </BuildActionEntries>
33
+ </BuildAction>
34
+ <TestAction
35
+ buildConfiguration = "Debug"
36
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
37
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
38
+ shouldUseLaunchSchemeArgsEnv = "YES">
39
+ <Testables>
40
+ </Testables>
41
+ </TestAction>
42
+ <LaunchAction
43
+ buildConfiguration = "Debug"
44
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
45
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
46
+ launchStyle = "0"
47
+ useCustomWorkingDirectory = "NO"
48
+ ignoresPersistentStateOnLaunch = "NO"
49
+ debugDocumentVersioning = "YES"
50
+ debugServiceExtension = "internal"
51
+ allowLocationSimulation = "YES">
52
+ <BuildableProductRunnable
53
+ runnableDebuggingMode = "0">
54
+ <BuildableReference
55
+ BuildableIdentifier = "primary"
56
+ BlueprintIdentifier = "__ROSEFC_TARGET_ID__"
57
+ BuildableName = "RosefcApp.app"
58
+ BlueprintName = "RosefcApp"
59
+ ReferencedContainer = "container:RosefcApp.xcodeproj">
60
+ </BuildableReference>
61
+ </BuildableProductRunnable>
62
+ </LaunchAction>
63
+ <ProfileAction
64
+ buildConfiguration = "Release"
65
+ shouldUseLaunchSchemeArgsEnv = "YES"
66
+ savedToolIdentifier = ""
67
+ useCustomWorkingDirectory = "NO"
68
+ debugDocumentVersioning = "YES">
69
+ <BuildableProductRunnable
70
+ runnableDebuggingMode = "0">
71
+ <BuildableReference
72
+ BuildableIdentifier = "primary"
73
+ BlueprintIdentifier = "__ROSEFC_TARGET_ID__"
74
+ BuildableName = "RosefcApp.app"
75
+ BlueprintName = "RosefcApp"
76
+ ReferencedContainer = "container:RosefcApp.xcodeproj">
77
+ </BuildableReference>
78
+ </BuildableProductRunnable>
79
+ </ProfileAction>
80
+ <AnalyzeAction
81
+ buildConfiguration = "Debug">
82
+ </AnalyzeAction>
83
+ <ArchiveAction
84
+ buildConfiguration = "Release"
85
+ revealArchiveInOrganizer = "YES">
86
+ </ArchiveAction>
87
+ </Scheme>
@@ -0,0 +1,62 @@
1
+ # Rosefc - Android release build (GitHub Actions).
2
+ #
3
+ # `rosefc export android` copies this file into the export tree as
4
+ # .github/workflows/android.yml, at the root of the exported Gradle project,
5
+ # so every path below is the project's own.
6
+ #
7
+ # No credential is written into this file. A signed build reads its keystore
8
+ # material from `${{ secrets.NAME }}` - see DEPLOY.md, the manual step.
9
+
10
+ name: Android
11
+
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ pull_request:
16
+ workflow_dispatch:
17
+
18
+ jobs:
19
+ apk:
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-java@v4
26
+ with:
27
+ distribution: temurin
28
+ java-version: '17'
29
+
30
+ # The export carries no Gradle wrapper (gradlew is a binary the emitter
31
+ # does not generate), so this action installs and caches Gradle itself.
32
+ - uses: gradle/actions/setup-gradle@v4
33
+ with:
34
+ gradle-version: '8.7'
35
+
36
+ # Unsigned until the signing config in DEPLOY.md is in place - the
37
+ # resulting APK still installs with `adb install`.
38
+ - name: gradle assembleRelease
39
+ run: gradle assembleRelease
40
+
41
+ - uses: actions/upload-artifact@v4
42
+ with:
43
+ name: rosefc-android-release
44
+ path: app/build/outputs/apk/release/*.apk
45
+ if-no-files-found: error
46
+
47
+ # Signed release: add the four repository secrets below, then decode the
48
+ # keystore before the build and add the `signingConfigs.release` block that
49
+ # reads keystore.properties (DEPLOY.md shows both).
50
+ #
51
+ # env:
52
+ # ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
53
+ # ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
54
+ # ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
55
+ # ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
56
+ #
57
+ # steps:
58
+ # - name: Decode the release keystore
59
+ # run: |
60
+ # echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > release.keystore
61
+ # printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \
62
+ # "$ANDROID_KEYSTORE_PASSWORD" "$ANDROID_KEY_ALIAS" "$ANDROID_KEY_PASSWORD" > keystore.properties
@@ -0,0 +1,96 @@
1
+ # Rosefc - Flutter build (GitHub Actions): APK + unsigned iOS app.
2
+ #
3
+ # `rosefc export flutter` copies this file into the export tree as
4
+ # .github/workflows/flutter.yml, at the root of the exported Flutter project,
5
+ # so every path below is the project's own.
6
+ #
7
+ # No credential is written into this file. A signed build reads its keystore
8
+ # or certificate from `${{ secrets.NAME }}` - see DEPLOY.md.
9
+
10
+ name: Flutter
11
+
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ pull_request:
16
+ workflow_dispatch:
17
+
18
+ jobs:
19
+ apk:
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-java@v4
26
+ with:
27
+ distribution: temurin
28
+ java-version: '17'
29
+
30
+ - uses: subosito/flutter-action@v2
31
+ with:
32
+ channel: stable
33
+ cache: true
34
+
35
+ - run: flutter pub get
36
+
37
+ # The export ships `lib/` and `pubspec.yaml` but no platform folders
38
+ # (the emitter generates none). `flutter create` fills in android/ and
39
+ # ios/ without touching the generated Dart - `--overwrite` is what would
40
+ # clobber it, so it is deliberately absent. --org matters: with no
41
+ # ios/Runner or android/app on disk, `flutter create` cannot infer one.
42
+ - name: Scaffold the platform folders
43
+ run: flutter create --platforms=android,ios --org dev.rosefc --project-name rosefc_app .
44
+
45
+ # Flutter falls back to the debug key until a release keystore is
46
+ # configured, so this APK builds and installs without one - signing it is
47
+ # the manual step (see DEPLOY.md).
48
+ - name: flutter build apk
49
+ run: flutter build apk --release
50
+
51
+ - uses: actions/upload-artifact@v4
52
+ with:
53
+ name: rosefc-flutter-apk
54
+ path: build/app/outputs/flutter-apk/app-release.apk
55
+ if-no-files-found: error
56
+
57
+ ios:
58
+ runs-on: macos-14
59
+
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+
63
+ - uses: subosito/flutter-action@v2
64
+ with:
65
+ channel: stable
66
+ cache: true
67
+
68
+ - run: flutter pub get
69
+
70
+ - name: Scaffold the platform folders
71
+ run: flutter create --platforms=android,ios --project-name rosefc_app .
72
+
73
+ # `flutter build ipa` needs a signed Xcode project, and the export ships
74
+ # no signing setup. Until a certificate exists, build the device app
75
+ # bundle without codesign, then package it as an .ipa by hand -
76
+ # Payload/Runner.app is the whole format.
77
+ - name: flutter build ios (unsigned)
78
+ run: flutter build ios --release --no-codesign
79
+
80
+ - name: Package an unsigned ipa
81
+ run: |
82
+ mkdir -p build/ipa/Payload
83
+ cp -R build/ios/iphoneos/Runner.app build/ipa/Payload/
84
+ cd build/ipa && zip -qry RosefcApp-unsigned.ipa Payload
85
+
86
+ - uses: actions/upload-artifact@v4
87
+ with:
88
+ name: rosefc-flutter-ipa
89
+ path: build/ipa/RosefcApp-unsigned.ipa
90
+ if-no-files-found: error
91
+
92
+ # Signed builds: add the secrets named in DEPLOY.md, then
93
+ # - Android: write android/key.properties from
94
+ # `${{ secrets.ANDROID_KEYSTORE_BASE64 }}` (base64 -d) et al.
95
+ # - iOS: import `${{ secrets.IOS_CERT_P12_BASE64 }}` into a temporary
96
+ # keychain and replace the two steps above with `flutter build ipa --release`.