This commit is contained in:
garronej 2021-12-01 13:39:57 +01:00
commit 5297ab00ab
54 changed files with 18162 additions and 0 deletions

View file

@ -0,0 +1,33 @@
import * as fs from "fs";
import * as path from "path";
import { inputNames, getInputDescription, getInputDefault } from "../inputHelper";
import { outputNames, getOutputDescription } from "../outputHelper";
const projectRoot = path.join(__dirname, "..", "..");
const packageJsonParsed = require(path.join(projectRoot, "package.json"));
fs.writeFileSync(
path.join(projectRoot, "action.yml"),
Buffer.from([
`name: '${packageJsonParsed["name"]}'`,
`description: '${packageJsonParsed["description"]}'`,
`author: '${packageJsonParsed["author"]}'`,
`inputs:`,
...inputNames.map((inputName, i) => [
` ${inputName}:`,
` required: ${i === 0 ? "true" : "false"}`,
` description: '${getInputDescription(inputName).replace(/'/g,"''")}'`,
...[getInputDefault(inputName)].filter(x=>x!==undefined).map(s=>` default: '${s}'`)
].join("\n")),
`outputs:`,
...outputNames.map((outputName, i) => [
` ${outputName}:`,
` description: '${getOutputDescription(outputName).replace(/'/g,"''")}'`
].join("\n")),
`runs:`,
` using: 'node12'`,
` main: 'dist/index.js'`
].join("\n"), "utf8")
);

47
src/dispatch_event.ts Normal file
View file

@ -0,0 +1,47 @@
import { getActionParamsFactory } from "./inputHelper";
import { createOctokit } from "./tools/createOctokit";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"owner",
"repo",
"event_type",
"client_payload_json",
"github_token"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
};
export async function action(
_actionName: "dispatch_event",
params: Params,
core: CoreLike
) {
const { owner, repo, event_type, client_payload_json, github_token } = params;
core.debug(JSON.stringify({ _actionName, params }));
const octokit = createOctokit({ github_token });
await octokit.repos.createDispatchEvent({
owner,
repo,
event_type,
...(!!client_payload_json ?
{ "client_payload": JSON.parse(client_payload_json) } :
{}
)
});
}

View file

@ -0,0 +1,61 @@
import fetch from "node-fetch";
const urlJoin: typeof import("path").join = require("url-join");
import { setOutputFactory } from "./outputHelper";
import { NpmModuleVersion } from "./tools/NpmModuleVersion";
import { getActionParamsFactory } from "./inputHelper";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"owner",
"repo",
"branch",
"compare_to_version"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
};
export const { setOutput } = setOutputFactory<"version" | "compare_result">();
export async function action(
_actionName: "get_package_json_version",
params: Params,
core: CoreLike
): Promise<Parameters<typeof setOutput>[0]> {
core.debug(JSON.stringify(params));
const { owner, repo, branch, compare_to_version } = params;
const version = await fetch(
urlJoin(
"https://raw.github.com",
owner,
repo,
branch,
"package.json"
)
)
.then(res => res.text())
.then(text => JSON.parse(text))
.then(({ version }) => version as string)
.catch(() => "")
;
core.debug(`Version on ${owner}/${repo}#${branch} is ${version}`);
return { 
version,
"compare_result": NpmModuleVersion.compare(
NpmModuleVersion.parse(version || "0.0.0"),
NpmModuleVersion.parse(compare_to_version)
).toString()
};
}

129
src/inputHelper.ts Normal file
View file

@ -0,0 +1,129 @@
import * as core from '@actions/core';
export const inputNames = [
"action_name",
"owner",
"repo",
"event_type",
"client_payload_json",
"branch",
"exclude_commit_from_author_names_json",
"module_name",
"compare_to_version",
"input_string",
"search_value",
"replace_value",
"should_webhook_be_enabled",
"github_token"
] as const;
export const availableActions = [
"get_package_json_version",
"dispatch_event",
"update_changelog",
"sync_package_and_package_lock_version",
"setup_repo_webhook_for_deno_land_publishing",
"is_well_formed_and_available_module_name",
"string_replace",
"tell_if_project_uses_npm_or_yarn",
"is_package_json_version_upgraded"
] as const;
export function getInputDescription(inputName: typeof inputNames[number]): string {
switch(inputName){
case "action_name": return [
`Action to run, one of: `,
availableActions.map(s=>`"${s}"`).join(", ")
].join("");
case "owner": return [
"Repository owner, example: 'garronej',",
"github.repository_owner"
].join("");
case "repo": return [
"Repository name, example: ",
"'evt', github.event.repository.name"
].join("");
case "event_type": return [
"see: https://developer.github.com/v3/",
"repos/#create-a-repository-dispatch-event"
].join("");
case "client_payload_json": return [
"Example '{\"p\":\"foo\"}' see: https://developer.github.com/v3/",
"repos/#create-a-repository-dispatch-event"
].join("");
case "branch": return "Example: default ( can also be a sha )";
case "exclude_commit_from_author_names_json": return [
"For update_changelog, do not includes commit from user ",
`certain committer in the CHANGELOG.md, ex: '["denoify_ci"]'`
].join("");
case "module_name": return [
`A candidate module name, Example: lodash`
].join("");
case "compare_to_version": return [
`For get_package_json_version, a version against which comparing the result`,
`if found version more recent than compare_to_version compare_result is 1`,
`if found version is equal to compare_to_version compare_result is 0`,
`if found version is older to compare_to_version compare_result -1`,
`Example: 0.1.3`
].join(" ");
case "input_string": return `For string_replace, the string to replace`;
case "search_value": return `For string_replace, Example '-' ( Will be used as arg for RegExp constructor )`;
case "replace_value": return `For string_replace, Example '_'`;
case "should_webhook_be_enabled": return `true|false, Should the create webhook be enabled, with setup_repo_webhook_for_deno_land_publishing`;
case "github_token": return "GitHub Personal access token";
}
}
export function getInputDefault(inputName: typeof inputNames[number]): string | undefined {
switch(inputName){
case "owner": return "${{github.repository_owner}}";
case "repo": return "${{github.event.repository.name}}";
case "branch": return "${{ github.sha }}";
case "github_token": return "${{ github.token }}";
case "exclude_commit_from_author_names_json": return '["actions"]';
case "should_webhook_be_enabled": return "true";
}
}
const getInput = (inputName: typeof inputNames[number]) => {
if (inputNames.indexOf(inputName) < 0) {
throw new Error(`${inputName} expected`);
}
return core.getInput(inputName);
}
export function getActionParamsFactory<U extends typeof inputNames[number]>(
params: {
inputNameSubset: readonly U[]
}
) {
const { inputNameSubset } = params;
function getActionParams() {
const params: Record<U, string> = {} as any;
inputNameSubset.forEach(inputName => params[inputName] = getInput(inputName));
return params;
};
return { getActionParams };
}
export function getActionName(): typeof availableActions[number] {
return getInput("action_name") as any;
}

