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: Initial setup and logic #1

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
172 changes: 172 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore

# Logs

logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)

report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Runtime data

pids
_.pid
_.seed
\*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover

lib-cov

# Coverage directory used by tools like istanbul

coverage
\*.lcov

# nyc test coverage

.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)

.grunt

# Bower dependency directory (https://bower.io/)

bower_components

# node-waf configuration

.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)

build/Release

# Dependency directories

node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)

web_modules/

# TypeScript cache

\*.tsbuildinfo

# Optional npm cache directory

.npm

# Optional eslint cache

.eslintcache

# Optional stylelint cache

.stylelintcache

# Microbundle cache

.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history

.node_repl_history

# Output of 'npm pack'

\*.tgz

# Yarn Integrity file

.yarn-integrity

# dotenv environment variable files

.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)

.cache
.parcel-cache

# Next.js build output

.next
out

# Nuxt.js build / generate output

.nuxt
dist

# Gatsby files

.cache/

# Comment in the public line in if your project uses Gatsby and not Next.js

# https://nextjs.org/blog/next-9-1#public-directory-support

# public

# vuepress build output

.vuepress/dist

# vuepress v2.x temp and cache directory

.temp
.cache

# Docusaurus cache and generated files

.docusaurus

# Serverless directories

.serverless/

# FuseBox cache

.fusebox/

# DynamoDB Local files

.dynamodb/

# TernJS port file

.tern-port

# Stores VSCode versions used for testing VSCode extensions

.vscode-test

# yarn v2

.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*

# IntelliJ based IDEs
.idea
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# conventional-pr-labels
# Conventional PR Labels [#TODO]

Add labels to Pull Requests using conventional commit pull requests

| Input | Description | Required | Default |
|-------|---------------------|----------|------------------------|
| token | GitHub secret token | false | `secrets.GITHUB_TOKEN` |
| | | | |
31 changes: 31 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: 'conventional-pr-labels'
description: "automatically adds labels to pull requests based on Conventional Commits. Works with GitHub automated releases"
author: Alexander Castillo
inputs:
token:
description: 'GitHub token for applying labels, defaults to using secrets.GITHUB_TOKEN'
required: false
default: ${{ github.token }}
type_labels:
description: 'what labels to apply to different conventional commit types'
required: false
default: '{"feat": "feature", "fix": "fix", "breaking": "breaking"}'
type: string
ignored_types:
description: 'Conventional Commit types that should have ignore_label applied'
required: false
default: '["chore"]'
type: string
ignore_label:
description: 'label to apply for ignored commits'
required: false
default: 'ignore-for-release'
type: string

branding:
icon: 'align-justify'
color: 'yellow'

runs:
using: 'node20'
main: 'dist/index.js'
Binary file added bun.lockb
Binary file not shown.
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "conventional-pr-labels",
"module": "src/index.ts",
"type": "module",
"scripts": {
"build": "bun build src/index.ts --outdir ./dist",
"test": "bun test",
"test:watch": "bun test --watch"
},
"devDependencies": {
"bun-types": "latest",
"js-yaml": "^4.1.0",
"mocha": "^10.2.0",
"sinon": "^16.1.0",
"typescript": "^5.2.2"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"@kevits/conventional-commit": "^1.0.0"
}
}
61 changes: 61 additions & 0 deletions src/github-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import github from "@actions/github";
import {GitHub} from "@actions/github/lib/utils";
import core from "@actions/core";

let cachedOctokit: InstanceType<typeof GitHub>;
const getOctokit = (): InstanceType<typeof GitHub> => {
if (cachedOctokit) {
return cachedOctokit;
}
const token = core.getInput('token');
cachedOctokit = github.getOctokit(token)
return cachedOctokit;
}

const getGitHubContext = (): { owner: string, repo: string, issue_number: number } => {
const owner = github.context.payload.repository?.owner.login;
const repo = github.context.payload.repository?.name;
const issue_number = github.context.payload.pull_request?.number;
if (!owner || !repo || !issue_number) {
throw new Error('Could not get owner, repo or issue_number from github context');
}
return {owner, repo, issue_number};
}

