Reputation: 2489
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:
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?
Upvotes: 2
Views: 836
Reputation: 1
I found this answer somewhere else (forgot the source so I cannot credit):
Your .clasp.json looks something like this:
{
"scriptId": "xxxxxxx2D1OrbSWQMvBpAcL59_n5Cr3StgqEsZycL9cuYc7wQ5BQrTOd",
"rootDir": "/Users/yourname/VSCode/myshinyapp"
}
In order to have multiple "environments" you make a new Appsscript Project as the same user and then copy the new id into .clasp.json. While .clasp.json only allows ONE scriptId, it does allow any other property.
In my case I have it like this:
{
"scriptId": "xxxxxxxxxxxxBCKzFzEzxlVKqrTmfwLCsSc9gc88yY1NW9p4PHQWb5wZ",
"rootDir": "/Users/yourname/VSCode/myshinyapp",
"devId": "xxxxxxxxxxxxBCKzFzEzxlVKqrTmfwLCsSc9gc88yY1NW9p4PHQWb5wZ",
"prodId": "xxxxxxxxxxxxxxxxxP5ZFMZvi3QnnzFOxElJcsMehJ4_QeMumzHAsll4"
}
Then in order to direct clasp, I just copy/paste the needed ID from below to scriptId
. Dirt simple, but it gets the job done.
Basically devId
and prodId
is just for me to manually copy from.
Since Prod is only deployed to on rare occassions, my workflow is as follows:
prodId
to scriptId
Upvotes: 0
Reputation: 10375
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);
Upvotes: 3