View file

@ -0,0 +1,100 @@
import fetch from "node-fetch";
const urlJoin: typeof import("path").join = require("url-join");
import { setOutputFactory } from "./outputHelper";
import { NpmModuleVersion } from "./tools/NpmModuleVersion";
import { getActionParamsFactory } from "./inputHelper";
import { createOctokit } from "./tools/createOctokit";
import { getLatestSemVersionedTagFactory } from "./tools/octokit-addons/getLatestSemVersionedTag";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"owner",
"repo",
"branch",
"github_token"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
};
export const { setOutput } = setOutputFactory<"from_version" | "to_version" | "is_upgraded_version">();
export async function action(
_actionName: "is_package_json_version_upgraded",
params: Params,
core: CoreLike
): Promise<Parameters<typeof setOutput>[0]> {
core.debug(JSON.stringify(params));
const { owner, repo, branch, github_token } = params;
const to_version = await getPackageJsonVersion({ owner, repo, branch });
if( to_version === undefined ){
throw new Error("No version in package.json on ${owner}/${repo}#${branch} (or repo is private)");
}
core.debug(`Version on ${owner}/${repo}#${branch} is ${NpmModuleVersion.stringify(to_version)}`);
const octokit = createOctokit({ github_token });
const { getLatestSemVersionedTag } = getLatestSemVersionedTagFactory({ octokit });
const { version: from_version } = await getLatestSemVersionedTag({ owner, repo })
.then(wrap => wrap === undefined ? { "version": NpmModuleVersion.parse("0.0.0") } : wrap);
core.debug(`Last version was ${NpmModuleVersion.stringify(from_version)}`);
const is_upgraded_version = NpmModuleVersion.compare(
to_version,
from_version
) === 1 ? "true" : "false";
core.debug(`Is version upgraded: ${is_upgraded_version}`);
return {
"to_version": NpmModuleVersion.stringify(to_version),
"from_version": NpmModuleVersion.stringify(from_version),
is_upgraded_version
};
}
//TODO: Find a way to make it work with private repo
async function getPackageJsonVersion(params: {
owner: string;
repo: string;
branch: string;
}): Promise<NpmModuleVersion | undefined> {
const { owner, repo, branch } = params;
const version = await fetch(
urlJoin(
`https://raw.github.com`,
owner,
repo,
branch,
"package.json"
)
)
.then(res => res.text())
.then(text => JSON.parse(text))
.then(({ version }) => version as string)
.catch(()=> undefined)
;
if( version === undefined){
return undefined;
}
return NpmModuleVersion.parse(version);
}

View file

@ -0,0 +1,58 @@
import { setOutputFactory } from "./outputHelper";
import { getActionParamsFactory } from "./inputHelper";
import { is404 } from "./tools/is404";
import validate_npm_package_name from "validate-npm-package-name";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"module_name"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
};
export const { setOutput } = setOutputFactory<
"is_valid_node_module_name" |
"is_valid_deno_module_name" |
"is_available_on_npm" |
"is_available_on_deno_land"
>();
export async function action(
_actionName: "is_well_formed_and_available_module_name",
params: Params,
core: CoreLike
): Promise<Parameters<typeof setOutput>[0]> {
const { module_name } = params;
const { validForNewPackages } = validate_npm_package_name(module_name);
const validForDenoPackages = validForNewPackages && module_name.indexOf("-") < 0
return {
"is_valid_node_module_name": validForNewPackages ? "true" : "false",
"is_available_on_npm":
!validForNewPackages ?
"false"
:
(await is404(`https://www.npmjs.com/package/${module_name}`)) ?
"true" : "false",
"is_valid_deno_module_name": validForDenoPackages ? "true" : "false",
"is_available_on_deno_land":
!validForDenoPackages ?
"false"
:
(await is404(`https://deno.land/x/${module_name}/`)) ?
"true" : "false"
};
}

113
src/main.ts Normal file
View file

