rosefc 0.0.0-stage → 0.2.0
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 +21 -0
- package/deploy/Dockerfile.node +25 -0
- package/deploy/Dockerfile.static +34 -0
- package/deploy/api/rosefd.ts +19 -0
- package/deploy/cloudflare/functions/[[path]].ts +59 -0
- package/deploy/cloudflare/wrangler.toml +14 -0
- package/deploy/docker-compose.yml +45 -0
- package/deploy/fly.toml +28 -0
- package/deploy/k8s/rosefd.yaml +128 -0
- package/deploy/native/RosefdApp.xcscheme +87 -0
- package/deploy/native/android.yml +62 -0
- package/deploy/native/flutter.yml +96 -0
- package/deploy/native/ios.yml +67 -0
- package/deploy/netlify/functions/rosefd.ts +36 -0
- package/deploy/netlify.toml +17 -0
- package/deploy/server.cjs +4 -0
- package/deploy/vercel.json +13 -0
- package/dist-cli/compiler.mjs +1693 -0
- package/dist-cli/edge.mjs +199 -0
- package/dist-cli/export.mjs +12287 -0
- package/dist-cli/rosefd.mjs +12783 -0
- package/dist-cli/runtime.mjs +6 -0
- package/dist-cli/test.mjs +1885 -0
- package/package.json +110 -4
- package/src/FRAMEWORK_VERSION +1 -0
- package/src/cli/content.ts +188 -0
- package/src/cli/edge.ts +298 -0
- package/src/cli/hot-vary.ts +27 -0
- package/src/cli/origin.ts +10 -0
- package/src/cli/prod-server.ts +817 -0
- package/src/cli/shell.ts +610 -0
- package/src/cli/shutdown.ts +56 -0
- package/src/cli/validator.ts +53 -0
- package/src/hash.ts +9 -0
- package/src/runtime/db.ts +166 -0
- package/src/runtime/index.ts +2992 -0
- package/src/runtime/lorawan.ts +149 -0
- package/src/types/rosefd.d.ts +327 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rosefd
|
|
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 `rosefd 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
|
+
# ROSEFD_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 rosefd-static .
|
|
7
|
+
# docker run -p 8080:8080 -e UPSTREAM=http://dynamic:3000 rosefd-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 rosefd-server .
|
|
27
|
+
|
|
28
|
+
FROM alpine:3.20
|
|
29
|
+
COPY --from=bin /app/rosefd-server /usr/local/bin/rosefd-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 ["rosefd-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 rosefd 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 'rosefd/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 - rosefd. `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 = "rosefd"
|
|
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
|
package/deploy/fly.toml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Fly.io: the rosefd 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
|
+
# `rosefd 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 = "rosefd"
|
|
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
|
+
# rosefd 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/rosefd.yaml
|
|
18
|
+
#
|
|
19
|
+
# Build the two images first (from the repo root):
|
|
20
|
+
# docker build -f deploy/Dockerfile.static -t rosefd-static .
|
|
21
|
+
# docker build -f deploy/Dockerfile.node -t rosefd .
|
|
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: rosefd-static
|
|
28
|
+
labels: { app: rosefd, tier: static }
|
|
29
|
+
spec:
|
|
30
|
+
replicas: 2
|
|
31
|
+
selector:
|
|
32
|
+
matchLabels: { app: rosefd, tier: static }
|
|
33
|
+
template:
|
|
34
|
+
metadata:
|
|
35
|
+
labels: { app: rosefd, 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: rosefd-static:latest
|
|
44
|
+
env:
|
|
45
|
+
- name: PORT
|
|
46
|
+
value: "8080"
|
|
47
|
+
- name: UPSTREAM
|
|
48
|
+
value: http://rosefd-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: rosefd-static
|
|
62
|
+
spec:
|
|
63
|
+
selector: { app: rosefd, 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: rosefd-dynamic
|
|
73
|
+
labels: { app: rosefd, tier: dynamic }
|
|
74
|
+
spec:
|
|
75
|
+
replicas: 2
|
|
76
|
+
selector:
|
|
77
|
+
matchLabels: { app: rosefd, tier: dynamic }
|
|
78
|
+
template:
|
|
79
|
+
metadata:
|
|
80
|
+
labels: { app: rosefd, tier: dynamic }
|
|
81
|
+
spec:
|
|
82
|
+
containers:
|
|
83
|
+
- name: dynamic
|
|
84
|
+
image: rosefd:latest
|
|
85
|
+
# `rosefd serve` is the production runner: the stdlib cluster (one
|
|
86
|
+
# worker per core), bounded bodies, timeouts, graceful drain.
|
|
87
|
+
command: ["rosefd", "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: rosefd-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: rosefd, 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: rosefd
|
|
118
|
+
spec:
|
|
119
|
+
rules:
|
|
120
|
+
- http:
|
|
121
|
+
paths:
|
|
122
|
+
- path: /
|
|
123
|
+
pathType: Prefix
|
|
124
|
+
backend:
|
|
125
|
+
service:
|
|
126
|
+
name: rosefd-static
|
|
127
|
+
port:
|
|
128
|
+
number: 80
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Rosefd - the shared scheme for the generated Xcode project.
|
|
4
|
+
|
|
5
|
+
`rosefd export xcode` copies this to
|
|
6
|
+
RosefdApp.xcodeproj/xcshareddata/xcschemes/RosefdApp.xcscheme and substitutes
|
|
7
|
+
__ROSEFD_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 = "__ROSEFD_TARGET_ID__"
|
|
27
|
+
BuildableName = "RosefdApp.app"
|
|
28
|
+
BlueprintName = "RosefdApp"
|
|
29
|
+
ReferencedContainer = "container:RosefdApp.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 = "__ROSEFD_TARGET_ID__"
|
|
57
|
+
BuildableName = "RosefdApp.app"
|
|
58
|
+
BlueprintName = "RosefdApp"
|
|
59
|
+
ReferencedContainer = "container:RosefdApp.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 = "__ROSEFD_TARGET_ID__"
|
|
74
|
+
BuildableName = "RosefdApp.app"
|
|
75
|
+
BlueprintName = "RosefdApp"
|
|
76
|
+
ReferencedContainer = "container:RosefdApp.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
|
+
# Rosefd - Android release build (GitHub Actions).
|
|
2
|
+
#
|
|
3
|
+
# `rosefd 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: rosefd-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
|
+
# Rosefd - Flutter build (GitHub Actions): APK + unsigned iOS app.
|
|
2
|
+
#
|
|
3
|
+
# `rosefd 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.rosefd --project-name rosefd_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: rosefd-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 rosefd_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 RosefdApp-unsigned.ipa Payload
|
|
85
|
+
|
|
86
|
+
- uses: actions/upload-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: rosefd-flutter-ipa
|
|
89
|
+
path: build/ipa/RosefdApp-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`.
|