Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions src/apple/ascClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import type {
WinBackOfferResource,
} from '../core/types/appleCatalog.js';
import { ACCESSIBILITY_SUPPORT_KEYS } from '../core/types/appleCatalog.js';
import type { MutableDeep } from '../core/types/mutable.js';
/** Scheme + host of the App Store Connect API; most resources hang off `/v1`, a few newer ones off `/v2`. */
const API_ORIGIN = 'https://api.appstoreconnect.apple.com';
const BASE_URL = `${API_ORIGIN}/v1`;
Expand Down Expand Up @@ -321,7 +322,7 @@ const pickListingFields = (
const pickAccessibilitySupport = (
attributes: Partial<AccessibilitySupport>,
): AccessibilitySupport => {
const support: AccessibilitySupport = {};
const support: MutableDeep<AccessibilitySupport> = {};
for (const key of ACCESSIBILITY_SUPPORT_KEYS) {
const fieldValue = attributes[key];
if (typeof fieldValue === 'boolean') support[key] = fieldValue;
Expand Down Expand Up @@ -1042,7 +1043,7 @@ export class AppStoreConnectClient {
return appleResources.map((localization) => {
let locale = localization.attributes.locale;
if (locale === undefined) locale = '';
const localizedExperience: AppClipLocalizationResource = {
const localizedExperience: MutableDeep<AppClipLocalizationResource> = {
id: localization.id,
locale,
};
Expand Down Expand Up @@ -1330,7 +1331,7 @@ export class AppStoreConnectClient {
name: string,
bundleIdResourceId: string,
certificateId: string,
deviceIds: string[],
deviceIds: readonly string[],
profileType: ProvisioningProfileType = AD_HOC_PROFILE_TYPE,
): Promise<ProfileResource> {
const appleResources = await this.createResource<{
Expand Down Expand Up @@ -1457,7 +1458,7 @@ export class AppStoreConnectClient {
});
}
/** Clear the StoreKit purchase history for one or more sandbox testers (a single batched request). */
async clearSandboxTesterPurchaseHistory(testerIds: string[]): Promise<void> {
async clearSandboxTesterPurchaseHistory(testerIds: readonly string[]): Promise<void> {
await this.createResource(this.v2('/sandboxTestersClearPurchaseHistoryRequest'), {
type: 'sandboxTestersClearPurchaseHistoryRequest',
relationships: {
Expand Down Expand Up @@ -1743,7 +1744,7 @@ export class AppStoreConnectClient {
type: 'subscriptionOfferCodes',
attributes: {
name: input.name,
customerEligibilities: input.customerEligibilities,
customerEligibilities: [...input.customerEligibilities],
offerEligibility: input.offerEligibility,
duration: input.duration,
offerMode: input.offerMode,
Expand Down Expand Up @@ -2066,7 +2067,7 @@ export class AppStoreConnectClient {
return { id: appleResources.id, inAppPurchaseId, subscriptionId, enabled, visibleForAllUsers };
}
/** Replace the app's promoted-purchase ordering with `orderedIds` (the product-page display order). */
async reorderPromotedPurchases(appId: string, orderedIds: string[]): Promise<void> {
async reorderPromotedPurchases(appId: string, orderedIds: readonly string[]): Promise<void> {
const requestDocument: components['schemas']['AppPromotedPurchasesLinkagesRequest'] = {
data: orderedIds.map((id) => ({ type: 'promotedPurchases', id })),
};
Expand Down Expand Up @@ -2244,13 +2245,13 @@ export class AppStoreConnectClient {
return betaTester;
}
/** Add existing testers to a beta group in one relationship call (invites external testers). */
async addTestersToGroup(groupId: string, testerIds: string[]): Promise<void> {
async addTestersToGroup(groupId: string, testerIds: readonly string[]): Promise<void> {
await this.request<unknown>('POST', `/betaGroups/${groupId}/relationships/betaTesters`, {
data: testerIds.map((id) => ({ type: 'betaTesters', id })),
});
}
/** Remove testers from a beta group; they keep app access through any other group they're in. */
async removeTestersFromGroup(groupId: string, testerIds: string[]): Promise<void> {
async removeTestersFromGroup(groupId: string, testerIds: readonly string[]): Promise<void> {
await this.request<unknown>('DELETE', `/betaGroups/${groupId}/relationships/betaTesters`, {
data: testerIds.map((id) => ({ type: 'betaTesters', id })),
});
Expand Down Expand Up @@ -2566,7 +2567,7 @@ export class AppStoreConnectClient {
*/
async updateAppAvailabilityTerritories(
availabilityId: string,
territories: string[],
territories: readonly string[],
): Promise<void> {
const desired = new Set(territories);
const territoryAvailabilities = await this.listAppTerritoryAvailabilities(availabilityId);
Expand Down Expand Up @@ -3278,15 +3279,15 @@ export class AppStoreConnectClient {
email: invite.email,
firstName: invite.firstName,
lastName: invite.lastName,
roles: invite.roles,
roles: [...invite.roles],
allAppsVisible: invite.allAppsVisible,
provisioningAllowed: invite.provisioningAllowed,
},
});
let email = appleResources.attributes.email;
if (email === undefined) email = invite.email;
let roles = appleResources.attributes.roles;
if (roles === undefined) roles = invite.roles;
if (roles === undefined) roles = [...invite.roles];
const pendingInvitation: UserInvitationResource = { id: appleResources.id, email, roles };
if (appleResources.attributes.firstName !== undefined)
Object.assign(pendingInvitation, { firstName: appleResources.attributes.firstName });
Expand Down Expand Up @@ -4478,7 +4479,10 @@ export class AppStoreConnectClient {
return { id: reservation.data.id, operations };
}
/** PUT a reserved asset's bytes to Apple's CDN, one operation (chunk) at a time, with transient-retry. */
private async putAssetBytes(operations: UploadOperation[], bytes: Buffer): Promise<void> {
private async putAssetBytes(
operations: readonly UploadOperation[],
bytes: Buffer,
): Promise<void> {
for (const operation of operations) {
let offset = operation.offset;
if (offset === undefined) offset = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/apple/generated/specPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function generatedHeader(spec: AscSpecMeta): string {
* Pick the real spec file from a zip's entry list, accepting Apple's occasional download suffix and
* skipping the macOS resource-fork sibling. Returns the matching entry or null when absent.
*/
export function pickSpecEntry(entries: string[]): string | null {
export function pickSpecEntry(entries: readonly string[]): string | null {
const specEntry = entries.find((entry) => {
const fileName = entry.split('/').at(-1);
return (
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const registerCompletionCommand = (program: Command): void => {
completion
.command(`${COMPLETE_SUBCOMMAND} [words...]`, { hidden: true })
.description('internal: emit completion candidates for the words typed so far')
.action((words: string[]) => {
.action((words: readonly string[]) => {
return runCliProgram(
completionCommandProgram({ operation: 'complete', words, commandTree: program }),
);
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/testflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const registerTestflightCommand = (program: Command): void => {
.option('--csv <path>', 'import testers from a CSV (email,firstName,lastName per line)')
.option('--dry-run', 'report what would change without inviting anyone', false)
.option('-y, --yes', 'skip the confirmation prompt', false)
.action((emails: string[], commandOptions: AddTesterOptions) =>
.action((emails: readonly string[], commandOptions: AddTesterOptions) =>
runCliProgram(
testflightCommandProgram({
operation: 'add',
Expand All @@ -118,7 +118,7 @@ export const registerTestflightCommand = (program: Command): void => {
.option('-g, --group <name>', "beta group to remove from (auto-selected if there's only one)")
.option('--dry-run', 'report what would change without removing anyone', false)
.option('-y, --yes', 'skip the confirmation prompt', false)
.action((emails: string[], commandOptions: TesterMutationOptions) =>
.action((emails: readonly string[], commandOptions: TesterMutationOptions) =>
runCliProgram(
testflightCommandProgram({
operation: 'remove',
Expand Down
2 changes: 1 addition & 1 deletion src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type EnvFlags = {
printEnv: boolean;
};
/** Commander reducer: collect a repeatable string option into an array. */
const collectEnv = (environmentFlag: string, previousFlags: string[]): string[] => {
const collectEnv = (environmentFlag: string, previousFlags: readonly string[]): string[] => {
return [...previousFlags, environmentFlag];
};
/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/adopt/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const capabilitiesAdopter: Adopter<ProfileEntitlementRequirements> = {
let profileEntitlements: Record<string, EntitlementValue> | null = null;
if (profileContent !== null)
profileEntitlements = yield* extractProfileEntitlements(profileContent);
const settingsByType: Record<string, CapabilitySetting[]> = {};
const settingsByType: Record<string, readonly CapabilitySetting[]> = {};
for (const capability of capabilities) {
if (capability.settings !== undefined)
settingsByType[capability.capabilityType] = capability.settings;
Expand Down
2 changes: 1 addition & 1 deletion src/core/adopt/certs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const certsAdopter: Adopter<CertAdopterRequirements> = {
Effect.gen(function* () {
const bundleResource = yield* appleCatalog.findBundleId(target.bundleId);
const certificates = yield* appleCatalog.listDistributionCertificates();
let profiles: ProfileResource[] = [];
let profiles: readonly ProfileResource[] = [];
if (bundleResource !== null)
profiles = yield* appleCatalog.listProfilesForBundleId(bundleResource.id);
const storedCredentials = yield* describeStoredCredentials(target.keyId);
Expand Down
10 changes: 6 additions & 4 deletions src/core/adopt/configWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export const aggregateProductPieces = (pieces: readonly ProductPiece[]): AppProd
}
subscriptionGroups.push(piece.group);
}
const products: AppProducts = {};
if (inAppPurchases.length > 0) products.inAppPurchases = inAppPurchases;
if (subscriptionGroups.length > 0) products.subscriptionGroups = subscriptionGroups;
return products;
if (inAppPurchases.length > 0 && subscriptionGroups.length > 0) {
return { inAppPurchases, subscriptionGroups };
}
if (inAppPurchases.length > 0) return { inAppPurchases };
if (subscriptionGroups.length > 0) return { subscriptionGroups };
return {};
};

/** Serialize a products block keyed by bundle id as an indented, paste-ready TypeScript section. */
Expand Down
3 changes: 2 additions & 1 deletion src/core/adopt/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
SubscriptionGroupConfig,
SubscriptionPeriod,
} from '../types/catalog.js';
import type { MutableDeep } from '../types/mutable.js';

/** Narrow an App Store in-app purchase type to the modeled config union. */
const parseInAppPurchaseType = (purchaseType: string): InAppPurchaseType | null => {
Expand Down Expand Up @@ -48,7 +49,7 @@ const productLocalizationsFromResources = (
localizations: readonly LocalizationResource[],
): ProductLocalization[] =>
localizations.map((localization) => {
const productLocalization: ProductLocalization = {
const productLocalization: MutableDeep<ProductLocalization> = {
locale: localization.locale,
name: localization.name,
};
Expand Down
4 changes: 3 additions & 1 deletion src/core/build/appleTargets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ export const discoverExtensionBundleIds = (
const targets = parsePbxprojTargets(projectText);
return splitMainAndExtensions(targets, mainBundleId).extensions;
});
export const multiTargetSigningWarnings = (readiness: TargetSigningReadiness[]): string[] => {
export const multiTargetSigningWarnings = (
readiness: readonly TargetSigningReadiness[],
): string[] => {
const warnings: string[] = [];
for (const target of readiness) {
if (!target.registered) {
Expand Down
17 changes: 11 additions & 6 deletions src/core/build/artifactRetention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
} from '../types/artifacts.js';
import { ArtifactIndexSchema } from '../types/artifacts.js';
import type { LaunchConfig } from '../types/config.js';
import type { MutableDeep } from '../types/mutable.js';

export const DEFAULT_RETENTION_DAYS = 30;
const DAY_MS = 24 * 60 * 60 * 1000;
Expand All @@ -26,7 +27,7 @@ const resolveArtifactIndexPath = (
/** Read and decode the newest-first artifact index; absent or malformed state yields []. */
export const readArtifactIndex = (
indexPath?: string,
): Effect.Effect<BuildArtifact[], never, ArtifactIndexRequirements> =>
): Effect.Effect<readonly BuildArtifact[], never, ArtifactIndexRequirements> =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const artifactIndexPath = yield* resolveArtifactIndexPath(indexPath);
Expand All @@ -44,7 +45,7 @@ export const readArtifactIndex = (

/** Persist the artifact index, creating its parent directory first. */
export const writeArtifactIndex = (
artifactIndex: BuildArtifact[],
artifactIndex: readonly BuildArtifact[],
indexPath?: string,
): Effect.Effect<void, PlatformError, ArtifactIndexRequirements> =>
Effect.gen(function* () {
Expand Down Expand Up @@ -86,7 +87,9 @@ const artifactGroupKey = (buildArtifact: BuildArtifact): string =>
`${buildArtifact.appName}:${buildArtifact.platform}`;

/** Newest artifact per app+platform group (by createdAt). */
const newestArtifactByGroup = (artifactIndex: BuildArtifact[]): Map<string, BuildArtifact> => {
const newestArtifactByGroup = (
artifactIndex: readonly BuildArtifact[],
): Map<string, BuildArtifact> => {
const newestByGroup = new Map<string, BuildArtifact>();
for (const buildArtifact of artifactIndex) {
const groupKey = artifactGroupKey(buildArtifact);
Expand All @@ -106,7 +109,7 @@ const newestArtifactByGroup = (artifactIndex: BuildArtifact[]): Map<string, Buil

/** Split an artifact index by retention + keep-newest-per-app+platform policy. */
export const planPrune = (
artifactIndex: BuildArtifact[],
artifactIndex: readonly BuildArtifact[],
pruneOptions: Pick<PruneOptions, 'now' | 'retentionDays' | 'app' | 'platform'>,
): { prune: BuildArtifact[]; keep: BuildArtifact[] } => {
const newestByGroup = newestArtifactByGroup(artifactIndex);
Expand Down Expand Up @@ -160,7 +163,7 @@ const readArtifactBytes = (

/** Stamp prunedAt on pruned rows without mutating the source index entries. */
const stampPrunedArtifacts = (
artifactIndex: BuildArtifact[],
artifactIndex: readonly BuildArtifact[],
prunedArtifacts: readonly BuildArtifact[],
prunedAt: string,
): BuildArtifact[] => {
Expand All @@ -179,7 +182,9 @@ export const runArtifactPrune = (
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const artifactIndex = yield* readArtifactIndex(pruneOptions.indexPath);
const policyInput: Pick<PruneOptions, 'now' | 'retentionDays' | 'app' | 'platform'> = {
const policyInput: MutableDeep<
Pick<PruneOptions, 'now' | 'retentionDays' | 'app' | 'platform'>
> = {
now: pruneOptions.now,
retentionDays: pruneOptions.retentionDays,
};
Expand Down
2 changes: 1 addition & 1 deletion src/core/build/buildDiagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export const diagnoseBuildLog = (log: string): BuildDiagnosis[] => {
return matched;
};
/** Render diagnoses as an indented, human-readable block for the terminal. Empty input -> empty string. */
export const formatDiagnoses = (diagnoses: BuildDiagnosis[]): string => {
export const formatDiagnoses = (diagnoses: readonly BuildDiagnosis[]): string => {
if (diagnoses.length === 0) return '';
let header = 'Likely causes:';
if (diagnoses.length === 1) header = 'Likely cause:';
Expand Down
4 changes: 2 additions & 2 deletions src/core/build/buildHistoryCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const toBuildRow = (artifact: BuildArtifact): BuildRow => {

/** Narrow build history to the requested app and platform. */
export const filterBuilds = (
storedBuilds: BuildArtifact[],
storedBuilds: readonly BuildArtifact[],
filters: Readonly<{ app?: string; platform?: Platform }>,
): BuildArtifact[] =>
storedBuilds.filter((storedBuild) => {
Expand All @@ -159,7 +159,7 @@ export const filterBuilds = (

/** Resolve a full id, build number, or `latest` against newest-first history. */
export const findBuild = (
storedBuilds: BuildArtifact[],
storedBuilds: readonly BuildArtifact[],
reference: string,
): BuildArtifact | undefined => {
if (reference === 'latest') return storedBuilds[0];
Expand Down
32 changes: 26 additions & 6 deletions src/core/build/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Data, Effect } from 'effect';
import type { AppDescriptor } from '../types/app.js';
import type { ResolvedBuildContext } from '../types/config.js';
import type { MutableDeep } from '../types/mutable.js';
import type { NotifyEvent } from '../services/notify.js';
import { notify } from '../services/notify.js';
import { loadConfig } from '../config/config.js';
Expand Down Expand Up @@ -103,18 +105,36 @@ export const prepareBuild = (options: BuildRunOptions) =>
let configCheckDescription = 'no footguns';
if (findings.length > 0) configCheckDescription = `${findings.length} warning(s)`;
yield* log.step('config check', configCheckDescription);
let buildContext: ResolvedBuildContext = {
const draftApp: MutableDeep<AppDescriptor> = {
name: app.name,
dir: app.dir,
configPath: app.configPath,
};
if (app.bundleId !== undefined) draftApp.bundleId = app.bundleId;
if (app.packageName !== undefined) draftApp.packageName = app.packageName;
if (app.version !== undefined) draftApp.version = app.version;
if (app.iosEntitlements !== undefined) draftApp.iosEntitlements = { ...app.iosEntitlements };
if (app.iosExtensions !== undefined) draftApp.iosExtensions = [...app.iosExtensions];
if (app.androidVersionCode !== undefined) draftApp.androidVersionCode = app.androidVersionCode;
if (app.usesNonExemptEncryption !== undefined) {
draftApp.usesNonExemptEncryption = app.usesNonExemptEncryption;
}
let buildContext: MutableDeep<ResolvedBuildContext> = {
platform,
app,
app: draftApp,
profile,
env,
env: { ...env },
explain: options.explain,
dryRun,
forceClean: options.forceClean === true,
};
if (options.ccache !== undefined) buildContext = { ...buildContext, ccache: options.ccache };
if (platform === 'android') {
let androidRelease = resolveAndroidRelease(options, profile);
const resolvedAndroidRelease = resolveAndroidRelease(options, profile);
const androidRelease: MutableDeep<typeof resolvedAndroidRelease> = {
track: resolvedAndroidRelease.track,
rollout: resolvedAndroidRelease.rollout,
};
const releaseNotes = yield* resolveAndroidSubmitReleaseNotes(
config,
app.dir,
Expand All @@ -135,13 +155,13 @@ export const prepareBuild = (options: BuildRunOptions) =>
}),
);
if (releaseNotes.length > 0) {
androidRelease = { ...androidRelease, releaseNotes };
androidRelease.releaseNotes = [...releaseNotes];
}
buildContext = { ...buildContext, android: androidRelease };
}
if (options.distribution !== undefined)
buildContext = { ...buildContext, distribution: options.distribution };
return { config, app, profile, env, buildContext, log };
return { config, app: draftApp, profile, env, buildContext, log };
});
/**
* Run a build, then fire any configured completion notification. Throws with a clear message on any
Expand Down
Loading
Loading