Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #9065: uncaught error "ENOENT: no such file or directory" #9097

Merged
merged 3 commits into from Mar 11, 2022

Conversation

jitbasemartin
Copy link
Contributor

Description

Fixes #9065: uncaught error "ENOENT: no such file or directory" when an error occurs during the stack up

Checklist

  • I have added tests that prove my fix is effective or that my feature works
    Suggestion of where to write it ?
  • I have updated the CHANGELOG-PENDING file with my change
  • Yes, there are changes in this PR that warrants bumping the Pulumi Service API version

@github-actions
Copy link

github-actions bot commented Mar 3, 2022

PR is now waiting for a maintainer to take action.

Note for the maintainer: Commands available:

  • /run-acceptance-tests - used to test run the acceptance tests for the project
  • /run-codegen - used to test the Pull Request against downstream codegen
  • /run-docs-gen - used to test the Pull Request against documentation generation
  • /run-containers - used to test the Pull Request against the containers tests

Copy link
Member

@Frassle Frassle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable

sdk/nodejs/automation/stack.ts Outdated Show resolved Hide resolved
@github-actions
Copy link

github-actions bot commented Mar 3, 2022

PR is now waiting for a maintainer to take action.

Note for the maintainer: Commands available:

  • /run-acceptance-tests - used to test run the acceptance tests for the project
  • /run-codegen - used to test the Pull Request against downstream codegen
  • /run-docs-gen - used to test the Pull Request against documentation generation
  • /run-containers - used to test the Pull Request against the containers tests

@jitbasemartin
Copy link
Contributor Author

@Frassle Do you need something else to merge this PR ?

@Frassle
Copy link
Member

Frassle commented Mar 9, 2022

No sorry just slipped my attention, I'm on PTO tomorrow but I'll pick this up on Friday 👍

@Frassle Frassle self-assigned this Mar 9, 2022
@Frassle Frassle merged commit b698ed3 into pulumi:master Mar 11, 2022
@hcharley
Copy link

I just experienced this bug. Any word on when this will be released?

@hcharley
Copy link

hcharley commented Mar 23, 2022

For now I'll try and patch the JS file by hand. I could probably figure out how to install a pulumi prerelease if I hadn't started doing this already:

/workspace/node_modules/@pulumi/pulumi/automation/stack.js

"use strict";
// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const os = require("os");
const path = require("path");
const readline = require("readline");
const upath = require("upath");
const grpc = require("@grpc/grpc-js");
const TailFile = require("@logdna/tail-file");
const log = require("../log");
const cmd_1 = require("./cmd");
const errors_1 = require("./errors");
const server_1 = require("./server");
const langrpc = require("../proto/language_grpc_pb.js");
/**
 * Stack is an isolated, independently configurable instance of a Pulumi program.
 * Stack exposes methods for the full pulumi lifecycle (up/preview/refresh/destroy), as well as managing configuration.
 * Multiple Stacks are commonly used to denote different phases of development
 * (such as development, staging and production) or feature branches (such as feature-x-dev, jane-feature-x-dev).
 *
 * @alpha
 */