@ -0,0 +1,113 @@
import * as core from '@actions/core'
import * as get_package_json_version from "./get_package_json_version";
import * as dispatch_event from "./dispatch_event";
import * as sync_package_and_package_lock_version from "./sync_package_and_package_lock_version";
import * as setup_repo_webhook_for_deno_land_publishing from "./setup_repo_webhook_for_deno_land_publishing";
import * as is_well_formed_and_available_module_name from "./is_well_formed_and_available_module_name";
import * as tell_if_project_uses_npm_or_yarn from "./tell_if_project_uses_npm_or_yarn";
import * as string_replace from "./string_replace";
import * as is_package_json_version_upgraded from "./is_package_json_version_upgraded";
import { getActionName } from "./inputHelper";
import * as update_changelog from "./update_changelog";
async function run(): Promise<null> {
const action_name = getActionName();
switch (action_name) {
case "get_package_json_version":
get_package_json_version.setOutput(
await get_package_json_version.action(
action_name,
get_package_json_version.getActionParams(),
core
)
);
return null;
case "dispatch_event":
await dispatch_event.action(
action_name,
dispatch_event.getActionParams(),
core
);
return null;
case "update_changelog":
await update_changelog.action(
action_name,
update_changelog.getActionParams(),
core
);
return null;
case "sync_package_and_package_lock_version":
await sync_package_and_package_lock_version.action(
action_name,
sync_package_and_package_lock_version.getActionParams(),
core
);
return null;
case "setup_repo_webhook_for_deno_land_publishing":
setup_repo_webhook_for_deno_land_publishing.setOutput(
await setup_repo_webhook_for_deno_land_publishing.action(
action_name,
setup_repo_webhook_for_deno_land_publishing.getActionParams(),
core
)
);
return null;
case "is_well_formed_and_available_module_name":
is_well_formed_and_available_module_name.setOutput(
await is_well_formed_and_available_module_name.action(
action_name,
is_well_formed_and_available_module_name.getActionParams(),
core
)
);
return null;
case "string_replace":
string_replace.setOutput(
await string_replace.action(
action_name,
string_replace.getActionParams(),
core
)
);
return null;
case "tell_if_project_uses_npm_or_yarn":
tell_if_project_uses_npm_or_yarn.setOutput(
await tell_if_project_uses_npm_or_yarn.action(
action_name,
tell_if_project_uses_npm_or_yarn.getActionParams(),
core
)
);
return null;
case "is_package_json_version_upgraded":
is_package_json_version_upgraded.setOutput(
await is_package_json_version_upgraded.action(
action_name,
is_package_json_version_upgraded.getActionParams(),
core
)
);
return null;
}
if (1 === 0 + 1) {
throw new Error(`${action_name} Not supported by this toolkit`);
}
}
(async () => {
try {
await run()
} catch (error) {
core.setFailed(error.message);
}
})();

51
src/outputHelper.ts Normal file
View file

@ -0,0 +1,51 @@
import * as core from '@actions/core'
import { objectKeys } from "evt/dist/tools/typeSafety/objectKeys";
export const outputNames = [
"version",
"is_valid_node_module_name",
"is_valid_deno_module_name",
"is_available_on_npm",
"is_available_on_deno_land",
"was_already_published",
"compare_result",
"replace_result",
"was_hook_created",
"npm_or_yarn",
"from_version",
"to_version",
"is_upgraded_version"
] as const;
export function getOutputDescription(inputName: typeof outputNames[number]): string {
switch (inputName) {
case "version": return "Output of get_package_json_version";
case "is_valid_node_module_name": return "true|false";
case "is_valid_deno_module_name": return "true|false";
case "is_available_on_npm": return "true|false";
case "is_available_on_deno_land": return "true|false";
case "was_already_published": return "true|false";
case "compare_result": return "1|0|-1";
case "replace_result": return "Output of string_replace";
case "was_hook_created": return "true|false";
case "npm_or_yarn": return "npm|yarn";
case "from_version": return "Output of is_package_json_version_upgraded, string";
case "to_version": return "Output of is_package_json_version_upgraded, string";
case "is_upgraded_version": return "Output of is_package_json_version_upgraded, true|false";
}
}
export function setOutputFactory<U extends typeof outputNames[number]>() {
function setOutput(outputs: Record<U, string>): void {
objectKeys(outputs)
.forEach(outputName => core.setOutput(outputName, outputs[outputName]));
};
return { setOutput };
}

View file

@ -0,0 +1,56 @@
import { setOutputFactory } from "./outputHelper";
import { getActionParamsFactory } from "./inputHelper";
import { createOctokit } from "./tools/createOctokit";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"owner",
"repo",
"should_webhook_be_enabled",
"github_token"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
warning: (message: string) => void;
};
export const { setOutput } = setOutputFactory<"was_hook_created">();
export async function action(
_actionName: "setup_repo_webhook_for_deno_land_publishing",
params: Params,
core: CoreLike
): Promise<Parameters<typeof setOutput>[0]> {
const { owner, repo, should_webhook_be_enabled, github_token } = params;
const octokit = createOctokit({ github_token });
try {
await octokit.repos.createWebhook({
owner,
repo,
"active": should_webhook_be_enabled === "true",
"events": ["create"],
"config": {
"url": `https://api.deno.land/webhook/gh/${repo}?subdir=deno_dist%252F`,
"content_type": "json"
}
});
} catch {
return { "was_hook_created": "false" };
}
return { "was_hook_created": "true" };
}

