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

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

Merged
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
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