@makefully/adaptfully 5.2.0 → 5.3.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/CHANGELOG.md +13 -0
- package/lib/node/archive.js +23 -6
- package/lib/node/deploy.js +6 -1
- package/lib/node/index.js +17 -0
- package/lib/node/pipeline.js +72 -12
- package/lib/node/store-assets-ledger.js +194 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 5.3.1 — 2026-11-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Automatic GitHub Releases on `package.json` version bumps (via the Release workflow).
|
|
10
|
+
|
|
11
|
+
## 5.3.0 — 2026-11-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **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).
|
|
16
|
+
- **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/`.
|
|
17
|
+
|
|
5
18
|
## 5.2.0 — 2026-11-11
|
|
6
19
|
|
|
7
20
|
### Added
|
package/lib/node/archive.js
CHANGED
|
@@ -163,20 +163,37 @@ export function createArchive(deployFolder, contents, options = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
* Prior artifact zip plus
|
|
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
|
-
|
|
179
|
-
|
|
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' });
|
package/lib/node/deploy.js
CHANGED
|
@@ -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, {
|
|
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';
|
package/lib/node/pipeline.js
CHANGED
|
@@ -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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
+
}
|