@_linked/cli 1.17.0 → 1.19.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +124 -5
  3. package/defaults/setup-publish/github/workflows/pr.yml +29 -0
  4. package/defaults/setup-publish/github/workflows/publish.yml +17 -70
  5. package/lib/esm/app-release/app-assets-store.d.ts +23 -0
  6. package/lib/esm/app-release/app-assets-store.d.ts.map +1 -0
  7. package/lib/esm/app-release/app-assets-store.js +33 -0
  8. package/lib/esm/app-release/app-assets-store.js.map +1 -0
  9. package/lib/esm/app-release/create-release-manifest.d.ts +114 -0
  10. package/lib/esm/app-release/create-release-manifest.d.ts.map +1 -0
  11. package/lib/esm/app-release/create-release-manifest.js +238 -0
  12. package/lib/esm/app-release/create-release-manifest.js.map +1 -0
  13. package/lib/esm/app-release/paths.d.ts +5 -0
  14. package/lib/esm/app-release/paths.d.ts.map +1 -0
  15. package/lib/esm/app-release/paths.js +44 -0
  16. package/lib/esm/app-release/paths.js.map +1 -0
  17. package/lib/esm/app-release/publisher.d.ts +25 -0
  18. package/lib/esm/app-release/publisher.d.ts.map +1 -0
  19. package/lib/esm/app-release/publisher.js +238 -0
  20. package/lib/esm/app-release/publisher.js.map +1 -0
  21. package/lib/esm/app-release/resolve-build-target.d.ts +19 -0
  22. package/lib/esm/app-release/resolve-build-target.d.ts.map +1 -0
  23. package/lib/esm/app-release/resolve-build-target.js +30 -0
  24. package/lib/esm/app-release/resolve-build-target.js.map +1 -0
  25. package/lib/esm/app-release/static-assets.d.ts +13 -0
  26. package/lib/esm/app-release/static-assets.d.ts.map +1 -0
  27. package/lib/esm/app-release/static-assets.js +55 -0
  28. package/lib/esm/app-release/static-assets.js.map +1 -0
  29. package/lib/esm/app-release/types.d.ts +71 -0
  30. package/lib/esm/app-release/types.d.ts.map +1 -0
  31. package/lib/esm/app-release/types.js +2 -0
  32. package/lib/esm/app-release/types.js.map +1 -0
  33. package/lib/esm/cli.js +81 -19
  34. package/lib/esm/cli.js.map +1 -1
  35. package/lib/esm/commands/build-app.d.ts +55 -0
  36. package/lib/esm/commands/build-app.d.ts.map +1 -0
  37. package/lib/esm/commands/build-app.js +178 -0
  38. package/lib/esm/commands/build-app.js.map +1 -0
  39. package/lib/esm/commands/publish-app.d.ts +18 -0
  40. package/lib/esm/commands/publish-app.d.ts.map +1 -0
  41. package/lib/esm/commands/publish-app.js +124 -0
  42. package/lib/esm/commands/publish-app.js.map +1 -0
  43. package/lib/esm/commands/serve-app.d.ts +14 -0
  44. package/lib/esm/commands/serve-app.d.ts.map +1 -0
  45. package/lib/esm/commands/serve-app.js +49 -0
  46. package/lib/esm/commands/serve-app.js.map +1 -0
  47. package/lib/esm/commands/setup-publish.d.ts +39 -4
  48. package/lib/esm/commands/setup-publish.d.ts.map +1 -1
  49. package/lib/esm/commands/setup-publish.js +142 -83
  50. package/lib/esm/commands/setup-publish.js.map +1 -1
  51. package/lib/esm/index.d.ts +11 -0
  52. package/lib/esm/index.d.ts.map +1 -1
  53. package/lib/esm/index.js +9 -0
  54. package/lib/esm/index.js.map +1 -1
  55. package/lib/esm/lifecycle.d.ts +1 -1
  56. package/lib/esm/lifecycle.d.ts.map +1 -1
  57. package/lib/esm/lifecycle.js +17 -8
  58. package/lib/esm/lifecycle.js.map +1 -1
  59. package/package.json +2 -2
  60. package/defaults/setup-publish/dual-branch/changeset/README.md +0 -20
  61. package/defaults/setup-publish/dual-branch/changeset/config.json +0 -14
  62. package/defaults/setup-publish/dual-branch/github/workflows/changeset-check.yml +0 -43
  63. package/defaults/setup-publish/dual-branch/github/workflows/ci.yml +0 -44
  64. package/defaults/setup-publish/dual-branch/github/workflows/publish.yml +0 -191
  65. package/defaults/setup-publish/github/workflows/changeset-check.yml +0 -44
  66. package/defaults/setup-publish/github/workflows/ci.yml +0 -43
