Skip to content

Commit abd3c7b

Browse files
committed
fix: Implement install command
1 parent 6c05d77 commit abd3c7b

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

lib/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Command } from "commander";
22
import { build } from "./build";
3+
import { install } from "./install";
34

45
const program = new Command();
56

7+
program.command("install [actions...]").action(install);
68
program.command("build [workflows...]").action(build);
79

810
program.parse(process.argv);

lib/cli/install.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import * as fs from "node:fs";
2+
import * as path from "node:path";
3+
import { parse as parseYaml } from "yaml";
4+
import { getCommit, getFileContent, getLatestRelease } from "../util/git";
5+
import { toUpperCamelCase } from "../util/util";
6+
7+
export async function install(actions: string[]) {
8+
const actionJs = `import * as fs from "node:fs";
9+
import * as path from "node:path";
10+
11+
function action(name, params) {
12+
const githubDir = path.resolve(process.cwd(), ".github");
13+
const actionsJsonPath = path.resolve(githubDir, "actions.json");
14+
const actionsLockJsonPath = path.resolve(githubDir, "actions.lock.json");
15+
const actionsJson = JSON.parse(fs.readFileSync(actionsJsonPath, "utf8"));
16+
const actionsLockJson = JSON.parse(fs.readFileSync(actionsLockJsonPath, "utf8"));
17+
18+
const targetActionVersion = actionsJson[name];
19+
if (!targetActionVersion) throw new Error(\`\${name} is not installed\`);
20+
21+
const targetActionCommit = actionsLockJson.actions[\`\${name}@\${targetActionVersion}\`];
22+
if (!targetActionCommit) throw new Error(\`\${name} is not installed\`);
23+
24+
return { name: \`\${name}@\${targetActionCommit}\`, params };
25+
}
26+
27+
export { action };
28+
`;
29+
30+
const actionDtsLines: string[] = [
31+
`import type { UsesStep } from "ghats";
32+
33+
export declare function action<T extends InstalledAction>(
34+
name: T,
35+
params?: InstalledActionParams<T>,
36+
): {
37+
name: string;
38+
params?: Omit<UsesStep, "kind" | "action">;
39+
};
40+
41+
export type InstalledActionParams<T extends InstalledAction> = Omit<
42+
UsesStep,
43+
"kind" | "action" | "with"
44+
> & { with: InstalledActionInputs<T> };
45+
`,
46+
];
47+
48+
// TODO: load existing actions.json and actions.lock.json
49+
const actionsJson: Record<string, string> = {};
50+
const actionsLockJson: { actions: Record<string, string> } = { actions: {} };
51+
52+
for (const action of actions) {
53+
const [owner, repo] = action.split("/"); // TODO: use regex
54+
if (!owner || !repo) {
55+
throw new Error(`Invalid action format: ${action}`);
56+
}
57+
58+
const latestRelease = await getLatestRelease(owner, repo);
59+
const version = latestRelease.tag_name;
60+
const commit = await getCommit(owner, repo, version);
61+
62+
// TODO: cache action.yml
63+
const actionYamlRaw = await getFileContent(
64+
owner,
65+
repo,
66+
commit.sha,
67+
"action.yml",
68+
);
69+
const actionYaml = parseYaml(actionYamlRaw);
70+
const inputsDefinition = buildInputsTypeDefinition(
71+
action,
72+
actionYaml.inputs,
73+
);
74+
actionDtsLines.push(inputsDefinition);
75+
76+
actionsJson[action] = version;
77+
actionsLockJson.actions[`${owner}/${repo}@${version}`] = commit.sha;
78+
}
79+
80+
fs.mkdirSync(path.resolve(process.cwd(), ".github"), { recursive: true });
81+
fs.writeFileSync(
82+
path.resolve(process.cwd(), ".github/actions.json"),
83+
JSON.stringify(actionsJson, null, 2),
84+
);
85+
fs.writeFileSync(
86+
path.resolve(process.cwd(), ".github/actions.lock.json"),
87+
JSON.stringify(actionsLockJson, null, 2),
88+
);
89+
90+
actionDtsLines.push(
91+
`export type InstalledAction = ${Object.keys(actionsJson)
92+
.map((action) => JSON.stringify(action))
93+
.join(" | ")};`,
94+
);
95+
96+
actionDtsLines.push(
97+
`export type InstalledActionInputs<T extends InstalledAction> = {`,
98+
Object.keys(actionsJson)
99+
.map((action) => {
100+
return ` ${JSON.stringify(action)}: InstalledActionInputs${toUpperCamelCase(action)};`;
101+
})
102+
.join("\n"),
103+
`}[T];`,
104+
);
105+
106+
const dir = path.resolve(process.cwd(), "node_modules/.ghats");
107+
fs.mkdirSync(dir, { recursive: true });
108+
fs.writeFileSync(
109+
path.resolve(dir, "action.d.ts"),
110+
actionDtsLines.join("\n") + "\n",
111+
);
112+
fs.writeFileSync(path.resolve(dir, "action.js"), actionJs);
113+
}
114+
115+
function buildInputsTypeDefinition(
116+
action: string,
117+
inputs: Record<
118+
string,
119+
{
120+
description?: string;
121+
default?: string;
122+
required?: boolean;
123+
}
124+
>,
125+
) {
126+
const lines: string[] = [];
127+
lines.push(
128+
`export type InstalledActionInputs${toUpperCamelCase(action)} = {`,
129+
);
130+
131+
for (const [key, value] of Object.entries(inputs)) {
132+
lines.push(" /**");
133+
if (value.description) {
134+
lines.push(` ${value.description}`);
135+
}
136+
if (value.default) {
137+
lines.push(` @default ${JSON.stringify(value.default)}`);
138+
}
139+
lines.push(" */");
140+
lines.push(` ${JSON.stringify(key)}${value.required ? "" : "?"}: string;`);
141+
}
142+
143+
lines.push("};");
144+
145+
return lines.join("\n") + "\n";
146+
}

0 commit comments

Comments
 (0)