@makefully/adaptfully 5.2.0 → 5.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 5.3.0 — 2026-11-11
6
+
7
+ ### Added
8
+
9
+ - **Store-assets upload ledger** — `lib/node/store-assets-ledger.js` owns `meta/store-assets/upload-ledger.json` (sequenced SHA-256 set skip for Play/ASC listing sync).
10
+ - **Opt-in listing deploys** — `adaptfully deploy <platform> --deployment *-listing` may target keys not in the platform release fan-out; assets-only zips skip the prior AAB/IPA tree and package `credentialsFrom` sibling folders under `meta/deployments/`.
11
+
5
12
  ## 5.2.0 — 2026-11-11
6
13
 
7
14
  ### Added
@@ -163,20 +163,37 @@ export function createArchive(deployFolder, contents, options = {}) {
163
163
  }
164
164
 
165
165
  /**
166
- * Prior artifact zip plus one deployment folder for /deploy.
166
+ * Prior artifact zip plus deployment folder(s) for /deploy.
167
+ * Always packages non-deployment meta (including store-assets) from META_DIR.
167
168
  *
168
169
  * @param {string} artifactPath
169
- * @param {string} publishDir
170
+ * @param {string} publishDir Primary deployment folder (…/meta/deployments/<key>)
170
171
  * @param {string} [contents]
171
- * @param {{ log?: (message: string) => void }} [options]
172
+ * @param {{ log?: (message: string) => void, deploymentDirs?: string[], includeArtifact?: boolean }} [options]
172
173
  */