39
src/string_replace.ts Normal file
View file

@ -0,0 +1,39 @@
import { setOutputFactory } from "./outputHelper";
import { getActionParamsFactory } from "./inputHelper";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"input_string",
"search_value",
"replace_value"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
};
export const { setOutput } = setOutputFactory<"replace_result">();
export async function action(
_actionName: "string_replace",
params: Params,
core: CoreLike
): Promise<Parameters<typeof setOutput>[0]> {
core.debug(JSON.stringify(params));
const { input_string, search_value, replace_value } = params;
return {
"replace_result": input_string.replace(
new RegExp(search_value, "g"),
replace_value
)
};
}

View file

@ -0,0 +1,92 @@
import { getActionParamsFactory } from "./inputHelper";
import * as st from "scripting-tools";
import * as fs from "fs";
import { gitCommit } from "./tools/gitCommit";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"owner",
"repo",
"branch",
"github_token"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
};
export async function action(
_actionName: "sync_package_and_package_lock_version",
params: Params,
core: CoreLike
) {
core.debug(JSON.stringify(params));
const { owner, repo, branch, github_token } = params;
await gitCommit({
owner,
repo,
github_token,
"commitAuthorEmail": "actions@github.com",
"performChanges": async () => {
await st.exec(`git checkout ${branch}`);
const { version } = JSON.parse(
fs.readFileSync("package.json")
.toString("utf8")
);
if (!fs.existsSync("package-lock.json")) {
core.debug(`No package-lock.json tracked by ${owner}/${repo}#${branch}`);
return { "commit": false };
}
const packageLockJsonRaw = fs.readFileSync("package-lock.json")
.toString("utf8")
;
const packageLockJsonParsed = JSON.parse(
packageLockJsonRaw
);
if (packageLockJsonParsed.version === version) {
core.debug("Nothing to do, version in package.json and package-lock.json are the same");
return { "commit": false };
}
fs.writeFileSync(
"package-lock.json",
Buffer.from(
JSON.stringify(
(() => {
packageLockJsonParsed.version = version;
packageLockJsonParsed.packages[""].version = version;
return packageLockJsonParsed;
})(),
null,
packageLockJsonRaw
.replace(/\t/g, " ")
.match(/^(\s*)\"version\"/m)![1].length
) + packageLockJsonRaw.match(/}([\r\n]*)$/)![1],
"utf8"
)
);
return {
"commit": true,
"addAll": false,
"message": "Sync package.json and package.lock version"
};
}
});
}

View file

@ -0,0 +1,50 @@
import fetch from "node-fetch";
const urlJoin: typeof import("path").join = require("url-join");
import { setOutputFactory } from "./outputHelper";
import { getActionParamsFactory } from "./inputHelper";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"owner",
"repo",
"branch"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
};
export const { setOutput } = setOutputFactory<"npm_or_yarn">();
export async function action(
_actionName: "tell_if_project_uses_npm_or_yarn",
params: Params,
core: CoreLike
): Promise<Parameters<typeof setOutput>[0]> {
core.debug(JSON.stringify(params));
const { owner, repo } = params;
const branch = params.branch.split("/").reverse()[0];
const npm_or_yarn = await fetch(
urlJoin(
"https://raw.github.com",
owner,
repo,
branch,
"yarn.lock"
)
).then(res => res.status === 404 ? "npm" : "yarn")
core.debug(`Version on ${owner}/${repo}#${branch} is using ${npm_or_yarn}`);
return { npm_or_yarn };
}

View file

@ -0,0 +1,17 @@
import { action } from "../dispatch_event";
action(
"dispatch_event",
{
"owner": "garronej",
"repo": "test-repo",
"event_type": "example-event",
"client_payload_json": JSON.stringify({ "foo": "bar" }),
"github_token": ""
},
{ "debug": console.log }
);

View file

@ -0,0 +1,22 @@
import { action } from "../get_package_json_version";
(async () => {
const repo = "denoify";
const result = await action("get_package_json_version", {
"owner": "garronej",
repo,
"branch": "aa5da60301bea4cf0e80e98a4579f7076b544a44",
"compare_to_version": "0.0.0"
}, { "debug": console.log });
console.log(result);
})();

View file

@ -0,0 +1,20 @@
import { action } from "../is_package_json_version_upgraded";
(async () => {
const repo = "keycloakify-demo-app";
const result = await action("is_package_json_version_upgraded", {
"owner": "InseeFrLab",
repo,
"branch": "4fc0ccb46bdb3912e0a215ca3ae45aed458ea6a4",
"github_token": ""
}, { "debug": console.log });
console.log(result);
})();

View file

@ -0,0 +1,33 @@
import { action } from "../is_well_formed_and_available_module_name";
(async () => {
for (const module_name of [
"congenial_pancake",
"evt",
"_okay",
"mai-oui-c-clair"
]) {
console.log({ module_name });
const resp = await action(
"is_well_formed_and_available_module_name",
{ module_name },
{ "debug": console.log }
);
console.log(resp);
}
})();

View file

@ -0,0 +1,27 @@
import { action } from "../setup_repo_webhook_for_deno_land_publishing";
import * as st from "scripting-tools";
(async () => {
st.enableCmdTrace();
const repo = "reimagined_octo_winner_kay";
const resp = await action(
"setup_repo_webhook_for_deno_land_publishing",
{
"owner": "garronej",
"repo": "reimagined_octo_winner_kay",
"should_webhook_be_enabled": "false",
"github_token": ""
},
{
"debug": console.log,
"warning": console.warn
}
);
console.log(resp);
})();

