diff --git a/docs/resources/(resources)/fastlane/fastlane-project.mdx b/docs/resources/(resources)/fastlane/fastlane-project.mdx new file mode 100644 index 00000000..1a7d7e36 --- /dev/null +++ b/docs/resources/(resources)/fastlane/fastlane-project.mdx @@ -0,0 +1,85 @@ +--- +title: fastlane-project +description: A reference page for the fastlane-project resource +--- + +The fastlane-project resource manages **per-project** fastlane initialization — the equivalent of +running `fastlane init` in a project directory. It writes `fastlane/Appfile` (app identifier, +credentials, and team info) and `fastlane/Fastfile` (your lane definitions) under the target +directory. Use it alongside the [`fastlane`](/docs/resources/fastlane/fastlane) resource, which +handles installing the fastlane CLI itself. + +## Parameters + +- **directory**: *(string, required)* Path to the project directory. Configuration is written to: + - `/fastlane/Appfile` + - `/fastlane/Fastfile` + +- **appIdentifier**: *(string, optional)* iOS bundle identifier or Android package name, written as + `app_identifier` in the Appfile. + +- **appleId**: *(string, optional)* Apple ID email used to authenticate with App Store Connect, + written as `apple_id` in the Appfile. + +- **teamId**: *(string, optional)* Apple Developer Portal team ID, written as `team_id` in the + Appfile. + +- **itcTeamId**: *(string, optional)* App Store Connect team ID, only needed if it differs from + `teamId`. Written as `itc_team_id` in the Appfile. + +- **jsonKeyFile**: *(string, optional)* Path to the Google Play service account JSON key file, + written as `json_key_file` in the Appfile. + +- **fastfile**: *(string, optional)* Raw Ruby content for `fastlane/Fastfile` — defines your lanes + (e.g. lanes calling `build_app`, `upload_to_app_store`, `gradle`, `upload_to_play_store`). Defaults + to a minimal starter lane if not provided. + +## Example usage + +### iOS App Store release lane + +```json title="codify.jsonc" +[ + { + "type": "fastlane-project", + "directory": "~/projects/my-ios-app", + "appIdentifier": "com.company.myiosapp", + "appleId": "developer@company.com", + "teamId": "ABCDE12345", + "fastfile": "default_platform(:ios)\n\nplatform :ios do\n lane :release do\n build_app(scheme: \"MyApp\")\n upload_to_app_store(\n api_key_path: \"fastlane/api_key.json\",\n skip_metadata: true,\n skip_screenshots: true,\n submit_for_review: false\n )\n end\nend\n", + "os": ["macOS"] + } +] +``` + +### Android Play Store release setup + +```json title="codify.jsonc" +[ + { + "type": "fastlane" + }, + { + "type": "fastlane-project", + "directory": "~/projects/my-android-app", + "appIdentifier": "com.company.myandroidapp", + "jsonKeyFile": "~/projects/my-android-app/play-store-key.json", + "fastfile": "default_platform(:android)\n\nplatform :android do\n lane :deploy do\n gradle(task: \"bundle\", build_type: \"Release\")\n upload_to_play_store(track: \"production\")\n end\nend\n", + "dependsOn": ["fastlane"] + } +] +``` + +## Notes + +- The `fastlane` resource must be applied before `fastlane-project` (it declares a dependency + automatically). +- Multiple `fastlane-project` entries can coexist — each unique `directory` is a separate resource + instance, useful for monorepos with several apps. +- Rather than driving the interactive `fastlane init` wizard (which is known to be unreliable in + non-interactive/CI environments), this resource writes the `Appfile` and `Fastfile` directly, + giving full declarative control over their contents. +- Destroying a `fastlane-project` resource removes the entire `/fastlane` folder, + including any metadata or screenshots fastlane itself may have added there. +- The `fastfile` parameter manages the entire file — treat it as the single source of truth for your + lanes rather than editing it by hand. diff --git a/docs/resources/(resources)/fastlane/fastlane.mdx b/docs/resources/(resources)/fastlane/fastlane.mdx new file mode 100644 index 00000000..47fee3b8 --- /dev/null +++ b/docs/resources/(resources)/fastlane/fastlane.mdx @@ -0,0 +1,59 @@ +--- +title: fastlane +description: A reference page for the fastlane resource +--- + +The fastlane resource installs [fastlane](https://docs.fastlane.tools/) — the open-source automation +tool for building, signing, testing, and releasing iOS and Android apps. It handles installation via +Homebrew on macOS and RubyGems on Linux. Use it alongside the +[`fastlane-project`](/docs/resources/fastlane/fastlane-project) resource, which configures fastlane +within a specific project directory. + +## Parameters + +- **version**: *(string, optional)* Specific fastlane version to install (e.g. `"2.223.1"`). On Linux + this pins the gem version installed via `gem install`. On macOS, Homebrew always installs its + latest available formula version, so this field is informational only there. + +## Example usage + +### Install a pinned fastlane version + +```json title="codify.jsonc" +[ + { + "type": "fastlane", + "version": "2.223.1" + } +] +``` + +### Install fastlane and initialize a project + +```json title="codify.jsonc" +[ + { + "type": "fastlane" + }, + { + "type": "fastlane-project", + "directory": "~/projects/my-app", + "appIdentifier": "com.company.myapp", + "dependsOn": ["fastlane"] + } +] +``` + +## Notes + +- **Ruby prerequisite**: fastlane requires Ruby 3.0 or newer (fastlane recommends 3.3+). Before + installing, this resource checks for `ruby -v` on the system. If Ruby is missing or older than + 3.0, apply fails with an error recommending the [`rbenv`](/docs/resources/ruby/rbenv) resource in + this plugin to install and manage a compatible Ruby version. fastlane's own docs discourage using + the macOS system Ruby, so a user-managed Ruby (via rbenv or similar) is the expected setup. +- **macOS**: installed via Homebrew (`brew install fastlane`), which bundles its own Ruby dependency. +- **Linux**: no apt/dnf package exists for fastlane, so it is installed via `gem install fastlane` + against whatever Ruby is on the PATH. `build-essential` is installed first since some fastlane + dependencies (e.g. nokogiri) compile native extensions. +- Uninstalling this resource removes the fastlane binary/gem but leaves any per-project + `fastlane-project` configuration untouched. diff --git a/docs/resources/(resources)/fastlane/meta.json b/docs/resources/(resources)/fastlane/meta.json new file mode 100644 index 00000000..60311ea3 --- /dev/null +++ b/docs/resources/(resources)/fastlane/meta.json @@ -0,0 +1,7 @@ +{ + "title": "Fastlane", + "pages": [ + "fastlane", + "fastlane-project" + ] +} diff --git a/src/index.ts b/src/index.ts index a9106dd0..f43ff82e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,8 @@ import { AsdfPluginResource } from './resources/asdf/asdf-plugin.js'; import { AwsCliResource } from './resources/aws-cli/cli/aws-cli.js'; import { AwsProfileResource } from './resources/aws-cli/profile/aws-profile.js'; import { DnfResource } from './resources/dnf/dnf.js'; +import { FastlaneResource } from './resources/fastlane/fastlane.js'; +import { FastlaneProjectResource } from './resources/fastlane/fastlane-project.js'; import { GoenvResource } from './resources/go/goenv/goenv.js'; import { DockerResource } from './resources/docker/docker.js'; import { EnvFileResource } from './resources/file/env-file/env-file-resource.js'; @@ -159,6 +161,8 @@ runPlugin(Plugin.create( new SyncthingDeviceResource(), new SyncthingFolderResource(), new RbenvResource(), + new FastlaneResource(), + new FastlaneProjectResource(), new OpenClawResource(), new RustResource(), new GithubCliResource(), diff --git a/src/resources/fastlane/fastlane-project.ts b/src/resources/fastlane/fastlane-project.ts new file mode 100644 index 00000000..0919726f --- /dev/null +++ b/src/resources/fastlane/fastlane-project.ts @@ -0,0 +1,274 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +import { untildify } from '../../utils/untildify.js'; + +const DEFAULT_FASTFILE = [ + 'default_platform(:ios)', + '', + 'platform :ios do', + ' lane :example do', + ' # Add your lane actions here, e.g. build_app, upload_to_app_store', + ' end', + 'end', + '', +].join('\n'); + +const schema = z + .object({ + directory: z + .string() + .describe( + 'Path to the project directory. Configuration is written to /fastlane/Appfile ' + + 'and /fastlane/Fastfile.', + ), + appIdentifier: z + .string() + .optional() + .describe('iOS bundle identifier or Android package name, written as app_identifier in the Appfile.'), + appleId: z + .string() + .optional() + .describe('Apple ID email used to authenticate with App Store Connect, written as apple_id in the Appfile.'), + teamId: z + .string() + .optional() + .describe('Apple Developer Portal team ID, written as team_id in the Appfile.'), + itcTeamId: z + .string() + .optional() + .describe('App Store Connect team ID, only needed if it differs from teamId. Written as itc_team_id in the Appfile.'), + jsonKeyFile: z + .string() + .optional() + .describe('Path to the Google Play service account JSON key file, written as json_key_file in the Appfile.'), + fastfile: z + .string() + .optional() + .describe( + 'Raw Ruby content for fastlane/Fastfile — defines your lanes (e.g. lanes calling build_app, ' + + 'upload_to_app_store, gradle, upload_to_play_store).', + ), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/fastlane/fastlane-project' }) + .describe('Per-project fastlane initialization (Appfile + Fastfile)'); + +export type FastlaneProjectConfig = z.infer; + +const defaultConfig: Partial = { + fastfile: DEFAULT_FASTFILE, +}; + +const exampleAppStoreRelease: ExampleConfig = { + title: 'iOS App Store release lane', + description: 'Initialize fastlane for an iOS project with a lane that builds the app and uploads it to App Store Connect.', + configs: [{ + type: 'fastlane-project', + directory: '~/projects/my-ios-app', + appIdentifier: 'com.company.myiosapp', + appleId: 'developer@company.com', + teamId: 'ABCDE12345', + fastfile: [ + 'default_platform(:ios)', + '', + 'platform :ios do', + ' lane :release do', + ' build_app(scheme: "MyApp")', + ' upload_to_app_store(', + ' api_key_path: "fastlane/api_key.json",', + ' skip_metadata: true,', + ' skip_screenshots: true,', + ' submit_for_review: false', + ' )', + ' end', + 'end', + '', + ].join('\n'), + os: ['macOS'], + }], +}; + +const exampleAndroidPlayStoreRelease: ExampleConfig = { + title: 'Android Play Store release setup', + description: 'Install fastlane and initialize it for an Android project with a lane that builds a release bundle and uploads it to the Play Store.', + configs: [ + { + type: 'fastlane', + }, + { + type: 'fastlane-project', + directory: '~/projects/my-android-app', + appIdentifier: 'com.company.myandroidapp', + jsonKeyFile: '~/projects/my-android-app/play-store-key.json', + fastfile: [ + 'default_platform(:android)', + '', + 'platform :android do', + ' lane :deploy do', + ' gradle(task: "bundle", build_type: "Release")', + ' upload_to_play_store(track: "production")', + ' end', + 'end', + '', + ].join('\n'), + dependsOn: ['fastlane'], + }, + ], +}; + +export class FastlaneProjectResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'fastlane-project', + defaultConfig, + exampleConfigs: { + example1: exampleAppStoreRelease, + example2: exampleAndroidPlayStoreRelease, + }, + operatingSystems: [OS.Darwin, OS.Linux], + schema, + dependencies: ['fastlane'], + parameterSettings: { + directory: { type: 'directory', canModify: false }, + appIdentifier: { canModify: true }, + appleId: { canModify: true }, + teamId: { canModify: true }, + itcTeamId: { canModify: true }, + jsonKeyFile: { canModify: true }, + fastfile: { canModify: true }, + }, + allowMultiple: { + identifyingParameters: ['directory'], + }, + }; + } + + async refresh(parameters: Partial): Promise | null> { + if (!parameters.directory) { + return null; + } + + const fastlaneDir = resolveFastlaneDir(parameters.directory); + + try { + await fs.access(resolveFastfilePath(parameters.directory)); + } catch { + return null; + } + + const result: Partial = { ...parameters }; + + if (parameters.fastfile != null) { + try { + result.fastfile = await fs.readFile(resolveFastfilePath(parameters.directory), 'utf8'); + } catch { + result.fastfile = undefined; + } + } + + let appfileContent: string | undefined; + try { + appfileContent = await fs.readFile(path.join(fastlaneDir, 'Appfile'), 'utf8'); + } catch { + appfileContent = undefined; + } + + if (parameters.appIdentifier != null) { + result.appIdentifier = extractAppfileValue(appfileContent, 'app_identifier'); + } + if (parameters.appleId != null) { + result.appleId = extractAppfileValue(appfileContent, 'apple_id'); + } + if (parameters.teamId != null) { + result.teamId = extractAppfileValue(appfileContent, 'team_id'); + } + if (parameters.itcTeamId != null) { + result.itcTeamId = extractAppfileValue(appfileContent, 'itc_team_id'); + } + if (parameters.jsonKeyFile != null) { + result.jsonKeyFile = extractAppfileValue(appfileContent, 'json_key_file'); + } + + return result; + } + + async create(plan: CreatePlan): Promise { + const { directory, fastfile } = plan.desiredConfig; + const fastlaneDir = resolveFastlaneDir(directory); + + await fs.mkdir(fastlaneDir, { recursive: true }); + await fs.writeFile(path.join(fastlaneDir, 'Appfile'), generateAppfile(plan.desiredConfig), 'utf8'); + await fs.writeFile(path.join(fastlaneDir, 'Fastfile'), fastfile ?? DEFAULT_FASTFILE, 'utf8'); + } + + async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + const { directory, fastfile } = plan.desiredConfig; + const fastlaneDir = resolveFastlaneDir(directory); + + if (pc.name === 'fastfile') { + await fs.writeFile(path.join(fastlaneDir, 'Fastfile'), fastfile ?? DEFAULT_FASTFILE, 'utf8'); + return; + } + + await fs.writeFile(path.join(fastlaneDir, 'Appfile'), generateAppfile(plan.desiredConfig), 'utf8'); + } + + async destroy(plan: DestroyPlan): Promise { + const { directory } = plan.currentConfig; + if (!directory) { + return; + } + + await fs.rm(resolveFastlaneDir(directory), { recursive: true, force: true }); + } +} + +function resolveFastlaneDir(directory: string): string { + return path.join(untildify(directory), 'fastlane'); +} + +function resolveFastfilePath(directory: string): string { + return path.join(resolveFastlaneDir(directory), 'Fastfile'); +} + +function generateAppfile(config: Partial): string { + const lines: string[] = []; + + if (config.appIdentifier) { + lines.push(`app_identifier("${config.appIdentifier}")`); + } + if (config.appleId) { + lines.push(`apple_id("${config.appleId}")`); + } + if (config.teamId) { + lines.push(`team_id("${config.teamId}")`); + } + if (config.itcTeamId) { + lines.push(`itc_team_id("${config.itcTeamId}")`); + } + if (config.jsonKeyFile) { + lines.push(`json_key_file("${config.jsonKeyFile}")`); + } + + return lines.length > 0 ? `${lines.join('\n')}\n` : ''; +} + +function extractAppfileValue(content: string | undefined, key: string): string | undefined { + if (!content) { + return undefined; + } + + const match = content.match(new RegExp(`${key}\\("([^"]*)"\\)`)); + return match?.[1]; +} diff --git a/src/resources/fastlane/fastlane.ts b/src/resources/fastlane/fastlane.ts new file mode 100644 index 00000000..158c8e9d --- /dev/null +++ b/src/resources/fastlane/fastlane.ts @@ -0,0 +1,156 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + PackageManager, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; + +const schema = z + .object({ + version: z + .string() + .optional() + .describe( + 'Specific fastlane version to install (e.g. "2.223.1"). On Linux this pins the gem ' + + 'version installed via `gem install`. On macOS (Homebrew), Homebrew always installs its ' + + 'latest available formula version, so this field is informational only there.', + ), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/fastlane/fastlane' }) + .describe('fastlane installation — mobile app build, signing, testing, and release automation'); + +export type FastlaneConfig = z.infer; + +const defaultConfig: Partial = {}; + +const examplePinnedVersion: ExampleConfig = { + title: 'Install a pinned fastlane version', + description: 'Install a specific fastlane version for reproducible CI/CD builds across machines.', + configs: [{ + type: 'fastlane', + version: '2.223.1', + }], +}; + +const exampleWithProjectInit: ExampleConfig = { + title: 'Install fastlane and initialize a project', + description: 'Install fastlane, then initialize it in a project directory with a starter lane.', + configs: [ + { + type: 'fastlane', + }, + { + type: 'fastlane-project', + directory: '~/projects/my-app', + appIdentifier: 'com.company.myapp', + dependsOn: ['fastlane'], + }, + ], +}; + +export class FastlaneResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'fastlane', + defaultConfig, + exampleConfigs: { + example1: examplePinnedVersion, + example2: exampleWithProjectInit, + }, + operatingSystems: [OS.Darwin, OS.Linux], + schema, + parameterSettings: { + version: { type: 'version', canModify: true }, + }, + }; + } + + async refresh(parameters: Partial): Promise | null> { + const $ = getPty(); + const { status, data } = await $.spawnSafe('fastlane --version'); + if (status === SpawnStatus.ERROR) { + return null; + } + + const result: Partial = {}; + if (parameters.version) { + const match = data.match(/(\d+\.\d+\.\d+)/); + result.version = match?.[1]; + } + + return result; + } + + async create(plan: CreatePlan): Promise { + await assertRubyAvailable(); + + const { version } = plan.desiredConfig; + if (Utils.isMacOS()) { + await Utils.installViaPkgMgr('fastlane', undefined, PackageManager.BREW); + return; + } + + await Utils.installViaPkgMgr('build-essential'); + const $ = getPty(); + // Deliberately not using requiresRoot here: fastlane's own docs recommend against installing + // gems into a sudo-owned system Ruby. Users are expected to bring a user-writable Ruby (e.g. + // via the rbenv resource), matching the ruby check above. + await $.spawn(`gem install fastlane${version ? ` -v ${version}` : ''}`, { interactive: true }); + } + + async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'version' || Utils.isMacOS()) { + return; + } + + const { version } = plan.desiredConfig; + const $ = getPty(); + await $.spawn(`gem install fastlane${version ? ` -v ${version}` : ''}`, { interactive: true }); + } + + async destroy(plan: DestroyPlan): Promise { + if (Utils.isMacOS()) { + await Utils.uninstallViaPkgMgr('fastlane', undefined, PackageManager.BREW); + return; + } + + const $ = getPty(); + await $.spawnSafe('gem uninstall fastlane --all --executables', { interactive: true }); + } +} + +async function assertRubyAvailable(): Promise { + const $ = getPty(); + const { status, data } = await $.spawnSafe('ruby -v'); + if (status === SpawnStatus.ERROR) { + throw new Error( + 'fastlane requires Ruby 3.0 or newer, but Ruby was not found on this system. ' + + 'Install Ruby first — the rbenv resource in this plugin can install and manage Ruby versions for you.', + ); + } + + const match = data.match(/ruby (\d+)\.(\d+)\.(\d+)/); + if (!match) { + throw new Error( + 'fastlane requires Ruby 3.0 or newer. Unable to determine the installed Ruby version. ' + + 'The rbenv resource in this plugin can install and manage Ruby versions for you.', + ); + } + + const major = Number(match[1]); + if (major < 3) { + throw new Error( + `fastlane requires Ruby 3.0 or newer, but found Ruby ${match[1]}.${match[2]}.${match[3]}. ` + + 'Use the rbenv resource in this plugin to install and set a newer Ruby version.', + ); + } +} diff --git a/test/fastlane/fastlane-project.test.ts b/test/fastlane/fastlane-project.test.ts new file mode 100644 index 00000000..da53a274 --- /dev/null +++ b/test/fastlane/fastlane-project.test.ts @@ -0,0 +1,110 @@ +import { PluginTester } from '@codifycli/plugin-test'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const TEST_DIR = path.join(os.tmpdir(), 'codify-fastlane-project-test'); +const FASTLANE_DIR = path.join(TEST_DIR, 'fastlane'); +const APPFILE_PATH = path.join(FASTLANE_DIR, 'Appfile'); +const FASTFILE_PATH = path.join(FASTLANE_DIR, 'Fastfile'); + +describe('fastlane-project resource integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + beforeAll(async () => { + await fs.mkdir(TEST_DIR, { recursive: true }); + }); + + afterAll(async () => { + await fs.rm(TEST_DIR, { recursive: true, force: true }); + }); + + it('Can initialize a project with an Appfile and Fastfile', { timeout: 120_000 }, async () => { + const initialFastfile = [ + 'default_platform(:ios)', + '', + 'platform :ios do', + ' lane :test do', + ' end', + 'end', + '', + ].join('\n'); + + await PluginTester.fullTest( + pluginPath, + [{ + type: 'fastlane-project', + directory: TEST_DIR, + appIdentifier: 'com.company.myapp', + teamId: 'ABCDE12345', + fastfile: initialFastfile, + }], + { + validateApply: async () => { + const appfileContent = await fs.readFile(APPFILE_PATH, 'utf8'); + expect(appfileContent).toContain('app_identifier("com.company.myapp")'); + expect(appfileContent).toContain('team_id("ABCDE12345")'); + + const fastfileContent = await fs.readFile(FASTFILE_PATH, 'utf8'); + expect(fastfileContent).toBe(initialFastfile); + }, + testModify: { + modifiedConfigs: [{ + type: 'fastlane-project', + directory: TEST_DIR, + appIdentifier: 'com.company.myapp', + teamId: 'ZYXWV98765', + fastfile: initialFastfile, + }], + validateModify: async () => { + const appfileContent = await fs.readFile(APPFILE_PATH, 'utf8'); + expect(appfileContent).toContain('team_id("ZYXWV98765")'); + }, + }, + validateDestroy: async () => { + const exists = await fs.access(FASTLANE_DIR).then(() => true).catch(() => false); + expect(exists).toBe(false); + }, + }, + ); + }); + + it('Can manage a Fastfile lane for Android Play Store deployment', { timeout: 120_000 }, async () => { + const fastfile = [ + 'default_platform(:android)', + '', + 'platform :android do', + ' lane :deploy do', + ' gradle(task: "bundle", build_type: "Release")', + ' upload_to_play_store(track: "production")', + ' end', + 'end', + '', + ].join('\n'); + + await PluginTester.fullTest( + pluginPath, + [{ + type: 'fastlane-project', + directory: TEST_DIR, + appIdentifier: 'com.company.myandroidapp', + jsonKeyFile: '~/play-store-key.json', + fastfile, + }], + { + validateApply: async () => { + const appfileContent = await fs.readFile(APPFILE_PATH, 'utf8'); + expect(appfileContent).toContain('json_key_file("~/play-store-key.json")'); + + const fastfileContent = await fs.readFile(FASTFILE_PATH, 'utf8'); + expect(fastfileContent).toContain('upload_to_play_store(track: "production")'); + }, + validateDestroy: async () => { + const exists = await fs.access(FASTLANE_DIR).then(() => true).catch(() => false); + expect(exists).toBe(false); + }, + }, + ); + }); +}); diff --git a/test/fastlane/fastlane.test.ts b/test/fastlane/fastlane.test.ts new file mode 100644 index 00000000..0267112e --- /dev/null +++ b/test/fastlane/fastlane.test.ts @@ -0,0 +1,35 @@ +import { SpawnStatus } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import * as path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +describe('fastlane resource integration tests', () => { + const pluginPath = path.resolve('./src/index.ts'); + + it('Installs Ruby as a prerequisite', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { type: 'rbenv', rubyVersions: ['3.3.0'], global: '3.3.0' }, + ], { + skipUninstall: true, + validateApply: async () => { + const { data } = await testSpawn('ruby -v'); + expect(data).toContain('3.3.0'); + }, + }); + }); + + it('Installs fastlane', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { type: 'fastlane' }, + ], { + validateApply: async () => { + const result = await testSpawn('fastlane --version'); + expect(result.status).toBe(SpawnStatus.SUCCESS); + }, + validateDestroy: async () => { + const result = await testSpawn('fastlane --version'); + expect(result.status).toBe(SpawnStatus.ERROR); + }, + }); + }); +});