class Stack {
    constructor(name, workspace, mode) {
        this.name = name;
        this.workspace = workspace;
        switch (mode) {
            case "create":
                this.ready = workspace.createStack(name);
                return this;
            case "select":
                this.ready = workspace.selectStack(name);
                return this;
            case "createOrSelect":
                this.ready = workspace.createStack(name).catch((err) => {
                    if (err instanceof errors_1.StackAlreadyExistsError) {
                        return workspace.selectStack(name);
                    }
                    throw err;
                });
                return this;
            default:
                throw new Error(`unexpected Stack creation mode: ${mode}`);
        }
    }
    /**
     * Creates a new stack using the given workspace, and stack name.
     * It fails if a stack with that name already exists
     *
     * @param name The name identifying the Stack.
     * @param workspace The Workspace the Stack was created from.
     */
    static create(name, workspace) {
        return __awaiter(this, void 0, void 0, function* () {
            const stack = new Stack(name, workspace, "create");
            yield stack.ready;
            return stack;
        });
    }
    /**
     * Selects stack using the given workspace, and stack name.
     * It returns an error if the given Stack does not exist.
     *
     * @param name The name identifying the Stack.
     * @param workspace The Workspace the Stack was created from.
     */
    static select(name, workspace) {
        return __awaiter(this, void 0, void 0, function* () {
            const stack = new Stack(name, workspace, "select");
            yield stack.ready;
            return stack;
        });
    }
    /**
     * Tries to create a new stack using the given workspace and
     * stack name if the stack does not already exist,
     * or falls back to selecting the existing stack. If the stack does not exist,
     * it will be created and selected.
     *
     * @param name The name identifying the Stack.
     * @param workspace The Workspace the Stack was created from.
     */
    static createOrSelect(name, workspace) {
        return __awaiter(this, void 0, void 0, function* () {
            const stack = new Stack(name, workspace, "createOrSelect");
            yield stack.ready;
            return stack;
        });
    }
    readLines(logPath, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            const eventLogTail = new TailFile(logPath, { startPos: 0, pollFileIntervalMs: 200 })
                .on("tail_error", (err) => {
                throw err;
            });
            yield eventLogTail.start();
            const lineSplitter = readline.createInterface({ input: eventLogTail });
            lineSplitter.on("line", (line) => {
                let event;
                try {
                    event = JSON.parse(line);
                    callback(event);
                }
                catch (e) {
                    log.warn(`Failed to parse engine event
If you're seeing this warning, please comment on https://github.com/pulumi/pulumi/issues/6768 with the event and any
details about your environment.

Event: ${line}\n${e.toString()}`);
                }
            });
            return {
                tail: eventLogTail,
                rl: lineSplitter,
            };
        });
    }
    /**
     * Creates or updates the resources in a stack by executing the program in the Workspace.
     * https://www.pulumi.com/docs/reference/cli/pulumi_up/
     *
     * @param opts Options to customize the behavior of the update.
     */
    up(opts) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const args = ["up", "--yes", "--skip-preview"];
            let kind = execKind.local;
            let program = this.workspace.program;
            if (opts) {
                if (opts.program) {
                    program = opts.program;
                }
                if (opts.message) {
                    args.push("--message", opts.message);
                }
                if (opts.expectNoChanges) {
                    args.push("--expect-no-changes");
                }
                if (opts.diff) {
                    args.push("--diff");
                }
                if (opts.replace) {
                    for (const rURN of opts.replace) {
                        args.push("--replace", rURN);
                    }
                }
                if (opts.target) {
                    for (const tURN of opts.target) {
                        args.push("--target", tURN);
                    }
                }
                if (opts.targetDependents) {
                    args.push("--target-dependents");
                }
                if (opts.parallel) {
                    args.push("--parallel", opts.parallel.toString());
                }
                if (opts.userAgent) {
                    args.push("--exec-agent", opts.userAgent);
                }
                if (opts.color) {
                    args.push("--color", opts.color);
                }
            }
            let onExit = (hasError) => { return; };
            let didError = false;
            if (program) {
                kind = execKind.inline;
                const server = new grpc.Server({
                    "grpc.max_receive_message_length": server_1.maxRPCMessageSize,
                });
                const languageServer = new server_1.LanguageServer(program);
                server.addService(langrpc.LanguageRuntimeService, languageServer);
                const port = yield new Promise((resolve, reject) => {
                    server.bindAsync(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure(), (err, p) => {
                        if (err) {
                            reject(err);
                        }
                        else {
                            resolve(p);
                        }
                    });
                });
                server.start();
                onExit = (hasError) => {
                    languageServer.onPulumiExit(hasError);
                    server.forceShutdown();
                };
                args.push(`--client=127.0.0.1:${port}`);
            }
            args.push("--exec-kind", kind);
            let logPromise;
            let logFile;
            // Set up event log tailing
            if ((_a = opts) === null || _a === void 0 ? void 0 : _a.onEvent) {
                const onEvent = opts.onEvent;
                logFile = createLogFile("up");
                args.push("--event-log", logFile);
                logPromise = this.readLines(logFile, (event) => {
                    onEvent(event);
                });
            }
            const upPromise = this.runPulumiCmd(args, (_b = opts) === null || _b === void 0 ? void 0 : _b.onOutput);
            let upResult;
            try {
                upResult = yield upPromise
            }
            catch (e) {
                didError = true;
                throw e;
            }
            finally {
                onExit(didError);
                yield cleanUp(logFile, yield logPromise);
            }
            // TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050
            const outputs = yield this.outputs();
            const summary = yield this.info();
            return {
                stdout: upResult.stdout,
                stderr: upResult.stderr,
                summary: summary,
                outputs: outputs,
            };
        });
    }
    /**
     * Performs a dry-run update to a stack, returning pending changes.
     * https://www.pulumi.com/docs/reference/cli/pulumi_preview/
     *
     * @param opts Options to customize the behavior of the preview.
     */
    preview(opts) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const args = ["preview"];
            let kind = execKind.local;
            let program = this.workspace.program;
            if (opts) {
                if (opts.program) {
                    program = opts.program;
                }
                if (opts.message) {
                    args.push("--message", opts.message);
                }
                if (opts.expectNoChanges) {
                    args.push("--expect-no-changes");
                }
                if (opts.diff) {
                    args.push("--diff");
                }
                if (opts.replace) {
                    for (const rURN of opts.replace) {
                        args.push("--replace", rURN);
                    }
                }
                if (opts.target) {
                    for (const tURN of opts.target) {
                        args.push("--target", tURN);
                    }
                }
                if (opts.targetDependents) {
                    args.push("--target-dependents");
                }
                if (opts.parallel) {
                    args.push("--parallel", opts.parallel.toString());
                }
                if (opts.userAgent) {
                    args.push("--exec-agent", opts.userAgent);
                }
                if (opts.color) {
                    args.push("--color", opts.color);
                }
            }
            let onExit = (hasError) => { return; };
            let didError = false;
            if (program) {
                console.log('Running program....')
                kind = execKind.inline;
                const server = new grpc.Server({
                    "grpc.max_receive_message_length": server_1.maxRPCMessageSize,
                });
                const languageServer = new server_1.LanguageServer(program);
                server.addService(langrpc.LanguageRuntimeService, languageServer);
                const port = yield new Promise((resolve, reject) => {
                    server.bindAsync(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure(), (err, p) => {
                        if (err) {
                            reject(err);
                        }
                        else {
                            resolve(p);
                        }
                    });
                });
                
                server.start();
                onExit = (hasError) => {
                    languageServer.onPulumiExit(hasError);
                    server.forceShutdown();
                };
                args.push(`--client=127.0.0.1:${port}`);
            }
            args.push("--exec-kind", kind);
            // Set up event log tailing
            const logFile = createLogFile("preview");
            args.push("--event-log", logFile);
            let summaryEvent;
            const rlPromise = this.readLines(logFile, (event) => {
                var _a;
                if (event.summaryEvent) {
                    summaryEvent = event.summaryEvent;
                }
                if ((_a = opts) === null || _a === void 0 ? void 0 : _a.onEvent) {
                    const onEvent = opts.onEvent;
                    onEvent(event);
                }
            });
            const prePromise = this.runPulumiCmd(args, (_a = opts) === null || _a === void 0 ? void 0 : _a.onOutput);
            let preResult;
            let rlResult;
            try {
              // TODO: This line in the preview function also probably needs to be fixed:
                [preResult, rlResult] = yield prePromise;
            }
            catch (e) {
                didError = true;
                throw e;
            }
            finally {
                onExit(didError);
                yield cleanUp(logFile, yield rlPromise);
            }
            if (!summaryEvent) {
                log.warn("Failed to parse summary event, but preview succeeded. PreviewResult `changeSummary` will be empty.");
            }
            return {
                stdout: preResult.stdout,
                stderr: preResult.stderr,
                changeSummary: ((_b = summaryEvent) === null || _b === void 0 ? void 0 : _b.resourceChanges) || {},
            };
        });
    }
    /**
     * Compares the current stack’s resource state with the state known to exist in the actual
     * cloud provider. Any such changes are adopted into the current stack.
     *
     * @param opts Options to customize the behavior of the refresh.
     */
    refresh(opts) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const args = ["refresh", "--yes", "--skip-preview"];
            if (opts) {
                if (opts.message) {
                    args.push("--message", opts.message);
                }
                if (opts.expectNoChanges) {
                    args.push("--expect-no-changes");
                }
                if (opts.target) {
                    for (const tURN of opts.target) {
                        args.push("--target", tURN);
                    }
                }
                if (opts.parallel) {
                    args.push("--parallel", opts.parallel.toString());
                }
                if (opts.userAgent) {
                    args.push("--exec-agent", opts.userAgent);
                }
                if (opts.color) {
                    args.push("--color", opts.color);
                }
            }
            let logPromise;
            let logFile;
            // Set up event log tailing
            if ((_a = opts) === null || _a === void 0 ? void 0 : _a.onEvent) {
                const onEvent = opts.onEvent;
                logFile = createLogFile("refresh");
                args.push("--event-log", logFile);
                logPromise = this.readLines(logFile, (event) => {
                    onEvent(event);
                });
            }
            const kind = this.workspace.program ? execKind.inline : execKind.local;
            args.push("--exec-kind", kind);
            const refPromise = this.runPulumiCmd(args, (_b = opts) === null || _b === void 0 ? void 0 : _b.onOutput);
            const [refResult, logResult] = yield Promise.all([refPromise, logPromise]);
            yield cleanUp(logFile, logResult);
            const summary = yield this.info();
            return {
                stdout: refResult.stdout,
                stderr: refResult.stderr,
                summary: summary,
            };
        });
    }
    /**
     * Destroy deletes all resources in a stack, leaving all history and configuration intact.
     *
     * @param opts Options to customize the behavior of the destroy.
     */
    destroy(opts) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const args = ["destroy", "--yes", "--skip-preview"];
            if (opts) {
                if (opts.message) {
                    args.push("--message", opts.message);
                }
                if (opts.target) {
                    for (const tURN of opts.target) {
                        args.push("--target", tURN);
                    }
                }
                if (opts.targetDependents) {
                    args.push("--target-dependents");
                }
                if (opts.parallel) {
                    args.push("--parallel", opts.parallel.toString());
                }
                if (opts.userAgent) {
                    args.push("--exec-agent", opts.userAgent);
                }
                if (opts.color) {
                    args.push("--color", opts.color);
                }
            }
            let logPromise;
            let logFile;
            // Set up event log tailing
            if ((_a = opts) === null || _a === void 0 ? void 0 : _a.onEvent) {
                const onEvent = opts.onEvent;
                logFile = createLogFile("destroy");
                args.push("--event-log", logFile);
                logPromise = this.readLines(logFile, (event) => {
                    onEvent(event);
                });
            }
            const kind = this.workspace.program ? execKind.inline : execKind.local;
            args.push("--exec-kind", kind);
            const desPromise = this.runPulumiCmd(args, (_b = opts) === null || _b === void 0 ? void 0 : _b.onOutput);
            const [desResult, logResult] = yield Promise.all([desPromise, logPromise]);
            yield cleanUp(logFile, logResult);
            const summary = yield this.info();
            return {
                stdout: desResult.stdout,
                stderr: desResult.stderr,
                summary: summary,
            };
        });
    }
    /**
     * Returns the config value associated with the specified key.
     *
     * @param key The key to use for the config lookup
     */
    getConfig(key) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.getConfig(this.name, key);
        });
    }
    /**
     * Returns the full config map associated with the stack in the Workspace.
     */
    getAllConfig() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.getAllConfig(this.name);
        });
    }
    /**
     * Sets a config key-value pair on the Stack in the associated Workspace.
     *
     * @param key The key to set.
     * @param value The config value to set.
     */
    setConfig(key, value) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.setConfig(this.name, key, value);
        });
    }
    /**
     * Sets all specified config values on the stack in the associated Workspace.
     *
     * @param config The map of config key-value pairs to set.
     */
    setAllConfig(config) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.setAllConfig(this.name, config);
        });
    }
    /**
     * Removes the specified config key from the Stack in the associated Workspace.
     *
     * @param key The config key to remove.
     */
    removeConfig(key) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.removeConfig(this.name, key);
        });
    }
    /**
     * Removes the specified config keys from the Stack in the associated Workspace.
     *
     * @param keys The config keys to remove.
     */
    removeAllConfig(keys) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.removeAllConfig(this.name, keys);
        });
    }
    /**
     * Gets and sets the config map used with the last update.
     */
    refreshConfig() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.refreshConfig(this.name);
        });
    }
    /**
     * Gets the current set of Stack outputs from the last Stack.up().
     */
    outputs() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.stackOutputs(this.name);
        });
    }
    /**
     * Returns a list summarizing all previous and current results from Stack lifecycle operations
     * (up/preview/refresh/destroy).
     */
    history(pageSize, page) {
        return __awaiter(this, void 0, void 0, function* () {
            const args = ["stack", "history", "--json", "--show-secrets"];
            if (pageSize) {
                if (!page || page < 1) {
                    page = 1;
                }
                args.push("--page-size", Math.floor(pageSize).toString(), "--page", Math.floor(page).toString());
            }
            const result = yield this.runPulumiCmd(args);
            return JSON.parse(result.stdout, (key, value) => {
                if (key === "startTime" || key === "endTime") {
                    return new Date(value);
                }
                return value;
            });
        });
    }
    info() {
        return __awaiter(this, void 0, void 0, function* () {
            const history = yield this.history(1 /*pageSize*/);
            if (!history || history.length === 0) {
                return undefined;
            }
            return history[0];
        });
    }
    /**
     * Cancel stops a stack's currently running update. It returns an error if no update is currently running.
     * Note that this operation is _very dangerous_, and may leave the stack in an inconsistent state
     * if a resource operation was pending when the update was canceled.
     * This command is not supported for local backends.
     */
    cancel() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.runPulumiCmd(["cancel", "--yes"]);
        });
    }
    /**
     * exportStack exports the deployment state of the stack.
     * This can be combined with Stack.importStack to edit a stack's state (such as recovery from failed deployments).
     */
    exportStack() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.exportStack(this.name);
        });
    }
    /**
     * importStack imports the specified deployment state into a pre-existing stack.
     * This can be combined with Stack.exportStack to edit a stack's state (such as recovery from failed deployments).
     *
     * @param state the stack state to import.
     */
    importStack(state) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.workspace.importStack(this.name, state);
        });
    }
    runPulumiCmd(args, onOutput) {
        return __awaiter(this, void 0, void 0, function* () {
            let envs = {
                "PULUMI_DEBUG_COMMANDS": "true",
            };
            const pulumiHome = this.workspace.pulumiHome;
            if (pulumiHome) {
                envs["PULUMI_HOME"] = pulumiHome;
            }
            envs = Object.assign(Object.assign({}, envs), this.workspace.envVars);
            const additionalArgs = yield this.workspace.serializeArgsForOp(this.name);
            args = [...args, "--stack", this.name, ...additionalArgs];
            const result = yield cmd_1.runPulumiCmd(args, this.workspace.workDir, envs, onOutput);
            yield this.workspace.postCommandCallback(this.name);
            return result;
        });
    }
}
exports.Stack = Stack;
/**
 * Returns a stack name formatted with the greatest possible specificity:
 * org/project/stack or user/project/stack
 * Using this format avoids ambiguity in stack identity guards creating or selecting the wrong stack.
 * Note that filestate backends (local file, S3, Azure Blob) do not support stack names in this
 * format, and instead only use the stack name without an org/user or project to qualify it.
 * See: https://github.com/pulumi/pulumi/issues/2522
 *
 * @param org The org (or user) that contains the Stack.
 * @param project The project that parents the Stack.
 * @param stack The name of the Stack.
 */