View file

@ -0,0 +1,17 @@
import { action } from "../string_replace";
import * as st from "scripting-tools";
(async () => {
const { replace_result } = await action("string_replace", {
"input_string": "octo-computing-machine",
"search_value": "-",
"replace_value": "_"
},
{ "debug": console.log }
);
console.log({ replace_result });
})();

View file

@ -0,0 +1,20 @@
import { action } from "../sync_package_and_package_lock_version";
(async () => {
const repo = "literate-sniffle";
await action("sync_package_and_package_lock_version", {
"owner": "garronej",
repo,
"branch": "master",
"github_token": ""
},
{ "debug": console.log }
);
})();

View file

@ -0,0 +1,17 @@
import { action } from "../tell_if_project_uses_npm_or_yarn";
(async () => {
const repo = "powerhooks";
await action("tell_if_project_uses_npm_or_yarn", {
"owner": "garronej",
repo,
"branch": "refs/heads/master"
},
{ "debug": console.log }
);
})();

View file

@ -0,0 +1,25 @@
import { action } from "../update_changelog";
import * as st from "scripting-tools";
(async () => {
st.enableCmdTrace();
const repo = "sturdy_umbrella";
await action("update_changelog", {
"owner": "garronej",
repo,
"branch": "dev",
"exclude_commit_from_author_names_json": JSON.stringify(["denoify_ci"]),
"github_token": ""
},
{
"debug": console.log,
"warning": console.log
}
);
})();

View file

@ -0,0 +1,90 @@
export type NpmModuleVersion = {
major: number;
minor: number;
patch: number;
};
export namespace NpmModuleVersion {
export function parse(versionStr: string): NpmModuleVersion {
const match = versionStr.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-beta.([0-9]+))?/);
if( !match ){
throw new Error(`${versionStr} is not a valid NPM version`);
}
return {
"major": parseInt(match[1]),
"minor": parseInt(match[2]),
"patch": parseInt(match[3])
};
};
export function stringify(v: NpmModuleVersion) {
return `${v.major}.${v.minor}.${v.patch}`;
}
/**
*
* v1 < v2 => -1
* v1 === v2 => 0
* v1 > v2 => 1
*
*/
export function compare(v1: NpmModuleVersion, v2: NpmModuleVersion): -1 | 0 | 1 {
const sign = (n: number): -1 | 0 | 1 => n === 0 ? 0 : (n < 0 ? -1 : 1);
if (v1.major === v2.major) {
if (v1.minor === v2.minor) {
return sign(v1.patch - v2.patch);
} else {
return sign(v1.minor - v2.minor);
}
} else {
return sign(v1.major - v2.major);
}
}
export function bumpType(
params: {
versionBehindStr: string;
versionAheadStr: string;
}
): "SAME" | "MAJOR" | "MINOR" | "PATCH" {
const versionAhead = parse(params.versionAheadStr);
const versionBehind = parse(params.versionBehindStr);
if( compare(versionBehind, versionAhead) === 1 ){
throw new Error(`Version regression ${versionBehind} -> ${versionAhead}`);
}
if (versionBehind.major !== versionAhead.major) {
return "MAJOR";
} else if (versionBehind.minor !== versionAhead.minor) {
return "MINOR";
} else if (versionBehind.patch !== versionAhead.patch) {
return "PATCH";
} else {
return "SAME";
}
}
}

View file

@ -0,0 +1,10 @@
import { Octokit } from "@octokit/rest";
export function createOctokit(params: { github_token: string; }) {
const { github_token } = params;
return new Octokit({ ...(github_token !== "" ? { "auth": github_token } : {}) });
}

60
src/tools/gitCommit.ts Normal file
View file

@ -0,0 +1,60 @@
import * as st from "scripting-tools";
export async function gitCommit(
params: {
owner: string;
repo: string;
commitAuthorEmail: string;
performChanges: () => Promise<{ commit: false; } | { commit: true; addAll: boolean; message: string; }>;
github_token: string;
}
) {
const { owner, repo, commitAuthorEmail, performChanges, github_token } = params;
await st.exec(`git clone https://github.com/${owner}/${repo}`);
const cwd = process.cwd();
process.chdir(repo);
const changesResult = await (async () => {
try {
return await performChanges();
} catch (error) {
return error as Error;
}
})()
if (!(changesResult instanceof Error) && changesResult.commit) {
await st.exec(`git config --local user.email "${commitAuthorEmail}"`);
await st.exec(`git config --local user.name "${commitAuthorEmail.split("@")[0]}"`);
if (changesResult.addAll) {
await st.exec(`git add -A`);
}
await st.exec(`git commit -am "${changesResult.message}"`);
await st.exec(`git push "https://${owner}:${github_token}@github.com/${owner}/${repo}.git"`);
}
process.chdir(cwd);
await st.exec(`rm -r ${repo}`);
if (changesResult instanceof Error) {
throw changesResult;
}
}

6
src/tools/is404.ts Normal file
View file

@ -0,0 +1,6 @@
import fetch from "node-fetch";
export function is404(url: string): Promise<boolean> {
return fetch(url).then(({ status }) => status === 404);
}

View file

