1
votes

I was wondering how to have different stages (dev. prod) when developing a Google Apps Script webapp. Afaik there is nothing out of the box, especially when you want to have one G Suite Account for dev and another for prod.

The only solution how to handle this, I came up with, is to use Clasp and switch between two GSuite Accounts with clasp logout / login

My questions are:

  • Does this work, or are there any reference to one GSuite Account which break when pushing to another account?
  • Is there a better approach?

I think it is better to explain it on an example:

I have a Google App Script project setup with clasp on domain test.mydomain.com with the user [email protected]

Now I need to push this with clasp to another project (same name) on a different domain prod.mydomain.com. This project is shared with another user where I have the credentials.

The problem is when I now login with the user in clasp I would need to create a project first right? But the project already exists. So how can I handle that?

1
Obviously you just need set up 2 different clasp projectsKos
zlZimon, could you please clarify why would using a different account for production mean? If the Web App is run under the authority of the owner, it will always run like that, and you can push from any account that has access. Otherwise it will run under the authority of user accessing, and you can still push from any account. If you don't want to exhaust prod. account quotas - you can log in locally. I am honestly curious - could you give some more details on your use case?Oleg Valter

1 Answers

2
votes

Does this work?

Yes, just make sure the account has edit access to be able to push (but I am sure you know that).

Is there a better approach?

There sort of is (unless you implicitly meant it), to quote from issue #42 of the clasp project GitHub:

New flag:

clasp login --local Saves .clasprc.json locally, but would use global clasp credentials.

clasp login # global for all projects
clasp login --creds creds.json # your own creds
clasp login --local # local project. Useful for multiple Google accounts.
clasp login --creds creds.json --local # own creds, local

So technically you can use multiple accounts, but in the end, it boils down to your login / logout technique.


Regrading discussion on switching between apps script projects with CLASP, I ended up writing a basic utility script for switching between Apps Script projects to push to (no dependencies, but if you want to manage flags in style, check out the popular Yargs package):

const fs = require('fs').promises;
const { promisify } = require("util");
const { spawn } = require("child_process");

const rl = require("readline");

const promiseAns = () => {
    const dummy = rl.createInterface({
        input: process.stdin
    });

    dummy.question[promisify.custom] = function (query) {
        return new Promise((resolve) => this.question( query, resolve));
    };

    return promisify(dummy.question);
};



/**
 * @summary asks to confirm and exits if ok
 * @param {import("readline").Interface} cons 
 * @param {string} init
 */
const checkExit = async (cons, init) =>{ 

    if ( !/exit/.test(init) ) {
        return;
    }

    const question = promiseAns();

    const ans = await question.bind(cons)(`Are you sure (Y|n)?\n`);

    if (/^Y(?:es)?/i.test(ans)) {
        process.exit();
    }
}

/**
 * @summary repeat question until matches
 * @param {import("readline").Interface} cons 
 * @param {string} query 
 * @param {(ans: string) => boolean} condition 
 * @param {(ans: string) => any} onSuccess 
 */
const askUntil = (cons, query, condition, onSuccess) => cons.question(query, async (ans) => {

    await checkExit(cons, ans);

    if (!condition(ans)) {
        return askUntil(cons, query, condition, onSuccess);
    }

    onSuccess(ans);
});

/**
 * @summary makes readline interface
 */
const makeRL = () => {

    const cons = rl.createInterface({
        input: process.stdin,
        output: process.stdout,
    });

    cons.on("line", (ln) => console.log(ln));

    return cons;
};

process.on("unhandledRejection", (err) => {
    console.log(err);
    process.exit();
});

const APP_CONFIG = {

    paths: {

        dotclasp: "./.clasp.json",

        ids: "./ids.txt"

    }

};

(async (configuration) => {

    const cons = makeRL();

    const { paths: { dotclasp, ids } } = configuration;

    const text = await fs.readFile(ids, { encoding: "utf-8" });

    const lines = text.split(/\r?\n/);

    const relevant = lines.filter((line) => /^(\w+)\s+\w+/.test(line));

    const lookup = {};

    relevant.forEach((lbl) => {
        const [label, id] = lbl.split(/\s+/);
        lookup[label] = id;
    });

    const config = require(dotclasp);

    const [label] = Object.entries(lookup).find(([, id]) => config.scriptId === id);

    cons.emit("line", `Currently selected: ${label}`);

    const { argv } = process;

    const push = argv.includes("--push");

    askUntil(cons, `Select project (${Object.keys(lookup).join(" | ")})\n`, (ans) => lookup[ans], async (ans) => {

        config.scriptId = lookup[ans];

        try {
            await fs.writeFile(dotclasp, JSON.stringify(config), { encoding: "utf-8" });
            cons.emit("line", `switched to ${ans}`);
        }
        catch {
            cons.emit("line", `failed to switch to ${ans}`);
        }

        if (!push) {
            process.exit();
        }

        const cp = spawn(`clasp push --force`, {
            stdio: "inherit",
            shell: true
        });

        cp.on("error", ({ message }) => {
            cons.write(message);
            process.exit();
        });

        cp.on("exit", () => {
            cons.emit("line", `pushed to ${ans}`);
            process.exit();
        });

    });

})(APP_CONFIG);

sample run