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

Deploy option to enable continuous deployment #1745

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
76 changes: 44 additions & 32 deletions src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ class Deployer {
}

private async cloudBuild(deployTarget: DeployTargetInfo) {
if (deployTarget.create) return false; // TODO
if (deployTarget.create) {
throw Error("Incorrect deployTarget state");
tophtucker marked this conversation as resolved.
Show resolved Hide resolved
}
const {deployPollInterval: pollInterval = DEPLOY_POLL_INTERVAL_MS} = this.deployOptions;
await this.apiClient.postProjectBuild(deployTarget.project.id);
const spinner = this.effects.clack.spinner();
Expand All @@ -225,20 +227,25 @@ class Deployer {
deployTarget.workspace.login
}/${deployTarget.project.slug}/deploys/${latestCreatedDeployId}`
);
return latestCreatedDeployId;
// latestCreatedDeployId is initially null for a new project, but once
// it changes to a string it can never change back; since we know it has
// changed, we assert here that it’s not null
return latestCreatedDeployId!;
}
await new Promise((resolve) => setTimeout(resolve, pollInterval));
}
}

private async maybeLinkGitHub(deployTarget: DeployTargetInfo): Promise<boolean> {
if (!this.effects.isTty || deployTarget.create) return false;
if (deployTarget.create) {
throw Error("Incorrect deployTarget state");
tophtucker marked this conversation as resolved.
Show resolved Hide resolved
}
if (!this.effects.isTty) return false;
Copy link
Contributor Author

@tophtucker tophtucker Oct 16, 2024

Choose a reason for hiding this comment

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

this should move down to the "else" block below. we only need to check tty when we're depending on user interaction. if the build_environment_id and source are already set up just fine, then we should return true even if it's not an interactive terminal.

(fil notes: there's also process.stdout.isTTY, which says if we can write color codes and such.)

if (deployTarget.environment.build_environment_id && deployTarget.environment.source) {
// can do cloud build
return true;
} else {
// TODO Where should it look for .git? only supports projects at root rn…
// const isGit = existsSync(this.deployOptions.config.root + "/.git");
// We only support cloud builds from the root directory so this ignores this.deployOptions.config.root
const isGit = existsSync(".git");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

todo: log cwd when running yarn deploy. you can run yarn deploy from a child directory like src, but i think it still runs in the context of the root directory, in which case this is correct.

if (isGit) {
const remotes = (await promisify(exec)("git remote -v", {cwd: this.deployOptions.config.root})).stdout
Copy link
Contributor Author

@tophtucker tophtucker Oct 16, 2024

Choose a reason for hiding this comment

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

note that in loader.ts we make our promises "by hand" (and with spawn instead of exec), but in create.ts we already use promisify, so… seems fine!

Expand All @@ -264,7 +271,9 @@ class Deployer {
} else {
// repo not auth’ed; link to auth page and poll for auth
this.effects.clack.log.info(
`Authorize Observable to access the ${bold(repoName)} repository: ${link("https://github.com/apps/observable-data-apps-dev/installations/select_target")}`
`Authorize Observable to access the ${bold(repoName)} repository: ${link(
"https://github.com/apps/observable-data-apps-dev/installations/select_target"
)}`
);
const spinner = this.effects.clack.spinner();
spinner.start("Waiting for repository to be authorized");
Expand Down Expand Up @@ -292,23 +301,19 @@ class Deployer {
}
}
} else {
throw new CliError("No GitHub remote"); // TODO better error
this.effects.clack.log.error("No GitHub remote found");
}
} else {
throw new CliError("Not at root of a git repository"); // TODO better error
this.effects.clack.log.error("Not at root of a git repository");
}
}
return false;
}

private async startNewDeploy(): Promise<GetDeployResponse> {
const deployConfig = await this.getUpdatedDeployConfig();
const deployTarget = await this.getDeployTarget(deployConfig);
const deployConfig2 = await this.getUpdatedDeployConfig(); // TODO inelegant… move cd prompt to getUpdatedDeployConfig?
let deployId;
if (deployConfig2.continuousDeployment) {
// TODO move maybeLinkGitHub so that continuous deployment is only enabled if it succeeds
await this.maybeLinkGitHub(deployTarget);
const {deployConfig, deployTarget} = await this.getDeployTarget(await this.getUpdatedDeployConfig());
let deployId: string | null;
if (deployConfig.continuousDeployment) {
deployId = await this.cloudBuild(deployTarget);
} else {
const buildFilePaths = await this.getBuildFilePaths();
Expand Down Expand Up @@ -360,8 +365,6 @@ class Deployer {
);
}

// TODO validate continuousDeployment

if (deployConfig.projectId && (!deployConfig.projectSlug || !deployConfig.workspaceLogin)) {
const spinner = this.effects.clack.spinner();
this.effects.clack.log.warn("The `projectSlug` or `workspaceLogin` is missing from your deploy.json.");
Expand Down Expand Up @@ -394,7 +397,9 @@ class Deployer {
}

// Get the deploy target, prompting the user as needed.
private async getDeployTarget(deployConfig: DeployConfig): Promise<DeployTargetInfo> {
private async getDeployTarget(
deployConfig: DeployConfig
): Promise<{deployTarget: DeployTargetInfo; deployConfig: DeployConfig}> {
let deployTarget: DeployTargetInfo;
if (deployConfig.workspaceLogin && deployConfig.projectSlug) {
try {
Expand Down Expand Up @@ -481,11 +486,11 @@ class Deployer {
workspaceId: deployTarget.workspace.id,
accessLevel: deployTarget.accessLevel
});
// TODO(toph): initial env config
deployTarget = {
create: false,
workspace: deployTarget.workspace,
project,
// TODO: In the future we may have a default environment
environment: {
automatic_builds_enabled: null,
build_environment_id: null,
Expand Down Expand Up @@ -515,33 +520,40 @@ class Deployer {
}
}

let continuousDeployment = deployConfig.continuousDeployment;
let {continuousDeployment} = deployConfig;
if (continuousDeployment === null) {
continuousDeployment = !!(await this.effects.clack.confirm({
const enable = await this.effects.clack.confirm({
message: wrapAnsi(
`Do you want to enable continuous deployment? ${faint(
"This builds in the cloud instead of on this machine and redeploys whenever you push to this repository."
"This builds in the cloud and redeploys whenever you push to this repository."
)}`,
this.effects.outputColumns
),
active: "Yes",
inactive: "No"
}));
active: "Yes, enable and build in cloud",
inactive: "No, build locally"
});
if (this.effects.clack.isCancel(enable)) throw new CliError("User canceled deploy", {print: false, exitCode: 0});
continuousDeployment = enable;
}

// Disables continuous deployment if there’s no env/source & we can’t link GitHub
if (continuousDeployment) continuousDeployment = await this.maybeLinkGitHub(deployTarget);
Comment on lines +543 to +544
Copy link
Contributor Author

Choose a reason for hiding this comment

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

fil says: if you enable continuous deployment, it should stay on, and if we can't connect to github for whatever reason (you're not in a repo, or no git remote, or no link in our database), the deploy should just fail.


const newDeployConfig = {
projectId: deployTarget.project.id,
projectSlug: deployTarget.project.slug,
workspaceLogin: deployTarget.workspace.login,
continuousDeployment
};

await this.effects.setDeployConfig(
this.deployOptions.config.root,
this.deployOptions.deployConfigPath,
{
projectId: deployTarget.project.id,
projectSlug: deployTarget.project.slug,
workspaceLogin: deployTarget.workspace.login,
continuousDeployment
},
newDeployConfig,
this.effects
);

return deployTarget;
return {deployConfig: newDeployConfig, deployTarget};
}

// Create the new deploy on the server.
Expand Down
Loading