@ -0,0 +1,55 @@
import { getCommonOriginFactory } from "./getCommonOrigin";
import { listCommitFactory } from "./listCommit";
import type { Commit } from "./getCommitAsyncIterable";
import type { Octokit } from "@octokit/rest";
/** Take two branch that have a common origin and list all the
* commit that have been made on the branch that is ahead since it
* has been forked from the branch that is behind.
* From the older to the newest.
* */
export function getCommitAheadFactory(
params: { octokit: Octokit; }
) {
const { octokit } = params;
const { getCommonOrigin } = getCommonOriginFactory({ octokit });
const { listCommit } = listCommitFactory({ octokit });
async function getCommitAhead(
params: {
owner: string;
repo: string;
branchBehind: string;
branchAhead: string;
}
): Promise<{ commits: Commit[]; }> {
const { owner, repo, branchBehind, branchAhead } = params;
const { sha } = await getCommonOrigin({
owner,
repo,
"branch1": branchBehind,
"branch2": branchAhead
});
const commits = await listCommit({
owner,
repo,
"branch": branchAhead,
sha
});
return { commits };
}
return { getCommitAhead };
}

View file

@ -0,0 +1,100 @@
import type { AsyncReturnType } from "evt/dist/tools/typeSafety/AsyncReturnType";
import type { Octokit } from "@octokit/rest";
/*
.sha
.commit.message
.author.type
.author.type !== "User"
*/
/** Alias for the non exported ReposListCommitsResponseData type alias */
export type Commit = AsyncReturnType<Octokit["repos"]["listCommits"]>["data"][number];
const per_page = 30;
/** Iterate over the commits of a repo's branch */
export function getCommitAsyncIterableFactory(params: { octokit: Octokit; }) {
const { octokit } = params;
function getCommitAsyncIterable(
params: {
owner: string;
repo: string;
branch: string;
}
): AsyncIterable<Commit> {
const { owner, repo, branch } = params;
let commits: Commit[] = [];
let page = 0;
let isLastPage: boolean | undefined = undefined;
const getReposListCommitsResponseData = (params: { page: number }) =>
octokit.repos.listCommits({
owner,
repo,
per_page,
"page": params.page,
"sha": branch
}).then(({ data }) => data)
;
return {
[Symbol.asyncIterator]() {
return {
"next": async ()=> {
if (commits.length === 0) {
if (isLastPage) {
return { "done": true, "value": undefined };
}
page++;
commits = await getReposListCommitsResponseData({ page });
if (commits.length === 0) {
return { "done": true, "value": undefined };
}
isLastPage =
commits.length !== per_page ||
(await getReposListCommitsResponseData({ "page": page + 1 })).length === 0
;
}
const [commit, ...rest] = commits;
commits = rest;
return {
"value": commit,
"done": false
};
}
};
}
};
}
return { getCommitAsyncIterable };
}

View file

@ -0,0 +1,90 @@
import { getCommitAsyncIterableFactory } from "./getCommitAsyncIterable";
import type { Octokit } from "@octokit/rest";
/** Return the sha of the first common commit between two branches */
export function getCommonOriginFactory(params: { octokit: Octokit; }) {
const { octokit } = params;
const { getCommitAsyncIterable } = getCommitAsyncIterableFactory({ octokit });
async function getCommonOrigin(
params: {
owner: string;
repo: string;
branch1: string;
branch2: string;
}
): Promise<{ sha: string; }> {
const { owner, repo, branch1, branch2 } = params;
const [
commitAsyncIterable1,
commitAsyncIterable2
] = ([branch1, branch2] as const)
.map(branch => getCommitAsyncIterable({ owner, repo, branch }))
;
let shas1: string[] = [];
let shas2: string[] = [];
while (true) {
const [
itRes1,
itRes2
] =
await Promise.all(
([commitAsyncIterable1, commitAsyncIterable2] as const)
.map(
commitAsyncIterable => commitAsyncIterable[Symbol.asyncIterator]()
.next()
)
)
;
let sha1: string | undefined = undefined;
if (!itRes1.done) {
sha1 = itRes1.value.sha;
shas1.push(sha1);
}
let sha2: string | undefined = undefined;
if (!itRes2.done) {
sha2 = itRes2.value.sha;
shas2.push(sha2);
}
if (!!sha1 && shas2.includes(sha1)) {
return { "sha": sha1 };
}
if (!!sha2 && shas1.includes(sha2)) {
return { "sha": sha2 };
}
if (itRes1.done && itRes2.done) {
throw new Error("No common origin");
}
}
}
return { getCommonOrigin };
}

View file

@ -0,0 +1,48 @@
import { listTagsFactory } from "./listTags";
import type { Octokit } from "@octokit/rest";
import { NpmModuleVersion } from "../NpmModuleVersion";
export function getLatestSemVersionedTagFactory(params: { octokit: Octokit; }) {
const { octokit } = params;
async function getLatestSemVersionedTag(
params: {
owner: string;
repo: string;
}
): Promise<{
tag: string;
version: NpmModuleVersion;
} | undefined> {
const { owner, repo } = params;
const semVersionedTags: { tag: string; version: NpmModuleVersion; }[] = [];
const { listTags } = listTagsFactory({ octokit });
for await (const tag of listTags({ owner, repo })) {
const match = tag.match(/^v?([0-9]+\.[0-9]+\.[0-9]+)$/);
if (!match) {
continue;
}
semVersionedTags.push({
tag,
"version": NpmModuleVersion.parse( match[1])
});
}
return semVersionedTags
.sort(({ version: vX }, { version: vY }) => NpmModuleVersion.compare(vY, vX))[0];
};
return { getLatestSemVersionedTag };
}

