Skip to content

Commit

Permalink
feat(cli/update): add --summary, --report, and --prefix options (#45)
Browse files Browse the repository at this point in the history
* ci: create update.yml

* feat(cli/update): add --summary, --report, and --prefix options

* fix(git): obtain version info correctly

* refactor!: use async functions where we can
  • Loading branch information
hasundue authored Oct 22, 2023
1 parent 470f0bd commit cc4d630
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 128 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Update

on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
update:
name: Update
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure Git
run: |
git config --global user.name '${{ github.actor }}'
git config --global user.email '${{ github.actor }}@users.noreply.github.com'
- name: Run Molt
run: deno task update --commit --pre-commit test --summary title.txt --report body.txt

- name: Push changes
run: git push

- name: Create Pull Request
run: |
gh pr create --title "$(cat title.txt)" --body "$(cat body.txt)"
172 changes: 120 additions & 52 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { existsSync } from "./lib/std/fs.ts";
import { distinct } from "./lib/std/collections.ts";
import { parse as parseJsonc } from "./lib/std/jsonc.ts";
import { dirname, extname, join } from "./lib/std/path.ts";
import { colors, Command, List, Select } from "./lib/x/cliffy.ts";
import { colors, Command, Input, List, Select } from "./lib/x/cliffy.ts";
import { $ } from "./lib/x/dax.ts";
import { URI } from "./lib/uri.ts";
import { DependencyUpdate } from "./lib/update.ts";
import { FileUpdate } from "./lib/file.ts";
import { commitAll } from "./lib/git.ts";
import { GitCommitSequence } from "./lib/git.ts";

const { gray, yellow, bold } = colors;
const { gray, yellow, bold, cyan } = colors;

const checkCommand = new Command()
.description("Check for the latest version of dependencies")
Expand All @@ -21,18 +22,7 @@ async function checkAction(
...entrypoints: string[]
) {
_ensureJsFiles(entrypoints);
console.log("🔎 Checking for updates...");
const updates = await Promise.all(
entrypoints.map(async (entrypoint) =>
await DependencyUpdate.collect(entrypoint, {
importMap: options.importMap ?? await _findImportMap(entrypoint),
})
),
).then((results) => results.flat());
if (!updates.length) {
console.log("🍵 No updates found");
return;
}
const updates = await _collect(entrypoints, options);
_list(updates);
const action = await Select.prompt({
message: "Choose an action",
Expand All @@ -48,9 +38,13 @@ async function checkAction(
case "write":
return _write(updates);
case "commit": {
const prefix = await Input.prompt({
message: "Prefix for commit messages",
default: "build(deps): ",
});
const suggestions = _getTasks();
if (!suggestions.length) {
return _commit(updates);
return _commit(updates, { prefix });
}
const preCommit = await List.prompt(
{
Expand All @@ -64,7 +58,7 @@ async function checkAction(
suggestions,
},
);
return _commit(updates, { preCommit, postCommit });
return _commit(updates, { preCommit, postCommit, prefix });
}
}
}
Expand All @@ -79,6 +73,12 @@ const updateCommand = new Command()
.option("--post-commit <tasks...:string>", "Run tasks after each commit", {
depends: ["commit"],
})
.option("--prefix <prefix:string>", "Prefix for commit messages", {
depends: ["commit"],
default: "build(deps): ",
})
.option("--summary <file:string>", "Write a summary of changes to file")
.option("--report <file:string>", "Write a report of changes to file")
.arguments("<entrypoints...:string>")
.action(updateAction);

Expand All @@ -88,26 +88,38 @@ async function updateAction(
importMap?: string;
preCommit?: string[];
postCommit?: string[];
prefix: string;
summary?: string;
report?: string;
},
...entrypoints: string[]
) {
console.log("🔎 Checking for updates...");
const updates = await Promise.all(
entrypoints.map(async (entrypoint) =>
await DependencyUpdate.collect(entrypoint, {
importMap: options.importMap ?? await _findImportMap(entrypoint),
})
),
).then((results) => results.flat());
if (!updates.length) {
console.log("🍵 No updates found");
return;
}
const updates = await _collect(entrypoints, options);
_list(updates);
if (options.commit) {
return _commit(updates, options);
}
return _write(updates);
return _write(updates, options);
}

async function _collect(
entrypoints: string[],
options: { importMap?: string },
): Promise<DependencyUpdate[]> {
return await $.progress("Checking for updates").with(async () => {
const updates = await Promise.all(
entrypoints.map(async (entrypoint) =>
await DependencyUpdate.collect(entrypoint, {
importMap: options.importMap ?? await _findImportMap(entrypoint),
})
),
).then((results) => results.flat());
if (!updates.length) {
console.log("🍵 No updates found");
Deno.exit(0);
}
return updates;
});
}

async function _findImportMap(entrypoint: string): Promise<string | undefined> {
Expand Down Expand Up @@ -158,41 +170,81 @@ function _list(updates: DependencyUpdate[]) {
console.log();
}

function _write(updates: DependencyUpdate[]) {
console.log();
console.log("Writing changes...");
const results = FileUpdate.collect(updates);
FileUpdate.writeAll(results, {
onWrite: (module) => console.log(` 💾 ${URI.relative(module.specifier)}`),
async function _write(
updates: DependencyUpdate[],
options?: {
summary?: string;
report?: string;
},
) {
const results = await FileUpdate.collect(updates);
await FileUpdate.writeAll(results, {
onWrite: (module) => console.log(`💾 ${URI.relative(module.specifier)}`),
});
console.log();
if (options?.summary) {
await Deno.writeTextFile(options.summary, "Update dependencies");
console.log(`📄 ${options.summary}`);
}
if (options?.report) {
const content = distinct(
updates.map((u) => `- ${u.name} ${u.version.from} => ${u.version.to}`),
).join("\n");
await Deno.writeTextFile(options.report, content);
console.log(`📄 ${options.report}`);
}
}

function _commit(
async function _commit(
updates: DependencyUpdate[],
options?: {
preCommit?: string[];
postCommit?: string[];
prefix: string;
summary?: string;
report?: string;
},
) {
console.log("\nCommitting changes...");
commitAll(updates, {
const commits = GitCommitSequence.from(updates, {
groupBy: (dependency) => dependency.name,
preCommit: () => {
options?.preCommit?.forEach((task) => _task(task));
},
postCommit: (commit) => {
console.log(`📝 ${commit.message}`);
options?.postCommit?.forEach((task) => _task(task));
},
composeCommitMessage: ({ group, version }) =>
`${options?.prefix}bump ${group}` +
(version?.from ? ` from ${version?.from}` : "") +
(version?.to ? ` to ${version?.to}` : ""),
preCommit: options?.preCommit
? async (commit) => {
console.log(`\n📝 Commiting "${commit.message}"...`);
for (const task of options?.preCommit ?? []) {
await _task(task);
}
}
: undefined,
postCommit: options?.postCommit
? async (commit) => {
console.log(`📝 ${commit.message}`);
for (const task of options?.postCommit ?? []) {
await _task(task);
}
}
: undefined,
});
await GitCommitSequence.exec(commits);
console.log();
if (options?.summary) {
await Deno.writeTextFile(options.summary, _summary(commits));
console.log(`📄 ${options.summary}`);
}
if (options?.report) {
await Deno.writeTextFile(options.report, _report(commits));
console.log(`📄 ${options.report}`);
}
}

function _task(task: string): void {
const { code, stderr } = new Deno.Command(Deno.execPath(), {
args: ["task", task],
}).outputSync();
if (code !== 0) {
console.error(new TextDecoder().decode(stderr));
async function _task(task: string) {
console.log(`\n🔨 Running task ${cyan(task)}...`);
try {
await $`deno task -q ${task}`;
} catch {
Deno.exit(1);
}
}
Expand Down Expand Up @@ -250,6 +302,22 @@ async function _findFileUp(entrypoint: string, root: string) {
return hits;
}

function _summary(sequence: GitCommitSequence): string {
if (sequence.commits.length === 0) {
return "No updates";
}
if (sequence.commits.length === 1) {
return sequence.commits[0].message;
}
const groups = sequence.commits.map((commit) => commit.group).join(", ");
const full = `Updated ${groups}`;
return (full.length <= 50) ? full : "Updated dependencies";
}

function _report(sequence: GitCommitSequence): string {
return sequence.commits.map((commit) => `- ${commit.message}`).join("\n");
}

const main = new Command()
.name("molt")
.description("A tool for updating dependencies in Deno projects")
Expand Down
4 changes: 2 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"tasks": {
"run": "deno run --allow-env --allow-read --allow-net=deno.land --allow-write=.",
"run": "deno run --allow-env --allow-read --allow-write=. --allow-run --allow-net=deno.land",
"lock": "deno cache --lock=deno.lock --lock-write ./lib/**/*.ts && git add deno.lock",
"test": "deno test -A",
"test:fast": "deno task test --no-check",
"dev": "deno fmt && deno lint && deno task lock && deno task test",
"update": "deno task -q run --allow-run ./cli.ts check lib/**/*.ts",
"update": "deno task -q run ./cli.ts check lib/**/*.ts",
"install": "deno install -f -A --name molt cli.ts"
},
"fmt": {
Expand Down
Loading

0 comments on commit cc4d630

Please sign in to comment.