Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ export function parseArgs(args: string[]) {
"limit",
"config",
"entrypoint",
"org",
],
collect: ["grep", "include", "exclude"],
default: {
static: true,
limit: "100",
config: Deno.env.get("DEPLOYCTL_CONFIG_FILE"),
token: Deno.env.get("DENO_DEPLOY_TOKEN"),
org: Deno.env.get("DEPLOYCTL_ORGANIZATION"),
},
});
return parsed;
Expand Down
11 changes: 8 additions & 3 deletions src/config_inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { API, APIError } from "./utils/api.ts";
import TokenProvisioner from "./utils/access_token.ts";
import { wait } from "./utils/spinner.ts";
import { error } from "./error.ts";
import organization from "./utils/organization.ts";

const NONAMES = ["src", "lib", "code", "dist", "build", "shared", "public"];

Expand All @@ -24,7 +25,7 @@ interface InferredArgs {
* - Otherwise, use the directory name from where DeployCTL is being executed,
* unless the name is useless like "src" or "dist".
*/
async function inferProject(api: API, dryRun: boolean) {
async function inferProject(api: API, dryRun: boolean, orgName?: string) {
wait("").start().warn(
"No project name or ID provided with either the --project arg or a config file.",
);
Expand All @@ -41,6 +42,9 @@ async function inferProject(api: API, dryRun: boolean) {
);
return projectName;
}
const org = orgName
? await organization.getByNameOrCreate(api, orgName)
: null;
for (;;) {
let spinner;
if (projectName) {
Expand All @@ -51,7 +55,7 @@ async function inferProject(api: API, dryRun: boolean) {
spinner = wait("Creating new project with a random name...").start();
}
try {
const project = await api.createProject(projectName);
const project = await api.createProject(projectName, org?.id);
if (projectName) {
spinner.succeed(
`Guessed project name '${project.name}'.`,
Expand Down Expand Up @@ -203,6 +207,7 @@ export default async function inferConfig(
help?: boolean;
version?: boolean;
"dry-run"?: boolean;
org?: string;
},
) {
if (args.help || args.version) {
Expand All @@ -212,7 +217,7 @@ export default async function inferConfig(
? API.fromToken(args.token)
: API.withTokenProvisioner(TokenProvisioner);
if (args.project === undefined) {
args.project = await inferProject(api, !!args["dry-run"]);
args.project = await inferProject(api, !!args["dry-run"], args.org);
}

if (args.entrypoint === undefined) {
Expand Down
26 changes: 23 additions & 3 deletions src/subcommands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { parseEntrypoint } from "../utils/entrypoint.ts";
import { walk } from "../utils/walk.ts";
import TokenProvisioner from "../utils/access_token.ts";
import { Args as RawArgs } from "../args.ts";
import organization from "../utils/organization.ts";

const help = `deployctl deploy
Deploy a script with static files to Deno Deploy.
Expand Down Expand Up @@ -68,7 +69,8 @@ OPTIONS:
--import-map=<PATH> Path to the import map file to use.
-h, --help Prints this help information
--prod Create a production deployment (default is preview deployment except the first deployment)
-p, --project=<NAME|ID> The project to deploy to
-p, --project=<NAME|ID> The project in which to deploy. If it does not exist yet, it will be created (see --org).
--org=<ORG> The organization in which to create the project. Defaults to the user's personal organization
--entrypoint=<PATH|URL> The file that Deno Deploy will run. Also available as positional argument, which takes precedence
--token=<TOKEN> The API token to use (defaults to DENO_DEPLOY_TOKEN env var)
--dry-run Dry run the deployment process.
Expand All @@ -84,6 +86,7 @@ export interface Args {
include: string[];
token: string | null;
project: string | null;
org?: string;
entrypoint: string | null;
importMap: string | null;
dryRun: boolean;
Expand All @@ -101,6 +104,7 @@ export default async function (rawArgs: RawArgs): Promise<void> {
prod: !!rawArgs.prod,
token: rawArgs.token ? String(rawArgs.token) : null,
project: rawArgs.project ? String(rawArgs.project) : null,
org: rawArgs.org,
entrypoint: positionalEntrypoint !== null
? positionalEntrypoint
: rawArgs["entrypoint"]
Expand Down Expand Up @@ -142,6 +146,7 @@ export default async function (rawArgs: RawArgs): Promise<void> {
prod: args.prod,
token: args.token,
project: args.project,
org: args.org,
include: args.include,
exclude: args.exclude,
dryRun: args.dryRun,
Expand All @@ -161,6 +166,7 @@ interface DeployOpts {
include: string[];
token: string | null;
project: string;
org?: string;
dryRun: boolean;
config: string | null;
saveConfig: boolean;
Expand All @@ -180,12 +186,15 @@ async function deploy(opts: DeployOpts): Promise<void> {
let projectIsEmpty = false;
let project = await api.getProject(opts.project);
if (project === null) {
const org = opts.org
? await organization.getByNameOrCreate(api, opts.org)
: null;
projectInfoSpinner.stop();
const projectCreationSpinner = wait(
`Project '${opts.project}' not found in any of the user's organizations. Creating...`,
`Project '${opts.project}' not found. Creating...`,
).start();
try {
project = await api.createProject(opts.project);
project = await api.createProject(opts.project, org?.id);
} catch (e) {
error(e.message);
}
Expand All @@ -195,6 +204,17 @@ async function deploy(opts: DeployOpts): Promise<void> {
);
projectIsEmpty = true;
} else {
if (opts.org && project.organization.name === null) {
projectInfoSpinner.fail(
`The project is in your personal organization and you requested the org '${opts.org}' in the args`,
);
Deno.exit(1);
} else if (opts.org && project.organization.name !== opts.org) {
projectInfoSpinner.fail(
`The project is in the organization '${project.organization.name}' and you requested the org '${opts.org}' in the args`,
);
Deno.exit(1);
}
const deploymentsListing = await api.getDeployments(project.id);
if (deploymentsListing === null) {
projectInfoSpinner.fail("Project deployments details not found.");
Expand Down
20 changes: 20 additions & 0 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
LogQueryRequestParams,
ManifestEntry,
Metadata,
Organization,
PersistedLog,
Project,
PushDeploymentRequest,
Expand Down Expand Up @@ -154,6 +155,25 @@ export class API {
}
}

async getOrganizationByName(name: string): Promise<Organization | undefined> {
const organizations: Organization[] = await this.#requestJson(
`/organizations`,
);
for (const org of organizations) {
if (org.name === name) {
return org;
}
}
}

async createOrganization(name: string): Promise<Organization> {
const body = { name };
return await this.#requestJson(
`/organizations`,
{ method: "POST", body },
);
}

async getProject(id: string): Promise<Project | null> {
try {
return await this.#requestJson(`/projects/${id}`);
Expand Down
13 changes: 13 additions & 0 deletions src/utils/api_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,24 @@ export interface Project {
productionDeployment?: Deployment | null;
hasProductionDeployment: boolean;
organizationId: string;
organization: Organization;
createdAt: string;
updatedAt: string;
envVars: string[];
}

export type Organization = UserOrganization | NormalOrganization;

export interface UserOrganization {
id: string;
name: null;
}

export interface NormalOrganization {
id: string;
name: string;
}

export interface DeploymentsSummary {
page: number;
count: number;
Expand Down
34 changes: 34 additions & 0 deletions src/utils/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { error } from "../error.ts";
import { API } from "./api.ts";
import { Organization } from "./api_types.ts";
import { interruptSpinner, wait } from "./spinner.ts";

export default {
getByNameOrCreate: async (
api: API,
name: string,
): Promise<Organization> => {
const interruptedSpinner = interruptSpinner();
let org;
try {
let spinner = wait(
`You have specified the organization ${name}. Fetching details...`,
).start();
org = await api.getOrganizationByName(name);
if (!org) {
spinner.stop();
spinner = wait(
`Organization '${name}' not found. Creating...`,
).start();
org = await api.createOrganization(name);
spinner.succeed(`Created new organization '${org!.name}'.`);
} else {
spinner.stop();
}
} catch (e) {
error(e.message);
}
interruptedSpinner.resume();
return org;
},
};