function fullyQualifiedStackName(org, project, stack) {
    return `${org}/${project}/${stack}`;
}
exports.fullyQualifiedStackName = fullyQualifiedStackName;
const execKind = {
    local: "auto.local",
    inline: "auto.inline",
};
const delay = (duration) => new Promise(resolve => setTimeout(resolve, duration));
const createLogFile = (command) => {
    const logDir = fs.mkdtempSync(upath.joinSafe(os.tmpdir(), `automation-logs-${command}-`));
    const logFile = upath.joinSafe(logDir, "eventlog.txt");
    // just open/close the file to make sure it exists when we start polling.
    fs.closeSync(fs.openSync(logFile, "w"));
    return logFile;
};
const cleanUp = (logFile, rl) => __awaiter(void 0, void 0, void 0, function* () {
    if (rl) {
        // stop tailing
        yield rl.tail.quit();
        // close the readline interface
        rl.rl.close();
    }
    if (logFile) {
        // remove the logfile
        if (fs.rm) {
            // remove with Node JS 15.X+
            fs.rm(path.dirname(logFile), { recursive: true }, () => { return; });
        }
        else {
            // remove with Node JS 14.X
            fs.rmdir(path.dirname(logFile), { recursive: true }, () => { return; });
        }
    }
});
//# sourceMappingURL=stack.js.map