173
174
  export function createDeployArchive(artifactPath, publishDir, contents, options = {}) {
174
175
  const
175
176
  zip = archiver('zip', { zlib: { level: 0 } }),
176
- log = options.log ?? console.log;
177
+ log = options.log ?? console.log,
178
+ includeArtifact = options.includeArtifact !== false,
179
+ extraDirs = Array.isArray(options.deploymentDirs) ? options.deploymentDirs : [];
180
+
181
+ if (includeArtifact && artifactPath && fs.existsSync(artifactPath)) {
182
+ zip.directory(toPosixDir(artifactPath), false);
183
+ }
177
184
 
178
- zip.directory(toPosixDir(artifactPath), false);
179
- appendMeta(zip, publishDir, [], { log });
185
+ // Prefer meta/deployments/<key> packaging (not legacy meta/publish) so
186
+ // credentialsFrom sibling keys resolve under the same layout Wrapfully reads.
187
+ const dirs = [];
188
+ if (publishDir) {
189
+ dirs.push(publishDir);
190
+ }
191
+ for (const dir of extraDirs) {
192
+ if (dir && !dirs.includes(dir)) {
193
+ dirs.push(dir);
194
+ }
195
+ }
196
+ appendMeta(zip, null, dirs, { log });
180
197
 
181
198
  if (contents) {
182
199
  zip.append(contents, { name: 'package.json' });
@@ -227,6 +227,7 @@ export async function submitWrapfullyChore ({
227
227
  * platformKey?: string,
228
228
  * deploymentKey?: string,
229
229
  * artifactPath?: string,
230
+ * includeArtifact?: boolean,
230
231
  * accessToken?: string,
231
232
  * encrypt?: boolean,
232
233
  * encryptPublicKey?: string,
@@ -273,7 +274,11 @@ export async function send (
273
274
  if (!options.deploymentKey) {
274
275
  throw new Error('deploy stage requires options.deploymentKey');
275
276
  }
276
- archiveStream = createDeployArchive(options.artifactPath, options.publishDir, contents, {log});
277
+ archiveStream = createDeployArchive(options.artifactPath, options.publishDir, contents, {
278
+ log,
279
+ deploymentDirs: options.deploymentDirs,
280
+ includeArtifact: options.includeArtifact !== false,
281
+ });
277
282
  } else if (stage === 'release') {
278
283
  archiveStream = createReleaseArchive(deployFolder, contents, options.deploymentDirs ?? [], {log});
279
284
  } else {
package/lib/node/index.js CHANGED
@@ -115,3 +115,20 @@ export {
115
115
  runAppleSigning,
116
116
  runOpenssl,
117
117
  } from './apple-signing.js';
118
+ export {
119
+ LEDGER_FILENAME,
120
+ LEDGER_VERSION,
121
+ emptyLedger,
122
+ ledgerFileExists,
123
+ ledgerMatchesSet,
124
+ ledgerPath,
125
+ listOrderedImageFiles,
126
+ makeSetKey,
127
+ readLedger,
128
+ recordSetSuccess,
129
+ sequencesEqual,
130
+ sha256Buffer,
131
+ sha256File,
132
+ sha256Sequence,
133
+ writeLedger,
134
+ } from './store-assets-ledger.js';
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
1
3
  import path from 'node:path';
2
4
  import { loadProjectConfig, resolveServerUrl, resolveAccessToken, resolveEncryptFlag } from './config.js';
3
5
  import { send } from './deploy.js';
@@ -21,6 +23,28 @@ import { androidKeystoreFromCli } from './android-keystore.js';
21
23
  import { appleSigningFromCli } from './apple-signing.js';
22
24
  import { buildGameId } from './fs-name.js';
23
25
 
26
+ /**
27
+ * @param {string} deploymentDir
28
+ * @returns {object | null}
29
+ */
30
+ function readDeploymentManifest(deploymentDir) {
31
+ try {
32
+ const raw = fs.readFileSync(path.join(deploymentDir, 'manifest.json'), 'utf8');
33
+ return JSON.parse(raw);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Listing sync deployers do not need the prior AAB/IPA artifact tree.
41
+ * @param {string | null | undefined} type
42
+ * @returns {boolean}
43
+ */
44
+ function isListingDeployerType(type) {
45
+ return type === 'google-listing' || type === 'apple-listing';
46
+ }
47
+
24
48
  /** @typedef {'prebuild' | 'build' | 'deploy' | 'release'} AdaptfullyStage */
25
49
 
26
50
  const VALID_STAGES = new Set(['prebuild', 'build', 'deploy', 'release']);
@@ -66,11 +90,17 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
66
90
  ?? resolveExtractRoot(outputRoot, platformKey);
67
91
 
68
92
  if (options.deployment) {
93
+ const publishDir = resolvePublishDir(options.deployment);
94
+ const exists = fs.existsSync(publishDir);
69
95
  if (!deployments.includes(options.deployment)) {
70
- throw new Error(
71
- `Deployment "${options.deployment}" is not configured for platform "${platformKey}". `
72
- + `Available deployments: ${deployments.join(', ')}`,
73
- );
96
+ // Deploy stage may target opt-in keys (e.g. android-listing) that are
97
+ // intentionally omitted from the platform's release fan-out list.
98
+ if (stage !== 'deploy' || !exists) {
99
+ throw new Error(
100
+ `Deployment "${options.deployment}" is not configured for platform "${platformKey}". `
101
+ + `Available deployments: ${deployments.join(', ')}`,
102
+ );
103
+ }
74
104
  }
75
105
  deployments = [options.deployment];
76
106
  } else if (stage === 'build') {
@@ -83,10 +113,19 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
83
113
 
84
114
  let artifactPath = options.artifactPath;
85
115
  if (stage === 'deploy' && !artifactPath) {
86
- artifactPath = resolveBuildArtifactDir(pkg, {
87
- outputRoot: options.outputRoot,
88
- platformKey,
89
- });
116
+ const primaryKey = deployments[0];
117
+ const primaryManifest = primaryKey
118
+ ? readDeploymentManifest(resolvePublishDir(primaryKey))
119
+ : null;
120
+ if (isListingDeployerType(primaryManifest?.type)) {
121
+ // Assets-only: meta/store-assets + deployment folders; skip heavy binary tree.
122
+ artifactPath = fs.mkdtempSync(path.join(os.tmpdir(), 'adaptfully-listing-'));
123
+ } else {
124
+ artifactPath = resolveBuildArtifactDir(pkg, {
125
+ outputRoot: options.outputRoot,
126
+ platformKey,
127
+ });
128
+ }
90
129
  }
91
130
 
92
131
  const prebuiltDir = stage === 'deploy'
@@ -153,9 +192,25 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
153
192
  for (const deploymentKey of deployments) {
154
193
  const publishDir = deploymentKey === 'zip' ? undefined : resolvePublishDir(deploymentKey);
155
194
  const wrapStage = stage === 'deploy' ? 'deploy' : 'build';
156
- const deploymentDirs = wrapStage === 'build'
195
+ /** @type {string[]} */
196
+ let deploymentDirs = wrapStage === 'build'
157
197
  ? resolveBuildCredentialDirs(platformKey, pkg)
158
198
  : [];
199
+ let includeArtifact = true;
200
+
201
+ if (wrapStage === 'deploy' && publishDir) {
202
+ const manifest = readDeploymentManifest(publishDir);
203
+ if (isListingDeployerType(manifest?.type)) {
204
+ includeArtifact = Boolean(options.artifactPath);
205
+ deploymentDirs = [];
206
+ if (manifest?.credentialsFrom) {
207
+ const sibling = resolvePublishDir(String(manifest.credentialsFrom));
208
+ if (fs.existsSync(sibling)) {
209
+ deploymentDirs.push(sibling);
210
+ }
211
+ }
212
+ }
213
+ }
159
214
 
160
215
  log(
161
216
  `adaptfully: ${stage} ${platformKey} → `
@@ -165,9 +220,13 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
165
220
 
166
221
  if (deploymentDirs.length) {
167
222
  log(
168
- `adaptfully: build signing credentials → ${
169
- deploymentDirs.map((dir) => path.resolve(dir)).join(', ')
170
- }`,
223
+ wrapStage === 'deploy'
224
+ ? `adaptfully: listing credentialsFrom → ${
225
+ deploymentDirs.map((dir) => path.resolve(dir)).join(', ')
226
+ }`
227
+ : `adaptfully: build signing credentials → ${
228
+ deploymentDirs.map((dir) => path.resolve(dir)).join(', ')
229
+ }`,
171
230
  );
172
231
  }
173
232
  log(`adaptfully: extract → ${extractRoot}`);
@@ -187,6 +246,7 @@ export async function runAdaptfullyStage(stage, platformKey, options) {
187
246
  deploymentDirs,
188
247
  deploymentKey: wrapStage === 'deploy' ? deploymentKey : undefined,
189
248
  artifactPath,
249
+ includeArtifact,
190
250
  },
191
251
  );
192
252
 
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Upload ledger for store listing image sets (Play, ASC, future platforms).
3
+ * Lives at assets/meta/store-assets/upload-ledger.json (zip: meta/store-assets/…).
4
+ *
5
+ * Set key: `{platform}/{locale}/{slot}` e.g. google/en-US/phoneScreenshots
6
+ */
7
+ import fs from 'node:fs';
8
+ import fsp from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import crypto from 'node:crypto';
11
+
12
+ export const LEDGER_FILENAME = 'upload-ledger.json';
13
+ export const LEDGER_VERSION = 1;
14
+
15
+ /**
16
+ * @param {string} storeAssetsRoot Absolute path to store-assets (…/meta/store-assets)
17
+ * @returns {string}
18
+ */
19
+ export function ledgerPath(storeAssetsRoot) {
20
+ return path.join(storeAssetsRoot, LEDGER_FILENAME);
21
+ }
22
+
23
+ /**
24
+ * @param {string} platform
25
+ * @param {string} locale
26
+ * @param {string} slot
27
+ * @returns {string}
28
+ */
29
+ export function makeSetKey(platform, locale, slot) {
30
+ return `${platform}/${locale}/${slot}`;
31
+ }
32
+
33
+ /**
34
+ * @param {string | Buffer | Uint8Array} data
35
+ * @returns {string}
36
+ */
37
+ export function sha256Buffer(data) {
38
+ return crypto.createHash('sha256').update(data).digest('hex');
39
+ }
40
+
41
+ /**
42
+ * @param {string} filePath
43
+ * @returns {Promise<string>}
44
+ */
45
+ export async function sha256File(filePath) {
46
+ const hash = crypto.createHash('sha256');
47
+ const handle = await fsp.open(filePath, 'r');
48
+ try {
49
+ const stream = handle.createReadStream();
50
+ for await (const chunk of stream) {
51
+ hash.update(chunk);
52
+ }
53
+ } finally {
54
+ await handle.close();
55
+ }
56
+ return hash.digest('hex');
57
+ }
58
+
59
+ /**
60
+ * Ordered SHA-256 for each path (display / upload order).
61
+ * @param {string[]} filePaths Absolute paths
62
+ * @returns {Promise<string[]>}
63
+ */
64
+ export async function sha256Sequence(filePaths) {
65
+ const out = [];
66
+ for (const filePath of filePaths) {
67
+ out.push(await sha256File(filePath));
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * @param {string[]} a
74
+ * @param {string[]} b
75
+ * @returns {boolean}
76
+ */
77
+ export function sequencesEqual(a, b) {
78
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
79
+ return false;
80
+ }
81
+ for (let i = 0; i < a.length; i += 1) {
82
+ if (a[i] !== b[i]) {
83
+ return false;
84
+ }
85
+ }
86
+ return true;
87
+ }
88
+
89
+ /**
90
+ * @returns {{ version: number, sets: Record<string, object> }}
91
+ */
92
+ export function emptyLedger() {
93
+ return { version: LEDGER_VERSION, sets: {} };
94
+ }
95
+
96
+ /**
97
+ * @param {string} storeAssetsRoot
98
+ * @returns {Promise<{ version: number, sets: Record<string, object> }>}
99
+ */
100
+ export async function readLedger(storeAssetsRoot) {
101
+ const file = ledgerPath(storeAssetsRoot);
102
+ try {
103
+ const raw = await fsp.readFile(file, 'utf8');
104
+ const parsed = JSON.parse(raw);
105
+ if (!parsed || typeof parsed !== 'object') {
106
+ return emptyLedger();
107
+ }
108
+ const sets = parsed.sets && typeof parsed.sets === 'object' ? parsed.sets : {};
109
+ return { version: Number(parsed.version) || LEDGER_VERSION, sets };
110
+ } catch (err) {
111
+ if (err && (err.code === 'ENOENT' || err instanceof SyntaxError)) {
112
+ return emptyLedger();
113
+ }
114
+ throw err;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @param {string} storeAssetsRoot
120
+ * @param {{ version?: number, sets: Record<string, object> }} ledger
121
+ * @returns {Promise<void>}
122
+ */
123
+ export async function writeLedger(storeAssetsRoot, ledger) {
124
+ await fsp.mkdir(storeAssetsRoot, { recursive: true });
125
+ const payload = {
126
+ version: ledger.version || LEDGER_VERSION,
127
+ sets: ledger.sets || {},
128
+ };
129
+ await fsp.writeFile(ledgerPath(storeAssetsRoot), `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
130
+ }
131
+
132
+ /**
133
+ * @param {{ version: number, sets: Record<string, object> }} ledger
134
+ * @param {string} setKey
135
+ * @param {string[]} sequence
136
+ * @param {boolean} [force]
137
+ * @returns {boolean} true when local sequence matches last successful upload
138
+ */
139
+ export function ledgerMatchesSet(ledger, setKey, sequence, force = false) {
140
+ if (force) {
141
+ return false;
142
+ }
143
+ const entry = ledger?.sets?.[setKey];
144
+ if (!entry || !Array.isArray(entry.sha256Sequence)) {
145
+ return false;
146
+ }
147
+ return sequencesEqual(entry.sha256Sequence, sequence);
148
+ }
149
+
150
+ /**
151
+ * Record a successful upload for one set. Does not touch other sets.
152
+ * @param {{ version: number, sets: Record<string, object> }} ledger
153
+ * @param {string} setKey
154
+ * @param {{ sha256Sequence: string[], assetIds?: string[], uploadedAt?: string }} update
155
+ */
156
+ export function recordSetSuccess(ledger, setKey, update) {
157
+ if (!ledger.sets) {
158
+ ledger.sets = {};
159
+ }
160
+ ledger.sets[setKey] = {
161
+ sha256Sequence: [...update.sha256Sequence],
162
+ uploadedAt: update.uploadedAt || new Date().toISOString(),
163
+ ...(Array.isArray(update.assetIds) ? { assetIds: [...update.assetIds] } : {}),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Sync-friendly: list image files in a directory in alphanumeric order.
169
+ * @param {string} dir
170
+ * @returns {Promise<string[]>} Absolute paths
171
+ */
172
+ export async function listOrderedImageFiles(dir) {
173
+ let names;
174
+ try {
175
+ names = await fsp.readdir(dir);
176
+ } catch (err) {
177
+ if (err && err.code === 'ENOENT') {
178
+ return [];
179
+ }
180
+ throw err;
181
+ }
182
+ const images = names
183
+ .filter((name) => /\.(png|jpe?g|webp)$/i.test(name))
184
+ .sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
185
+ return images.map((name) => path.join(dir, name));
186
+ }
187
+
188
+ /**
189
+ * @param {string} storeAssetsRoot
190
+ * @returns {boolean}
191
+ */
192
+ export function ledgerFileExists(storeAssetsRoot) {
193
+ return fs.existsSync(ledgerPath(storeAssetsRoot));
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makefully/adaptfully",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Platform abstraction and Wrapfully deploy client for Makefully games",
5
5
  "type": "module",
6
6
  "main": "./lib/node/index.js",