Compare commits

...
3 changed files with 133 additions and 18 deletions

No files matched your search

+2
View File
@@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- [build-tools] Add a `copy_before_upload` option to `eas/upload_artifact` to copy changing files before they are archived. ([#4164](https://github.com/expo/eas-cli/pull/4164) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
### 🧹 Chores
@@ -5,12 +5,14 @@ import path from 'path';
import { createGlobalContextMock } from '../../../__tests__/utils/context';
import { createTestIosJob } from '../../../__tests__/utils/job';
import { createMockLogger } from '../../../__tests__/utils/logger';
import { BuildContext } from '../../../context';
import { ArtifactToUpload, BuildContext } from '../../../context';
import { CustomBuildContext } from '../../../customBuildContext';
import { createUploadArtifactBuildFunction } from '../uploadArtifact';
describe(createUploadArtifactBuildFunction, () => {
const contextUploadArtifact = jest.fn(async () => ({ artifactId: randomUUID() }));
const contextUploadArtifact = jest.fn(async (_spec: { artifact: ArtifactToUpload }) => ({
artifactId: randomUUID(),
}));
const ctx = new BuildContext(createTestIosJob({}), {
env: {
__API_SERVER_URL: 'http://api.expo.test',
@@ -99,6 +101,43 @@ describe(createUploadArtifactBuildFunction, () => {
}
});
it('copies artifacts to a temporary directory before uploading when requested', async () => {
const globalContext = createGlobalContextMock({});
const sourceDirectory = path.join(globalContext.defaultWorkingDirectory, 'live-logs');
const sourceFile = path.join(sourceDirectory, 'emulator.log');
let snapshotPath: string | undefined;
try {
await fs.promises.mkdir(sourceDirectory, { recursive: true });
await fs.promises.writeFile(sourceFile, 'log contents');
contextUploadArtifact.mockImplementationOnce(async ({ artifact }) => {
const [currentSnapshotPath] = artifact.paths;
snapshotPath = currentSnapshotPath;
expect(currentSnapshotPath).not.toBe(sourceDirectory);
await expect(
fs.promises.readFile(path.join(currentSnapshotPath, 'emulator.log'), 'utf-8')
).resolves.toBe('log contents');
return { artifactId: randomUUID() };
});
const buildStep = uploadArtifact.createBuildStepFromFunctionCall(globalContext, {
callInputs: {
copy_before_upload: true,
path: sourceDirectory,
type: 'other',
},
});
await buildStep.executeAsync();
expect(snapshotPath).toBeDefined();
await expect(fs.promises.access(snapshotPath!)).rejects.toThrow();
await expect(fs.promises.readFile(sourceFile, 'utf-8')).resolves.toBe('log contents');
} finally {
await fs.promises.rm(sourceDirectory, { force: true, recursive: true });
}
});
it('does not throw for undefined type input', async () => {
const buildStep = uploadArtifact.createBuildStepFromFunctionCall(createGlobalContextMock({}), {
callInputs: {
@@ -11,6 +11,9 @@ import {
BuildStepOutput,
} from '@expo/steps';
import assert from 'assert';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { CustomBuildContext } from '../../customBuildContext';
import { FindArtifactsError, findArtifacts } from '../../utils/artifacts';
@@ -70,6 +73,12 @@ export function createUploadArtifactBuildFunction(ctx: CustomBuildContext): Buil
required: false,
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
}),
BuildStepInput.createProvider({
id: 'copy_before_upload',
required: false,
defaultValue: false,
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
}),
BuildStepInput.createProvider({
id: 'metadata',
required: false,
@@ -118,22 +127,29 @@ export function createUploadArtifactBuildFunction(ctx: CustomBuildContext): Buil
throw result.reason;
});
const artifactSpec = {
type: parseArtifactTypeInput({
platform: ctx.job.platform,
inputValue: `${inputs.type.value ?? ''}`,
}),
paths: artifactPaths,
name: (inputs.name.value || inputs.key.value) as string,
};
const artifact = isGenericArtifact(artifactSpec)
? {
...artifactSpec,
metadata: inputs.metadata.value as Record<string, unknown> | undefined,
}
: artifactSpec;
const artifactType = parseArtifactTypeInput({
platform: ctx.job.platform,
inputValue: `${inputs.type.value ?? ''}`,
});
let artifactSnapshot: ArtifactSnapshot | null = null;
try {
if (inputs.copy_before_upload.value && artifactPaths.length > 0) {
artifactSnapshot = await copyArtifactsToTemporaryDirectoryAsync(artifactPaths);
logger.info('Copied artifacts to a temporary directory before upload.');
}
const artifactSpec = {
type: artifactType,
paths: artifactSnapshot?.paths ?? artifactPaths,
name: (inputs.name.value || inputs.key.value) as string,
};
const artifact = isGenericArtifact(artifactSpec)
? {
...artifactSpec,
metadata: inputs.metadata.value as Record<string, unknown> | undefined,
}
: artifactSpec;
const { artifactId } = await ctx.runtimeApi.uploadArtifact({ artifact, logger });
if (artifactId) {
@@ -141,17 +157,75 @@ export function createUploadArtifactBuildFunction(ctx: CustomBuildContext): Buil
}
} catch (error) {
if (inputs.ignore_error.value) {
logger.error({ err: error }, `Failed to upload ${artifact.type}. Ignoring error.`);
logger.error({ err: error }, `Failed to upload ${artifactType}. Ignoring error.`);
// Ignoring error.
return;
}
throw error;
} finally {
if (artifactSnapshot) {
await fs.promises.rm(artifactSnapshot.directory, { force: true, recursive: true });
}
}
},
});
}
interface ArtifactSnapshot {
directory: string;
paths: string[];
}
async function copyArtifactsToTemporaryDirectoryAsync(
artifactPaths: string[]
): Promise<ArtifactSnapshot> {
const snapshotDirectory = await fs.promises.mkdtemp(
path.join(os.tmpdir(), 'eas-upload-artifact-')
);
try {
const commonParentDirectory = getCommonParentDirectory(artifactPaths);
const snapshotPaths = await Promise.all(
artifactPaths.map(async artifactPath => {
const snapshotPath = path.join(
snapshotDirectory,
path.relative(commonParentDirectory, artifactPath)
);
await fs.promises.mkdir(path.dirname(snapshotPath), { recursive: true });
await fs.promises.cp(artifactPath, snapshotPath, {
errorOnExist: true,
force: false,
recursive: true,
});
return snapshotPath;
})
);
return { directory: snapshotDirectory, paths: snapshotPaths };
} catch (error) {
await fs.promises.rm(snapshotDirectory, { force: true, recursive: true });
throw error;
}
}
function getCommonParentDirectory(artifactPaths: string[]): string {
let commonParentDirectory = path.dirname(path.resolve(artifactPaths[0]));
for (const artifactPath of artifactPaths.slice(1)) {
const resolvedArtifactPath = path.resolve(artifactPath);
while (
resolvedArtifactPath !== commonParentDirectory &&
!resolvedArtifactPath.startsWith(`${commonParentDirectory}${path.sep}`)
) {
const parentDirectory = path.dirname(commonParentDirectory);
if (parentDirectory === commonParentDirectory) {
break;
}
commonParentDirectory = parentDirectory;
}
}
return commonParentDirectory;
}
/**
* Initially, upload_artifact supported application-archive and build-artifact.
* Then, mistakenly, support for it was removed in favor of supporting ManagedArtifactType