@hcharley
Copy link

hcharley commented Mar 23, 2022

FYI, I think the preview function probably needs to be fixed as well:

            const prePromise = this.runPulumiCmd(args, (_a = opts) === null || _a === void 0 ? void 0 : _a.onOutput);
            let preResult;
            let rlResult;
            try {
              // TODO: This line in the preview function also probably needs to be fixed:
                [preResult, rlResult] = yield Promise.all([prePromise, rlPromise]);
            }
            catch (e) {
                didError = true;
                throw e;
            }
            finally {
                onExit(didError);
                yield cleanUp(logFile, rlResult);
            }

Screen Shot 2022-03-23 at 10 01 07 AM

EDIT: I updated the stack.js file above to try and fix for preview as well.

@mikhailshilkov
Copy link
Member

The fix from this PR should be released in 3.27.0 later today

Frassle added a commit that referenced this pull request Mar 23, 2022
Same fix as in #9097 but for the preview command.
@Frassle Frassle mentioned this pull request Mar 23, 2022
@hcharley
Copy link

Thank you @mikhailshilkov!

Frassle added a commit that referenced this pull request Mar 23, 2022
* Fix #9065 for previews

Same fix as in #9097 but for the preview command.

* Add to CHANGELOG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ENOENT: no such file or directory, stat '/var/folders/.../T/automation-logs-up-.../eventlog.txt'
4 participants