View file

@ -0,0 +1,93 @@
import type { AsyncReturnType } from "evt/dist/tools/typeSafety/AsyncReturnType";
import type { Octokit } from "@octokit/rest";
/** Alias for the non exported PullsListResponseData type alias */
export type PullRequest = AsyncReturnType<Octokit["pulls"]["list"]>["data"][number];
const per_page = 99;
export function getPullRequestAsyncIterableFactory(params: { octokit: Octokit; }) {
const { octokit } = params;
function getPullRequestAsyncIterable(
params: {
owner: string;
repo: string;
state: "open" | "closed" | "all"
}
): AsyncIterable<PullRequest> {
const { owner, repo, state } = params;
let pullRequests: PullRequest[] = [];
let page = 0;
let isLastPage: boolean | undefined = undefined;
const getPullsListResponseData = (params: { page: number }) =>
octokit.pulls.list({
owner,
repo,
state,
per_page,
"page": params.page
}).then(({ data }) => data)
;
return {
[Symbol.asyncIterator]() {
return {
"next": async ()=> {
if (pullRequests.length === 0) {
if (isLastPage) {
return { "done": true, "value": undefined };
}
page++;
pullRequests = await getPullsListResponseData({ page });
if (pullRequests.length === 0) {
return { "done": true, "value": undefined };
}
isLastPage =
pullRequests.length !== per_page ||
(await getPullsListResponseData({ "page": page + 1 })).length === 0
;
}
const [pullRequest, ...rest] = pullRequests;
pullRequests = rest;
return {
"value": pullRequest,
"done": false
};
}
};
}
};
}
return { getPullRequestAsyncIterable };
}

View file

@ -0,0 +1,4 @@
export { getCommitAheadFactory as getChangeLogFactory } from "./getCommitAhead";
export { Commit, getCommitAsyncIterableFactory } from "./getCommitAsyncIterable";
export { getCommonOriginFactory } from "./getCommonOrigin";
export { listCommitFactory } from "./listCommit";

View file

@ -0,0 +1,51 @@
import type { Octokit } from "@octokit/rest";
import { getCommitAsyncIterableFactory } from "./getCommitAsyncIterable";
import type { Commit } from "./getCommitAsyncIterable";
/** Return the list of commit since given sha (excluded)
* ordered from the oldest to the newest */
export function listCommitFactory(
params: { octokit: Octokit }
) {
const { octokit } = params;
const { getCommitAsyncIterable } = getCommitAsyncIterableFactory({ octokit });
async function listCommit(
params: {
owner: string;
repo: string;
branch: string;
sha: string;
}
): Promise<Commit[]> {
const { owner, repo, branch, sha } = params;
const commitAsyncIterable = getCommitAsyncIterable({
owner,
repo,
branch
});
const commits: Commit[]= [];
for await (const commit of commitAsyncIterable) {
if( commit.sha === sha ){
break;
}
commits.push(commit);
}
return commits.reverse();
}
return { listCommit };
}

View file

@ -0,0 +1,82 @@
import type { Octokit } from "@octokit/rest";
const per_page = 99;
export function listTagsFactory(params: { octokit: Octokit; }) {
const { octokit } = params;
const octokit_repo_listTags = (
async (
params: {
owner: string;
repo: string;
per_page: number;
page: number;
}
) => {
return octokit.repos.listTags(params);
}
);
async function* listTags(
params: {
owner: string;
repo: string;
}
): AsyncGenerator<string> {
const { owner, repo } = params;
let page = 1;
while (true) {
const resp = await octokit_repo_listTags({
owner,
repo,
per_page,
"page": page++
});
for (const branch of resp.data.map(({ name }) => name)) {
yield branch;
}
if (resp.data.length < 99) {
break;
}
}
}
/** Returns the same "latest" tag as deno.land/x, not actually the latest though */
async function getLatestTag(
params: {
owner: string;
repo: string;
}
): Promise<string | undefined> {
const { owner, repo } = params;
const itRes = await listTags({ owner, repo }).next();
if (itRes.done) {
return undefined;
}
return itRes.value;
}
return { listTags, getLatestTag };
}

View file

@ -0,0 +1,22 @@
import { getCommitAheadFactory } from "../../octokit-addons/getCommitAhead";
import { Octokit } from "@octokit/rest";
(async ()=>{
const octokit = new Octokit();
const { getCommitAhead } = getCommitAheadFactory({ octokit });
const { commits } = await getCommitAhead({
"owner": "garronej",
"repo": "test-repo",
"branchBehind": "garronej-patch-1",
"branchAhead": "master"
});
const messages = commits.map(({ commit })=> commit.message );
console.log(JSON.stringify(messages, null, 2));
})();

View file

@ -0,0 +1,28 @@
import { getCommitAsyncIterableFactory } from "../../octokit-addons/getCommitAsyncIterable";
import { createOctokit } from "../../createOctokit";
(async function () {
const octokit = createOctokit({ "github_token": "" });
const { getCommitAsyncIterable } = getCommitAsyncIterableFactory({ octokit });
const commitAsyncIterable = getCommitAsyncIterable({
"owner": "garronej",
"repo": "test-repo",
"branch": "master"
});
for await (const commit of commitAsyncIterable) {
console.log(commit.commit.message);
}
console.log("done");
})();

View file

