├── .gitignore ├── shots └── banner.png ├── .npmignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── package.json ├── LICENSE ├── CONTRIBUTING.md ├── README.md ├── CODE_OF_CONDUCT.md ├── main.mjs └── src └── helperFunctions.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | test/ -------------------------------------------------------------------------------- /shots/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhadmus/postPipe/HEAD/shots/banner.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | package-lock.json 4 | .vscode/ 5 | .DS_Store 6 | .idea/ 7 | safe/ 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postpipe", 3 | "version": "2.0.0", 4 | "description": "An helper bot for deploying API collections to pipelines", 5 | "main": "index.js", 6 | "bin":{ 7 | "postPipe": "main.mjs" 8 | }, 9 | "files": [ 10 | "main.mjs", 11 | "src/" 12 | ], 13 | "scripts": { 14 | "test": "postPipe" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/bhadmus/postPipe.git" 19 | }, 20 | "keywords": [ 21 | "postman", 22 | "github", 23 | "gitlab", 24 | "cli", 25 | "bitbucket" 26 | ], 27 | "author": "Ademola Bhadmus", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/bhadmus/postPipe/issues" 31 | }, 32 | "homepage": "https://github.com/bhadmus/postPipe#readme", 33 | "dependencies": { 34 | "child_process": "^1.0.2", 35 | "dotenv": "^16.4.5", 36 | "form-data": "4.0.0", 37 | "fs-extra": "^11.2.0", 38 | "inquirer": "^10.1.8", 39 | "node-fetch": "^3.3.2", 40 | "path": "^0.12.7" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ademola Bhadmus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Here's a basic `CONTRIBUTING.md` file that outlines how contributors can help with your project. You can modify this based on your project's specific needs. 2 | 3 | --- 4 | 5 | # Contributing to PostPipe 6 | 7 | Thank you for considering contributing to **PostPipe**! We appreciate your efforts in improving the project. Below are the guidelines to help you get started. 8 | 9 | ## Table of Contents 10 | 11 | 1. [Getting Started](#getting-started) 12 | 2. [How to Contribute](#how-to-contribute) 13 | - [Reporting Bugs](#reporting-bugs) 14 | - [Suggesting Enhancements](#suggesting-enhancements) 15 | - [Submitting Pull Requests](#submitting-pull-requests) 16 | 3. [Code Style](#code-style) 17 | 4. [License](#license) 18 | 19 | --- 20 | 21 | ## Getting Started 22 | 23 | 1. Fork the repository. 24 | 2. Clone your fork: 25 | ```bash 26 | git clone https://github.com/bhadmus/postPipe.git 27 | ``` 28 | 3. Install the project dependencies: 29 | ```bash 30 | npm install 31 | ``` 32 | 4. Create a new branch for your changes: 33 | ```bash 34 | git checkout -b feature-or-bugfix-branch 35 | ``` 36 | 37 | ## How to Contribute 38 | 39 | ### Reporting Bugs 40 | 41 | If you find a bug in the project, please open an issue with the following details: 42 | 43 | - A clear, concise description of the problem. 44 | - Steps to reproduce the issue. 45 | - Your environment (Node.js version, OS, etc.). 46 | - Any error logs or relevant screenshots. 47 | 48 | ### Suggesting Enhancements 49 | 50 | We welcome suggestions to improve the package! When suggesting an enhancement: 51 | 52 | - Explain the feature and how it improves the project. 53 | - Provide examples of how the feature might be used. 54 | 55 | ### Submitting Pull Requests 56 | 57 | 1. Fork and clone the repository as explained in [Getting Started](#getting-started). 58 | 2. Make your changes in a new branch. 59 | 3. Ensure that your code is well-documented and follows our [Code Style](#code-style) guidelines. 60 | 4. Write or update tests as necessary. 61 | 5. Ensure all tests pass: 62 | ```bash 63 | npm test 64 | ``` 65 | 6. Commit your changes: 66 | ```bash 67 | git commit -m "Brief description of the feature or fix" 68 | ``` 69 | 7. Push your changes: 70 | ```bash 71 | git push origin feature-or-bugfix-branch 72 | ``` 73 | 8. Open a pull request on the main repository. Clearly describe the changes and why they are necessary. 74 | 75 | ## Code Style 76 | 77 | - Use **ES6+** features. 78 | - Maintain **consistent indentation** and use spaces, not tabs. 79 | - Keep functions small and well-commented if they’re complex. 80 | - Write meaningful commit messages. 81 | 82 | 83 | 84 | ## License 85 | 86 | By contributing to this project, you agree that your contributions will be licensed under the same license as the project itself, which is [MIT](LICENSE). 87 | 88 | --- 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Image](shots/banner.png) 2 | # API Collection to CI 3 | 4 | 5 | ## Overview 6 | 7 | This project is a spin-off of this amazing [Python Package](https://pypi.org/project/postman-github-sync/) find the parent repository [here](https://github.com/BlazinArtemis/postman_to_github_actions). This brilliant SDET [Seyi 'nexus' Ajadi](https://github.com/BlazinArtemis) conceived the idea to ensure that engineers can focus on testing and creating collections while pipeline config will be handled via a simple Q&A. I merely expanded it to work on more pipelines. 8 | 9 | postPipe removes the hassle of manually uploading Postman collections, whether they are from a UID or a saved file, into your CI/CD pipeline on GitHub, GitLab, or Bitbucket. It automates the process of exporting collections, setting up the required YAML pipeline, and making the initial commit, allowing you to focus on development rather than CI configuration. 10 | 11 | ## Prerequisites 12 | To use postPipe, you will need: 13 | - Postman API Key. 14 | - **GitHub Setup:** GitHub Personal Access Token 15 | - **GitLab Setup:** GitLab Personal Access Token 16 | - **Bitbucket Setup:** Bitbucket App Password 17 | 18 | ## Retrieving Postman API KEY 19 | - Login to Postman Web or Desktop 20 | - Click on the desired collection 21 | - Click on **`Run`** 22 | - Check the **`Automate runs via CLI`** radio button 23 | - Click on **`Add API Key`** 24 | - Click **`Generate Key`** 25 | - Insert a name and click **`Generate`** 26 | - Copy the Postman API Key 27 | 28 | ## Creating and Retrieving Tokens and App Password 29 | ### GitHub 30 | - Login to GitHub 31 | - Click on your profile picture in the upper-right corner, then click **Settings.** 32 | - Scroll down and select **Developer settings.** 33 | - Click Personal access tokens on the left menu, then click classic tokens or fine grained tokens. 34 | - Click _"Generate new token"_, name it, set the expiration(or create a never expiring one). 35 | - Select scopes (like repo and workPipe, you can select all(recommended)). Click Generate token and copy the token. 36 | - Save the token somewhere secure as once lost, it cannot be retrieved 37 | ### GitLab 38 | - Login to GitLab 39 | - Click on your profile picture in the upper-left corner and select Edit profile or Preference. 40 | - Click **Access tokens** on the left side bar 41 | - Click on **Add new token** 42 | - Name it, set expiration, and check all boxes. 43 | - Save the token somewhere secure as once lost, it cannot be retrieved 44 | ### Bitbucket 45 | - Login to Bitbucket 46 | - Click the Settings icon on the upper-right corner 47 | - Click *Personal Bitbucket Settings* 48 | - Click **App passwords** 49 | - Name and create an app password and check all boxes 50 | - Save the token somewhere secure as once lost, it cannot be retrieved 51 | 52 | ## Environment Variables Setup 53 | ### Windows 54 | ``` 55 | setx GITHUB_TOKEN "your_github_token" 56 | setx POSTMAN_API_KEY "your_postman_api_key" 57 | ``` 58 | or 59 | ``` 60 | set GITHUB_TOKEN= 61 | set POSTMAN_API_KEY= 62 | ``` 63 | 64 | ### MacOS/Linux 65 | ``` 66 | export GITHUB_TOKEN="your_github_token" 67 | export POSTMAN_API_KEY="your_postman_api_key" 68 | ``` 69 | > [!NOTE] 70 | > For _**GitLab**_ and _**Bitbucket**_, replace **GITHUB** with **GITLAB** or **BITBUCKET** to have **GITLAB_TOKEN** or **BITBUCKET_TOKEN** 71 | 72 | ## Usage 73 | - `postPipe` to run generally whereby the user can select from the three pipelines 74 | - `postPipe --github` to run a GitHub setup pipeline. 75 | - `postPipe --gitlab` to run a GitLab setup pipeline. 76 | - `postPipe --bitbucket` to run a Bitbucket setup pipeline. 77 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /main.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from "fs-extra"; 4 | import path from "path"; 5 | import { fileURLToPath } from "url"; 6 | import fetch from "node-fetch"; 7 | import inquirer from "inquirer"; 8 | import { config } from "dotenv"; 9 | import { 10 | generateYaml, 11 | makeCommit, 12 | exportPostmanCollection, 13 | createRepo, 14 | verifyGithubActionsWorkflow, 15 | initializeRepoWithReadme, 16 | readmeExists, 17 | createAndInstallPackageJson, 18 | exportPostmanEnvironment, 19 | moveJsonFile, 20 | } from "./src/helperFunctions.mjs"; 21 | 22 | // Load environment variables 23 | config(); 24 | 25 | // Get the directory name of the current module 26 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 27 | 28 | // Use path.resolve to construct the correct file path 29 | const packageJsonPath = path.resolve(__dirname, "package.json"); 30 | 31 | // Create a root folder to collect the collection and environment 32 | 33 | const requestDir = [ 34 | path.join(process.cwd(), "endpoints", "collection"), 35 | path.join(process.cwd(), "endpoints", "environment"), 36 | ]; 37 | 38 | for (const dir of requestDir) { 39 | fs.ensureDir(dir); 40 | } 41 | 42 | // Read the package.json file 43 | const { version } = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); 44 | 45 | const args = process.argv.slice(2); 46 | 47 | if (args.includes("--version") || args.includes("-V")) { 48 | console.log(`Version: ${version}`); 49 | process.exit(0); 50 | } 51 | 52 | async function startProcess(versionChoice) { 53 | let versionToken; 54 | switch (versionChoice) { 55 | case "GitHub": 56 | versionToken = process.env.GITHUB_TOKEN; 57 | if (!versionToken) { 58 | const { token } = await inquirer.prompt({ 59 | type: "input", 60 | name: "token", 61 | message: "Enter your GitHub token:", 62 | }); 63 | versionToken = token; 64 | process.env.GITHUB_TOKEN = versionToken; 65 | fs.appendFileSync( 66 | path.resolve(process.env.HOME, ".bashrc"), 67 | `\nexport GITHUB_TOKEN=${versionToken}\n` 68 | ); 69 | } 70 | break; 71 | case "Gitlab": 72 | versionToken = process.env.GITLAB_TOKEN; 73 | if (!versionToken) { 74 | const { token } = await inquirer.prompt({ 75 | type: "input", 76 | name: "token", 77 | message: "Enter your Gitlab token:", 78 | }); 79 | versionToken = token; 80 | process.env.GITLAB_TOKEN = versionToken; 81 | fs.appendFileSync( 82 | path.resolve(process.env.HOME, ".bashrc"), 83 | `\nexport GITLAB_TOKEN=${versionToken}\n` 84 | ); 85 | } 86 | break; 87 | case "Bitbucket": 88 | versionToken = process.env.BITBUCKET_TOKEN; 89 | if (!versionToken) { 90 | const { token } = await inquirer.prompt({ 91 | type: "input", 92 | name: "token", 93 | message: "Enter your Bitbucket token:", 94 | }); 95 | versionToken = token; 96 | process.env.BITBUCKET_TOKEN = versionToken; 97 | fs.appendFileSync( 98 | path.resolve(process.env.HOME, ".bashrc"), 99 | `\nexport BITBUCKET_TOKEN=${versionToken}\n` 100 | ); 101 | } 102 | } 103 | 104 | const { environmentRequired } = await inquirer.prompt({ 105 | type: "confirm", 106 | name: "environmentRequired", 107 | message: "Does the Postman collection need an environment?", 108 | default: false, 109 | }); 110 | 111 | const { collectionSource } = await inquirer.prompt({ 112 | type: "list", 113 | name: "collectionSource", 114 | message: "Is the Postman collection from UID or filepath?", 115 | choices: ["UID", "filepath"], 116 | default: "UID", 117 | }); 118 | 119 | let collectionFilePath; 120 | let environmentFilePath; 121 | // let rootCollectionFilePath; 122 | // let rootEnvironmentFilePath; 123 | 124 | if (!environmentRequired) { 125 | if (collectionSource === "UID") { 126 | let postmanApiKey = process.env.POSTMAN_API_KEY; 127 | if (!postmanApiKey) { 128 | const { apiKey } = await inquirer.prompt({ 129 | type: "input", 130 | name: "apiKey", 131 | message: "Enter your Postman API key:", 132 | }); 133 | postmanApiKey = apiKey; 134 | process.env.POSTMAN_API_KEY = postmanApiKey; 135 | fs.appendFileSync( 136 | path.resolve(process.env.HOME, ".bashrc"), 137 | `\nexport POSTMAN_API_KEY=${postmanApiKey}\n` 138 | ); 139 | } 140 | const { collectionId } = await inquirer.prompt({ 141 | type: "input", 142 | name: "collectionId", 143 | message: "Enter the Postman collection ID:", 144 | }); 145 | collectionFilePath = path.join( 146 | process.cwd(), 147 | "endpoints", 148 | "collection", 149 | "collection.json" 150 | ); 151 | await exportPostmanCollection( 152 | postmanApiKey, 153 | collectionId, 154 | collectionFilePath 155 | ); 156 | } else if (collectionSource === "filepath") { 157 | const { filePath } = await inquirer.prompt({ 158 | type: "input", 159 | name: "filePath", 160 | message: "Enter the path to the Postman collection JSON file:", 161 | }); 162 | collectionFilePath = moveJsonFile( 163 | filePath, 164 | path.join(process.cwd(), "endpoints", "collection") 165 | ); 166 | } else { 167 | console.log("Invalid option. Exiting."); 168 | return; 169 | } 170 | } else { 171 | const { environmentSource } = await inquirer.prompt({ 172 | type: "list", 173 | name: "environmentSource", 174 | message: "Is the Postman environment from UID or filepath?", 175 | choices: ["UID", "filepath"], 176 | default: "UID", 177 | }); 178 | if (environmentSource === "UID" && collectionSource === "UID") { 179 | let postmanApiKey = process.env.POSTMAN_API_KEY; 180 | if (!postmanApiKey) { 181 | const { apiKey } = await inquirer.prompt({ 182 | type: "input", 183 | name: "apiKey", 184 | message: "Enter your Postman API key:", 185 | }); 186 | postmanApiKey = apiKey; 187 | process.env.POSTMAN_API_KEY = postmanApiKey; 188 | fs.appendFileSync( 189 | path.resolve(process.env.HOME, ".bashrc"), 190 | `\nexport POSTMAN_API_KEY=${postmanApiKey}\n` 191 | ); 192 | } 193 | const { collectionId } = await inquirer.prompt({ 194 | type: "input", 195 | name: "collectionId", 196 | message: "Enter the Postman collection ID:", 197 | }); 198 | collectionFilePath = path.join( 199 | process.cwd(), 200 | "endpoints", 201 | "collection", 202 | "collection.json" 203 | ); 204 | const { environmentId } = await inquirer.prompt({ 205 | type: "input", 206 | name: "environmentId", 207 | message: "Enter the Postman environment ID:", 208 | }); 209 | environmentFilePath = path.join( 210 | process.cwd(), 211 | "endpoints", 212 | "collection", 213 | "environment.json" 214 | ); 215 | await exportPostmanCollection( 216 | postmanApiKey, 217 | collectionId, 218 | collectionFilePath 219 | ); 220 | await exportPostmanEnvironment( 221 | postmanApiKey, 222 | environmentId, 223 | environmentFilePath 224 | ); 225 | } else if ( 226 | environmentSource === "filepath" && 227 | collectionSource === "filepath" 228 | ) { 229 | const { filePath } = await inquirer.prompt({ 230 | type: "input", 231 | name: "filePath", 232 | message: "Enter the path to the Postman collection JSON file:", 233 | }); 234 | collectionFilePath = moveJsonFile(filePath, path.join(process.cwd(), 'endpoints', 'collection')); 235 | const { envPath } = await inquirer.prompt({ 236 | type: "input", 237 | name: "envPath", 238 | message: "Enter the path to the Postman environment JSON file:", 239 | }); 240 | environmentFilePath = moveJsonFile(envPath, path.join(process.cwd(), 'endpoints', 'environment')); 241 | } else if (environmentSource === "filepath" && collectionSource === "UID") { 242 | let postmanApiKey = process.env.POSTMAN_API_KEY; 243 | if (!postmanApiKey) { 244 | const { apiKey } = await inquirer.prompt({ 245 | type: "input", 246 | name: "apiKey", 247 | message: "Enter your Postman API key:", 248 | }); 249 | postmanApiKey = apiKey; 250 | process.env.POSTMAN_API_KEY = postmanApiKey; 251 | fs.appendFileSync( 252 | path.resolve(process.env.HOME, ".bashrc"), 253 | `\nexport POSTMAN_API_KEY=${postmanApiKey}\n` 254 | ); 255 | } 256 | const { collectionId } = await inquirer.prompt({ 257 | type: "input", 258 | name: "collectionId", 259 | message: "Enter the Postman collection ID:", 260 | }); 261 | collectionFilePath = path.join( 262 | process.cwd(), 263 | "endpoints", 264 | "collection", 265 | "collection.json" 266 | ); 267 | const { envPath } = await inquirer.prompt({ 268 | type: "input", 269 | name: "envPath", 270 | message: "Enter the path to the Postman environment JSON file:", 271 | }); 272 | environmentFilePath = moveJsonFile(envPath, path.join(process.cwd(), 'endpoints', 'environment')); 273 | await exportPostmanCollection( 274 | postmanApiKey, 275 | collectionId, 276 | collectionFilePath 277 | ); 278 | } else if (environmentSource === "UID" && collectionSource === "filepath") { 279 | let postmanApiKey = process.env.POSTMAN_API_KEY; 280 | if (!postmanApiKey) { 281 | const { apiKey } = await inquirer.prompt({ 282 | type: "input", 283 | name: "apiKey", 284 | message: "Enter your Postman API key:", 285 | }); 286 | postmanApiKey = apiKey; 287 | process.env.POSTMAN_API_KEY = postmanApiKey; 288 | fs.appendFileSync( 289 | path.resolve(process.env.HOME, ".bashrc"), 290 | `\nexport POSTMAN_API_KEY=${postmanApiKey}\n` 291 | ); 292 | } 293 | const { filePath } = await inquirer.prompt({ 294 | type: "input", 295 | name: "filePath", 296 | message: "Enter the path to the Postman collection JSON file:", 297 | }); 298 | collectionFilePath = moveJsonFile(filePath, path.join(process.cwd(), 'endpoints', 'collection')); 299 | const { environmentId } = await inquirer.prompt({ 300 | type: "input", 301 | name: "environmentId", 302 | message: "Enter the Postman environment ID:", 303 | }); 304 | environmentFilePath = path.join( 305 | process.cwd(), 306 | "endpoints", 307 | "collection", 308 | "environment.json" 309 | ); 310 | await exportPostmanEnvironment( 311 | postmanApiKey, 312 | environmentId, 313 | environmentFilePath 314 | ); 315 | } 316 | } 317 | 318 | if (!fs.existsSync(collectionFilePath)) { 319 | throw new Error("Collection file not found. Exiting."); 320 | } 321 | 322 | const { repoChoice } = await inquirer.prompt({ 323 | type: "list", 324 | name: "repoChoice", 325 | message: "Is it an existing or new repository?", 326 | choices: ["existing", "new"], 327 | default: "new", 328 | }); 329 | 330 | let repoFullName; 331 | if (repoChoice === "existing") { 332 | const { repoName } = await inquirer.prompt({ 333 | type: "input", 334 | name: "repoName", 335 | message: "Enter the full repository name (e.g., username/repo):", 336 | }); 337 | repoFullName = repoName; 338 | switch (versionChoice) { 339 | case "GitHub": 340 | case "Gitlab": 341 | case "Bitbucket": 342 | if (!(await readmeExists(versionChoice, versionToken, repoFullName))) { 343 | if ( 344 | !(await initializeRepoWithReadme( 345 | versionChoice, 346 | versionToken, 347 | repoFullName 348 | )) 349 | ) { 350 | return; 351 | } 352 | } 353 | break; 354 | } 355 | } else if (repoChoice === "new") { 356 | const { repoName } = await inquirer.prompt({ 357 | type: "input", 358 | name: "repoName", 359 | message: "Enter the repository name:", 360 | }); 361 | repoFullName = await createRepo(versionChoice, versionToken, repoName); 362 | if (!repoFullName) { 363 | console.log("Failed to create the repository. Exiting."); 364 | return; 365 | } 366 | if ( 367 | !(await initializeRepoWithReadme( 368 | versionChoice, 369 | versionToken, 370 | repoFullName 371 | )) 372 | ) { 373 | return; 374 | } 375 | } else { 376 | console.log("Invalid option. Exiting."); 377 | return; 378 | } 379 | 380 | let outputYamlFilePath; 381 | switch (versionChoice) { 382 | case "GitHub": 383 | const githubDir = path.resolve(process.cwd(), ".github", "workflows"); 384 | fs.ensureDirSync(githubDir); 385 | outputYamlFilePath = path.resolve(githubDir, "postman-tests.yml"); 386 | break; 387 | case "Gitlab": 388 | outputYamlFilePath = path.resolve(process.cwd(), ".gitlab-ci.yml"); 389 | break; 390 | case "Bitbucket": 391 | outputYamlFilePath = path.resolve( 392 | process.cwd(), 393 | "bitbucket-pipelines.yml" 394 | ); 395 | } 396 | const relativeCollectionPath = path.relative(process.cwd(), collectionFilePath).replace(/\\/g, "/"); 397 | const relativeEnvironmentPath = environmentFilePath 398 | ? path.relative(process.cwd(), environmentFilePath).replace(/\\/g, "/") 399 | : null; 400 | 401 | generateYaml( 402 | versionChoice, 403 | relativeCollectionPath, 404 | relativeEnvironmentPath, 405 | outputYamlFilePath 406 | ); 407 | 408 | if (!fs.existsSync(outputYamlFilePath)) { 409 | throw new Error("YAML file not found. Exiting."); 410 | } 411 | 412 | const projectDir = process.cwd(); 413 | createAndInstallPackageJson(projectDir, { collectionFilePath }); 414 | 415 | if ( 416 | !fs.existsSync(path.resolve(projectDir, "package.json")) || 417 | !fs.existsSync(path.resolve(projectDir, "package-lock.json")) 418 | ) { 419 | throw new Error("package.json or package-lock.json not found. Exiting."); 420 | } 421 | 422 | let commitFiles; 423 | if (environmentFilePath) { 424 | commitFiles = [ 425 | outputYamlFilePath, 426 | collectionFilePath, 427 | environmentFilePath, 428 | path.resolve(projectDir, "package.json"), 429 | path.resolve(projectDir, "package-lock.json"), 430 | ]; 431 | } else { 432 | commitFiles = [ 433 | outputYamlFilePath, 434 | collectionFilePath, 435 | path.resolve(projectDir, "package.json"), 436 | path.resolve(projectDir, "package-lock.json"), 437 | ]; 438 | } 439 | const commitMessage = "Create Pipeline Config"; 440 | await makeCommit( 441 | versionChoice, 442 | versionToken, 443 | repoFullName, 444 | commitFiles, 445 | commitMessage 446 | ); 447 | 448 | if (versionChoice === "GitHub") { 449 | await new Promise((resolve) => setTimeout(resolve, 10000)); 450 | 451 | const workflowCreated = await verifyGithubActionsWorkflow(repoFullName); 452 | if (workflowCreated) { 453 | console.log("GitHub Actions workflow was successfully created."); 454 | } else { 455 | console.log("Failed to create GitHub Actions workflow."); 456 | } 457 | } 458 | } 459 | 460 | async function main() { 461 | try { 462 | let versionChoice; 463 | if (args.includes("--github")) { 464 | versionChoice = "GitHub"; 465 | } else if (args.includes("--gitlab")) { 466 | versionChoice = "Gitlab"; 467 | } else if (args.includes("--bitbucket")) { 468 | versionChoice = "Bitbucket"; 469 | } 470 | 471 | if (versionChoice) { 472 | await startProcess(versionChoice); 473 | } else { 474 | const { versionChoice: chosenVersionChoice } = await inquirer.prompt({ 475 | type: "list", 476 | name: "versionChoice", 477 | message: "Which version control tool do you need?", 478 | choices: ["GitHub", "Bitbucket", "Gitlab"], 479 | default: "GitHub", 480 | }); 481 | await startProcess(chosenVersionChoice); 482 | } 483 | } catch (error) { 484 | console.error("An error occurred:", error.message); 485 | } 486 | } 487 | 488 | main().catch((error) => console.error(error)); 489 | -------------------------------------------------------------------------------- /src/helperFunctions.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fetch from "node-fetch"; 4 | import fs from "fs-extra"; 5 | import path from "path"; 6 | import { execSync } from "child_process"; 7 | import inquirer from "inquirer"; 8 | import FormData from "form-data"; 9 | 10 | /** 11 | * Create a package.json file with specified dependencies and install them. 12 | * @param {string} dirPath - Directory where the package.json will be created. 13 | * @param {Object} dependencies - Dependencies to include in package.json. 14 | */ 15 | export function createAndInstallPackageJson(dirPath, dependencies) { 16 | const packageJsonPath = path.join(dirPath, "package.json"); 17 | 18 | // Create package.json content 19 | const packageJsonContent = { 20 | name: "postman-collection-runner", 21 | version: "1.0.0", 22 | description: 23 | "A Node.js project for running Postman collections using Newman", 24 | main: "index.js", 25 | scripts: { 26 | test: `npx newman run ${dependencies.collectionFilePath}`, 27 | }, 28 | dependencies: { 29 | newman: "6.1.3", 30 | "newman-reporter-htmlextra": "1.23.1", 31 | }, 32 | }; 33 | 34 | // Write package.json to the specified directory 35 | fs.writeFileSync( 36 | packageJsonPath, 37 | JSON.stringify(packageJsonContent, null, 2), 38 | "utf8" 39 | ); 40 | console.log("package.json created successfully."); 41 | 42 | // Install the dependencies 43 | execSync("npm install", { cwd: dirPath, stdio: "inherit" }); 44 | 45 | console.log("Dependencies installed successfully."); 46 | } 47 | 48 | export function moveJsonFile(sourceFilePath, targetDir) { 49 | try { 50 | // Check if sourceFilePath is defined 51 | if (!sourceFilePath) { 52 | throw new Error("Source file path is undefined."); 53 | } 54 | 55 | // Debug: Log the input sourceFilePath 56 | console.log("Input sourceFilePath:", sourceFilePath); 57 | 58 | // Resolve the source file path 59 | const resolvedSourceFilePath = path.normalize(sourceFilePath); 60 | console.log("Resolved source file path:", resolvedSourceFilePath); 61 | 62 | // Ensure the source file exists and is a JSON file 63 | if (!fs.existsSync(resolvedSourceFilePath)) { 64 | throw new Error(`Source file does not exist: ${resolvedSourceFilePath}`); 65 | } 66 | if (path.extname(resolvedSourceFilePath).toLowerCase() !== ".json") { 67 | throw new Error(`Source file is not a JSON file: ${resolvedSourceFilePath}`); 68 | } 69 | 70 | // Ensure the target directory exists 71 | try { 72 | fs.ensureDirSync(targetDir); 73 | console.log("Target directory created or already exists:", targetDir); 74 | } catch (error) { 75 | throw new Error(`Failed to create target directory: ${error.message}`); 76 | } 77 | 78 | // Extract the filename from the source path 79 | let fileName; 80 | try { 81 | fileName = path.basename(resolvedSourceFilePath); 82 | console.log("File name:", fileName); 83 | } catch (error) { 84 | throw new Error(`Failed to extract filename: ${error.message}`); 85 | } 86 | 87 | // Construct the target file path 88 | let targetFilePath; 89 | try { 90 | targetFilePath = path.join(targetDir, fileName); 91 | console.log("Target file path:", targetFilePath); 92 | } catch (error) { 93 | throw new Error(`Failed to construct target file path: ${error.message}`); 94 | } 95 | 96 | // Move the file 97 | try { 98 | fs.moveSync(resolvedSourceFilePath, targetFilePath, { overwrite: true }); 99 | console.log(`File moved successfully to: ${targetFilePath}`); 100 | } catch (error) { 101 | throw new Error(`Failed to move the file: ${error.message}`); 102 | } 103 | 104 | // Verify the file was moved 105 | if (fs.existsSync(targetFilePath)) { 106 | console.log("File verification successful."); 107 | return targetFilePath; // Return the path of the moved file 108 | } else { 109 | throw new Error("Failed to verify the moved file."); 110 | } 111 | } catch (error) { 112 | console.error("An error occurred while moving the file:", error.message); 113 | throw error; // Rethrow the error if you want the calling code to handle it 114 | } 115 | } 116 | 117 | /** 118 | * Generate a specific YAML file for running Postman tests. 119 | * @param {string} versionChoice - Version Control tool type. 120 | * @param {string} collectionFilePath - Path to the Postman collection JSON file. 121 | * @param {string} environmentFilePath - Path to the Postman environment JSON file. 122 | * @param {string} outputYamlFilePath - Path where the YAML file should be saved. 123 | */ 124 | 125 | 126 | export function generateYaml( 127 | versionChoice, 128 | collectionFilePath, 129 | environmentFilePath, 130 | outputYamlFilePath 131 | ) { 132 | const outputDir = path.dirname(outputYamlFilePath); 133 | fs.ensureDirSync(outputDir); 134 | /** SOLUTION 135 | * CREATE A ROOT FOLDER FOR WHAT'S TO BE CONVERTED 136 | * EXTRACT THE COLLECTION INTO THE NEW JSON FILE IN A SPECIFIED PLACE IN THE ROOT FOLDER 137 | * THAT FORMS THE COLLECTION FILE PATH. SAME MODEL FOR ENVIRONMENT FILE PATH AS WELL. 138 | * IN SHAA ALLAH 139 | */ 140 | 141 | let yamlFileContent; 142 | switch (versionChoice) { 143 | case "GitHub": 144 | if (!environmentFilePath) { 145 | yamlFileContent = ` 146 | 147 | name: Run Postman Collection 148 | 149 | on: [push] 150 | 151 | jobs: 152 | run-postman-collection: 153 | runs-on: ubuntu-latest 154 | 155 | steps: 156 | - name: Checkout repository 157 | uses: actions/checkout@v4 158 | 159 | - name: Set up node 160 | uses: actions/setup-node@v3 161 | with: 162 | node-version: '18' 163 | 164 | - name: Install Dependencies 165 | run: | 166 | npm install 167 | echo "check dependencies version" 168 | npx newman --version 169 | npx newman-reporter-htmlextra --version 170 | 171 | - name: Run Postman collection 172 | run: | 173 | echo "create report directory" 174 | mkdir -p ./newman 175 | npx newman run ${collectionFilePath} -r cli,htmlextra --reporter-htmlextra-export ./newman/results.html 176 | 177 | - name: Upload Test Results 178 | uses: actions/upload-artifact@v4 179 | with: 180 | name: postman-test-results 181 | path: ./newman/results.html 182 | `; 183 | } else { 184 | yamlFileContent = ` 185 | 186 | name: Run Postman Collection 187 | 188 | on: [push] 189 | 190 | jobs: 191 | run-postman-collection: 192 | runs-on: ubuntu-latest 193 | 194 | steps: 195 | - name: Checkout repository 196 | uses: actions/checkout@v4 197 | 198 | - name: Set up node 199 | uses: actions/setup-node@v3 200 | with: 201 | node-version: '18' 202 | 203 | - name: Install Dependencies 204 | run: | 205 | npm install 206 | echo "check dependencies version" 207 | npx newman --version 208 | npx newman-reporter-htmlextra --version 209 | 210 | - name: Run Postman collection 211 | run: | 212 | echo "create report directory" 213 | mkdir -p ./newman 214 | npx newman run ${collectionFilePath} -e ${environmentFilePath} -r cli,htmlextra --reporter-htmlextra-export ./newman/results.html 215 | 216 | - name: Upload Test Results 217 | uses: actions/upload-artifact@v4 218 | with: 219 | name: postman-test-results 220 | path: ./newman/results.html 221 | `; 222 | } 223 | 224 | console.log( 225 | `Generating GitHub Actions YAML file at ${outputYamlFilePath} successfully` 226 | ); 227 | fs.writeFileSync(outputYamlFilePath, yamlFileContent, "utf8"); 228 | console.log("YAML file generated successfully."); 229 | break; 230 | 231 | case "Gitlab": 232 | if (!environmentFilePath) { 233 | yamlFileContent = ` 234 | stages: 235 | - test 236 | 237 | pipeline: 238 | image: node:latest 239 | stage: test 240 | script: 241 | # Install from package.json 242 | - npm i 243 | - npx newman run ${collectionFilePath} -r cli,htmlextra --reporter-htmlextra-export ./newman/results.html 244 | 245 | artifacts: 246 | paths: 247 | - ./newman/results.html 248 | `; 249 | } else { 250 | yamlFileContent = ` 251 | stages: 252 | - test 253 | 254 | pipeline: 255 | image: node:latest 256 | stage: test 257 | script: 258 | # Install from package.json 259 | - npm i 260 | - npx newman run ${collectionFilePath} -e ${environmentFilePath} -r cli,htmlextra --reporter-htmlextra-export ./newman/results.html 261 | 262 | artifacts: 263 | paths: 264 | - ./newman/results.html 265 | `; 266 | } 267 | 268 | console.log( 269 | `Generating Gitlab YAML file at ${outputYamlFilePath} successfully` 270 | ); 271 | fs.writeFileSync(outputYamlFilePath, yamlFileContent, "utf8"); 272 | console.log("YAML file generated successfully."); 273 | 274 | break; 275 | 276 | case "Bitbucket": 277 | if (!environmentFilePath) { 278 | yamlFileContent = ` 279 | 280 | image: node:latest 281 | 282 | pipelines: 283 | default: 284 | - step: 285 | script: 286 | - npm i 287 | - mkdir -p ./newman 288 | - npx newman --version 289 | - npx newman run ${collectionFilePath} -r cli,htmlextra --reporter-htmlextra-export ./newman/results.html 290 | 291 | artifacts: 292 | - newman/results.html 293 | 294 | 295 | `; 296 | } else { 297 | yamlFileContent = ` 298 | 299 | image: postman/newman 300 | 301 | pipelines: 302 | default: 303 | - step: 304 | script: 305 | - npm i 306 | - mkdir -p ./newman 307 | - newman --version 308 | - npx newman run ${collectionFilePath} -e ${environmentFilePath} -r cli,htmlextra --reporter-htmlextra-export ./newman/results.html 309 | 310 | artifacts: 311 | - newman/results.html 312 | `; 313 | } 314 | 315 | console.log( 316 | `Generating Gitlab YAML file at ${outputYamlFilePath} successfully` 317 | ); 318 | fs.writeFileSync(outputYamlFilePath, yamlFileContent, "utf8"); 319 | console.log("YAML file generated successfully."); 320 | } 321 | } 322 | 323 | /** 324 | * Commit multiple files to the specified repository in a single commit. 325 | * @param {string} versionChoice - Version Control tool type. 326 | * @param {string} token - GitHub personal access token. 327 | * @param {string} repoFullName - Full repository name (e.g., username/repo). 328 | * @param {Array} files - List of file paths to commit. 329 | * @param {string} message - Commit message. 330 | */ 331 | 332 | export async function makeCommit( 333 | versionChoice, 334 | token, 335 | repoFullName, 336 | files, 337 | message 338 | ) { 339 | switch (versionChoice) { 340 | case "GitHub": 341 | try { 342 | const headers = { 343 | Authorization: `token ${token}`, 344 | Accept: "application/vnd.github.v3+json", 345 | }; 346 | 347 | const branch = "main"; 348 | const fileBlobs = []; 349 | 350 | // Process each file to be committed 351 | for (const file of files) { 352 | // Ensure the file exists 353 | if (!fs.existsSync(file)) { 354 | throw new Error(`File not found: ${file}`); 355 | } 356 | 357 | // Read file content as base64 358 | const content = fs.readFileSync(file, "base64"); 359 | 360 | // Normalize the path and ensure it uses forward slashes 361 | const repoFilePath = path.relative(process.cwd(), file).replace(/\\/g, "/"); 362 | 363 | // Validate the path to ensure it doesn't contain illegal characters 364 | if (!/^[a-zA-Z0-9_\-./]+$/.test(repoFilePath)) { 365 | throw new Error(`Invalid file path: ${repoFilePath}`); 366 | } 367 | 368 | console.log(`Preparing to commit file: ${repoFilePath}`); // Debugging 369 | 370 | // Add the file to the list of blobs to be committed 371 | fileBlobs.push({ 372 | path: repoFilePath, 373 | content: content, 374 | }); 375 | } 376 | 377 | // Fetch the latest commit SHA for the branch 378 | const refResponse = await fetch( 379 | `https://api.github.com/repos/${repoFullName}/git/refs/heads/${branch}`, 380 | { headers } 381 | ); 382 | const refData = await refResponse.json(); 383 | if (!refResponse.ok) { 384 | throw new Error(`Failed to fetch reference: ${refData.message}`); 385 | } 386 | const latestCommitSha = refData.object.sha; 387 | 388 | // Fetch the tree SHA of the latest commit 389 | const commitResponse = await fetch( 390 | `https://api.github.com/repos/${repoFullName}/git/commits/${latestCommitSha}`, 391 | { headers } 392 | ); 393 | const commitData = await commitResponse.json(); 394 | if (!commitResponse.ok) { 395 | throw new Error(`Failed to fetch commit: ${commitData.message}`); 396 | } 397 | const treeSha = commitData.tree.sha; 398 | 399 | // Create a new tree with the updated files 400 | const treeResponse = await fetch( 401 | `https://api.github.com/repos/${repoFullName}/git/trees`, 402 | { 403 | method: "POST", 404 | headers, 405 | body: JSON.stringify({ 406 | base_tree: treeSha, 407 | tree: fileBlobs.map((file) => ({ 408 | path: file.path, 409 | mode: "100644", // File mode (100644 for normal files) 410 | type: "blob", // Type of object (blob for files) 411 | content: Buffer.from(file.content, "base64").toString("utf8"), // Decode base64 content 412 | })), 413 | }), 414 | } 415 | ); 416 | const treeData = await treeResponse.json(); 417 | if (!treeResponse.ok) { 418 | throw new Error(`Failed to create tree: ${treeData.message}`); 419 | } 420 | 421 | // Create a new commit 422 | const newCommitResponse = await fetch( 423 | `https://api.github.com/repos/${repoFullName}/git/commits`, 424 | { 425 | method: "POST", 426 | headers, 427 | body: JSON.stringify({ 428 | message: message, // Commit message 429 | tree: treeData.sha, // SHA of the new tree 430 | parents: [latestCommitSha], // Parent commit SHA 431 | }), 432 | } 433 | ); 434 | const newCommitData = await newCommitResponse.json(); 435 | if (!newCommitResponse.ok) { 436 | throw new Error(`Failed to create commit: ${newCommitData.message}`); 437 | } 438 | 439 | // Update the branch to point to the new commit 440 | const updateResponse = await fetch( 441 | `https://api.github.com/repos/${repoFullName}/git/refs/heads/${branch}`, 442 | { 443 | method: "PATCH", 444 | headers, 445 | body: JSON.stringify({ 446 | sha: newCommitData.sha, // SHA of the new commit 447 | }), 448 | } 449 | ); 450 | const updateData = await updateResponse.json(); 451 | if (!updateResponse.ok) { 452 | throw new Error(`Failed to update branch: ${updateData.message}`); 453 | } 454 | 455 | console.log(`Commit successfully created: ${newCommitData.sha}`); 456 | } catch (error) { 457 | console.error( 458 | "An error occurred while committing files to GitHub:", 459 | error.message 460 | ); 461 | throw error; 462 | } 463 | break; 464 | 465 | case "Gitlab": 466 | try { 467 | const headers = { 468 | Authorization: `Bearer ${token}`, 469 | "Content-Type": "application/json", 470 | Accept: "application/vnd.gitlab.v4+json", 471 | }; 472 | 473 | // Step 1: Get the authenticated user's ID 474 | const userResponse = await fetch("https://gitlab.com/api/v4/user", { 475 | headers, 476 | }); 477 | const userData = await userResponse.json(); 478 | 479 | if (!userResponse.ok) { 480 | throw new Error(`Failed to fetch user ID: ${userData.message}`); 481 | } 482 | 483 | const userId = userData.id; 484 | 485 | // Step 2: Get the project ID based on the repoFullName 486 | const projectsResponse = await fetch( 487 | `https://gitlab.com/api/v4/users/${userId}/projects`, 488 | { headers } 489 | ); 490 | const projectsData = await projectsResponse.json(); 491 | 492 | if (!projectsResponse.ok) { 493 | throw new Error( 494 | `Failed to fetch projects for user ID ${userId}: ${projectsData.message}` 495 | ); 496 | } 497 | 498 | // Find the project that matches the repoFullName 499 | const project = projectsData.find( 500 | (proj) => proj.path_with_namespace === repoFullName 501 | ); 502 | 503 | if (!project) { 504 | throw new Error( 505 | `Project ${repoFullName} not found for user ID ${userId}` 506 | ); 507 | } 508 | 509 | const projectId = project.id; 510 | const branch = "main"; 511 | 512 | // Step 3: Prepare the actions for the commit with existence check 513 | const actions = await Promise.all( 514 | files.map(async (file) => { 515 | const repoFilePath = path 516 | .relative(process.cwd(), file) 517 | .replace(/\\/g, "/"); 518 | 519 | // Check if the file exists in the repository 520 | const fileExistsResponse = await fetch( 521 | `https://gitlab.com/api/v4/projects/${projectId}/repository/files/${encodeURIComponent( 522 | repoFilePath 523 | )}/raw?ref=${branch}`, 524 | { headers } 525 | ); 526 | 527 | const content = fs.readFileSync(file, "utf8"); 528 | 529 | // Determine the correct action based on the file's existence 530 | const actionType = fileExistsResponse.ok ? "update" : "create"; 531 | 532 | return { 533 | action: actionType, 534 | file_path: repoFilePath, 535 | content: content, 536 | }; 537 | }) 538 | ); 539 | 540 | // Step 4: Create the commit with the updated files 541 | const commitResponse = await fetch( 542 | `https://gitlab.com/api/v4/projects/${projectId}/repository/commits`, 543 | { 544 | method: "POST", 545 | headers, 546 | body: JSON.stringify({ 547 | branch, 548 | commit_message: message, 549 | actions, 550 | }), 551 | } 552 | ); 553 | 554 | const commitData = await commitResponse.json(); 555 | 556 | if (!commitResponse.ok) { 557 | throw new Error(`Failed to create commit: ${commitData.message}`); 558 | } 559 | 560 | console.log(`Commit successfully created: ${commitData.id}`); 561 | } catch (error) { 562 | console.error( 563 | "An error occurred while committing files to GitLab:", 564 | error.message 565 | ); 566 | throw error; 567 | } 568 | break; 569 | 570 | case "Bitbucket": 571 | try { 572 | const authHeader = `Basic ${Buffer.from( 573 | `${username}:${token}` 574 | ).toString("base64")}`; 575 | const baseUrl = `https://api.bitbucket.org/2.0/repositories/${repoFullName}`; 576 | const branch = "main"; 577 | 578 | // Step 1: Check if the pipeline configuration file exists 579 | const pipelineFileUrl = `${baseUrl}/src/${branch}/bitbucket-pipelines.yml`; 580 | let pipelineFileResponse = await fetch(pipelineFileUrl, { 581 | headers: { 582 | Authorization: authHeader, 583 | Accept: "application/json", 584 | }, 585 | }); 586 | 587 | if (pipelineFileResponse.status === 404) { 588 | console.warn(`No Commits Yet. Enabling Bitbucket pipelines.`); 589 | 590 | // Enable pipelines if the configuration file does not exist 591 | const pipelineEnableUrl = `${baseUrl}/pipelines_config`; 592 | const enablePipelineResponse = await fetch(pipelineEnableUrl, { 593 | method: "PUT", 594 | headers: { 595 | "Content-Type": "application/json", 596 | Authorization: authHeader, 597 | }, 598 | body: JSON.stringify({ enabled: true }), 599 | }); 600 | 601 | if (!enablePipelineResponse.ok) { 602 | const errorText = await enablePipelineResponse.text(); 603 | throw new Error(`Failed to enable pipelines: ${errorText}`); 604 | } 605 | } else if (!pipelineFileResponse.ok) { 606 | const errorText = await pipelineFileResponse.text(); 607 | throw new Error( 608 | `Failed to check pipeline configuration file: ${errorText}` 609 | ); 610 | } 611 | 612 | // Step 2: Fetch the latest commit SHA for the branch 613 | const branchResponse = await fetch( 614 | `${baseUrl}/refs/branches/${branch}`, 615 | { 616 | headers: { 617 | Authorization: authHeader, 618 | Accept: "application/json", 619 | }, 620 | } 621 | ); 622 | 623 | if (!branchResponse.ok) { 624 | const errorText = await branchResponse.text(); 625 | throw new Error(`Failed to fetch branch: ${errorText}`); 626 | } 627 | 628 | const branchData = await branchResponse.json(); 629 | const latestCommitSha = branchData.target.hash; 630 | 631 | // Step 3: Prepare form-data for the commit 632 | const form = new FormData(); 633 | form.append("message", message); // Commit message 634 | form.append("branch", branch); // Branch to commit to 635 | form.append("parents", latestCommitSha); // Parent commit SHA 636 | 637 | // Add each file to the form-data 638 | for (const file of files) { 639 | const content = fs.readFileSync(file); // Read file content as buffer 640 | const repoFilePath = path 641 | .relative(process.cwd(), file) 642 | .replace(/\\/g, "/"); // Convert to relative path with forward slashes 643 | form.append(repoFilePath, content, { 644 | filename: repoFilePath, 645 | contentType: "application/octet-stream", // MIME type for binary data 646 | }); 647 | } 648 | 649 | // Step 4: Send the commit request 650 | const commitResponse = await fetch(`${baseUrl}/src`, { 651 | method: "POST", 652 | headers: { 653 | Authorization: authHeader, 654 | ...form.getHeaders(), // Headers required for multipart/form-data 655 | }, 656 | body: form, // Multipart form-data body 657 | }); 658 | 659 | // Step 5: Check for response and log accordingly 660 | if (!commitResponse.ok) { 661 | const errorText = await commitResponse.text(); 662 | throw new Error(`Failed to create commit: ${errorText}`); 663 | } else { 664 | console.log(`Commit successfully created`); 665 | } 666 | } catch (error) { 667 | console.error( 668 | "An error occurred while committing files to Bitbucket:", 669 | error.message 670 | ); 671 | throw error; 672 | } 673 | break; 674 | 675 | default: 676 | throw new Error("Unsupported version control platform"); 677 | } 678 | } 679 | 680 | /** 681 | * Export a Postman collection to a file. 682 | * @param {string} apiKey - Postman API key. 683 | * @param {string} collectionId - Postman collection ID. 684 | * @param {string} outputFilePath - Path where the JSON file should be saved. 685 | */ 686 | export async function exportPostmanCollection( 687 | apiKey, 688 | collectionId, 689 | outputFilePath 690 | ) { 691 | try { 692 | console.log( 693 | `Exporting Postman collection ${collectionId} to ${outputFilePath}` 694 | ); 695 | const response = await fetch( 696 | `https://api.getpostman.com/collections/${collectionId}`, 697 | { 698 | headers: { 699 | "X-Api-Key": apiKey, 700 | }, 701 | } 702 | ); 703 | const data = await response.json(); 704 | if (!response.ok) { 705 | throw new Error(`Failed to export Postman collection: ${data.error}`); 706 | } 707 | fs.writeFileSync(outputFilePath, JSON.stringify(data, null, 2), "utf8"); 708 | console.log("Postman collection exported successfully."); 709 | } catch (error) { 710 | console.error( 711 | "An error occurred while exporting the Postman collection:", 712 | error.message 713 | ); 714 | throw error; 715 | } 716 | } 717 | /** 718 | * Export a Postman collection to a file. 719 | * @param {string} apiKey - Postman API key. 720 | * @param {string} environmentId - Postman environment ID. 721 | * @param {string} outputFilePath - Path where the JSON file should be saved. 722 | */ 723 | export async function exportPostmanEnvironment( 724 | apiKey, 725 | environmentId, 726 | outputFilePath 727 | ) { 728 | try { 729 | console.log( 730 | `Exporting Postman collection ${environmentId} to ${outputFilePath}` 731 | ); 732 | const response = await fetch( 733 | `https://api.getpostman.com/environments/${environmentId}`, 734 | { 735 | headers: { 736 | "X-Api-Key": apiKey, 737 | }, 738 | } 739 | ); 740 | const data = await response.json(); 741 | if (!response.ok) { 742 | throw new Error(`Failed to export Postman environment: ${data.error}`); 743 | } 744 | fs.writeFileSync(outputFilePath, JSON.stringify(data, null, 2), "utf8"); 745 | console.log("Postman environment exported successfully."); 746 | } catch (error) { 747 | console.error( 748 | "An error occurred while exporting the Postman environment:", 749 | error.message 750 | ); 751 | throw error; 752 | } 753 | } 754 | 755 | /** 756 | * Create a new repository. 757 | * @param {string} versionChoice - Version Control tool type. 758 | * @param {string} token - GitHub personal access token. 759 | * @param {string} repoName - Name of the repository to create. 760 | * @returns {string} - Full name of the created repository (e.g., username/repo). 761 | */ 762 | let username; 763 | let selectedWorkspace; 764 | 765 | export async function createRepo(versionChoice, token, repoName) { 766 | switch (versionChoice) { 767 | case "GitHub": 768 | try { 769 | console.log(`Creating GitHub repository ${repoName}`); 770 | const response = await fetch("https://api.github.com/user/repos", { 771 | method: "POST", 772 | headers: { 773 | Authorization: `token ${token}`, 774 | "Content-Type": "application/json", 775 | }, 776 | body: JSON.stringify({ 777 | name: repoName, 778 | private: false, 779 | }), 780 | }); 781 | const data = await response.json(); 782 | if (!response.ok) { 783 | throw new Error( 784 | `Failed to create GitHub repository: ${data.message}` 785 | ); 786 | } 787 | console.log("GitHub repository created successfully:", data.full_name); 788 | return data.full_name; 789 | } catch (error) { 790 | console.error( 791 | "An error occurred while creating the GitHub repository:", 792 | error.message 793 | ); 794 | throw error; 795 | } 796 | break; 797 | 798 | case "Gitlab": 799 | try { 800 | console.log(`Creating GitLab repository ${repoName}`); 801 | const response = await fetch("https://gitlab.com/api/v4/projects", { 802 | method: "POST", 803 | headers: { 804 | Authorization: `Bearer ${token}`, 805 | "Content-Type": "application/json", 806 | Accept: "application/json", 807 | }, 808 | body: JSON.stringify({ 809 | name: repoName, 810 | visibility: "public", // Set to 'private' for a private repository 811 | }), 812 | }); 813 | const data = await response.json(); 814 | if (!response.ok) { 815 | throw new Error( 816 | `Failed to create GitLab repository: ${data.message}` 817 | ); 818 | } 819 | console.log( 820 | "GitLab repository created successfully:", 821 | data.path_with_namespace 822 | ); 823 | return data.path_with_namespace; 824 | } catch (error) { 825 | console.error( 826 | "An error occurred while creating the GitLab repository:", 827 | error.message 828 | ); 829 | throw error; 830 | } 831 | break; 832 | 833 | case "Bitbucket": 834 | try { 835 | // Get username from user 836 | const { bitBucketName } = await inquirer.prompt({ 837 | type: "input", 838 | name: "bitBucketName", 839 | message: "What is your BitBucket username?", 840 | }); 841 | username = bitBucketName; 842 | 843 | // Fetch available workspaces 844 | const workspacesResponse = await fetch( 845 | `https://api.bitbucket.org/2.0/workspaces`, 846 | { 847 | method: "GET", 848 | headers: { 849 | Authorization: `Basic ${Buffer.from( 850 | `${username}:${token}` 851 | ).toString("base64")}`, 852 | Accept: "application/json", 853 | }, 854 | } 855 | ); 856 | 857 | const workspacesData = await workspacesResponse.json(); 858 | if (!workspacesResponse.ok) { 859 | throw new Error( 860 | `Failed to fetch workspaces: ${workspacesData.error.message}` 861 | ); 862 | } 863 | 864 | const workspaces = workspacesData.values.map( 865 | (workspace) => workspace.slug 866 | ); 867 | 868 | if (workspaces.length === 0) { 869 | throw new Error("No workspaces found for this account."); 870 | } 871 | 872 | // Select the first workspace or prompt the user to select one 873 | selectedWorkspace = workspaces[0]; // Or you can prompt to choose from workspaces if there are multiple 874 | 875 | console.log(`Selected workspace: ${selectedWorkspace}`); 876 | 877 | // Create the Bitbucket repository 878 | console.log( 879 | `Creating Bitbucket repository ${repoName} in workspace ${selectedWorkspace}` 880 | ); 881 | 882 | const repoResponse = await fetch( 883 | `https://api.bitbucket.org/2.0/repositories/${selectedWorkspace}/${repoName}`, 884 | { 885 | method: "POST", 886 | headers: { 887 | Authorization: `Basic ${Buffer.from( 888 | `${username}:${token}` 889 | ).toString("base64")}`, 890 | "Content-Type": "application/json", 891 | Accept: "application/json", 892 | }, 893 | body: JSON.stringify({ 894 | scm: "git", 895 | is_private: false, 896 | }), 897 | } 898 | ); 899 | 900 | const repoData = await repoResponse.json(); 901 | if (!repoResponse.ok) { 902 | throw new Error( 903 | `Failed to create Bitbucket repository: ${ 904 | repoData.error?.message || "Unknown error" 905 | }` 906 | ); 907 | } 908 | 909 | console.log( 910 | "Bitbucket repository created successfully:", 911 | repoData.full_name 912 | ); 913 | return repoData.full_name; 914 | } catch (error) { 915 | console.error( 916 | "An error occurred while creating the Bitbucket repository:", 917 | error.message 918 | ); 919 | throw error; 920 | } 921 | } 922 | } 923 | 924 | /** 925 | * Verify if a GitHub Actions workflow is created. 926 | * @param {string} repoFullName - Full repository name (e.g., username/repo). 927 | * @returns {boolean} - True if workflow exists, false otherwise. 928 | */ 929 | 930 | export async function verifyGithubActionsWorkflow( 931 | repoFullName, 932 | branch = "main", 933 | retries = 3 934 | ) { 935 | try { 936 | console.log( 937 | `Verifying GitHub Actions workflow for repository ${repoFullName}` 938 | ); 939 | const apiUrl = `https://api.github.com/repos/${repoFullName}/actions/workflows`; 940 | for (let attempt = 1; attempt <= retries; attempt++) { 941 | const response = await fetch(apiUrl); 942 | const data = await response.json(); 943 | if (!response.ok) { 944 | throw new Error( 945 | `Failed to verify GitHub Actions workflow: ${data.message}` 946 | ); 947 | } 948 | console.log( 949 | `Attempt ${attempt}: GitHub Actions workflow verification - ${ 950 | data.total_count > 0 ? "exists" : "does not exist" 951 | }` 952 | ); 953 | if (data.total_count > 0) { 954 | return true; 955 | } 956 | await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds between retries 957 | } 958 | return false; 959 | } catch (error) { 960 | console.error( 961 | "An error occurred while verifying the GitHub Actions workflow:", 962 | error.message 963 | ); 964 | throw error; 965 | } 966 | } 967 | 968 | /** 969 | * Initialize a repository with a README file. 970 | * @param {string} versionChoice - Version Control tool type. 971 | * @param {string} token - GitHub personal access token. 972 | * @param {string} repoFullName - Full repository name (e.g., username/repo). 973 | * @returns {boolean} - True if initialization is successful, false otherwise. 974 | */ 975 | 976 | export async function initializeRepoWithReadme( 977 | versionChoice, 978 | token, 979 | repoFullName 980 | ) { 981 | switch (versionChoice) { 982 | case "GitHub": 983 | try { 984 | console.log( 985 | `Initializing repository ${repoFullName} with a README file` 986 | ); 987 | const apiUrl = `https://api.github.com/repos/${repoFullName}/contents/README.md`; 988 | const body = JSON.stringify({ 989 | message: "Initial commit", 990 | content: Buffer.from("# " + repoFullName.split("/").pop()).toString( 991 | "base64" 992 | ), 993 | }); 994 | const response = await fetch(apiUrl, { 995 | method: "PUT", 996 | headers: { 997 | Authorization: `token ${token}`, 998 | "Content-Type": "application/json", 999 | }, 1000 | body, 1001 | }); 1002 | if (!response.ok) { 1003 | const data = await response.json(); 1004 | throw new Error(`Failed to initialize repository: ${data.message}`); 1005 | } 1006 | console.log("Repository initialized with README successfully."); 1007 | return true; 1008 | } catch (error) { 1009 | console.error( 1010 | "An error occurred while initializing the repository with README:", 1011 | error.message 1012 | ); 1013 | throw error; 1014 | } 1015 | break; 1016 | case "Gitlab": 1017 | try { 1018 | console.log( 1019 | `Initializing GitLab repository ${repoFullName} with a README file` 1020 | ); 1021 | 1022 | const [namespace, project] = repoFullName.split("/"); 1023 | const apiUrl = `https://gitlab.com/api/v4/projects/${encodeURIComponent( 1024 | namespace 1025 | )}%2F${encodeURIComponent(project)}/repository/files/README.md`; 1026 | 1027 | const body = JSON.stringify({ 1028 | branch: "main", 1029 | content: "# " + repoFullName.split("/").pop(), 1030 | commit_message: "Initial commit", 1031 | }); 1032 | 1033 | const response = await fetch(apiUrl, { 1034 | method: "POST", // In GitLab, `POST` is used to create a new file 1035 | headers: { 1036 | Authorization: `Bearer ${token}`, 1037 | "Content-Type": "application/json", 1038 | }, 1039 | body, 1040 | }); 1041 | 1042 | if (!response.ok) { 1043 | const data = await response.json(); 1044 | throw new Error(`Failed to initialize repository: ${data.message}`); 1045 | } 1046 | console.log("GitLab repository initialized with README successfully."); 1047 | return true; 1048 | } catch (error) { 1049 | console.error( 1050 | "An error occurred while initializing the GitLab repository with README:", 1051 | error.message 1052 | ); 1053 | throw error; 1054 | } 1055 | break; 1056 | case "Bitbucket": 1057 | try { 1058 | console.log( 1059 | `Initializing Bitbucket repository ${repoFullName} with a README file` 1060 | ); 1061 | 1062 | // const [workspace, repoSlug] = repoFullName.split("/"); 1063 | const apiUrl = `https://api.bitbucket.org/2.0/repositories/${repoFullName}/src`; 1064 | 1065 | const body = new URLSearchParams({ 1066 | message: "Initial commit", 1067 | branch: "main", 1068 | [`README.md`]: "# " + repoFullName.split("/").pop(), 1069 | }); 1070 | 1071 | const response = await fetch(apiUrl, { 1072 | method: "POST", 1073 | headers: { 1074 | Authorization: `Basic ${Buffer.from( 1075 | `${username}:${token}` 1076 | ).toString("base64")}`, 1077 | "Content-Type": "application/x-www-form-urlencoded", 1078 | }, 1079 | body, 1080 | }); 1081 | 1082 | if (!response.ok) { 1083 | const data = await response.json(); 1084 | throw new Error( 1085 | `Failed to initialize repository: ${data.error.message}` 1086 | ); 1087 | } 1088 | console.log( 1089 | "Bitbucket repository initialized with README successfully." 1090 | ); 1091 | return true; 1092 | } catch (error) { 1093 | console.error( 1094 | "An error occurred while initializing the Bitbucket repository with README:", 1095 | error.message 1096 | ); 1097 | throw error; 1098 | } 1099 | } 1100 | } 1101 | 1102 | /** 1103 | * Check if README.md exists in the repository. 1104 | * @param {string} token - GitHub personal access token. 1105 | * @param {string} repoFullName - Full repository name (e.g., username/repo). 1106 | * @returns {boolean} - True if README.md exists, false otherwise. 1107 | */ 1108 | // COMPLETE FOR GITLAB AND BITBUCKET LATER 1109 | export async function readmeExists(versionChoice, token, repoFullName) { 1110 | switch (versionChoice) { 1111 | case "GitHub": 1112 | try { 1113 | console.log( 1114 | `Checking if README.md exists in GitHub repository ${repoFullName}` 1115 | ); 1116 | const apiUrl = `https://api.github.com/repos/${repoFullName}/contents/README.md`; 1117 | const response = await fetch(apiUrl, { 1118 | headers: { 1119 | Authorization: `token ${token}`, 1120 | }, 1121 | }); 1122 | console.log( 1123 | `README.md ${response.status === 200 ? "exists" : "does not exist"}` 1124 | ); 1125 | return response.status === 200; 1126 | } catch (error) { 1127 | console.error( 1128 | "An error occurred while checking if README.md exists in GitHub:", 1129 | error.message 1130 | ); 1131 | throw error; 1132 | } 1133 | break 1134 | 1135 | case "Gitlab": 1136 | try { 1137 | // Split the repoFullName into namespace and project 1138 | const [namespace, project] = repoFullName.split("/"); 1139 | 1140 | // Build the API URL to check for the README.md file in the default branch (main) 1141 | const apiUrl = `https://gitlab.com/api/v4/projects/${encodeURIComponent( 1142 | namespace 1143 | )}%2F${encodeURIComponent( 1144 | project 1145 | )}/repository/files/README.md?ref=main`; 1146 | 1147 | console.log( 1148 | `Checking if README.md exists in GitLab repository ${repoFullName}...` 1149 | ); 1150 | 1151 | const response = await fetch(apiUrl, { 1152 | headers: { 1153 | Authorization: `Bearer ${token}`, 1154 | }, 1155 | }); 1156 | 1157 | if (response.status === 200) { 1158 | console.log("README.md exists in the repository."); 1159 | return true; 1160 | } else if (response.status === 404) { 1161 | console.log("README.md does not exist in the repository."); 1162 | return false; 1163 | } else { 1164 | console.log("Unexpected response:", response.status); 1165 | return false; 1166 | } 1167 | } catch (error) { 1168 | console.error( 1169 | "An error occurred while checking for README.md in GitLab:", 1170 | error.message 1171 | ); 1172 | throw error; 1173 | } 1174 | break; 1175 | 1176 | case "Bitbucket": 1177 | const { bitBucketName } = await inquirer.prompt({ 1178 | type: "input", 1179 | name: "bitBucketName", 1180 | message: "What is your BitBucket username?", 1181 | }); 1182 | username = bitBucketName; 1183 | try { 1184 | console.log( 1185 | `Checking if README.md exists in BitBucket repository ${repoFullName}` 1186 | ); 1187 | const apiUrl = `https://api.bitbucket.org/2.0/repositories/${repoFullName}/src/main/README.md`; 1188 | const response = await fetch(apiUrl, { 1189 | headers: { 1190 | Authorization: `Basic ${Buffer.from( 1191 | `${username}:${token}` 1192 | ).toString("base64")}`, 1193 | }, 1194 | }); 1195 | const readmeExists = response.status === 200; 1196 | 1197 | console.log(`README.md ${readmeExists ? "exists" : "does not exist"}`); 1198 | return readmeExists; 1199 | } catch (error) { 1200 | console.error( 1201 | "An error occurred while checking if README.md exists:", 1202 | error.message 1203 | ); 1204 | throw error; 1205 | } 1206 | } 1207 | } 1208 | --------------------------------------------------------------------------------