Skip to content

Instantly share code, notes, and snippets.

@markgarrigan
Created July 22, 2025 15:26
Show Gist options
  • Save markgarrigan/893fd3abd74fd6fab35496360821f9a6 to your computer and use it in GitHub Desktop.
Save markgarrigan/893fd3abd74fd6fab35496360821f9a6 to your computer and use it in GitHub Desktop.
const axios = require('axios');
const RALLY_API_HOST = 'https://rally1.rallydev.com'
const RALLY_API_PATH = `/slm/webservice/v2.0`
const RALLY_WORKSPACE = '4729135433'
const RALLY_PROJECT = {
pidi: 344842856592, // 5 - PIDI Intake
dss: 89480156508, // AS DS - Infrastructure
}
const baseURL = `${RALLY_API_HOST}${RALLY_API_PATH}`
const RALLY_FIELDS = [
'FormattedID',
'Name',
'ScheduleState',
'RequesterName',
'SEMAnnualizedSavingsEstimate',
'CreationDate',
'Project',
'FlowStateChangedDate',
'Attachments',
]
const RALLY_FIELDS_DETAILED = [
...RALLY_FIELDS,
'Description',
'PlanEstimate',
'TaskStatus',
'PlannedDeploymentDate',
'Owner',
'Notes',
'AcceptanceCriteria',
'ObjectID',
'productIdentifier',
'LineofBusiness',
'OEAWorkEffortType',
'Client',
'Product',
'SubmittingTeam',
'Expedite',
'justification',
'OEAPSFinancialImpact',
'HardBenefitCategory',
'UnitedPlatform',
'ClaimType',
'InternalClient',
'PointinTime',
'AnalyticID',
'AnalyticName',
'ProviderSpecialty',
]
function transform(userStory, type = 'summary') {
const rv = {
summary: {
id: userStory.FormattedID || '',
name: userStory.Name || '',
scheduleState: userStory.ScheduleState || '',
requesterName: userStory.c_RequesterName || '',
semAnnualizedSavingsEstimate: userStory.c_SEMAnnualizedSavingsEstimate || '',
creationDate: userStory.CreationDate || '',
project: userStory.Project ? {
name: userStory.Project.Name || '',
creationDate: userStory.Project.CreationDate || '',
} : {},
flowStateChangedDate: userStory.FlowStateChangedDate || '',
attachmentCount: userStory.Attachments?.Count || '',
},
detailed: {
id: userStory.FormattedID || '', // US123456
name: userStory.Name || '', // plain text
description: userStory.Description || '', // html
notes: userStory.Notes || '', // html
acceptanceCriteria: userStory.c_AcceptanceCriteria || '', // html
owner: userStory.Owner?._refObjectName || '', // Eric Nelson
taskStatus: userStory.TaskStatus || '', // COMPLETED
planEstimate: userStory.PlanEstimate || '', // 3
plannedDeploymentDate: userStory.c_PlannedDeploymentDate || '', // 2019-11-03T16:14:12:999Z
scheduleState: userStory.ScheduleState || '',
projectId: userStory.Project?._ref?.split('/').pop(),
objectId: userStory.ObjectID,
productIdentifier: userStory.productIdentifier || '',
lineOfBusiness: userStory.c_LineofBusiness?._tagsNameArray?.map(t => t.Name) || [],
oeaWorkEffortType: userStory.c_OEAWorkEffortType || '',
client: userStory.c_Client?._tagsNameArray?.map(t => t.Name) || [],
product: userStory.c_Product || '',
submittingTeam: userStory.c_SubmittingTeam || '',
requesterName: userStory.c_RequesterName || '',
expedite: userStory.Expedite || '',
justification: userStory.justification || '',
oeaPSFinancialImpact: userStory.c_OEAPSFinancialImpact || '',
hardBenefitCategory: userStory.c_HardBenefitCategory || '',
unitedPlatform: userStory.c_UnitedPlatform || '',
claimType: userStory.c_ClaimType?._tagsNameArray?.map(t => t.Name) || [],
internalClient: userStory.c_InternalClient || '',
pointInTime: userStory.c_PointinTime || '',
analyticID: userStory.c_AnalyticID || '',
analyticName: userStory.c_AnalyticName || '',
providerSpecialty: userStory.c_ProviderSpecialty || '',
semAnnualizedSavingsEstimate: userStory.c_SEMAnnualizedSavingsEstimate || '',
creationDate: userStory.CreationDate || '',
project: userStory.Project ? {
name: userStory.Project.Name || '',
creationDate: userStory.Project.CreationDate || '',
} : {},
flowStateChangedDate: userStory.FlowStateChangedDate || '',
attachmentCount: userStory.Attachments?.Count || '',
}
}
return rv[type] || {}
}
function cleanObject(obj) {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v !== null && (typeof v !== 'object' || Object.keys(v).length > 0))
.map(([k, v]) => [k, v && typeof v === 'object' ? cleanObject(v) : v])
);
}
async function addAttachmentContent({ Workspace: workspaceId = RALLY_WORKSPACE, content } = {}) {
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`)
const url = `attachmentcontent/create?workspace=${workspace}`
const payload = { AttachmentContent: { Content: content } }
const response = await axios.post(url, payload, {
baseURL,
headers: { zsessionid: process.env.RALLY_READ_WRITE_API_KEY }
});
let result = response?.data?.CreateResult?.Object
if (!result) {
throw new Error('Error adding attachment content')
}
return result
}
async function addAttachmentToUserStory({ Workspace: workspaceId = RALLY_WORKSPACE, Artifact, Content, Name }) {
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`)
const url = `attachment/create?workspace=${workspace}`
const payload = {
Attachment: {
Content,
Artifact,
ContentType: 'application/octet-stream',
Name,
}
};
// call rally api to add attachment
const response = await axios.post(url, payload, {
baseURL,
headers: { zsessionid: process.env.RALLY_READ_WRITE_API_KEY }
});
const result = response?.data?.CreateResult?.Object
if (!result) {
throw new Error('Error adding attachment to user story')
}
return result
}
const userstories = {}
userstories.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => {
let { start = 0, results = [] } = options
// build url parameters
const fetch = encodeURIComponent(RALLY_FIELDS)
const query = encodeURIComponent('(SupportIssueID = pidianalyticsintake)')
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`)
// build the url
const url =
`/hierarchicalrequirement?` +
`start=${start}&pagesize=200&order=DragAndDropRank` +
`&fetch=${fetch}` +
`&query=${query}` +
`&workspace=${workspace}`
// call rally api
const headers = { zsessionid: process.env.RALLY_READ_API_KEY }
const response = await axios.get(url, {
baseURL,
headers,
})
// aggregate the results
const pageResults = response?.data?.QueryResult?.Results || []
if (pageResults.length) {
results = results.concat(pageResults.map(us => transform(us)))
// check response for additional pages
const total = response?.data?.QueryResult?.TotalResultCount || 0
if (results.length < total) {
await userstories.list({ Workspace: workspaceId, start: start + 200, results })
}
}
return results
}
userstories.get = async ({ Workspace: workspaceId = RALLY_WORKSPACE, id, ...options } = {}) => {
const fetch = encodeURIComponent(RALLY_FIELDS_DETAILED)
const query = encodeURIComponent(`(FormattedID = ${id})`)
const workspace = encodeURIComponent(`${baseURL}/workspace/${workspaceId}`)
// build the url
const url =
`/hierarchicalrequirement?` +
`start=1&pagesize=1&order=DragAndDropRank` +
`&fetch=${fetch}` +
`&query=${query}` +
`&workspace=${workspace}`
// call rally api
const response = await axios.get(url, {
baseURL,
headers: { zsessionid: process.env.RALLY_READ_API_KEY }
})
// make sure its there
const [userstory] = response?.data?.QueryResult?.Results || []
return options.raw ? userstory : transform(userstory, 'detailed')
}
userstories.create = async ({ projectId, Workspace: workspaceId = RALLY_WORKSPACE, ...HierarchicalRequirement } = {}) => {
const project = RALLY_PROJECT[projectId || 'pidi']
const projectRef = encodeURIComponent(`${baseURL}/project/${project}`)
const url = `/hierarchicalrequirement/create?project=${projectRef}`
const payload = { HierarchicalRequirement: cleanObject(HierarchicalRequirement) }
let response = {}
try {
response = await axios.post(url, payload, {
baseURL,
headers: { zsessionid: process.env.RALLY_READ_WRITE_API_KEY }
})
} catch (error) {
console.log('Begin User Story Create Error')
console.log(payload)
console.log(error)
console.log('End User Story Create Error')
throw new Error('Error calling the Rally API')
}
// check response
const userstory = response?.data?.CreateResult?.Object
const id = userstory?.FormattedID
if (!userstory || !id) {
throw new Error('User story not created')
}
return { id, userstory }
}
userstories.attachments = {}
userstories.attachments.create = async ({ id, filename, content }) => {
const { _ref: Artifact } = await userstories.get({id, raw: true})
const { _ref: Content } = await addAttachmentContent({content})
const result = await addAttachmentToUserStory({ Artifact, Content, Name: filename })
return result;
}
const iterations= {}
iterations.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => {
// call rally api
const headers = { zsessionid: process.env.RALLY_READ_API_KEY }
const dssResponse = await axios.get(`/project/89480156508/iterations?shallowFetch=Name,State,Project&pagesize=200&query=((State != "Accepted") and (Name contains "2025"))`, {
baseURL,
headers,
})
const cssResponse = await axios.get(`/project/601558843763/iterations?shallowFetch=Name,State,Project&pagesize=200&query=((State != "Accepted") and (Name contains "2025"))`, {
baseURL,
headers,
})
const combinedResponse = [...dssResponse.data.QueryResult.Results, ...cssResponse.data.QueryResult.Results];
return combinedResponse
}
const features= {}
features.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => {
// call rally api
const headers = { zsessionid: process.env.RALLY_READ_API_KEY }
const dssResponse = await axios.get(`/portfolioitem/feature?shallowFetch=FormattedID,Name&pagesize=200&query=((Project = "/project/89480156508") and (Name contains "2025"))`, {
baseURL,
headers,
})
const cssResponse = await axios.get(`/portfolioitem/feature?shallowFetch=FormattedID,Name&pagesize=200&query=((Project = "/project/601558843763") and (Name contains "2025"))`, {
baseURL,
headers,
})
const combinedResponse = [...dssResponse.data.QueryResult.Results, ...cssResponse.data.QueryResult.Results];
return combinedResponse
}
const releases= {}
releases.list = async ({ Workspace: workspaceId = RALLY_WORKSPACE, ...options } = {}) => {
// call rally api
const headers = { zsessionid: process.env.RALLY_READ_API_KEY }
const dssResponse = await axios.get(`/releases?shallowFetch=Name&pagesize=200&query=((Project = "/project/89480156508") and (Name contains "2025"))`, {
baseURL,
headers,
})
const cssResponse = await axios.get(`/releases?shallowFetch=Name&pagesize=200&query=((Project = "/project/601558843763") and (Name contains "2025"))`, {
baseURL,
headers,
})
const combinedResponse = [...dssResponse.data.QueryResult.Results, ...cssResponse.data.QueryResult.Results];
return combinedResponse
}
module.exports = {
userstories,
iterations,
features,
releases,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment