diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..188f289 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,203 @@ +{ + "parserOptions": { + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "sourceType": "module" + }, + + "env": { + "es6": true, + "node": true, + "mocha": true + }, + + "plugins": [], + + "globals": { + "cy": true, + "Cypress": true, + "document": false, + "navigator": false, + "window": false, + "expect": true, + "test": true, + "sinon": true, + "angular": true, + "inject": true + }, + + "rules": { + "accessor-pairs": 2, + "arrow-spacing": [2, {"before": true, "after": true}], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", {"allowSingleLine": true}], + "camelcase": [2, {"properties": "never"}], + "comma-dangle": [2, "never"], + "comma-spacing": [2, {"before": false, "after": true}], + "comma-style": [2, "last"], + "computed-property-spacing": [2, "never"], + "constructor-super": 2, + "curly": [2, "multi-line"], + "dot-location": [2, "property"], + "eol-last": [2, "always"], + "eqeqeq": [2, "allow-null"], + "func-call-spacing": [2, "never"], + "callback-return": [1, ["callback", "cb", "done"]], + "handle-callback-err": [2, "^(err|error)$"], + "indent": [2, 2, { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "FunctionDeclaration": { + "parameters": 1, + "body": 1 + }, + "FunctionExpression": { + "parameters": 1, + "body": 1 + } + }], + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "keyword-spacing": [2, {"before": true, "after": true}], + "linebreak-style": [2, "unix"], + "max-len": ["error", { + "code": 100, + "ignoreRegExpLiterals": true, + "ignorePattern": "\\s+require\\(|https?://" + }], + "new-cap": [2, {"newIsCap": true, "capIsNew": false}], + "new-parens": 2, + "newline-per-chained-call": [2, {"ignoreChainWithDepth": 4}], + "no-array-constructor": 2, + "no-caller": 2, + "no-class-assign": 2, + "no-cond-assign": 2, + "no-console": [1, {"allow": ["error"]}], + "no-const-assign": 2, + "no-constant-condition": [2, {"checkLoops": false}], + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-dupe-args": 2, + "no-dupe-class-members": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-duplicate-imports": 2, + "no-empty-character-class": 2, + "no-empty-pattern": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-global-assign": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": [2, {"allowLoop": false, "allowSwitch": false}], + "no-lone-blocks": 2, + "no-lonely-if": 2, + "no-mixed-operators": [2, { + "groups": [ + ["+", "-", "*", "/", "%", "**"], + ["&", "|", "^", "~", "<<", ">>", ">>>"], + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"] + ], + "allowSamePrecedence": false + }], + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, {"max": 2}], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + "no-new-symbol": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-return-assign": [2, "except-parens"], + "no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-shadow": ["error", { "allow": [ + "argv", + "callback", + "cb", + "done", + "err", + "params" + ] }], + "no-sparse-arrays": 2, + "no-tabs": 2, + "no-template-curly-in-string": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-unexpected-multiline": 2, + "no-unmodified-loop-condition": 2, + "no-unneeded-ternary": [2, {"defaultAssignment": false}], + "no-unreachable": 2, + "no-unsafe-finally": 2, + "no-unsafe-negation": 2, + "no-unused-vars": [2, {"vars": "all", "args": "none"}], + "no-useless-call": 2, + "no-useless-computed-key": 2, + "no-useless-constructor": 2, + "no-useless-escape": 2, + "no-useless-rename": 2, + "no-var": 2, + "no-whitespace-before-property": 2, + "no-with": 2, + "object-curly-spacing": [2, "never"], + "object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}], + "one-var": [2, {"initialized": "never"}], + "operator-linebreak": [2, "after", {"overrides": {"?": "before", ":": "before"}}], + "padded-blocks": [0, "never"], + "prefer-template": 2, + "prefer-const": [2, {"destructuring": "any", "ignoreReadBeforeAssign": true}], + "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], + "rest-spread-spacing": [2, "never"], + "semi": [2, "never"], + "semi-spacing": [2, {"before": false, "after": true}], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "always"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, {"words": true, "nonwords": false}], + "spaced-comment": [2, "always", + {"line": {"markers": ["*package", "!", ","]}, "block": {"balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"]}} + ], + "template-curly-spacing": [2, "never"], + "unicode-bom": [2, "never"], + "use-isnan": 2, + "valid-typeof": 2, + "wrap-iife": [2, "any", {"functionPrototypeMethods": true}], + "yield-star-spacing": [2, "both"], + "yoda": [2, "never"] + } +} diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/cli.js b/cli.js new file mode 100755 index 0000000..6dde622 --- /dev/null +++ b/cli.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +const argv = require('yargs') + .demandOption(['gh-token', 'owner', 'repo', 'releaseBranch', 'env', 'downstreamTag']) + .option('gh-approval-token', { + description: 'gh token to auto approve the opened pull request', + type: 'string' + }) + .option('customer', { + description: 'customer name', + type: 'string' + }) + .option('infrastructure-path', { + description: 'infrastructure path', + type: 'string', + default: 'livingdocs' + }) + .help(false) + .version(false) + .argv +const run = require('./index') + +run(argv) + .then((pullRequest) => { + console.log(` + The PR for the infrastructure bump has been opened at + ${pullRequest.html_url} + `) + }) + .catch((e) => { + console.log(e.message) + // delete branch + process.exit(1) + }) + +// node cli.js --owner livingdocsIO --repo infrastructure-onboarding-service \ +// --infrastructure-path onboarding-service --releaseBranch release-2024-11-16 --env stage \ +// --downstream-tag v4.20.11 --customer onboarding \ +// --gh-token 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 diff --git a/git/create-approval-for-pull-request.js b/git/create-approval-for-pull-request.js new file mode 100644 index 0000000..d37d186 --- /dev/null +++ b/git/create-approval-for-pull-request.js @@ -0,0 +1,25 @@ +const request = require('request-promise') + +// https://docs.github.com/en/rest/reference/pulls#create-a-review-for-a-pull-request +module.exports = async ({ + owner, repo, token, pullNumber, commitId, event = 'APPROVE' +}) => { + try { + return request({ + method: 'POST', + uri: `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, + body: { + commit_id: commitId, + event + }, + headers: { + 'Authorization': `token ${token}`, + 'User-Agent': 'Request-Promise', + 'X-GitHub-Api-Version': '2022-11-28' + }, + json: true + }) + } catch (error) { + throw error + } +} diff --git a/git/create-branch.js b/git/create-branch.js new file mode 100644 index 0000000..01348f8 --- /dev/null +++ b/git/create-branch.js @@ -0,0 +1,20 @@ +const request = require('request-promise') + +// https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#create-a-reference +module.exports = async ({owner, repo, token, ref, sha}) => { + try { + return await request({ + method: 'POST', + uri: `https://api.github.com/repos/${owner}/${repo}/git/refs`, + body: {ref, sha}, + headers: { + 'Authorization': `token ${token}`, + 'User-Agent': 'Request-Promise', + 'X-GitHub-Api-Version': '2022-11-28' + }, + json: true + }) + } catch (error) { + throw error + } +} diff --git a/git/create-pull-request.js b/git/create-pull-request.js new file mode 100644 index 0000000..f0b30d8 --- /dev/null +++ b/git/create-pull-request.js @@ -0,0 +1,22 @@ +const request = require('request-promise') + +// https://docs.github.com/en/rest/reference/pulls#create-a-pull-request +module.exports = async ({ + owner, repo, token, title, head, base, body +}) => { + try { + return request({ + method: 'POST', + uri: `https://api.github.com/repos/${owner}/${repo}/pulls`, + body: {title, head, base, body}, + headers: { + 'Authorization': `token ${token}`, + 'User-Agent': 'Request-Promise', + 'X-GitHub-Api-Version': '2022-11-28' + }, + json: true + }) + } catch (error) { + throw error + } +} diff --git a/git/get-content.js b/git/get-content.js new file mode 100644 index 0000000..3068455 --- /dev/null +++ b/git/get-content.js @@ -0,0 +1,25 @@ +const request = require('request-promise') + +// https://docs.github.com/en/rest/reference/repos#get-repository-content +// +// @return +module.exports = async ({ + owner, repo, token, path +}) => { + try { + console.log('get-content.js', `https://api.github.com/repos/${owner}/${repo}/contents/${path}`) + return await request({ + method: 'GET', + uri: `https://api.github.com/repos/${owner}/${repo}/contents/${path}`, + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `token ${token}`, + 'User-Agent': 'Request-Promise', + 'X-GitHub-Api-Version': '2022-11-28' + }, + json: true + }) + } catch (error) { + throw error + } +} diff --git a/git/get-sha-branch.js b/git/get-sha-branch.js new file mode 100644 index 0000000..3075c68 --- /dev/null +++ b/git/get-sha-branch.js @@ -0,0 +1,33 @@ +const request = require('request-promise') + +// https://docs.github.com/en/rest/reference/repos#list-repository-tags +// https://api.github.com/repos/livingdocsio/livingdocs-server/tags?access_token=1234 +// +// @return +// [ +// { +// "name": "v0.1", +// "commit": { +// "sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc", +// "url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc" +// } +// } +// ] +module.exports = async ({owner, repo, token, branch = 'main'}) => { + try { + const response = await request({ + uri: `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `token ${token}`, + 'User-Agent': 'Request-Promise' + }, + json: true + }) + + return response.object.sha + } catch (error) { + console.error('Error fetching branch SHA:', error.message) + throw error + } +} diff --git a/git/get-tags.js b/git/get-tags.js new file mode 100644 index 0000000..f3dabe5 --- /dev/null +++ b/git/get-tags.js @@ -0,0 +1,35 @@ +const request = require('request-promise') + +// https://docs.github.com/en/rest/reference/repos#list-repository-tags +// https://api.github.com/repos/livingdocsio/livingdocs-server/tags?access_token=1234 +// +// @return +// [ +// { +// "name": "v0.1", +// "commit": { +// "sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc", +// "url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc" +// } +// } +// ] +module.exports = async ({owner, repo, token, page = 1, perPage = 10}) => { + try { + return await request({ + uri: `https://api.github.com/repos/${owner}/${repo}/tags`, + qs: { + page, + per_page: perPage + }, + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `token ${token}`, + 'User-Agent': 'Request-Promise' + }, + json: true + }) + + } catch (error) { + throw error + } +} diff --git a/git/update-content.js b/git/update-content.js new file mode 100644 index 0000000..6ba8be9 --- /dev/null +++ b/git/update-content.js @@ -0,0 +1,24 @@ +const request = require('request-promise') + +// https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents +// +// @return +module.exports = async ({ + owner, repo, token, path, message, content, sha, branch +}) => { + try { + return await request({ + method: 'PUT', + uri: `https://api.github.com/repos/${owner}/${repo}/contents/${path}`, + body: {message, content, sha, branch}, + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `token ${token}`, + 'User-Agent': 'Request-Promise' + }, + json: true + }) + } catch (error) { + throw error + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..d0db44a --- /dev/null +++ b/index.js @@ -0,0 +1,176 @@ +const semver = require('semver') +// const gitGetTags = require('./git/get-tags') +const gitGetContent = require('./git/get-content') +const gitGetShaBranch = require('./git/get-sha-branch') +const gitCreateBranch = require('./git/create-branch') +const updateContent = require('./git/update-content') +const createPullRequest = require('./git/create-pull-request') +const createApprovalForPullRequest = require('./git/create-approval-for-pull-request') + +// main application +module.exports = async ({owner, repo, ghToken, ghApprovalToken, customer, infrastructurePath, env, releaseBranch, downstreamTag}) => { // eslint-disable-line max-len + const token = ghToken + const latestSha = await gitGetShaBranch({owner, repo, token}) + const downstreamTagWildcard = semver.coerce(downstreamTag).toString().replace(/\d+$/, 'x') // `4.20.11` -> `4.20.x` + const combinedChanges = [] + let lastCommit + + // create bump pr branch + const branchName = `${env}-${releaseBranch}` + console.log(`trying to create branch "${branchName}"`) + await gitCreateBranch({ + owner, + repo, + token, + ref: `refs/heads/${branchName}`, + sha: latestSha + }) + + // change the content in apps/${infrastructurePath}/${env}/flux/image-${customer}-editor.yaml + // replace: + // policy: + // semver: + // range: "x.x.x" + // with: + // policy: + // semver: + // range: "${downstreamTagWildcard}" + + const base64ObjEditor = await gitGetContent({ + owner, + repo, + token, + path: `apps/${infrastructurePath}/${env}/flux/image-${customer}-editor.yaml` + }) + + if (base64ObjEditor) { + const contentEditor = Buffer.from(base64ObjEditor.content, 'base64').toString() + const updatedContentEditor = contentEditor.replace(/range: ".*"/, `range: "${downstreamTagWildcard}"`) + const contentUpdateEditor = Buffer.from(updatedContentEditor).toString('base64') + combinedChanges.push({ + path: base64ObjEditor.path, + content: contentUpdateEditor, + sha: base64ObjEditor.sha + }) + } + + // // add commit + // const editorCommit = await updateContent({ + // owner, + // repo, + // token, + // path: base64ObjEditor.path, + // message: `chore(release-management): Bump editor version in ${env} for release management`, + // content: contentUpdateEditor, + // sha: base64ObjEditor.sha, + // branch: branchName + // }) + + // console.log(editorCommit) + // change the content in apps/${infrastructurePath}/${env}/flux/image-${customer}-server.yaml + + const base64ObjServer = await gitGetContent({ + owner, + repo, + token, + path: `apps/${infrastructurePath}/${env}/flux/image-${customer}-server.yaml`, + branch: branchName + }) + + if (base64ObjServer) { + const contentServer = Buffer.from(base64ObjServer.content, 'base64').toString() + const updatedContentServer = contentServer.replace(/range: ".*"/, `range: "${downstreamTagWildcard}"`) + const contentUpdateServer = Buffer.from(updatedContentServer).toString('base64') + combinedChanges.push({ + path: base64ObjServer.path, + content: contentUpdateServer, + sha: base64ObjServer.sha + }) + } + + if (env === 'prod') { + const base64ObjEditorStage = await gitGetContent({ + owner, + repo, + token, + path: `apps/${infrastructurePath}/stage/flux/image-${customer}-editor.yaml`, + branch: branchName + }) + + if (base64ObjEditorStage) { + const contentEditorStage = Buffer.from(base64ObjEditorStage.content, 'base64').toString() + const updatedContentEditorStage = contentEditorStage.replace(/range: ".*"/, `range: "x.x.x"`) + const contentUpdateEditorStage = Buffer.from(updatedContentEditorStage).toString('base64') + combinedChanges.push({ + path: base64ObjEditorStage.path, + content: contentUpdateEditorStage, + sha: base64ObjEditorStage.sha + }) + } + + const base64ObjServerStage = await gitGetContent({ + owner, + repo, + token, + path: `apps/${infrastructurePath}/stage/flux/image-${customer}-server.yaml`, + branch: branchName + }) + + if (base64ObjServerStage) { + const contentServerStage = Buffer.from(base64ObjServerStage.content, 'base64').toString() + const updatedContentServerStage = contentServerStage.replace(/range: ".*"/, `range: "x.x.x"`) + const contentUpdateServerStage = Buffer.from(updatedContentServerStage).toString('base64') + combinedChanges.push({ + path: base64ObjServerStage.path, + content: contentUpdateServerStage, + sha: base64ObjServerStage.sha + }) + } + } + + if (combinedChanges.length === 0) { + throw new Error('No files to update') + } + + for (const change of combinedChanges) { + lastCommit = await updateContent({ + owner, + repo, + token, + path: change.path, + message: `chore(release-management): Bump versions in ${env} on ${change.path}`, + content: change.content, + sha: change.sha, + branch: branchName + }) + } + + // create the bump pull request + const pullRequest = await createPullRequest({ + owner, + repo, + token, + title: `Bump versions in ${env} for release management`, + head: branchName, + base: 'main', + body: `## Motivation + +Bump editor and server versions for release management + ` + }) + + // auto approval for pull request + if (ghApprovalToken) { + await createApprovalForPullRequest({ + owner, + repo, + token: ghApprovalToken, + pullNumber: pullRequest.number, + commitId: lastCommit.commit.sha + }) + + // auto approve the pull request + } + + return pullRequest +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..554702f --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@livingdocs/create-infrastructure-pr", + "version": "0.0.0-development", + "description": "", + "main": "index.js", + "bin": "./cli.js", + "author": "Jordi Vives", + "license": "MIT", + "dependencies": { + "comment-json": "^4.2.3", + "lodash": "^4.17.4", + "request": "^2.88.0", + "request-promise": "^4.2.5", + "semver": "5.5.1", + "yargs": "^12.0.2" + }, + "scripts": { + "lint": "eslint .", + "semantic-release": "semantic-release", + "travis-deploy-once": "travis-deploy-once" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/livingdocsIO/create-infrastructure-pr.git" + }, + "devDependencies": { + "eslint": "^8.56.0", + "semantic-release": "^15.12.0", + "travis-deploy-once": "^5.0.9" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://github.com/livingdocsIO/create-infrastructure-pr/issues" + }, + "homepage": "https://github.com/livingdocsIO/create-infrastructure-pr#readme" +}