Skip to content

Commit

Permalink
feat: ✨ Free plan deprecation warning
Browse files Browse the repository at this point in the history
  • Loading branch information
robvanderleek committed Jan 11, 2025
1 parent 019f92f commit 1efe1b3
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 210 deletions.
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
6 changes: 5 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ export function shouldOpenPR(config: Config) {
}

export function prSkipCI(config: Config) {
return 'prSkipCI' in config && config.prSkipCI === true
return config.prSkipCI;
}

export function showFreePlanWarning(config: Config) {
return config.freePlanWarning;
}

export function isChatOpsCommand(s?: string) {
Expand Down
5 changes: 0 additions & 5 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ export function getDefaultBranch(ctx: any) {
return ctx.payload.repository.default_branch
}

export function isPrivateOrgRepo(ctx: any) {
const {repository} = ctx.payload
return repository.private && repository.owner.type === 'Organization'
}

export function getIssueLabels(ctx: any) {
const labels = ctx.payload.issue.labels
return labels.map((l: { name: string }) => l.name)
Expand Down
2 changes: 2 additions & 0 deletions src/entities/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface Config {
openPR: boolean;
openDraftPR: boolean;
prSkipCI: boolean;
freePlanWarning: boolean;
conventionalPrTitles: boolean;
conventionalStyle: 'semver' | 'gitmoji' | 'semver-no-gitmoji';
conventionalLabels: { [name: string]: { [name: string]: any } }
Expand Down Expand Up @@ -54,6 +55,7 @@ export function getDefaultConfig(): Config {
openPR: false,
openDraftPR: false,
prSkipCI: false,
freePlanWarning: true,
conventionalPrTitles: false,
conventionalStyle: 'semver',
conventionalLabels: {},
Expand Down
47 changes: 10 additions & 37 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import {
getIssueTitle,
getMilestoneNumber,
getRepoName,
getRepoOwnerLogin,
isPrivateOrgRepo
getRepoOwnerLogin
} from "./context";
import {Context, Probot} from "probot";
import {isProPlan} from "./plans";
import {hasValidSubscription} from "./plans";
import {interpolate} from "./interpolate";
import {Config} from "./entities/Config";
import {
Expand All @@ -26,13 +25,17 @@ import {
formatAsExpandingMarkdown,
getStringLengthInBytes,
isProduction,
isRunningInGitHubActions, makeGitSafe, makePrefixGitSafe,
pushMetric, sleep,
trimStringToByteLength, wildcardMatch
isRunningInGitHubActions,
makeGitSafe,
makePrefixGitSafe,
pushMetric,
sleep,
trimStringToByteLength,
wildcardMatch
} from "./utils";

export async function createIssueBranch(app: Probot, ctx: Context<any>, branchName: string, config: Config) {
if (await hasValidSubscriptionForRepo(app, ctx, config)) {
if (await hasValidSubscription(app, ctx, config)) {
const sha = await getSourceBranchHeadSha(ctx, config, app.log)
if (sha) {
await createBranch(app, ctx, config, branchName, sha);
Expand All @@ -42,36 +45,6 @@ export async function createIssueBranch(app: Probot, ctx: Context<any>, branchNa
}
}

async function hasValidSubscriptionForRepo(app: Probot, ctx: Context<any>, config: Config) {
if (isRunningInGitHubActions()) {
return true
}
if (isPrivateOrgRepo(ctx)) {
const isProPan = await isProPlan(app, ctx)
if (!isProPan) {
await addBuyProComment(ctx, config)
app.log.info('Added comment to buy Pro 🙏 plan')
return false
} else {
return true
}
} else {
const login = getRepoOwnerLogin(ctx)
app.log.info(`Creating branch in public repository from user/org: https://github.com/${login} ...`)
return true
}
}

const buyComment = 'Hi there :wave:\n\nUsing this App for a private organization repository requires a paid ' +
'subscription that you can buy on the [GitHub Marketplace](https://github.com/marketplace/create-issue-branch)\n\n' +
'If you are a non-profit organization or otherwise can not pay for such a plan, contact me by ' +
'[creating an issue](https://github.com/robvanderleek/create-issue-branch/issues)'

async function addBuyProComment(ctx: Context<any>, config: Config) {
config.silent = false;
await addComment(ctx, config, buyComment)
}

export async function getBranchNameFromIssue(ctx: Context<any>, config: Config) {
const title = getIssueTitle(ctx)
const result = await getBranchName(ctx, config, title)
Expand Down
98 changes: 55 additions & 43 deletions src/plans.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,74 @@
import {Context, Probot} from "probot";
import {getRepoOwnerId, getRepoOwnerLogin} from "./context";
import {showFreePlanWarning} from "./config";
import {addComment} from "./github";
import {Config} from "./entities/Config";
import {isRunningInGitHubActions, isRunningInTestEnvironment} from "./utils";

const PRO_PLAN_INTRODUCTION_DATE = new Date('2021-04-07T00:00:00.000Z')
export async function hasValidSubscription(app: Probot, ctx: Context<any>, config: Config) {
if (isRunningInGitHubActions() || isRunningInTestEnvironment()) {
return true;
}
if (isFreePaidSubscription(app, ctx)) {
return true;
}
if (await isPaidPlan(app, ctx)) {
return true;
}
await displayFreePlanWarning(ctx, config);
return false;
}

export async function isProPlan(app: Probot, ctx: Context<any>) {
export async function isPaidPlan(app: Probot, ctx: Context<any>) {
try {
const id = getRepoOwnerId(ctx)
const login = getRepoOwnerLogin(ctx)
app.log.info(`Checking Marketplace for organization: https://github.com/${login} ...`)
if (freeProSubscription(login)) {
app.log.info('Found free Pro ❤️ plan')
return true
}
const res = await ctx.octokit.apps.getSubscriptionPlanForAccount({account_id: id})
const purchase = res.data.marketplace_purchase
const login = getRepoOwnerLogin(ctx);
app.log.info(`Checking Marketplace for organization: https://github.com/${login} ...`);
const id = getRepoOwnerId(ctx);
const res = await ctx.octokit.apps.getSubscriptionPlanForAccount({account_id: id});
const purchase = res.data.marketplace_purchase;
if (purchase.plan && purchase.plan.price_model === 'FREE') {
app.log.info('Found Free plan')
return false
app.log.info('Found Free plan');
return false;
} else {
app.log.info('Found Pro 💰 plan')
return true
app.log.info('Found paid 💰 plan');
return true;
}
} catch (error) {
app.log.info('Marketplace purchase not found')
return false
app.log.info('Marketplace purchase not found');
return false;
}
}

function freeProSubscription(login: string) {
const organizations = ['PWrInSpace', 'KPLRCDBS', 'codemeistre', 'RaspberryPiFoundation', 'astro-pi',
'LOG680-01-Equipe-09', 'New-AutoMotive', 'EpitechMscPro2020', 'snaphu-msu', 'SerenKodi', 'oyunprotocol',
'web-illinois', 'PathologyDataScience', 'miranhas-github', 'DHBW-FN', 'lecoindesdevs', 'getcodelimit']
const match = organizations.find(o => o.toLowerCase() === login.toLowerCase())
return match !== undefined
export async function displayFreePlanWarning(ctx: Context<any>, config: Config) {
if (showFreePlanWarning(config)) {
let freePlanWarning = '';
freePlanWarning += 'Hi there :wave:\n\n';
freePlanWarning += 'You are using the free plan of the Create Issue Branch App.\n';
freePlanWarning += 'Due to its popularity, offering the App for free is becoming too costly for me.\n';
freePlanWarning += 'The free plan is therefore going to be deprecated on March 1st, 2025.\n\n';
freePlanWarning += 'If you want to continue using this App, please upgrade to the Developer plan.\n';
freePlanWarning += 'The Developer plan costs only $1 per month (or $10 yearly).\nYou can upgrade on the ';
freePlanWarning += '[GitHub Marketplace](https://github.com/marketplace/create-issue-branch)\n\n';
freePlanWarning += 'If you have any questions reach out to me by ';
freePlanWarning += '[opening an issue](https://github.com/robvanderleek/create-issue-branch/issues).\n';
freePlanWarning += 'To disable this message, insert `freePlanWarning: false` in your configuration YAML.\n';
config.silent = false;
await addComment(ctx, config, freePlanWarning);
}
}

export async function isActivatedBeforeProPlanIntroduction(app: Probot, ctx: Context<any>) {
let datestring
try {
const id = getRepoOwnerId(ctx)
const res = await ctx.octokit.apps.getSubscriptionPlanForAccount({account_id: id})
const purchase = res.data.marketplace_purchase
datestring = purchase.updated_at
} catch (error) {
const login = getRepoOwnerLogin(ctx)
app.log.debug('Checking App installation date...')
const github = await app.auth()
const installation = await github.apps.getUserInstallation({username: login})
app.log.debug(`Found installation date: ${installation.data.created_at}`)
datestring = installation.data.created_at
}
if (!datestring) {
export function isFreePaidSubscription(app: Probot, ctx: Context<any>): boolean {
const login = getRepoOwnerLogin(ctx)
const logins = ['PWrInSpace', 'KPLRCDBS', 'codemeistre', 'RaspberryPiFoundation', 'astro-pi',
'LOG680-01-Equipe-09', 'New-AutoMotive', 'EpitechMscPro2020', 'snaphu-msu', 'SerenKodi', 'oyunprotocol',
'web-illinois', 'PathologyDataScience', 'miranhas-github', 'DHBW-FN', 'lecoindesdevs', 'getcodelimit'];
const match = logins.find(o => o.toLowerCase() === login.toLowerCase());
if (match !== undefined) {
app.log.info('Found free ❤️ paid plan');
return true;
} else {
return false;
}
const installationDate = new Date(datestring)
const result = installationDate < PRO_PLAN_INTRODUCTION_DATE
app.log.info(`Installation date is ${result ? 'before' : 'after'} Pro plan introduction date`)
return result
}

export async function listAppSubscriptions(app: any) {
Expand Down
4 changes: 4 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export function isRunningInGitHubActions() {
return process.env.GITHUB_ACTIONS === 'true';
}

export function isRunningInTestEnvironment() {
return process.env.NODE_ENV === 'test';
}

export function getStringLengthInBytes(str: string) {
return (new TextEncoder().encode(str)).length
}
Expand Down
12 changes: 7 additions & 5 deletions src/webhooks/issue-assigned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../github";
import {isRunningInGitHubActions, logMemoryUsage} from "../utils";
import {setOutput} from "@actions/core";
import {displayFreePlanWarning} from "../plans";

export async function issueAssigned(app: Probot, ctx: Context<any>) {
app.log.debug('Issue was assigned');
Expand All @@ -26,6 +27,7 @@ export async function issueAssigned(app: Probot, ctx: Context<any>) {
}
}


async function handle(app: Probot, ctx: Context<any>, config: Config) {
if (skipForIssue(ctx, config)) {
app.log.info(`Skipping run for issue: ${getIssueTitle(ctx)}`)
Expand All @@ -36,15 +38,15 @@ async function handle(app: Probot, ctx: Context<any>, config: Config) {
app.log.info(`Skipping branch creation for issue: ${getIssueTitle(ctx)}`)
branchName = await getSourceBranch(ctx, config)
} else {
branchName = await getBranchNameFromIssue(ctx, config)
branchName = await getBranchNameFromIssue(ctx, config);
if (await branchExists(ctx, branchName)) {
app.log.info('Could not create branch as it already exists')
app.log.info('Could not create branch as it already exists');
if (isRunningInGitHubActions()) {
setOutput('branchName', branchName)
setOutput('branchName', branchName);
}
return
return;
}
await createIssueBranch(app, ctx, branchName, config)
await createIssueBranch(app, ctx, branchName, config);
}
const shouldCreatePR = shouldOpenPR(config)
if (shouldCreatePR) {
Expand Down
16 changes: 1 addition & 15 deletions tests/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
getIssueLabels,
getMilestoneNumber,
getRepoOwnerLogin,
getSender,
isPrivateOrgRepo
getSender
} from "../src/context";

test('get owner', () => {
Expand Down Expand Up @@ -36,19 +35,6 @@ test('get sender', () => {
expect(getSender(ctx)).toBe('robvanderleek')
})

test('is private Org repo', () => {
expect(isPrivateOrgRepo({payload: issueAssignedPayload})).toBeFalsy()

const payloadCopy = JSON.parse(JSON.stringify(issueAssignedPayload))
payloadCopy.repository.private = true

expect(isPrivateOrgRepo({payload: payloadCopy})).toBeFalsy()

payloadCopy.repository.owner.type = 'Organization'

expect(isPrivateOrgRepo({payload: payloadCopy})).toBeTruthy()
})

test('get Issue description', () => {
const ctx = {payload: issueOpenedPayload}

Expand Down
Loading

0 comments on commit 1efe1b3

Please sign in to comment.