sync-postman
Version:
CLI tool to sync Postman collections with GitHub for free
699 lines (585 loc) • 24.8 kB
JavaScript
const axios = require('axios');
require('dotenv').config();
const os = require('os');
const path = require('path');
const fs = require('fs');
const postmanApiUrl = 'https://api.getpostman.com';
const configPath = path.join(os.homedir(), '.sync-postman-config.json');
function loadConfig() {
if (fs.existsSync(configPath)) {
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
return {};
}
// Get Postman collections
async function getCollections(apiKey) {
const collections = [];
try {
const response = await axios.get(`${postmanApiUrl}/collections`, {
headers: { 'X-Api-Key': apiKey },
});
for (const collection of response.data.collections) {
const detailed = await axios.get(`${postmanApiUrl}/collections/${collection.uid}`, {
headers: { 'X-Api-Key': apiKey },
});
collections.push(detailed.data.collection);
}
} catch (error) {
console.error("Error fetching collections:", error.message);
}
return collections;
}
// Get Postman environments
async function getEnvironments(apiKey) {
const environments = [];
try {
const response = await axios.get(`${postmanApiUrl}/environments`, {
headers: { 'X-Api-Key': apiKey },
});
for (const env of response.data.environments) {
const detailed = await axios.get(`${postmanApiUrl}/environments/${env.uid}`, {
headers: { 'X-Api-Key': apiKey },
});
environments.push(detailed.data.environment);
}
} catch (error) {
console.error("Error fetching environments:", error.message);
}
return environments;
}
// Save content to GitHub
async function saveToGitHub(content, config, filePath, branch, message) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const repoOwner = config.GITHUB_USERNAME;
const headers = { Authorization: `token ${config.GITHUB_TOKEN}` };
try {
// Check if the file exists
const response = await axios.get(
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/${filePath}?ref=${branch}`,
{ headers }
);
const sha = response.data.sha;
// Update the existing file
await axios.put(
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/${filePath}`,
{
message,
content: Buffer.from(JSON.stringify(content, null, 2)).toString('base64'),
sha,
branch,
},
{ headers }
);
// console.log(`File '${filePath}' updated successfully on branch '${branch}'.`);
} catch (error) {
if (error.response?.status === 404) {
// Create the new file
try {
await axios.put(
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/${filePath}`,
{
message,
content: Buffer.from(JSON.stringify(content, null, 2)).toString('base64'),
branch,
},
{ headers }
);
} catch (createError) {
console.error(`Error creating the file '${filePath}':`, createError.message);
throw createError;
}
} else {
console.error(`Error checking or updating file '${filePath}':`, error.message);
throw error;
}
}
}
async function ensureBaseBranchExists(config, baseBranch) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const repoOwner = config.GITHUB_USERNAME;
const headers = { Authorization: `token ${config.GITHUB_TOKEN}` };
try {
// Check if the base branch exists
const response = await axios.get(
`https://api.github.com/repos/${repoOwner}/${repoName}/branches/${baseBranch}`,
{ headers }
);
console.log(`Base branch '${baseBranch}' exists.`);
} catch (error) {
if (error.response?.status === 404) {
console.log(`Base branch '${baseBranch}' does not exist.`);
// Attempt to create an initial commit
const commitMessage = "Initial commit";
const content = Buffer.from("# Initial Commit\nThis repository is initialized.").toString('base64');
const filePath = "README.md";
try {
await axios.put(
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/${filePath}`,
{
message: commitMessage,
content,
branch: baseBranch,
},
{ headers }
);
console.log(`Base branch '${baseBranch}' created successfully with an initial commit.`);
} catch (initError) {
if (initError.response?.status === 403) {
console.error(`You do not have permission to initialize the repository. Please ask the owner to create the base branch.`);
} else {
console.error(`Error initializing repository with branch '${baseBranch}':`, initError.message);
}
throw initError;
}
} else if (error.response?.status === 403) {
console.error(`You do not have permission to access the repository.`);
throw error;
} else {
console.error(`Error checking base branch '${baseBranch}':`, error.message);
throw error;
}
}
}
// Create a new branch
async function createBranch(config, baseBranch, newBranch) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const repoOwner = config.GITHUB_USERNAME;
try {
const baseBranchResponse = await axios.get(
`https://api.github.com/repos/${repoOwner}/${repoName}/git/ref/heads/${baseBranch}`,
{ headers: { Authorization: `token ${config.GITHUB_TOKEN}` } }
);
const baseSha = baseBranchResponse.data.object.sha;
await axios.post(
`https://api.github.com/repos/${repoOwner}/${repoName}/git/refs`,
{
ref: `refs/heads/${newBranch}`,
sha: baseSha,
},
{ headers: { Authorization: `token ${config.GITHUB_TOKEN}` } }
);
console.log(`Branch '${newBranch}' created successfully.`);
} catch (error) {
if (error.response?.status === 422) {
console.warn(`Branch '${newBranch}' already exists.`);
} else {
console.error(`Error creating branch '${newBranch}':`, error.message);
throw error;
}
}
}
// Create a pull request
async function createPullRequest(config, branchName, baseBranch = 'main') {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const repoOwner = config.GITHUB_USERNAME;
try {
const response = await axios.post(
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls`,
{
title: `Sync Postman collections and environments`,
head: branchName,
base: baseBranch,
body: `Automated sync of Postman collections and environments to branch '${branchName}'.`,
},
{ headers: { Authorization: `token ${config.GITHUB_TOKEN}` } }
);
const url = response.data.html_url;
const text = response.data.html_url;
const hyperlink = `\x1b]8;;${url}\x1b\\\x1b[32m${text}\x1b[0m\x1b]8;;\x1b\\`;
console.log(`Pull request created click here to review & merge : ${hyperlink}`);
} catch (error) {
console.error(`Error creating pull request:`, error.message);
throw error;
}
}
async function ensureBranchExists(config, branch) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const headers = { Authorization: `token ${config.GITHUB_TOKEN}` };
try {
const response = await axios.get(
`https://api.github.com/repos/${config.GITHUB_USERNAME}/${repoName}/branches/${branch}`,
{ headers }
);
return true;
} catch (error) {
if (error.response?.status === 404) {
//console.log(`Branch '${branch}' does not exist.`);
return false;
}
console.error(`Error checking branch '${branch}':`, error.message);
}
}
// Sync collections and environments to GitHub and create a pull request
async function pushOnGithub(config, newBranch, baseBranch = 'main') {
await ensureBaseBranchExists(config, baseBranch);
// create branch if not exists
const branchExists = await ensureBranchExists(config, newBranch);
// console.log("branchExists", branchExists)
if (!branchExists) {
// console.log("creating branch")
await createBranch(config, baseBranch, newBranch);
}
// Sync collections
const collections = await getCollections(config.POSTMAN_API_KEY);
const oldCollections = await fetchAllCollectionsFromGitHub(config, baseBranch);
// console.log("oldCollections here", oldCollections)
const isDuplicate = hasDuplicateCollection(collections);
if (isDuplicate) throw new Error('Duplicate collections not allowed');
for (const collection of collections) {
let updatedCollection = collection;
const isExists = oldCollections.find(item => item.name == collection.info.name);
if (isExists) {
const infoUpdates = {
_postman_id: isExists.content.info._postman_id,
createdAt: isExists.content.info.createdAt,
updatedAt: isExists.content.info.updatedAt,
lastUpdatedBy: isExists.content.info.lastUpdatedBy,
uid: isExists.content.info.uid
}
const itemUpdates = [];
for (let i = 0; i < collection.item.length; i++) {
const currItem = collection.item[i];
const findItem = isExists?.content?.item.find(ele => ele.name == currItem.name);
const obj = {
id: currItem.id,
updates: {
id: findItem.id,
uid: findItem.uid
}
}
itemUpdates.push(obj);
}
updatedCollection = updateCollection(collection, infoUpdates, itemUpdates)
}
const filePath = `Sync-Postman/Collections/${collection.info.name}.json`;
await saveToGitHub(updatedCollection, config, filePath, newBranch, `Sync collection: ${collection.info.name}`);
}
// Sync environments
const environments = await getEnvironments(config.POSTMAN_API_KEY);
const oldEnvironments = await fetchEnvironmentsFromGitHub(config, baseBranch);
// console.log("oldEnvironments env", oldEnvironments)
const isDuplicateEnv = hasDuplicateEnv(environments);
if (isDuplicateEnv) throw new Error('Duplicate env not allowed');;
// console.log("all from postman env", environments);
// console.log("all from github", oldEnvironments);
const updatedEnvironments = environments.map((env) => {
const existingEnv = oldEnvironments.find((oldEnv) => oldEnv.name === env.name);
if (existingEnv) {
return {
...env,
id: existingEnv.id,
uid: existingEnv.uid,
owner: existingEnv.owner,
createdAt: existingEnv.createdAt,
updatedAt: existingEnv.updatedAt,
};
}
return env;
});
const envFilePath = `Sync-Postman/Environments/environments.json`;
await saveToGitHub(updatedEnvironments, config, envFilePath, newBranch, "Sync all environments");
// Create a pull request
await createPullRequest(config, newBranch, baseBranch);
}
async function pullFromGithub(config, branch) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const headers = { Authorization: `token ${config.GITHUB_TOKEN}` };
const baseUrl = `https://api.github.com/repos/${config.GITHUB_USERNAME}/${repoName}/contents`;
const collectionsPath = `${baseUrl}/Sync-Postman/Collections?ref=${branch}`;
const environmentsPath = `${baseUrl}/Sync-Postman/Environments/environments.json`;
const postmanCollections = await getCollections(config.POSTMAN_API_KEY);
const postmanEnvironments = await getEnvironments(config.POSTMAN_API_KEY);
try {
// Pull collections
const response = await axios.get(collectionsPath, { headers });
const githubCollections = response.data;
for (const file of githubCollections) {
if (file.type === 'file' && file.name.endsWith('.json')) {
const fileResponse = await axios.get(file.download_url);
const githubCollection = fileResponse.data;
const existingCollection = postmanCollections.find(
(col) => col.info.name === githubCollection.info.name
);
if (existingCollection) {
console.log(`Updating collection: ${githubCollection.info.name}`);
await updatePostmanCollection(
config.POSTMAN_API_KEY,
existingCollection.info.uid,
githubCollection
);
} else {
console.log(`Pulling collection: ${githubCollection.info.name}`);
// Create the collection in Postman
await createPostmanCollection(config.POSTMAN_API_KEY, githubCollection);
}
}
}
// Pull environments
const envResponse = await axios.get(environmentsPath, { headers });
const githubEnvironments = JSON.parse(Buffer.from(envResponse.data.content, 'base64').toString('utf8'));
// console.log("githubEnvironments here", githubEnvironments)
// console.log("postmanEnvironments here", postmanEnvironments)
for (const githubEnvironment of githubEnvironments) {
const existingEnvironment = postmanEnvironments.find((env) => env.name == githubEnvironment.name);
if (existingEnvironment) {
console.log(`Updating environment: ${githubEnvironment.name}`);
// updating if exists
await updatePostmanEnvironment(
config.POSTMAN_API_KEY,
existingEnvironment.uid,
githubEnvironment
);
} else {
console.log(`Pulling environment: ${githubEnvironment.name}`);
// creating new
await createPostmanEnvironment(config.POSTMAN_API_KEY, githubEnvironment);
}
}
console.log('Pull operation completed.');
} catch (error) {
console.error('Error pulling from GitHub:', error.message);
throw error;
}
}
function hasDuplicateCollection(collections) {
let seenCollections = new Set();
for (const collection of collections) {
if (seenCollections.has(collection.info.name)) {
return true;
}
seenCollections.add(collection.info.name);
let seenItems = new Set();
for (const item of collection.item) {
if (seenItems.has(item.name)) {
return true;
}
seenItems.add(item.name);
}
}
return false;
}
function hasDuplicateEnv(records) {
const seenNames = new Set();
for (const record of records) {
if (seenNames.has(record.name)) {
return true;
}
seenNames.add(record.name);
}
return false;
}
// Create a new collection in Postman
async function createPostmanCollection(apiKey, collection) {
try {
const response = await axios.post(
`${postmanApiUrl}/collections`,
{ collection },
{ headers: { 'X-Api-Key': apiKey } }
);
console.log(`Collection '${collection.info.name}' added to Postman.`);
} catch (error) {
console.error(`Error creating Postman collection '${collection.info.name}':`, error.message);
throw error;
}
}
// Delete all Postman collections
async function deleteAllPostmanCollections(apiKey) {
try {
const response = await axios.get(`${postmanApiUrl}/collections`, {
headers: { 'X-Api-Key': apiKey },
});
const collections = response.data.collections ?? [];
for (const collection of collections) {
await axios.delete(`${postmanApiUrl}/collections/${collection.uid}`, {
headers: { 'X-Api-Key': apiKey },
});
}
} catch (error) {
console.error('Error deleting Postman collections:', error.message);
throw error;
}
}
// Hard pull collections from GitHub to Postman
async function hardPullPostmanCollections(config, branch) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const headers = { Authorization: `token ${config.GITHUB_TOKEN}` };
const baseUrl = `https://api.github.com/repos/${config.GITHUB_USERNAME}/${repoName}/contents`;
// Delete all existing collections in Postman
await deleteAllPostmanCollections(config.POSTMAN_API_KEY);
// Get all collections from GitHub
const collectionsPath = `${baseUrl}/Sync-Postman/Collections?ref=${branch}`;
try {
const response = await axios.get(collectionsPath, { headers });
const githubCollections = response.data;
for (const file of githubCollections) {
if (file.type === 'file' && file.name.endsWith('.json')) {
const fileResponse = await axios.get(file.download_url);
const githubCollection = fileResponse.data;
// Add the collection to Postman
await createPostmanCollection(config.POSTMAN_API_KEY, githubCollection);
}
}
console.log('Hard pull operation completed.');
} catch (error) {
console.error('Error pulling collections from GitHub:', error.message);
throw error;
}
}
async function fetchAllCollectionsFromGitHub(config, branch) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const headers = { Authorization: `token ${config.GITHUB_TOKEN}` };
const baseUrl = `https://api.github.com/repos/${config.GITHUB_USERNAME}/${repoName}/contents/Sync-Postman/Collections`;
try {
const response = await axios.get(`${baseUrl}?ref=${branch}`, { headers });
const files = response.data;
const collections = [];
for (const file of files) {
if (file.type === 'file' && file.name.endsWith('.json')) {
const fileResponse = await axios.get(file.download_url);
collections.push({
name: file.name.replace('.json', ''),
content: fileResponse.data,
});
}
}
// console.log('Fetched all collections from GitHub:', collections.map((c) => c.name));
return collections;
} catch (error) {
console.log('No collection found');
return [];
}
}
function updateCollection(collection, infoUpdates, itemUpdates) {
if (!collection || typeof collection !== 'object') {
throw new Error('Invalid collection object.');
}
// Update the `info` object with the provided key-value pairs
if (infoUpdates && typeof infoUpdates === 'object') {
for (const [key, value] of Object.entries(infoUpdates)) {
if (collection.info && key in collection.info) {
collection.info[key] = value;
}
}
}
// Update specific `item` in the `item` array based on `id`
if (itemUpdates && Array.isArray(itemUpdates)) {
itemUpdates.forEach(({ id, updates }) => {
const item = collection.item.find((item) => item.id === id);
if (item && updates && typeof updates === 'object') {
for (const [key, value] of Object.entries(updates)) {
if (key in item) {
item[key] = value;
}
}
}
});
}
return collection;
}
// Create a new Postman environment
async function createPostmanEnvironment(apiKey, environment) {
try {
const response = await axios.post(
`${postmanApiUrl}/environments`,
{ environment },
{ headers: { 'X-Api-Key': apiKey } }
);
console.log(`Environment '${environment.name}' added to Postman.`);
} catch (error) {
console.error(`Error creating Postman environment '${environment.name}':`, error.message);
throw error;
}
}
// Update an existing Postman collection
async function updatePostmanCollection(apiKey, uid, collection) {
try {
// Ensure the collection payload is correctly structured
if (!collection || !collection.info || !collection.item) {
throw new Error(`Invalid collection data for updating collection: ${uid}`);
}
// Postman API requires `info` and `item` fields for updating a collection
const payload = {
collection: {
info: {
name: collection.info.name,
schema: collection.info.schema,
},
item: collection.item,
},
};
// Make the PUT request to update the collection
const response = await axios.put(
`${postmanApiUrl}/collections/${uid}`,
payload,
{ headers: { 'X-Api-Key': apiKey } }
);
// console.log(`Collection '${collection.info.name}' updated successfully in Postman.`);
} catch (error) {
console.error(`Error updating Postman collection '${uid}':`, error.message);
throw error;
}
}
// Update an existing Postman environment
async function updatePostmanEnvironment(apiKey, uid, environment) {
try {
// Ensure the environment payload is correctly structured
if (!environment || !environment.name || !environment.values) {
throw new Error(`Invalid environment data for updating environment: ${uid}`);
}
// Construct the payload to meet Postman API requirements
const payload = {
environment: {
name: environment.name,
values: environment.values,
},
};
// Make the PUT request to update the environment
const response = await axios.put(
`${postmanApiUrl}/environments/${uid}`,
payload,
{ headers: { 'X-Api-Key': apiKey } }
);
// console.log(`Environment '${environment.name}' updated successfully in Postman.`);
} catch (error) {
console.error(`Error updating Postman environment '${environment.name}':`, error.message);
throw error;
}
}
// Helper function to fetch environments from GitHub
async function fetchEnvironmentsFromGitHub(config, branch) {
const repoName = config.GITHUB_REPO.split('/').pop().replace('.git', '');
const headers = { Authorization: `token ${config.GITHUB_TOKEN}` };
const envFilePath = `Sync-Postman/Environments/environments.json`;
try {
const response = await axios.get(
`https://api.github.com/repos/${config.GITHUB_USERNAME}/${repoName}/contents/${envFilePath}?ref=${branch}`,
{ headers }
);
const content = JSON.parse(Buffer.from(response.data.content, 'base64').toString('utf-8'));
return content;
} catch (error) {
if (error.response?.status === 404) {
console.log("No environments found on GitHub.");
return [];
}
console.error("Error fetching environments from GitHub:", error.message);
throw error;
}
}
function getOwnerName(url) {
const urlString = String(url);
const match = urlString.match(/github\.com\/([^\/]+)/);
if (match && match[1]) {
const ownerName = match[1];
return ownerName
} else {
console.error("Owner name could not be extracted.");
}
}
module.exports = {
loadConfig,
pushOnGithub,
pullFromGithub,
hardPullPostmanCollections,
getOwnerName
};