@ -0,0 +1,23 @@
import { getCommonOriginFactory } from "../../octokit-addons/getCommonOrigin";
import { Octokit } from "@octokit/rest";
(async function () {
const octokit = new Octokit();
const { getCommonOrigin } = getCommonOriginFactory({ octokit });
const { sha } = await getCommonOrigin({
"owner": "garronej",
"repo": "test-repo",
"branch1": "master",
"branch2": "garronej-patch-1"
});
console.log({ sha });
})();

View file

@ -0,0 +1,21 @@
import { listCommitFactory } from "../../octokit-addons/listCommit";
import { createOctokit } from "../../createOctokit";
(async ()=>{
const octokit = createOctokit({ "github_token": "" });
const { listCommit } = listCommitFactory({ octokit });
const commits= await listCommit({
"owner": "garronej",
"repo": "supreme_tribble",
"branch": "dev",
"sha": "84792f5719d0812e6917051b5c6331891187ca20"
});
console.log(JSON.stringify(commits, null, 2));
})();

192
src/update_changelog.ts Normal file
View file

@ -0,0 +1,192 @@
import { getActionParamsFactory } from "./inputHelper";
import * as st from "scripting-tools";
import { getCommitAheadFactory } from "./tools/octokit-addons/getCommitAhead";
import * as get_package_json_version from "./get_package_json_version";
import * as fs from "fs";
import { NpmModuleVersion } from "./tools/NpmModuleVersion";
import { gitCommit } from "./tools/gitCommit";
import { getLatestSemVersionedTagFactory } from "./tools/octokit-addons/getLatestSemVersionedTag";
import { createOctokit } from "./tools/createOctokit";
export const { getActionParams } = getActionParamsFactory({
"inputNameSubset": [
"owner",
"repo",
"branch",
"exclude_commit_from_author_names_json",
"github_token"
] as const
});
export type Params = ReturnType<typeof getActionParams>;
type CoreLike = {
debug: (message: string) => void;
warning: (message: string)=> void;
};
export async function action(
_actionName: "update_changelog",
params: Params,
core: CoreLike
) {
const {
owner,
repo,
github_token
} = params;
const branch = params.branch.split("/").reverse()[0];
core.debug(`params: ${JSON.stringify(params)}`);
const exclude_commit_from_author_names: string[] =
JSON.parse(params.exclude_commit_from_author_names_json)
;
const octokit = createOctokit({ github_token });
const { getCommitAhead } = getCommitAheadFactory({ octokit });
const { getLatestSemVersionedTag } = getLatestSemVersionedTagFactory({ octokit });
const { tag: branchBehind } = (await getLatestSemVersionedTag({ owner, repo })) ?? {};
if( branchBehind === undefined ){
core.warning(`It's the first release, not editing the CHANGELOG.md`);
return;
}
const { commits } = await getCommitAhead({
owner,
repo,
branchBehind,
"branchAhead": branch
}).catch(() => ({ "commits": undefined }));
if( commits === undefined ){
core.warning(`${branchBehind} probably does not exist`);
return;
}
const [
branchBehindVersion,
branchAheadVersion
] = await Promise.all(
([branchBehind, branch] as const)
.map(branch =>
get_package_json_version.action(
"get_package_json_version",
{
owner,
repo,
branch,
"compare_to_version": "0.0.0"
},
core
).then(({ version }) => version)
)
);
const bumpType = NpmModuleVersion.bumpType({
"versionAheadStr": branchAheadVersion,
"versionBehindStr": branchBehindVersion || "0.0.0"
});
if( bumpType === "SAME" ){
core.warning(`Version on ${branch} and ${branchBehind} are the same, not editing CHANGELOG.md`);
return;
}
await gitCommit({
owner,
repo,
github_token,
"commitAuthorEmail": "actions@github.com",
"performChanges": async () => {
await st.exec(`git checkout ${branch}`);
const { changelogRaw } = updateChangelog({
"changelogRaw":
fs.existsSync("CHANGELOG.md") ?
fs.readFileSync("CHANGELOG.md")
.toString("utf8")
: "",
"version": branchAheadVersion,
bumpType,
"body": commits
.reverse()
.filter(({ commit }) => !exclude_commit_from_author_names.includes(commit.author.name))
.map(({ commit }) => commit.message)
.filter(message => !/changelog/i.test(message))
.filter(message => !/^Merge branch /.test(message))
.filter(message => !/^GitBook: /.test(message))
.map(message => `- ${message} `)
.join("\n")
});
core.debug(`CHANGELOG.md: ${changelogRaw}`);
fs.writeFileSync(
"CHANGELOG.md",
Buffer.from(changelogRaw, "utf8")
);
return {
"commit": true,
"addAll": true,
"message": `Update changelog v${branchAheadVersion}`
};
}
});
}
function updateChangelog(
params: {
changelogRaw: string;
version: string;
bumpType: "MAJOR" | "MINOR" | "PATCH";
body: string;
}
): { changelogRaw: string; } {
const { body, version, bumpType } = params;
const dateString = (() => {
const now = new Date();
return new Date(now.getTime() - (now.getTimezoneOffset() * 60000))
.toISOString()
.split("T")[0];
})();
const changelogRaw = [
`${bumpType === "MAJOR" ? "#" : (bumpType === "MINOR" ? "##" : "###")}`,
` **${version}** (${dateString}) \n \n`,
`${body} \n \n`,
params.changelogRaw
].join("");
return { changelogRaw };
}