export const getExistingLabels = async (): Promise<string[]> => {
const octokit = getOctokit();
const {owner, repo, issue_number} = getGitHubContext();
const response = await octokit.rest.issues.listLabelsOnIssue({owner, repo, issue_number});

return response.data.map(label => label.name);
}

export const removeLabels = async (labels: string[]): Promise<void> => {
if (labels.length === 0) {
console.log('No labels to remove');
return;
}
const octokit = getOctokit();
const {owner, repo, issue_number} = getGitHubContext();

const promises = labels.map(async name => {
return octokit.rest.issues.removeLabel({owner, repo, issue_number, name}).catch(err => {
if (err.status === 404) {
return;
}
throw err;
});
});

await Promise.all(promises);
}

export const addLabels = async (labels: string[]): Promise<void> => {
if (labels.length === 0) {
console.log('No labels to add');
return;
}
const octokit = getOctokit();
const {owner, repo, issue_number} = getGitHubContext();
await octokit.rest.issues.addLabels({owner, repo, issue_number, labels})
}
30 changes: 30 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {parseHeader} from "@kevits/conventional-commit";
palexcast marked this conversation as resolved.
Show resolved Hide resolved
import {addLabels, getExistingLabels, removeLabels} from "./github-api.ts";
import {convertToLabels, findLabelsToAdd, findLabelsToRemove, hasInvalidTitleParts} from "./label-utils.ts";
import github from "@actions/github";
import core from "@actions/core";

const run = async () => {
const pullRequestTitle = github.context?.payload?.pull_request?.title;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be sure to document what events this action can be triggered by. I assume pull_request and pull_request_target will provide the same PR title, but if it's triggered by push then we'll have to figure out what PRs are connected to that branch. PR and PR target would be the safest in v1 I think.

if (!pullRequestTitle) {
console.warn('No pull request title found');
return
}

const titleParts = parseHeader(pullRequestTitle);
if (titleParts === null || hasInvalidTitleParts(titleParts)) {
console.warn('Could not parse pull request title');
palexcast marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const partsAsLabels = convertToLabels(titleParts);
const existingLabels = await getExistingLabels();

const labelsToRemove = findLabelsToRemove(partsAsLabels, existingLabels);
const labelsToAdd = findLabelsToAdd(partsAsLabels, existingLabels);

await removeLabels(labelsToRemove);
await addLabels(labelsToAdd);
}

run().catch(err => core.setFailed(err.message));
33 changes: 33 additions & 0 deletions src/label-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {CommitHeader} from "@kevits/conventional-commit";
import core from "@actions/core";

export const hasInvalidTitleParts = (titleParts: CommitHeader): boolean => {
return false; // TODO not entirely sure what to check here
}
export const convertToLabels = (titleParts: CommitHeader): string[] => {
const labelMap = JSON.parse(core.getInput('type_labels'))
const ignoredTypes = JSON.parse(core.getInput('ignored_types'))
const ignoreLabel = core.getInput('ignore_label')

const titlePartsArray = [titleParts.type, titleParts.scope, titleParts.description, titleParts.breaking ? 'breaking' : '']
.filter(Boolean)

// Convert each part to a label
const partsAsLabels = titlePartsArray.map(part => {
if (ignoredTypes.includes(part)) {
return ignoreLabel
}
return labelMap[part] ?? ''
}).filter(Boolean);

// Return unique labels only
return [...new Set(partsAsLabels)];
}

export const findLabelsToRemove = (labelsOnCommit: string[], existingLabels: string[]): string[] => {
return existingLabels.filter(label => !labelsOnCommit.includes(label));
}

export const findLabelsToAdd = (labelsOnCommit: string[], existingLabels: string[]): string[] => {
return labelsOnCommit.filter(label => !existingLabels.includes(label));
}
Empty file added test/.gitkeep
Empty file.
Loading