package/CHANGELOG.md CHANGED
@@ -1,5 +1,98 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#100](https://github.com/linked-fw/cli/pull/100) [`8ef0fd7`](https://github.com/linked-fw/cli/commit/8ef0fd7f810dfff26b95f5390565661b8c38c7ac) Thanks [@flyon](https://github.com/flyon)! - ## App release pipeline on the file-store API
8
+
9
+ A manifest-driven build/publish/serve flow for Linked apps, replacing the old recursive upload of
10
+ everything under `public/`.
11
+
12
+ ### New commands
13
+
14
+ ```bash
15
+ linked build-app --env <env> --target web # build + write public/bundles/linked-release.json
16
+ linked build-app --env <env> --publish # ...and upload it in the same run
17
+ linked build-app --env <env> --target capacitor # local, non-publishable manifest; never uploads
18
+ linked build-app --revision <sha> # identify the release outside a git checkout
19
+ linked build-app --allow-dirty # build uncommitted work as <sha>-dirty
20
+ linked publish-app --env <env> # dry run by default; pass --yes to upload
21
+ linked publish-app --manifest <path> # publish a manifest from another path
22
+ linked serve-app --env <env> # compiled runtime, no Vite/HMR
23
+ ```
24
+
25
+ `build-app` never uploads on its own — it writes a release manifest, and `publish-app` uploads
26
+ exactly the files that manifest lists.
27
+
28
+ ### Publishing destination
29
+
30
+ Uploads go through `LinkedFileStorage.getStore(FileStorePurposes.appAssets)`. An app that dedicates a
31
+ bundle store configures `setStore(FileStorePurposes.appAssets, store)`; an app with a single store
32
+ configures only `setDefaultStore(store)` and publishing reaches it through the purpose fallback, with
33
+ no extra configuration and no error.
34
+
35
+ ### Behaviour
36
+ - Object keys are `releases/<appVersion>-<revision>/<path>`, so releases no longer overwrite each
37
+ other. `publish.releasePrefix` in `linked.config.js` changes the base. A manifest may only upload
38
+ to its own prefix: each `objectKey` is recomputed from the prefix and source path before any
39
+ write, so an edited or foreign manifest cannot overwrite a different release, and source paths
40
+ that leave the app root (`../`, absolute, or through a symlink) are refused.
41
+ - The client bundle is built with `base = <accessURL>/<releasePrefix>/public/bundles/`, so the URLs
42
+ it emits for its own chunks and assets resolve under the release prefix on a plain static store
43
+ with no server rewrite. The value is recorded as `destination.baseURL`; the unused `publicRoot`
44
+ field is gone. The store is therefore resolved before Vite runs, and a release is built for one
45
+ store.
46
+ - The release revision comes from a clean `git rev-parse HEAD`. A dirty tree is refused (two builds
47
+ of the same uncommitted work would share a prefix and overwrite each other); `--allow-dirty`
48
+ builds as `<sha>-dirty`. `--revision <sha>`, `LINKED_RELEASE_REVISION` and `GITHUB_SHA` work
49
+ outside a Git checkout.
50
+ - Cache policy follows a file's origin, not its name: files in the Vite manifest are hashed by
51
+ construction and get `public, max-age=31536000, immutable`; declared static assets and the
52
+ release manifest get `public, max-age=60, must-revalidate`. The old filename heuristic gave
53
+ `og-image-1200x630.png` and `sw-v20260101.js` a year of immutable caching.
54
+ - `publish.staticAssets` globs are checked per match, not only per pattern, so brace expansion
55
+ (`public/{,../}secret/**`) and symlinks can no longer pull in files from outside `public/`.
56
+ - `APP_ENV` selects the Capacitor target only when it is literally `capacitor`. Any other value
57
+ (`production`, `staging`, …) builds web instead of silently producing an empty, non-publishable
58
+ manifest.
59
+ - Files are re-hashed locally against the manifest before upload. The post-upload check uses the
60
+ optional `IFileStore.statFile`; a store that lacks it, or reports no `sha256`, is warned about once
61
+ and skipped rather than failing. `etag` is never treated as a content hash.
62
+ - The manifest records the store's `accessURL` and publishing refuses a store that no longer matches.
63
+ - Objects are written with `preventDuplicates: false` and a store that reports a different location
64
+ than the key it was given fails the publish, rather than scattering renamed files.
65
+ - A failure partway through an upload names the file, the object key, the store and how many objects
66
+ were already uploaded, and says that re-running resumes the release.
67
+ - Standalone `publish-app` is a dry run unless `--yes`; only publishable `target: 'web'` manifests
68
+ upload; the manifest is read before a store is resolved, so a Capacitor manifest fails with a
69
+ manifest error rather than a storage error; nothing is ever deleted remotely; credentials are
70
+ redacted from publisher errors.
71
+ - `build-app`, `publish-app` and `serve-app` print a single error and exit 1 instead of surfacing an
72
+ unhandled rejection, and `--target`/`--publish`/`--revision`/`--allow-dirty` are rejected on the
73
+ legacy webpack path instead of being ignored.
74
+
75
+ ### New exports
76
+
77
+ From `@_linked/cli`: `buildViteApp`, `hasViteConfig`, `publishApp`, `serveCompiledApp`,
78
+ `validateCompiledAppArtifacts`, `createReleaseManifest`, `serializeReleaseManifest`,
79
+ `writeReleaseManifest`, `buildReleasePrefix`, `releaseObjectKey`, `getCacheControl`,
80
+ `planReleasePublish`, `publishRelease`, `loadReleaseManifest`, `resolveAppAssetsStore`,
81
+ `normalizeAccessURL`, `resolveBuildTarget`, `resolveDeclaredStaticAssets`, `resolveReleaseIdentity`,
82
+ `resolveSourceRevision`, `releaseBaseURL`, `VITE_OUTPUT_DIR`, the path helpers, and the release
83
+ types.
84
+
85
+ Apps must build with `build.outDir: 'public/bundles'` and `build.manifest: true` (both already set
86
+ by `createViteConfig`) and must not set `base` themselves; see the README.
87
+
88
+ Requires `@_linked/core` 2.20.1 for the purpose-named file stores and `IFileStore.statFile`.
89
+
90
+ ## 1.18.0
91
+
92
+ ### Minor Changes
93
+
94
+ - [#98](https://github.com/linked-fw/cli/pull/98) [`0d2c4c0`](https://github.com/linked-fw/cli/commit/0d2c4c04f7354af8add4a31c2780597b61d27098) Thanks [@flyon](https://github.com/flyon)! - `setup-publish` scaffolds the consolidated pipeline: two thin caller stubs (`pr.yml`, `publish.yml`) for the shared reusable workflows in `linked-fw/.github` instead of three standalone workflows, and `--configure-github` applies the uniform branch-protection profile (required check `checks / Build & Test`, non-strict, admins enforced). `publishConfig.provenance` is no longer stripped now that publishing is OIDC-first, and `--dual-branch` is a deprecated no-op.
95
+
3
96
  ## 1.17.0
4
97
 
5
98
  ### Minor Changes
package/README.md CHANGED
@@ -34,6 +34,8 @@ linked create-component <name> # add a React component file
34
34
  ```bash
35
35
  linked build # build the current package (tsc + checks)
36
36
  linked build-app # build frontend + backend for the current app
37
+ linked build-app --target web # web release: build and write public/bundles/linked-release.json
38
+ linked build-app --target capacitor # Capacitor/local native build: local manifest only, never published
37
39
  linked build-workspace # build all linked packages in the workspace in dependency order
38
40
  linked build-updated # incremental: only packages that changed since last build
39
41
  linked build-package <file> # walk up from a file path to find its package and rebuild
@@ -43,18 +45,135 @@ linked build-package <file> # walk up from a file path to find its package
43
45
  in a package without `"linkedPackage": true` (it used to exit 0 there). A build that finishes with warnings still
44
46
  exits 0.
45
47
 
48
+ ### App release / production runtime
49
+
50
+ ```bash
51
+ linked build-app # build and write the release manifest (never uploads)
52
+ linked build-app --publish # ...and upload it in the same run
53
+ linked build-app --revision <sha> # identify the release without asking git
54
+ linked build-app --allow-dirty # allow uncommitted changes; revision gets a "-dirty" suffix
55
+ linked publish-app # dry-run the release manifest upload (pass --yes to write)
56
+ linked publish-app --yes # upload the release described by the manifest
57
+ linked publish-app --manifest <p> # publish a manifest other than the default path
58
+ linked serve-app # run the compiled backend without Vite/HMR (production runtime)
59
+ ```
60
+
61
+ **Required Vite config.** The release flow reads `vite build` output directly, and the paths are
62
+ hardcoded: the client bundle must land in `public/bundles` and Vite must write its manifest.
63
+ `createViteConfig()` from `@_linked/cli/vite` already sets both; a hand-written `vite.config.ts`
64
+ needs them explicitly:
65
+
66
+ ```ts
67
+ export default defineConfig({
68
+ build: {
69
+ outDir: 'public/bundles', // release object keys are <prefix>/public/bundles/...
70
+ manifest: true, // build-app reads public/bundles/.vite/manifest.json
71
+ },
72
+ });
73
+ ```
74
+
75
+ Without `manifest: true` the build fails with a missing `public/bundles/.vite/manifest.json`; with a
76
+ different `outDir` nothing is found to publish. Do not set `base` yourself — `build-app` passes it
77
+ (see "How a release is served").
78
+
79
+ **Building is not publishing.** `build-app` compiles the app and writes
80
+ `public/bundles/linked-release.json`, a manifest listing every file of the release with its sha256,
81
+ size, content type and cache policy. `publish-app` uploads exactly the files in that manifest —
82
+ nothing else in `public/` is touched. Pass `--publish` to `build-app` only when you want the two
83
+ chained in one command. `publish-app` is a dry run unless `--yes` is given, and `--manifest <path>`
84
+ publishes a manifest from somewhere other than `public/bundles/linked-release.json` (the path is
85
+ relative to the app root and must stay inside it).
86
+
87
+ **Where it publishes.** Uploads go to the store returned by
88
+ `LinkedFileStorage.getStore(FileStorePurposes.appAssets)`. Configure a dedicated bundle store with
89
+ `LinkedFileStorage.setStore(FileStorePurposes.appAssets, store)`; an app that configures only
90
+ `setDefaultStore(store)` publishes to that store through the purpose fallback, which needs no extra
91
+ configuration.
92
+
93
+ **Every release gets its own prefix.** Object keys are
94
+ `releases/<appVersion>-<revision>/<path>`, so publishing a new release cannot overwrite the
95
+ previous one and a rollback means pointing at an older prefix. Set `publish.releasePrefix` in
96
+ `linked.config.js` to change the `releases` base. A manifest may only upload to its own prefix: every
97
+ `objectKey` is recomputed from the prefix and the source path before anything is written, so an
98
+ edited or foreign manifest cannot reach into another release.
99
+
100
+ **How a release is served.** Because every release has its own key prefix, the bundle's own URLs have
101
+ to carry that prefix — nothing rewrites them at request time. `build-app` therefore resolves the
102
+ store and the release prefix *before* running Vite and passes
103
+
104
+ ```
105
+ base = <store accessURL>/<releasePrefix>/public/bundles/
106
+ ```
107
+
108
+ so the chunk and asset URLs the bundle emits are absolute URLs into the release prefix. This works on
109
+ a plain static store or CDN with no rewrite rules, and two releases can be served side by side. The
110
+ value is recorded as `destination.baseURL` in the release manifest.
111
+
112
+ The HTML entry tags are not part of the bundle — `@_linked/server` renders them as
113
+ `${STATIC_ACCESS_URL}/public/bundles/<file>`. So serving a particular release is one deployment
114
+ variable, which `build-app` prints when it finishes:
115
+
116
+ ```
117
+ STATIC_ACCESS_URL=<store accessURL>/releases/<appVersion>-<revision>
118
+ ```
119
+
120
+ Set that on the app server (or roll back by setting it to an older release prefix) and the entry
121
+ tags and everything the bundle loads resolve to the same release. Note the consequence of baking the
122
+ base in: a release is built for one store, and pointing it at another means rebuilding.
123
+
124
+ **Which revision identifies a release.** By default the short `git rev-parse HEAD` of a **clean**
125
+ tree. A dirty tree is refused, because two builds of the same uncommitted work would share a release
126
+ ID and prefix and silently overwrite each other while removed files linger; pass `--allow-dirty` to
127
+ build anyway as `<sha>-dirty`. Outside a Git checkout (an exported tarball, some CI images) pass
128
+ `--revision <sha>`, or set `LINKED_RELEASE_REVISION` or `GITHUB_SHA`; those win over Git and skip
129
+ the clean-tree check.
130
+
131
+ **Verification.** Files are re-hashed from disk and compared against the manifest immediately before
132
+ upload, so a build that changed since the manifest was written is refused. After upload the store is
133
+ asked for `statFile`; that method is optional and may report no `sha256`, in which case the remote
134
+ check is skipped with a single warning rather than a failure. An `etag` is never used as a content
135
+ hash. The manifest also records the store's `accessURL`, and publishing refuses to run against a
136
+ store that now writes somewhere else. A failure partway through names the file, the object key, the
137
+ store and how many objects were already uploaded; the manifest is uploaded last, so an incomplete
138
+ release is never marked complete, and re-running `publish-app` resumes it.
139
+
140
+ **The store must keep keys verbatim.** A release object has to be stored under exactly the key it
141
+ was given, or the URLs baked into the bundle point at nothing, so publishing passes
142
+ `preventDuplicates: false` and fails with a clear message if the store reports a different location.
143
+ `@_linked/server`'s `LocalFileStore` lowercases the keys it is handed, so an app publishing to it
144
+ needs lowercase asset filenames (`rollupOptions.output.hashCharacters: 'hex'` in `vite.config`).
145
+
146
+ **Cache policy follows the origin, not the filename.** Files listed in the Vite manifest are
147
+ content-hashed by construction and get `public, max-age=31536000, immutable`. Everything else —
148
+ declared `publish.staticAssets` and the release manifest — gets
149
+ `public, max-age=60, must-revalidate`. (Guessing from the filename was tried and dropped:
150
+ `og-image-1200x630.png` and `sw-v20260101.js` read as hashed, and a year of immutable caching on
151
+ those cannot be undone without renaming the file.)
152
+
153
+ **Extra static assets.** `publish.staticAssets` in `linked.config.js` lists globs of files under
154
+ `public/` to ship besides the bundle. Both the pattern and every match must resolve inside
155
+ `public/`, so brace expansion and symlinks cannot pull in files from elsewhere in the repo.
156
+
157
+ **Capacitor.** `--target capacitor` (or `APP_ENV=capacitor`, matched literally) writes a local,
158
+ non-publishable manifest for inspection. Its assets ship inside the native app, so no file store is
159
+ resolved, no `base` is set and nothing is uploaded, even with `--publish`. Any other `APP_ENV` value
160
+ (`production`, `staging`, …) is ignored by the target choice and builds web.
161
+
162
+ Use `linked start` for development; use `linked serve-app` after `linked build-app` for
163
+ staging/production.
164
+
46
165
  ### Publishing / release
47
166
 
48
167
  ```bash
49
- linked setup-publish # install a changesets-based publish workflow in the current repo
50
- linked setup-publish --dual-branch # use main + dev with @next prereleases
51
- linked setup-publish --configure-github # also set branch protection via gh CLI
168
+ linked setup-publish # install the changesets-based publish pipeline in the current repo
169
+ linked setup-publish --configure-github # also apply the uniform branch protection via gh CLI
52
170
  linked setup-publish --scope community # use NPM_AUTH_TOKEN_CM instead of NPM_AUTH_TOKEN
53
171
  ```
54
172
 
55
173
  `setup-publish` writes:
56
174
 
57
- - `.github/workflows/ci.yml`, `publish.yml`, `changeset-check.yml`
175
+ - `.github/workflows/pr.yml` and `publish.yml` — thin callers of the shared reusable workflows in
176
+ `linked-fw/.github`, pinned `@v1`. Any pre-consolidation `ci.yml` / `changeset-check.yml` is removed.
58
177
  - `.changeset/config.json` + `README.md`
59
178
  - `.gitignore` entries
60
179
  - `publishConfig: {access: public}` + `@changesets/cli` devDeps in `package.json`
@@ -140,7 +259,7 @@ Templates live in `defaults/`:
140
259
  - `defaults/app-with-backend/` — used by `linked create-app`
141
260
  - `defaults/app-static/` — minimal static app
142
261
  - `defaults/package/` — used by `linked create-package`
143
- - `defaults/setup-publish/` — workflow + changeset files written by `linked setup-publish` (single-branch default; `dual-branch/` subdirectory for the `--dual-branch` variant)
262
+ - `defaults/setup-publish/` — caller workflows + changeset files written by `linked setup-publish` (`main`-only; `--dual-branch` is a deprecated no-op)
144
263
 
145
264
  ### `linked create-app` template structure
146
265
 
@@ -0,0 +1,29 @@
1
+ # Calls the shared workflow in linked-fw/.github, which runs both PR gates — `Build & Test` and
2
+ # `Check for changeset` — as jobs of one workflow. The per-repo ci.yml and changeset-check.yml
3
+ # this replaces shared a trigger and were both PR gates.
4
+ #
5
+ # The required status check on `main` is `checks / Build & Test`: GitHub reports a job invoked
6
+ # through `workflow_call` as `<caller-job-id> / <job name>`, and the caller job below is `checks`.
7
+ # Renaming this job renames the check, and a branch protection still pinning the old name waits
8
+ # forever on a check that no longer reports.
9
+ name: PR
10
+
11
+ on:
12
+ pull_request:
13
+ branches:
14
+ - main
15
+
16
+ jobs:
17
+ checks:
18
+ uses: linked-fw/.github/.github/workflows/pr.yml@v1
19
+ # The changeset reminder posts a PR comment. A reusable workflow only holds what its caller
20
+ # granted it, and the org default is read-only.
21
+ permissions:
22
+ contents: read
23
+ pull-requests: write
24
+ with:
25
+ # Left false so a package with no test script passes instead of failing on a missing one.
26
+ # Set to true — and `test-script` to the unit-only script where `test` is not CI-safe —
27
+ # once this package has a suite that must exist.
28
+ require-tests: false
29
+ run-e2e: false
@@ -1,3 +1,9 @@
1
+ # Calls the shared workflow in linked-fw/.github. That repo is public, so community repos in
2
+ # linked-cm call the same workflow — only the npm secret differs.
3
+ #
4
+ # This FILENAME is what npm's trusted-publisher check validates — not the shared workflow's.
5
+ # Renaming this file invalidates this package's trusted publisher, and the only symptom is
6
+ # that releases quietly start needing manual approval again.
1
7
  name: Publish
2
8
 
3
9
  on:
@@ -9,76 +15,17 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
9
15
 
10
16
  jobs:
11
17
  release:
12
- name: Publish Release
13
- runs-on: ubuntu-latest
18
+ uses: linked-fw/.github/.github/workflows/publish.yml@v1
14
19
  permissions:
15
20
  contents: write
16
21
  pull-requests: write
17
- steps:
18
- # Author the changesets "Version Packages" PR as the org release App (not the default
19
- # GITHUB_TOKEN) so its CI runs without a manual "Approve and run" gate. Requires the org
20
- # App installed on this repo + org secrets RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY.
21
- - name: Generate GitHub App token
22
- id: app-token
23
- uses: actions/create-github-app-token@v2
24
- with:
25
- app-id: ${{ secrets.RELEASE_APP_ID }}
26
- private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
27
-
28
- - name: Checkout
29
- uses: actions/checkout@v4
30
- with:
31
- token: ${{ steps.app-token.outputs.token }}
32
-
33
- - name: Setup Node.js
34
- uses: actions/setup-node@v4
35
- with:
36
- node-version: 22.16.0
37
- registry-url: 'https://registry.npmjs.org'
38
-
39
- - name: Install dependencies
40
- run: npm ci --legacy-peer-deps
41
-
42
- - name: Build
43
- run: npm run build
44
-
45
- - name: Create release Pull Request or Publish
46
- id: changesets
47
- uses: changesets/action@v1
48
- with:
49
- publish: npx changeset publish
50
- title: 'chore: release'
51
- commit: 'chore: release'
52
- # npm auth uses the npm token below. Do NOT switch to OIDC trusted publishing
53
- # (id-token: write + publishConfig.provenance) unless you FIRST register this package's
54
- # trusted publisher on npmjs.com — otherwise publishes fail with 404 / permission errors.
55
- env:
56
- GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
57
- NODE_AUTH_TOKEN: ${{ secrets.{{NPM_SECRET_NAME}} }}
58
- NPM_TOKEN: ${{ secrets.{{NPM_SECRET_NAME}} }}
59
-
60
- # When changesets opened/updated the version PR (rather than publishing), merge it so the
61
- # release ships without a manual click. The App token is used so the merge's push to `main`
62
- # re-triggers this workflow, which — now that no changesets remain — runs `changeset publish`.
63
- #
64
- # NB: DIRECT-merge (not `--auto`). If `main` requires a review, GitHub's auto-merge queue does
65
- # NOT honor branch-protection review-bypass allowances and would wait forever on
66
- # REVIEW_REQUIRED; a required *status check* is not bypassable either. This repo's `main` is
67
- # check-only (no required review), so a direct merge succeeds once the required check is
68
- # green — we retry until then.
69
- - name: Merge the version PR to publish
70
- if: steps.changesets.outputs.hasChangesets == 'true' && steps.changesets.outputs.published != 'true'
71
- env:
72
- GH_TOKEN: ${{ steps.app-token.outputs.token }}
73
- run: |
74
- pr=$(gh pr list --base main --head changeset-release/main --state open --json number --jq '.[0].number // empty')
75
- if [ -z "$pr" ]; then echo "No open version PR (nothing to merge)."; exit 0; fi
76
- for i in $(seq 1 40); do
77
- if gh pr merge "$pr" --merge 2>/tmp/merge_err; then
78
- echo "Merged version PR #$pr on attempt $i — publish will run on the resulting push to main."
79
- exit 0
80
- fi
81
- echo "Attempt $i/40: version PR #$pr not mergeable yet ($(tr -d '\n' < /tmp/merge_err)); waiting for the required check…"
82
- sleep 15
83
- done
84
- echo "::warning::Version PR #$pr not merged after retries — merge it manually to publish."
22
+ # Needed HERE as well as in the shared workflow: a reusable workflow cannot widen the
23
+ # permissions its caller was granted, and without this npm never gets an id-token and
24
+ # every release silently falls back to the staged approval queue.
25
+ id-token: write
26
+ # Named rather than `inherit`: `inherit` forwards everything this repo can see, which is
27
+ # how a community repo would end up forwarding a first-party token.
28
+ secrets:
29
+ NPM_TOKEN: ${{ secrets.{{NPM_SECRET_NAME}} }}
30
+ RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
31
+ RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
@@ -0,0 +1,23 @@
1
+ import type { IFileStore } from '@_linked/core/interfaces/IFileStore';
2
+ export interface ResolveAppAssetsStoreDependencies {
3
+ /**
4
+ * Loads the app's storage bootstrap file. Importing it is what registers the
5
+ * stores on `LinkedFileStorage`; the module's own exports are not inspected.
6
+ */
7
+ loadStorageConfig?: () => Promise<unknown>;
8
+ /** Injection point for tests. Defaults to `LinkedFileStorage.getStore`. */
9
+ getStore?: (purpose: string) => IFileStore;
10
+ }
11
+ /**
12
+ * Resolve the store that release artifacts are published to.
13
+ *
14
+ * Apps that dedicate a CDN bucket to bundles configure
15
+ * `LinkedFileStorage.setStore(FileStorePurposes.appAssets, …)`. Apps with a
16
+ * single store configure only a default store and `getStore` falls back to it
17
+ * — that is a supported setup, not an error, so there is no check for a
18
+ * dedicated store here.
19
+ */
20
+ export declare const resolveAppAssetsStore: (dependencies?: ResolveAppAssetsStoreDependencies) => Promise<IFileStore>;
21
+ /** `accessURL` values are compared ignoring a trailing slash. */
22
+ export declare const normalizeAccessURL: (accessURL: string) => string;
23
+ //# sourceMappingURL=app-assets-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-assets-store.d.ts","sourceRoot":"","sources":["../../../src/app-release/app-assets-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,qCAAqC,CAAC;AAEpE,MAAM,WAAW,iCAAiC;IAChD;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAChC,eAAc,iCAAsC,KACnD,OAAO,CAAC,UAAU,CAapB,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,GAAI,WAAW,MAAM,KAAG,MACT,CAAC"}
@@ -0,0 +1,33 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { FileStorePurposes, LinkedFileStorage, } from '@_linked/core/utils/LinkedFileStorage';
11
+ /**
12
+ * Resolve the store that release artifacts are published to.
13
+ *
14
+ * Apps that dedicate a CDN bucket to bundles configure
15
+ * `LinkedFileStorage.setStore(FileStorePurposes.appAssets, …)`. Apps with a
16
+ * single store configure only a default store and `getStore` falls back to it
17
+ * — that is a supported setup, not an error, so there is no check for a
18
+ * dedicated store here.
19
+ */
20
+ export const resolveAppAssetsStore = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (dependencies = {}) {
21
+ const loadStorageConfig = dependencies.loadStorageConfig ||
22
+ (() => __awaiter(void 0, void 0, void 0, function* () {
23
+ const { loadBackendStorageConfig } = yield import('../lifecycle.js');
24
+ return loadBackendStorageConfig();
25
+ }));
26
+ yield loadStorageConfig();
27
+ const getStore = dependencies.getStore ||
28
+ ((purpose) => LinkedFileStorage.getStore(purpose));
29
+ return getStore(FileStorePurposes.appAssets);
30
+ });
31
+ /** `accessURL` values are compared ignoring a trailing slash. */
32
+ export const normalizeAccessURL = (accessURL) => (accessURL || '').trim().replace(/\/+$/, '');
33
+ //# sourceMappingURL=app-assets-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-assets-store.js","sourceRoot":"","sources":["../../../src/app-release/app-assets-store.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EACL,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uCAAuC,CAAC;AAa/C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,YAEd,EAAE,mDADvB,eAAkD,EAAE;IAEpD,MAAM,iBAAiB,GACrB,YAAY,CAAC,iBAAiB;QAC9B,CAAC,GAAS,EAAE;YACV,MAAM,EAAC,wBAAwB,EAAC,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;YACnE,OAAO,wBAAwB,EAAE,CAAC;QACpC,CAAC,CAAA,CAAC,CAAC;IACL,MAAM,iBAAiB,EAAE,CAAC;IAE1B,MAAM,QAAQ,GACZ,YAAY,CAAC,QAAQ;QACrB,CAAC,CAAC,OAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7D,OAAO,QAAQ,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAC/C,CAAC,CAAA,CAAC;AAEF,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,SAAiB,EAAU,EAAE,CAC9D,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC"}
@@ -0,0 +1,114 @@
1
+ import type { AppBuildTarget, LinkedAppReleaseManifest } from './types.js';
2
+ export interface CreateReleaseManifestOptions {
3
+ appRoot: string;
4
+ environmentNames: string[];
5
+ target: AppBuildTarget;
6
+ /**
7
+ * `accessURL` of the store this release will be published to. Omitted (or
8
+ * null) for a Capacitor build, which produces a local, non-publishable
9
+ * manifest and never touches a file store.
10
+ */
11
+ accessURL?: string | null;
12
+ staticAssets?: string[];
13
+ /** Base key releases are nested under. Defaults to `releases`. */
14
+ releasePrefix?: string;
15
+ builtAt?: string;
16
+ sourceRevision?: string;
17
+ /** Allow a revision derived from a working tree with uncommitted changes. */
18
+ allowDirty?: boolean;
19
+ /**
20
+ * Public URL every asset of this release is served from — the release
21
+ * prefix resolved against the store's `accessURL`, and the value the client
22
+ * bundle was built with as Vite's `base`. Empty for a Capacitor build.
23
+ */
24
+ baseURL?: string;
25
+ }
26
+ export declare const DEFAULT_RELEASE_PREFIX = "releases";
27
+ export declare const MANIFEST_FILE_NAME = "linked-release.json";
28
+ export declare const MANIFEST_SOURCE_PATH = "public/bundles/linked-release.json";
29
+ export declare const MANIFEST_CONTENT_TYPE = "application/json; charset=utf-8";
30
+ /**
31
+ * Cache-Control rule, decided by where a file came from rather than by what it
32
+ * is called.
33
+ *
34
+ * Files listed in the Vite manifest are content-hashed by construction — Vite
35
+ * emitted the name, so the bytes behind that key can never change and the
36
+ * object is immutable for a year. Everything else (declared static assets, the
37
+ * release manifest) is revalidated after a minute, so a re-published release is
38
+ * picked up quickly. Guessing "is this hashed?" from the filename was tried and
39
+ * removed: `og-image-1200x630.png`, `sw-v20260101.js` and `icon-FacebookRound.svg`
40
+ * all read as hashed, and a year of immutable caching on those cannot be undone
41
+ * without renaming the file.
42
+ */
43
+ export declare const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
44
+ export declare const ENTRY_CACHE_CONTROL = "public, max-age=60, must-revalidate";
45
+ /** Where a release file came from; this is what decides its cache policy. */
46
+ export type ReleaseFileOrigin = 'vite-bundle' | 'static-asset';
47
+ export declare const getCacheControl: (origin: ReleaseFileOrigin) => string;
48
+ export interface ResolveSourceRevisionOptions {
49
+ appRoot: string;
50
+ /** `--revision`, or a caller-supplied value. Used verbatim when given. */
51
+ revision?: string;
52
+ /** `--allow-dirty`: build from a dirty tree, tagging the revision `-dirty`. */
53
+ allowDirty?: boolean;
54
+ env?: NodeJS.ProcessEnv;
55
+ }
56
+ /**
57
+ * Determine the revision that identifies this release.
58
+ *
59
+ * An explicit `--revision`, then `LINKED_RELEASE_REVISION`, then `GITHUB_SHA`
60
+ * win over Git, so a release can be built from an exported tarball or a CI
61
+ * checkout that is not a working repository. Falling back to `git rev-parse`
62
+ * requires a clean tree: two builds of the same uncommitted work would
63
+ * otherwise share a release ID and a prefix, silently overwriting each other
64
+ * while files removed in between linger under the prefix.
65
+ */
66
+ export declare const resolveSourceRevision: ({ appRoot, revision, allowDirty, env, }: ResolveSourceRevisionOptions) => string;
67
+ /**
68
+ * Build the key prefix for one release. Every object of a release lives under
69
+ * it, so publishing a new release cannot overwrite the previous one and a
70
+ * rollback is a matter of pointing at the older prefix.
71
+ */
72
+ export declare const buildReleasePrefix: (releaseId: string, basePrefix?: string) => string;
73
+ export declare const releaseObjectKey: (releasePrefix: string, sourcePath: string) => string;
74
+ /** Directory `vite build` writes the client bundle to. See the README. */
75
+ export declare const VITE_OUTPUT_DIR = "public/bundles";
76
+ /**
77
+ * Public URL the client bundle of a release is served from. Object keys are
78
+ * `<releasePrefix>/public/bundles/…` under the store's `accessURL`, and this is
79
+ * the value the build passes to Vite as `base`, so the root-absolute URLs the
80
+ * bundle emits for its own chunks and assets resolve under the release prefix
81
+ * without any server rewrite.
82
+ */
83
+ export declare const releaseBaseURL: (accessURL: string, releasePrefix: string) => string;
84
+ /**
85
+ * The release's root URL: `accessURL` plus the release prefix, without the
86
+ * bundle directory. `@_linked/server` builds the HTML entry tags as
87
+ * `${STATIC_ACCESS_URL}/public/bundles/<file>`, so pointing `STATIC_ACCESS_URL`
88
+ * at this value is what makes a running app serve one particular release —
89
+ * a value to deploy, not server code to write or a rewrite rule to configure.
90
+ */
91
+ export declare const releaseStaticAccessURL: (accessURL: string, releasePrefix: string) => string;
92
+ export interface ReleaseIdentity {
93
+ appName: string;
94
+ appVersion: string;
95
+ sourceRevision: string;
96
+ releaseId: string;
97
+ releasePrefix: string;
98
+ }
99
+ /**
100
+ * Work out the release ID and prefix before anything is built. The client
101
+ * bundle has to be built with the release prefix as its `base`, so the prefix
102
+ * cannot wait until the manifest is written.
103
+ */
104
+ export declare const resolveReleaseIdentity: (options: {
105
+ appRoot: string;
106
+ releasePrefix?: string;
107
+ sourceRevision?: string;
108
+ allowDirty?: boolean;
109
+ env?: NodeJS.ProcessEnv;
110
+ }) => ReleaseIdentity;
111
+ export declare const createReleaseManifest: (options: CreateReleaseManifestOptions) => LinkedAppReleaseManifest;
112
+ export declare const serializeReleaseManifest: (manifest: LinkedAppReleaseManifest) => string;
113
+ export declare const writeReleaseManifest: (appRoot: string, manifest: LinkedAppReleaseManifest) => string;
114
+ //# sourceMappingURL=create-release-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-release-manifest.d.ts","sourceRoot":"","sources":["../../../src/app-release/create-release-manifest.ts"],"names":[],"mappings":"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"}