├── .atlassian └── OWNER ├── .github └── CODEOWNERS ├── .gitignore ├── apps └── sample-dataprovider-app-statuspage │ ├── .nvmrc │ ├── .husky │ ├── .gitignore │ └── pre-commit │ ├── ui │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── styles.ts │ │ ├── index.tsx │ │ ├── EmailField.tsx │ │ ├── APITokenField.tsx │ │ ├── App.test.tsx │ │ └── App.tsx │ ├── jest.config.js │ ├── public │ │ └── index.html │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── README.md │ ├── .prettierignore │ ├── src │ ├── constants.ts │ ├── entry │ │ ├── data-provider │ │ │ ├── types.ts │ │ │ ├── callback.ts │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── test.ts.snap │ │ │ │ ├── helpers │ │ │ │ │ └── forge-helper.ts │ │ │ │ └── test.ts │ │ │ └── index.ts │ │ └── webtriggers │ │ │ └── process-statuspage-incident-event.ts │ ├── client │ │ ├── statuspage-status.ts │ │ └── statuspage-manage.ts │ ├── index.ts │ ├── utils │ │ ├── webtrigger-utils.ts │ │ ├── statuspage-utils.ts │ │ └── statuspage-incident-transformers.ts │ ├── __tests__ │ │ ├── helpers │ │ │ └── mock-atlassian-graphql.ts │ │ └── contract │ │ │ └── process-statuspage-event.test.ts │ ├── resolvers.ts │ ├── mocks.ts │ └── types.ts │ ├── .prettierrc.json │ ├── .eslintignore │ ├── jest.config.js │ ├── tsconfig.json │ ├── .gitignore │ ├── manifest.yml │ ├── .eslintrc.js │ ├── package.json │ └── README.md ├── snippets ├── scripts │ ├── convert-backstage-config │ │ ├── requirements.txt │ │ ├── compass.yaml │ │ ├── backstage.yaml │ │ ├── README.md │ │ ├── convert_handlers.py │ │ └── convert_to_compass.py │ ├── jira-components-to-custom-field │ │ ├── requirements.txt │ │ ├── README.md │ │ └── jira_components_to_jira_custom_field.py │ ├── jira-components-to-compass-components │ │ ├── requirements.txt │ │ └── readme.md │ ├── compass-github-importer │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ ├── package-lock.json │ │ └── index.js │ ├── compass-gitlab-importer │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ └── package-lock.json │ ├── components-forge-field-to-compass-components │ │ ├── requirements.txt │ │ ├── README.md │ │ └── jiraProjectsInfo.py │ ├── compass-bitbucket-importer │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ ├── index.js │ │ └── package-lock.json │ ├── compass-new-relic-importer │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ └── index.js │ ├── jira-components-to-csv │ │ ├── requirements.txt │ │ ├── README.md │ │ └── jira_components_to_csv.py │ ├── bulk-delete-components │ │ ├── README.md │ │ └── delete_components.py │ ├── update-component │ │ └── update-components.sh │ └── search-components │ │ ├── search-components.sh │ │ └── search_components.py ├── graphql │ ├── delete-metric-source │ │ ├── deleteMetricSource.graphql │ │ └── README.md │ ├── delete-metric-definition │ │ ├── deleteMetricDefinition.graphql │ │ └── README.md │ ├── create-webhook │ │ ├── createWebhook.graphql │ │ └── README.md │ ├── create-metric-definition │ │ ├── createMetricDefinition.graphql │ │ └── README.md │ ├── create-template │ │ ├── createTemplate.graphql │ │ └── README.md │ ├── create-component │ │ ├── createComponent.graphql │ │ └── README.md │ ├── create-metric-source │ │ ├── createMetricSource.graphql │ │ └── README.md │ ├── add-document-to-component │ │ ├── addDocument.graphql │ │ ├── fetchDocumentationCategoryID.graphql │ │ └── README.md │ ├── create-component-from-template │ │ ├── createComponentFromTemplate.graphql │ │ └── README.md │ ├── get-component-custom-field-definitions │ │ ├── getComponentCustomFieldDefinitions.graphql │ │ └── README.md │ ├── get-metric-definitions │ │ ├── getMetricDefinitions.graphql │ │ └── README.md │ ├── get-metric-values-for-component │ │ ├── getMetricValues.graphql │ │ └── README.md │ ├── get-metric-values-for-metric-definition │ │ ├── getMetricDefinition.graphql │ │ └── README.md │ ├── update-component-custom-field-value │ │ ├── updateComponentCustomFieldValue.graphql │ │ └── README.md │ ├── README.md │ ├── search-components │ │ ├── searchComponents.graphql │ │ └── README.md │ └── add-metric-to-components │ │ └── README.md └── forge-graphql-sdk │ ├── search-components │ ├── README.md │ └── search-components.ts │ ├── get-component-scorecards-with-scores │ ├── README.md │ └── get-component-scorecards-with-scores.ts │ └── README.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /.atlassian/OWNER: -------------------------------------------------------------------------------- 1 | jcampbell2 -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @atlassian-labs/compass-magnets -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | __pycache__/ 4 | 5 | venv/ 6 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.nvmrc: -------------------------------------------------------------------------------- 1 | 14.19.0 2 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /snippets/scripts/convert-backstage-config/requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml 2 | -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-custom-field/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.25.1 -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-compass-components/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.25.1 -------------------------------------------------------------------------------- /snippets/scripts/compass-github-importer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .vscode/ 4 | .env -------------------------------------------------------------------------------- /snippets/scripts/compass-gitlab-importer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .vscode/ 4 | .env -------------------------------------------------------------------------------- /snippets/scripts/components-forge-field-to-compass-components/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.25.1 -------------------------------------------------------------------------------- /snippets/scripts/compass-bitbucket-importer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .vscode/ 4 | .env -------------------------------------------------------------------------------- /snippets/scripts/compass-new-relic-importer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .vscode/ 4 | .env -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.prettierignore: -------------------------------------------------------------------------------- 1 | .artifactory/* 2 | ui/build 3 | ui/public 4 | src/generated 5 | -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-csv/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | pandas>=2.2.1 3 | openpyxl>=3.1.2 4 | datetime>=5.5 -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum CUSTOM_METRICS { 2 | OPEN_INCIDENTS = 'Open Statuspage incidents', 3 | } 4 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "jsxSingleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /snippets/graphql/delete-metric-source/deleteMetricSource.graphql: -------------------------------------------------------------------------------- 1 | mutation deleteMetricSource ($input:CompassDeleteMetricSourceInput!){ 2 | compass { 3 | deleteMetricSource( 4 | input: $input 5 | ) { 6 | success 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /snippets/forge-graphql-sdk/search-components/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | Run this in your Compass Forge app to retrieve components based on a [variety of parameters](https://developer.atlassian.com/cloud/compass/forge-graphql-toolkit/TypeAliases/CompassQueryFieldFilter/). -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/src/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const FormWrapper = styled.div` 4 | width: 500px; 5 | height: 100%; 6 | `; 7 | 8 | export const Centered = styled.div` 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | `; 13 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.eslintignore: -------------------------------------------------------------------------------- 1 | # Required to ignore subpackage's node_modules folders. 2 | **/node_modules/**/* 3 | 4 | # Ignore all built artefacts. 5 | **/dist/**/* 6 | 7 | # Sidekick container in bitbucket pipelines (authless pipelines) 8 | .artifactory/* 9 | 10 | # UI 11 | /ui/build/**/* 12 | /ui/public/**/* 13 | -------------------------------------------------------------------------------- /snippets/graphql/delete-metric-definition/deleteMetricDefinition.graphql: -------------------------------------------------------------------------------- 1 | mutation deleteMetricDefinition($input: CompassDeleteMetricDefinitionInput!) { 2 | compass { 3 | deleteMetricDefinition(input: $input) { 4 | deletedMetricDefinitionId 5 | errors { 6 | message 7 | } 8 | success 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | import '@atlaskit/css-reset/dist/bundle.css'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root'), 13 | ); 14 | -------------------------------------------------------------------------------- /snippets/graphql/create-webhook/createWebhook.graphql: -------------------------------------------------------------------------------- 1 | mutation createWebhook($input: CompassCreateWebhookInput!) { 2 | compass { 3 | createWebhook(input: $input) { 4 | success 5 | webhookDetails { 6 | id 7 | url 8 | } 9 | errors { 10 | ...CommonMutationError 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /snippets/graphql/create-metric-definition/createMetricDefinition.graphql: -------------------------------------------------------------------------------- 1 | mutation createMetricDefinition ($input: CompassCreateMetricDefinitionInput!) { 2 | compass { 3 | createMetricDefinition( 4 | input: $input 5 | ) { 6 | createdMetricDefinition { 7 | id 8 | name 9 | } 10 | success 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | coverageThreshold: { 6 | global: { 7 | branches: 80, 8 | functions: 85, 9 | lines: 85, 10 | statements: 85, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /snippets/graphql/create-template/createTemplate.graphql: -------------------------------------------------------------------------------- 1 | mutation createTemplate($cloudId: ID!, $componentDetails: CreateCompassComponentInput!) { 2 | compass { 3 | createComponent(cloudId: $cloudId, input: $componentDetails) { 4 | success 5 | componentDetails { 6 | id 7 | name 8 | typeId 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /snippets/forge-graphql-sdk/get-component-scorecards-with-scores/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | Run this in your Compass Forge app as an example of retrieving information via the `requestGraph` method. Replace `COMPONENT-ID` with a valid [Compass component ARI](https://developer.atlassian.com/cloud/compass/config-as-code/manage-components-with-config-as-code/#find-a-component-s-id). -------------------------------------------------------------------------------- /snippets/graphql/create-component/createComponent.graphql: -------------------------------------------------------------------------------- 1 | mutation createComponent($cloudId: ID!, $componentDetails: CreateCompassComponentInput!) { 2 | compass { 3 | createComponent(cloudId: $cloudId, input: $componentDetails) { 4 | success 5 | componentDetails { 6 | id 7 | name 8 | typeId 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/entry/data-provider/types.ts: -------------------------------------------------------------------------------- 1 | type DataProviderPayload = { 2 | url: string; 3 | ctx: { 4 | cloudId: string; 5 | extensionId: string; 6 | }; 7 | }; 8 | 9 | type CallbackPayload = { 10 | success: boolean; 11 | url: string; 12 | errorMessage?: string; 13 | }; 14 | 15 | export { DataProviderPayload, CallbackPayload }; 16 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/src/EmailField.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from '@atlaskit/form'; 2 | import TextField from '@atlaskit/textfield'; 3 | 4 | export const EmailField = () => ( 5 |
6 | 7 | {({ fieldProps }) => } 8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /snippets/graphql/create-metric-source/createMetricSource.graphql: -------------------------------------------------------------------------------- 1 | mutation createMetricSource ($input:CompassCreateMetricSourceInput!){ 2 | compass { 3 | createMetricSource( 4 | input: $input 5 | ) { 6 | success 7 | createdMetricSource { 8 | title 9 | id 10 | metricDefinition { 11 | id 12 | } 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/src/APITokenField.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from '@atlaskit/form'; 2 | import TextField from '@atlaskit/textfield'; 3 | 4 | export const APITokenField = () => ( 5 |
6 | 7 | {({ fieldProps }) => } 8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Compass Forge Template 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /snippets/scripts/compass-bitbucket-importer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass-bitbucket-importer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "chalk": "4.1.2", 14 | "dotenv": "^16.4.5" 15 | } 16 | } -------------------------------------------------------------------------------- /snippets/scripts/compass-github-importer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass-github-importer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "chalk": "4.1.2", 14 | "dotenv": "^16.4.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /snippets/scripts/compass-gitlab-importer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass-gitlab-importer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "chalk": "4.1.2", 14 | "dotenv": "^16.4.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /snippets/graphql/add-document-to-component/addDocument.graphql: -------------------------------------------------------------------------------- 1 | mutation addDocument($input: CompassAddDocumentInput!) { 2 | compass { 3 | addDocument(input: $input) @optIn(to: "compass-beta") { 4 | success 5 | errors { 6 | message 7 | } 8 | documentDetails { 9 | id 10 | title 11 | url 12 | componentId 13 | documentationCategoryId 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /snippets/scripts/compass-new-relic-importer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass-new-relic-importer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "chalk": "4.1.2", 14 | "dotenv": "^16.4.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /snippets/graphql/create-component-from-template/createComponentFromTemplate.graphql: -------------------------------------------------------------------------------- 1 | mutation createComponentFromTemplate($input: CreateCompassComponentFromTemplateInput!) { 2 | compass @optIn(to: ["compass-beta"]) { 3 | createComponentFromTemplate(input: $input) { 4 | success 5 | 6 | componentDetails { 7 | ...CompassComponentCore 8 | } 9 | 10 | errors { 11 | ...CommonMutationError 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/client/statuspage-status.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from '@forge/api'; 2 | 3 | export async function getIncidents(baseUrl: string) { 4 | const result = await fetch(`${baseUrl}/api/v2/incidents.json`); 5 | const body = await result.json(); 6 | return body.incidents; 7 | } 8 | export async function getPageId(baseUrl: string) { 9 | const result = await fetch(`${baseUrl}/api/v2/summary.json`); 10 | const body = await result.json(); 11 | return body.page.id; 12 | } 13 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/index.ts: -------------------------------------------------------------------------------- 1 | import resolver from './resolvers'; 2 | import processStatuspageIncidentEvent from './entry/webtriggers/process-statuspage-incident-event'; 3 | import { dataProvider } from './entry/data-provider/index'; 4 | import { callback } from './entry/data-provider/callback'; 5 | 6 | // Custom UI backend 7 | export { resolver }; 8 | 9 | // webtriggers 10 | export { processStatuspageIncidentEvent }; 11 | 12 | // dataProvider 13 | export { dataProvider, callback }; 14 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # features 26 | /src/features.ts 27 | 28 | # errors 29 | /src/errors.ts 30 | -------------------------------------------------------------------------------- /snippets/forge-graphql-sdk/search-components/search-components.ts: -------------------------------------------------------------------------------- 1 | await graphqlGateway.compass.asApp().searchComponents({ 2 | cloudId: 'CLOUD_ID', 3 | query: { 4 | query: 'COMPONENT NAME HERE', 5 | fieldFilters: [ 6 | { 7 | name: 'ownerId', 8 | filter: { 9 | eq: 'ari:cloud:teams::team/TEAM-ID', 10 | }, 11 | }, 12 | { 13 | name: 'labels', 14 | filter: { 15 | in: ['LABEL-NAME'], 16 | }, 17 | }, 18 | ], 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /snippets/graphql/add-document-to-component/fetchDocumentationCategoryID.graphql: -------------------------------------------------------------------------------- 1 | query fetchDocumentationCategories($cloudId: ID!, $first: Int, $after: String) { 2 | compass { 3 | documentationCategories(cloudId: $cloudId, first: $first, after: $after) @optIn(to: "compass-beta") { 4 | nodes { 5 | id 6 | name 7 | } 8 | edges { 9 | node { 10 | id 11 | name 12 | } 13 | cursor 14 | } 15 | pageInfo { 16 | hasNextPage 17 | endCursor 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | roots: ['src'], 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testPathIgnorePatterns: ['/node_modules/', '/typings/', '/support/', '/dist/', '/fixtures/', '/helpers/'], 7 | collectCoverageFrom: ['src/**/*'], 8 | globals: { 9 | 'ts-jest': { 10 | tsconfig: 'tsconfig.json', 11 | }, 12 | }, 13 | transform: { 14 | '^.+\\.(ts|tsx)$': 'ts-jest', 15 | }, 16 | coverageDirectory: 'coverage', 17 | }; 18 | -------------------------------------------------------------------------------- /snippets/graphql/get-component-custom-field-definitions/getComponentCustomFieldDefinitions.graphql: -------------------------------------------------------------------------------- 1 | query getComponentCustomFields { 2 | compass { 3 | component(id: "") { 4 | ... on QueryError { 5 | identifier 6 | message 7 | extensions { 8 | statusCode 9 | errorType 10 | } 11 | } 12 | ... on CompassComponent { 13 | id 14 | name 15 | customFields { 16 | definition { 17 | id 18 | name 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2019", 5 | "sourceMap": true, 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "types": ["node", "jest"], 10 | "baseUrl": "./", 11 | "allowJs": true, 12 | "jsx": "react", 13 | "jsxFactory": "ForgeUI.createElement", 14 | "noImplicitAny": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "include": ["./src/**/*"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/utils/webtrigger-utils.ts: -------------------------------------------------------------------------------- 1 | import { WebtriggerResponse } from '../types'; 2 | 3 | export const serverResponse = ( 4 | message: string, 5 | parameters?: Record, 6 | statusCode = 200, 7 | ): WebtriggerResponse => { 8 | const body = JSON.stringify({ 9 | message, 10 | success: statusCode >= 200 && statusCode < 300, 11 | ...(parameters !== undefined && { parameters }), 12 | }); 13 | const defaultHeaders = { 14 | 'Content-Type': ['application/json'], 15 | }; 16 | 17 | return { 18 | body, 19 | statusCode, 20 | headers: defaultHeaders, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/entry/data-provider/callback.ts: -------------------------------------------------------------------------------- 1 | import { CallbackPayload } from './types'; 2 | import { serverResponse } from '../../utils/webtrigger-utils'; 3 | 4 | // This runs after dataProvider is invoked 5 | // https://developer.atlassian.com/cloud/compass/integrations/create-a-data-provider-app/#adding-a-callback-function 6 | export const callback = (input: CallbackPayload) => { 7 | const { success, errorMessage } = input; 8 | 9 | if (!success) { 10 | console.error({ 11 | message: 'Error processing dataProvider module', 12 | errorMessage, 13 | }); 14 | } 15 | 16 | return serverResponse('Callback finished'); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/utils/statuspage-utils.ts: -------------------------------------------------------------------------------- 1 | import { getPageId, getIncidents } from '../client/statuspage-status'; 2 | import { createSubscription } from '../client/statuspage-manage'; 3 | 4 | export async function getPageCode(url: string) { 5 | const pageCode = await getPageId(url.replace(/\/$/, '')); 6 | return pageCode; 7 | } 8 | export async function getPreviousIncidents(url: string) { 9 | const incidents = await getIncidents(url.replace(/\/$/, '')); 10 | return incidents; 11 | } 12 | export async function createWebhookSubscription(pageCode: string, token: string, email: string) { 13 | await createSubscription(pageCode, email, token); 14 | } 15 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/__tests__/helpers/mock-atlassian-graphql.ts: -------------------------------------------------------------------------------- 1 | export const mockCreateEvent = jest.fn(); 2 | export const mockInsertMetricValue = jest.fn(); 3 | 4 | export function mockAtlassianGraphQL() { 5 | const requestGraph = jest.fn(); 6 | 7 | // Global API mock 8 | (global as any).api = { 9 | asApp: () => ({ 10 | requestGraph, 11 | }), 12 | }; 13 | 14 | jest.mock('@atlassian/forge-graphql', () => ({ 15 | ...(jest.requireActual('@atlassian/forge-graphql') as any), 16 | compass: { 17 | asApp: () => ({ 18 | createEvent: mockCreateEvent, 19 | insertMetricValueByExternalId: mockInsertMetricValue, 20 | }), 21 | }, 22 | })); 23 | } 24 | -------------------------------------------------------------------------------- /snippets/graphql/get-metric-definitions/getMetricDefinitions.graphql: -------------------------------------------------------------------------------- 1 | query getMetricDefinitions($query: CompassMetricDefinitionsQuery!) { 2 | compass { 3 | metricDefinitions(query: $query) { 4 | ... on CompassMetricDefinitionsConnection { 5 | nodes { 6 | id 7 | name 8 | description 9 | type 10 | format { 11 | ... on CompassMetricDefinitionFormatSuffix { 12 | suffix 13 | } 14 | } 15 | derivedEventTypes 16 | } 17 | pageInfo { 18 | hasNextPage 19 | endCursor 20 | } 21 | } 22 | ... on QueryError { 23 | identifier 24 | message 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /snippets/graphql/get-metric-values-for-component/getMetricValues.graphql: -------------------------------------------------------------------------------- 1 | query getAMetricValue($componentID) { 2 | compass { 3 | component(id: $componentID) { 4 | ... on CompassComponent { 5 | metricSources(query: { first: 10 }) { 6 | ... on CompassComponentMetricSourcesConnection { 7 | nodes { 8 | metricDefinition { 9 | name 10 | } 11 | 12 | values { 13 | ... on CompassMetricSourceValuesConnection { 14 | nodes { 15 | value 16 | 17 | timestamp 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-csv/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | This Python script will grab all Jira components from a user-defined selection of Jira projects. 4 | This script will process a maximum of 1000 components. 5 | 6 | # Getting started 7 | 8 | - `brew install python` 9 | - `pip3 install -r requirements.txt` 10 | - Switch your desired Jira projects to use **Jira components**. Learn how to make the switch [here.](https://support.atlassian.com/jira-software-cloud/docs/switch-between-jira-and-compass-components/) 11 | - Fill in your email, API token, site URL, and list of project keys. [Learn how to get your Atlassian API token.](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/) 12 | - Run the program: `python3 jira_components_to_csv.py` 13 | -------------------------------------------------------------------------------- /snippets/graphql/delete-metric-source/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will delete a metric source given its ID. This will disconnect the metric from a specific component. 4 | 5 | Replace `id` below in the variables section with a valid metric source ARI. These metric sources can be found for a particular component using the [getMetricValuesForComponent](/snippets/graphql/get-metric-values-for-component/README.md) query. 6 | 7 | ### Query 8 | 9 | ```graphql 10 | mutation deleteMetricSource ($input:CompassDeleteMetricSourceInput!){ 11 | compass { 12 | deleteMetricSource( 13 | input: $input 14 | ) { 15 | success 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | ### Query Headers 22 | 23 | ``` 24 | { 25 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 26 | } 27 | ``` 28 | 29 | ### Query Variables 30 | 31 | ``` 32 | { 33 | "input": { 34 | "id": "your-metric-source-id" 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | dist/ 25 | 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | 51 | # Env files 52 | .env* 53 | 54 | # Test reports 55 | test-results/ 56 | -------------------------------------------------------------------------------- /snippets/scripts/convert-backstage-config/compass.yaml: -------------------------------------------------------------------------------- 1 | configVersion: 1 2 | description: This API enables CRUD operations on the Compass Test App 3 | fields: 4 | lifecycle: Pre-Release 5 | tier: 2 6 | labels: 7 | - compass 8 | - test-app 9 | - imported:backstage 10 | links: 11 | - name: Repository Link 12 | type: REPOSITORY 13 | url: https://bitbucket.org/atlassian-test/compass-test-repo 14 | - name: Confluence 15 | type: DOCUMENT 16 | url: https://atlassian-test.com/wiki/pages/104862234987/How+Your+App+is+Architected 17 | - name: Bitbucket Pipelines 18 | type: DASHBOARD 19 | url: https://bitbucket.org/atlassian-test/compass-test-repo/pipelines/results/page/1 20 | - name: Octopus 21 | type: DASHBOARD 22 | url: https://example.com/app#/projects/compass-test-repo/deployments 23 | - name: SignalFX 24 | type: DASHBOARD 25 | url: https://atlassian-test.signalfx.com/#/dashboard/FNx234coD8A0AA 26 | - name: Splunk 27 | type: DASHBOARD 28 | url: https://splunk/logs 29 | name: compass-test-app 30 | typeId: SERVICE 31 | -------------------------------------------------------------------------------- /snippets/graphql/get-metric-values-for-metric-definition/getMetricDefinition.graphql: -------------------------------------------------------------------------------- 1 | query getMetricDefinition ($cloudId: ID!, $metricDefinitionId: ID!){ 2 | compass { 3 | metricDefinition(cloudId: $cloudId, metricDefinitionId: $metricDefinitionId) { 4 | ... on CompassMetricDefinition { 5 | id 6 | name 7 | metricSources { 8 | ... on CompassMetricSourcesConnection { 9 | nodes { 10 | id 11 | values { 12 | ... on CompassMetricSourceValuesConnection { 13 | nodes { 14 | value 15 | timestamp 16 | } 17 | pageInfo { 18 | hasNextPage 19 | endCursor 20 | } 21 | } 22 | } 23 | component { 24 | id 25 | } 26 | } 27 | pageInfo { 28 | hasNextPage 29 | endCursor 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /snippets/forge-graphql-sdk/README.md: -------------------------------------------------------------------------------- 1 | # Forge GraphQL SDK Snippets for Compass 2 | 3 | Here you will find a directory of code snippets that do various things in Compass using the [Forge GraphQL SDK](https://www.npmjs.com/package/@atlassian/forge-graphql). 4 | 5 | | Example | Description | 6 | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 7 | | [Search components](search-components/) | Searches components using various optional filters. Requires the cloudId of the site. | 8 | | [Get component scorecards with scores](get-component-scorecards-with-scores/) | Gets a list of scorecards applied to a component and their accompanying scores. | 9 | -------------------------------------------------------------------------------- /snippets/graphql/update-component-custom-field-value/updateComponentCustomFieldValue.graphql: -------------------------------------------------------------------------------- 1 | mutation updateComponentCustomField($input: UpdateCompassComponentInput!) { 2 | compass { 3 | updateComponent(input: $input) { 4 | success 5 | errors { 6 | message 7 | extensions { 8 | statusCode 9 | errorType 10 | } 11 | } 12 | componentDetails { 13 | id 14 | name 15 | customFields { 16 | definition { 17 | id 18 | name 19 | } 20 | ...on CompassCustomBooleanField { 21 | booleanValue 22 | } 23 | ...on CompassCustomTextField { 24 | textValue 25 | } 26 | ...on CompassCustomNumberField { 27 | numberValue 28 | } 29 | ...on CompassCustomUserField { 30 | userValue { 31 | id 32 | name 33 | picture 34 | accountId 35 | canonicalAccountId 36 | accountStatus 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /snippets/forge-graphql-sdk/get-component-scorecards-with-scores/get-component-scorecards-with-scores.ts: -------------------------------------------------------------------------------- 1 | const scorecardsQuery = `query getComponentScorecardsWithScores($componentId: ID!) { 2 | compass { 3 | component(id: $componentId) { 4 | __typename 5 | ... on CompassComponent { 6 | id 7 | name 8 | 9 | scorecards { 10 | id 11 | name 12 | importance 13 | 14 | scorecardScore(query: { componentId: $componentId }) { 15 | totalScore 16 | maxTotalScore 17 | } 18 | } 19 | } 20 | ... on QueryError { 21 | message 22 | extensions { 23 | statusCode 24 | errorType 25 | } 26 | } 27 | } 28 | } 29 | }`; 30 | const variables = { 31 | componentId: 'COMPONENT-ID', 32 | }; 33 | const headers = { 'X-ExperimentalApi': 'compass-beta, compass-prototype' }; 34 | const req = await graphqlGateway.compass.api.asApp().requestGraph(scorecardsQuery, variables, headers); 35 | const result = await req.json(); 36 | 37 | console.log(result.data.compass.component.scorecards); -------------------------------------------------------------------------------- /snippets/graphql/delete-metric-definition/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This mutation will delete a given metric definition on your site. Note that only custom metric definitions can be deleted via API. 4 | 5 | Replace `id` below in the variables section with the id for the desired metric definition, which can be retrieved using [getMetricDefinitions](/snippets/graphql/get-metric-definitions/README.md) and execute the query. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 6 | 7 | ### Query 8 | 9 | ```graphql 10 | mutation deleteMetricDefinition($input: CompassDeleteMetricDefinitionInput!) { 11 | compass { 12 | deleteMetricDefinition(input: $input) { 13 | deletedMetricDefinitionId 14 | errors { 15 | message 16 | } 17 | success 18 | } 19 | } 20 | } 21 | ``` 22 | 23 | ### Query Headers 24 | 25 | ``` 26 | { 27 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 28 | } 29 | ``` 30 | 31 | ### Mutation Variables 32 | 33 | ``` 34 | { 35 | "input": { 36 | "id": "metric-definition-id" 37 | } 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/entry/data-provider/__tests__/__snapshots__/test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`dataProvider module successfully returns events and metrics in the expected format 1`] = ` 4 | Object { 5 | "builtIn": Object { 6 | "ari:cloud:compass::metric-definition/builtin/mttr-avg-last-10": Object { 7 | "derived": true, 8 | "initialValue": null, 9 | }, 10 | }, 11 | "custom": undefined, 12 | } 13 | `; 14 | 15 | exports[`dataProvider module successfully returns events and metrics in the expected format 2`] = ` 16 | Object { 17 | "incidents": Object { 18 | "initialValues": Array [ 19 | Object { 20 | "description": "This incident has been resolved.", 21 | "displayName": "test incident", 22 | "endTime": "2023-01-20T20:49:52.049Z", 23 | "id": "test", 24 | "lastUpdated": "2023-01-20T20:49:52.072Z", 25 | "severity": Object { 26 | "label": "critical", 27 | "level": "ONE", 28 | }, 29 | "startTime": "2023-01-20T20:49:36.249Z", 30 | "state": "RESOLVED", 31 | "updateSequenceNumber": 1674257428214, 32 | "url": "https://stspg.io/test", 33 | }, 34 | ], 35 | }, 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /snippets/graphql/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Snippets for Compass 2 | 3 | Here you will find a directory of GraphQL code snippets that do various things in Compass. 4 | 5 | | Example | Description | 6 | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 7 | | [Get metric values for a component](get-metric-values-for-component/) | A query that retrieves all metric values for a given component. Requires [the ARI of the component](https://developer.atlassian.com/cloud/compass/config-as-code/manage-components-with-config-as-code/#find-a-component-s-id). | 8 | | [Get metric definitions for a site](get-metric-definitions/) | A query that retrieves all metric definitions for a site. | 9 | | [Delete custom metric definition](delete-metric-definition/) | A query that deletes a custom metric definition. | -------------------------------------------------------------------------------- /snippets/scripts/bulk-delete-components/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | This is a python script to perform bulk deletion of components given a list of component IDs (in ARI format). 3 | 4 | Skip to step 2 if you already have a file of component IDs with one on each line to delete. 5 | 6 | 1. Run a search for the component ids to delete and place them in a file with one component ID per line. You can use the [search_components.py](/snippets/graphql/search-components/search_components.py) script (make sure to replace `email`, `api_token`, and `cloudId` in the file) to retrieve **ALL** of the components on your site and place them in a text file called `component_ids.txt`, but you may need to alter the search conditions based on what components you'd like to delete. 7 | 8 | 2. Assuming you have a text file called `component_ids.txt` with the list of component IDs to delete, you can then use the `delete_components.py` script to delete all of these components. Alternatively, without a separate text file, you may specify the component ids in the `component_ids` variables in the script. Make sure to replace `api_token` and `email` in the in the script. The script has a "dry_run" boolean that can be used to check how many components will be deleted before they are actually deleted. Set "dry_run = False" to delete the components. -------------------------------------------------------------------------------- /snippets/graphql/create-metric-definition/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will create a metric definition on your site. 4 | 5 | Replace `cloudId` and `name` below in the variables section with the cloudId for your site and the name for your metric definition, and `suffix` with the units of your metric (ex: minutes, req/s) and execute the query. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 6 | 7 | ### Query 8 | 9 | ```graphql 10 | mutation createMetricDefinition ($input: CompassCreateMetricDefinitionInput!) { 11 | compass { 12 | createMetricDefinition( 13 | input: $input 14 | ) { 15 | createdMetricDefinition { 16 | id 17 | name 18 | } 19 | success 20 | } 21 | } 22 | } 23 | 24 | ``` 25 | 26 | ### Query Headers 27 | 28 | ``` 29 | { 30 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 31 | } 32 | ``` 33 | 34 | ### Query Variables 35 | 36 | ``` 37 | { 38 | "input": { 39 | "cloudId": "", 40 | "name": "", 41 | "format": { 42 | "suffix": { 43 | "suffix": "" 44 | } 45 | } 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /snippets/graphql/get-component-custom-field-definitions/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will get all of the custom fields applied to a given component. 4 | 5 | Replace `` below in the query section. 6 | 7 | You can get a component's id by following the steps below: 8 | 1. In Compass, go to a component’s details page. Learn how to view a component's details 9 | 2. Select more actions (•••) then Copy component ID. 10 | 11 | You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 12 | 13 | ### Query 14 | 15 | ```graphql 16 | query getComponentCustomFields { 17 | compass { 18 | component(id: "") { 19 | ... on QueryError { 20 | identifier 21 | message 22 | extensions { 23 | statusCode 24 | errorType 25 | } 26 | } 27 | ... on CompassComponent { 28 | id 29 | name 30 | customFields { 31 | definition { 32 | id 33 | name 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ### Query Headers 43 | 44 | ``` 45 | { 46 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/entry/data-provider/__tests__/helpers/forge-helper.ts: -------------------------------------------------------------------------------- 1 | import fetch, { enableFetchMocks } from 'jest-fetch-mock'; 2 | 3 | export const storage = { 4 | set: jest.fn(), 5 | get: jest.fn(), 6 | delete: jest.fn(), 7 | query: jest.fn(), 8 | setSecret: jest.fn(), 9 | getSecret: jest.fn(), 10 | deleteSecret: jest.fn(), 11 | }; 12 | 13 | export const startsWith = jest.fn().mockImplementation(() => { 14 | return { 15 | condition: 'STARTS_WITH', 16 | value: '', 17 | }; 18 | }); 19 | 20 | export const webTrigger = { 21 | getUrl: jest.fn(), 22 | }; 23 | 24 | // This function is used to mock Forge's fetch API by using the mocked version 25 | // of `fetch` provided in the jest-fetch-mock library. 26 | // eslint-disable-next-line import/prefer-default-export 27 | export const mockForgeApi = (): void => { 28 | const requestGraph = jest.fn(); 29 | 30 | // Global API mock 31 | (global as any).api = { 32 | asApp: () => ({ 33 | requestGraph, 34 | }), 35 | }; 36 | 37 | jest.mock('@forge/api', () => ({ 38 | __esModule: true, 39 | default: 'mockedDefaultExport', 40 | fetch, // assign the fetch import to return the jest-fetch-mock version of fetch 41 | storage, 42 | webTrigger, 43 | startsWith, 44 | })); 45 | enableFetchMocks(); // enable jest-fetch-mock 46 | }; 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to compass-examples 2 | Thank you for considering a contribution to compass-examples! Pull requests, issues and comments are welcome. For pull requests, please: 3 | 4 | - Add tests for new features and bug fixes 5 | - Follow the existing style 6 | - Separate unrelated changes into multiple pull requests 7 | 8 | See the existing issues for things to start contributing. 9 | 10 | For bigger changes, please make sure you start a discussion first by creating an issue and explaining the intended change. 11 | 12 | Atlassian requires contributors to sign a Contributor License Agreement, known as a CLA. This serves as a record stating that the contributor is entitled to contribute the code/documentation/translation to the project and is willing to have it used in distributions and derivative works (or is willing to transfer ownership). 13 | 14 | Prior to accepting your contributions we ask that you please follow the appropriate link below to digitally sign the CLA. The Corporate CLA is for those who are contributing as a member of an organization and the individual CLA is for those contributing as an individual. 15 | 16 | - [CLA for corporate contributors](https://bitbucket.org/atlassian/oss-templates/src/master/CONTRIBUTING.md#:~:text=CLA%20for%20corporate%20contributors) 17 | - [CLA for individuals](https://opensource.atlassian.com/individual) 18 | -------------------------------------------------------------------------------- /snippets/scripts/convert-backstage-config/backstage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: compass-test-app 5 | title: Compass Test App 6 | description: This API enables CRUD operations on the Compass Test App 7 | labels: 8 | tier: "2" 9 | tags: 10 | - compass 11 | - test-app 12 | links: 13 | - url: https://bitbucket.org/atlassian-test/compass-test-repo 14 | title: Repository Link 15 | type: source 16 | - url: https://atlassian-test.com/wiki/pages/104862234987/How+Your+App+is+Architected 17 | title: Confluence 18 | type: docs 19 | - url: https://bitbucket.org/atlassian-test/compass-test-repo/pipelines/results/page/1 20 | title: Bitbucket Pipelines 21 | type: ci 22 | - url: https://example.com/app#/projects/compass-test-repo/deployments 23 | title: Octopus 24 | type: cd 25 | - url: https://atlassian-test.signalfx.com/#/dashboard/FNx234coD8A0AA 26 | title: SignalFX 27 | type: dashboard 28 | - url: https://splunk/logs 29 | title: Splunk 30 | type: logs 31 | spec: 32 | type: service 33 | lifecycle: beta 34 | owner: compass-platform-team 35 | system: api-compass 36 | providesApis: 37 | - API:compass-test-app 38 | consumesApis: 39 | - API:entities-api 40 | dependsOn: 41 | - Component:entities-api 42 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/resolvers.ts: -------------------------------------------------------------------------------- 1 | import Resolver from '@forge/resolver'; 2 | 3 | import { storage } from '@forge/api'; 4 | import { getPages } from './client/statuspage-manage'; 5 | 6 | const resolver = new Resolver(); 7 | 8 | resolver.define('validateAndConnectAPIKey', async (req) => { 9 | const { apiKey, email } = req.payload as { apiKey: string; email: string }; 10 | 11 | const pagesData = await getPages(apiKey); 12 | if (Object.entries(pagesData).length === 0) { 13 | return { 14 | success: false, 15 | }; 16 | } 17 | 18 | await storage.setSecret('manageAPIKey', apiKey); 19 | await storage.setSecret('manageEmail', email); 20 | return { 21 | success: true, 22 | }; 23 | }); 24 | 25 | resolver.define('disconnectAPIKey', async () => { 26 | await storage.deleteSecret('apiKeyName'); 27 | await storage.deleteSecret('manageEmail'); 28 | return { 29 | success: true, 30 | }; 31 | }); 32 | 33 | resolver.define('isAPIKeyConnected', async () => { 34 | const apiKey = await storage.getSecret('manageAPIKey'); 35 | if (apiKey !== undefined && apiKey !== null) { 36 | return { 37 | isConnected: false, 38 | }; 39 | } 40 | 41 | const pagesData = await getPages(apiKey); 42 | 43 | return { 44 | isConnected: Object.entries(pagesData).length !== 0, 45 | }; 46 | }); 47 | 48 | export default resolver.getDefinitions(); 49 | -------------------------------------------------------------------------------- /snippets/graphql/create-component/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will create a component on your site. 4 | 5 | Replace `cloudId` and `componentDetails` below in the variables section with the cloudId for your site and component information, and execute the query. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 6 | 7 | ### Query 8 | 9 | ```graphql 10 | mutation createComponent($cloudId: ID!, $componentDetails: CreateCompassComponentInput!) { 11 | compass { 12 | createComponent(cloudId: $cloudId, input: $componentDetails) { 13 | success 14 | componentDetails { 15 | id 16 | name 17 | typeId 18 | } 19 | } 20 | } 21 | } 22 | 23 | ``` 24 | 25 | ### Query Headers 26 | 27 | ``` 28 | { 29 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 30 | } 31 | ``` 32 | 33 | ### Query Variables 34 | 35 | We are using "SERVICE" as a component type in the example below, to learn about different component types, check this [link](https://developer.atlassian.com/cloud/compass/components/what-is-a-component/#component-types) 36 | 37 | ``` 38 | { 39 | "cloudId": "your-cloud-id", 40 | "componentDetails": { 41 | "name": "", 42 | "typeId": "SERVICE" 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-custom-field/README.md: -------------------------------------------------------------------------------- 1 | Use this script to copy values in your issues' Jira components to another existing custom field. 2 | This is useful if your in the process of migrating Jira components to Compass components and what to have a backup. 3 | 4 | ## Before you begin 5 | You'll need to prepare an API token for your Atlassian account. Learn how: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/ 6 | 7 | Other things you'll need: 8 | * Your site URL. 9 | * The exact project name you'd like to run this script for, like `Acme Software`. 10 | * The ID of the custom field you'd like to copy your data into. [Learn how](https://support.atlassian.com/jira-cloud-administration/docs/create-a-custom-field/) 11 | * * The ID should have a format like: `customfield_12345` 12 | * * Make sure the custom field is the right type. The custom field should be a **Labels** custom field. 13 | 14 | ## Local environment setup 15 | Install Python3: 16 | ```shell 17 | brew install python3 18 | ``` 19 | 20 | Install PIP3: 21 | ```shell 22 | python3 -m pip install --upgrade pip 23 | ``` 24 | 25 | Ensure you have a working python and pip: 26 | ```shell 27 | python3 --version 28 | python3 -m pip --version 29 | ``` 30 | 31 | Install script requirements: 32 | ```shell 33 | pip3 install -r requirements.txt 34 | ```` 35 | 36 | ## How to run locally 37 | ```shell 38 | python3 ./jira_components_to_jira_custom_field.py 39 | ``` 40 | -------------------------------------------------------------------------------- /snippets/scripts/update-component/update-components.sh: -------------------------------------------------------------------------------- 1 | ## 2 | # This is a sample bash script that can be used to update custom fields for a list of components. 3 | # Replace and with your own values. 4 | ## 5 | query='mutation updateComponentCustomField($input: UpdateCompassComponentInput!) { 6 | compass { 7 | updateComponent(input: $input) { 8 | success 9 | errors { 10 | message 11 | extensions { 12 | statusCode 13 | errorType 14 | } 15 | } 16 | componentDetails { 17 | id 18 | name 19 | customFields { 20 | definition { 21 | id 22 | name 23 | } 24 | ...on CompassCustomTextField { 25 | textValue 26 | } 27 | } 28 | } 29 | } 30 | } 31 | }' 32 | 33 | query="$(echo $query)" 34 | 35 | for id in ; do 36 | variables="{ 37 | \"input\": { 38 | \"id\": \"$id\", 39 | \"customFields\": [{ 40 | \"textField\": { 41 | \"definitionId\": \"\", 42 | \"textValue\": \"false\" 43 | } 44 | }] 45 | } 46 | }" 47 | curl https://api.atlassian.com/graphql \ 48 | -X POST \ 49 | -H "Content-Type: application/json" \ 50 | -H "Authorization: Basic " \ 51 | -d "{ \"query\":\"$query\", \"variables\": $variables }" 52 | done 53 | -------------------------------------------------------------------------------- /snippets/graphql/create-webhook/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will create a webhook for a component. This webhook is currently used to receive a JSON payload when a component is created from a template. Refer to [createTemplate](../create-template/README.md) to create a template, or [createComponentFromTemplate](../create-component-from-template/README.md) to create a component from a template. 4 | 5 | Replace the following variables in the variables section below, and execute the query. 6 | 7 | `component-id` - The component ID of the component to create a webhook for. 8 | 9 | `url-of-your-webhook` - The url of the webhook. 10 | 11 | 12 | You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 13 | 14 | ### Query 15 | 16 | ```graphql 17 | mutation createWebhook($input: CompassCreateWebhookInput!) { 18 | compass { 19 | createWebhook(input: $input) { 20 | success 21 | webhookDetails { 22 | id 23 | url 24 | } 25 | errors { 26 | ...CommonMutationError 27 | } 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ### Query Headers 34 | 35 | ``` 36 | { 37 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 38 | } 39 | ``` 40 | 41 | ### Query Variables 42 | 43 | ``` 44 | { 45 | "input": { 46 | "componentId": "", 47 | "url": "" 48 | } 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/entry/webtriggers/process-statuspage-incident-event.ts: -------------------------------------------------------------------------------- 1 | import graphqlGateway from '@atlassian/forge-graphql'; 2 | import { toIncidentEvent } from '../../utils/statuspage-incident-transformers'; 3 | import { WebtriggerRequest, WebtriggerResponse, StatuspageEvent } from '../../types'; 4 | import { serverResponse } from '../../utils/webtrigger-utils'; 5 | 6 | type Context = { 7 | installContext: string; 8 | }; 9 | 10 | export default async function processStatuspageIncidentEvent( 11 | request: WebtriggerRequest, 12 | context: Context, 13 | ): Promise { 14 | const { installContext } = context; 15 | 16 | const cloudId = installContext.split('/')[1]; 17 | const eventPayload = request.body; 18 | 19 | let parsedEvent: StatuspageEvent; 20 | 21 | try { 22 | parsedEvent = JSON.parse(eventPayload); 23 | } catch (error) { 24 | console.error({ message: 'Failed parsing webhook event', error }); 25 | return serverResponse('Invalid event format', null, 400); 26 | } 27 | 28 | // Don't send updates for non-incident events or scheduled updates 29 | if (!('incident' in parsedEvent) || parsedEvent.incident.status === 'scheduled') { 30 | return serverResponse('Processed webhook event'); 31 | } 32 | 33 | await graphqlGateway.compass.asApp().createEvent({ 34 | cloudId, 35 | event: { 36 | incident: toIncidentEvent(parsedEvent.incident, parsedEvent.page.id), 37 | }, 38 | }); 39 | 40 | return serverResponse('Processed webhook event'); 41 | } 42 | -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-compass-components/readme.md: -------------------------------------------------------------------------------- 1 | # Migrate Jira Project components to Compass 2 | 3 | Use this script to migrate your existing project components along with issues the components are used on to Compass. 4 | 5 | **This script is idempotent**. You can run it multiple times. 6 | It will only create components that do not already exist in Compass. 7 | It will update the components field on issues and replace the Jira component on those issues with the corresponding Compass component matching the name. 8 | By default, it will create Compass components with type SERVICE. 9 | You can change the type of the components later from Compass UI. 10 | 11 | _In case of any failures due to rate limit or error, re-run the script to migrate remaining components._ 12 | 13 | ## Prerequisites 14 | 15 | This script will work on projects that meet following criteria: 16 | 17 | * It is a company managed project 18 | * Compass Components are turned on for the project 19 | 20 | ## How to run the script 21 | Install python3 and then install the dependencies by running the following command: 22 | 23 | ```bash 24 | pip install -r requirements.txt 25 | ``` 26 | 27 | Run the script with 28 | 29 | ```bash 30 | python3 migrateJiraComponentsToCompassComponents.py 31 | ``` 32 | 33 | The script is interactive and will ask you for the following information: 34 | 35 | * Jira site hostname (e.g. `mycompany.atlassian.net`) 36 | * email address 37 | * API token (you can generate one [here](https://id.atlassian.com/manage-profile/security/api-tokens)) 38 | * Project key (e.g. `PROJ`) 39 | -------------------------------------------------------------------------------- /snippets/graphql/get-metric-definitions/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will retrieve a list of metric definitions for your site. 4 | 5 | Replace `cloudId` below in the variables section with the cloudId for your site and execute the query. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 6 | 7 | ### Query 8 | 9 | ```graphql 10 | query getMetricDefinitions($query: CompassMetricDefinitionsQuery!) { 11 | compass { 12 | metricDefinitions(query: $query) { 13 | ... on CompassMetricDefinitionsConnection { 14 | nodes { 15 | id 16 | name 17 | description 18 | type 19 | format { 20 | ... on CompassMetricDefinitionFormatSuffix { 21 | suffix 22 | } 23 | } 24 | derivedEventTypes 25 | } 26 | 27 | pageInfo { 28 | hasNextPage 29 | endCursor 30 | } 31 | } 32 | 33 | ... on QueryError { 34 | identifier 35 | message 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ### Query Headers 43 | 44 | ``` 45 | { 46 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 47 | } 48 | ``` 49 | 50 | ### Query Variables 51 | 52 | Exclude `after` variable on first run. 53 | 54 | ``` 55 | { 56 | "query": { 57 | "cloudId": "your-cloud-id", 58 | "first": 10, 59 | "after": "endCursor or null" 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/entry/data-provider/__tests__/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | import { storage, mockForgeApi } from './helpers/forge-helper'; 3 | /* eslint-disable import/first */ 4 | mockForgeApi(); 5 | import { dataProvider } from '../index'; 6 | import * as statusAPI from '../../../client/statuspage-status'; 7 | import * as createWebhookSubscription from '../../../client/statuspage-manage'; 8 | import { MOCK_STATUSPAGE_INCIDENT_UPDATE } from '../../../mocks'; 9 | 10 | const getPageIdSpy = jest.spyOn(statusAPI, 'getPageId'); 11 | const createWebhookSpy = jest.spyOn(createWebhookSubscription, 'createSubscription'); 12 | const getIncidentsSpy = jest.spyOn(statusAPI, 'getIncidents'); 13 | 14 | describe('dataProvider module', () => { 15 | it('successfully returns events and metrics in the expected format', async () => { 16 | storage.getSecret.mockResolvedValue('mock token'); 17 | 18 | getPageIdSpy.mockResolvedValue('mock-page-id'); 19 | createWebhookSpy.mockResolvedValue({ 20 | status: 200, 21 | }); 22 | getIncidentsSpy.mockResolvedValue([MOCK_STATUSPAGE_INCIDENT_UPDATE.incident]); 23 | 24 | const result = await dataProvider({ 25 | url: 'https://teststatuspage.statuspage.io/', 26 | ctx: { 27 | cloudId: 'ari:cloud:compass:122345:component/12345/12345', 28 | extensionId: 'mock-extension-id', 29 | }, 30 | }); 31 | 32 | expect(result.externalSourceId).toEqual('mock-page-id'); 33 | expect(result.metrics).toMatchSnapshot(); 34 | expect(result.events).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/client/statuspage-manage.ts: -------------------------------------------------------------------------------- 1 | import { fetch, webTrigger } from '@forge/api'; 2 | 3 | const BASE_URL = 'https://api.statuspage.io/v1'; 4 | 5 | async function makeGetRequest(url: string, token: string, options: any = {}) { 6 | return fetch(url, { 7 | method: 'GET', 8 | headers: { 9 | Authorization: `Bearer ${token}`, 10 | }, 11 | }); 12 | } 13 | async function makePostRequest(url: string, body: any, token: string) { 14 | return fetch(url, { 15 | method: 'POST', 16 | headers: { 17 | Authorization: `Bearer ${token}`, 18 | }, 19 | body: JSON.stringify(body), 20 | }); 21 | } 22 | export async function getPages(token: string) { 23 | const url = `${BASE_URL}/pages`; 24 | const result = await makeGetRequest(url, token); 25 | if (result.status !== 200) { 26 | throw Error('Bad Statuspage API response'); 27 | } 28 | return result.json(); 29 | } 30 | export async function createSubscription(pageCode: string, email: string, token: string) { 31 | const url = `${BASE_URL}/pages/${pageCode}/subscribers`; 32 | const webTriggerUrl = await webTrigger.getUrl('handle-statuspage-event'); 33 | const body = { 34 | subscriber: { 35 | email, 36 | endpoint: webTriggerUrl, 37 | }, 38 | }; 39 | 40 | const result = await makePostRequest(url, body, token); 41 | // Webhook for this page has already been added 42 | if (result.status === 409) { 43 | return result.json(); 44 | } 45 | if (result.status !== 201) { 46 | throw Error('Bad Statuspage API response'); 47 | } 48 | return result.json(); 49 | } 50 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render } from '@testing-library/react'; 3 | import { invoke as realInvoke } from '@forge/bridge'; 4 | import App from './App'; 5 | 6 | jest.mock('@forge/bridge', () => ({ 7 | invoke: jest.fn(), 8 | })); 9 | 10 | const invoke: jest.Mock = realInvoke as any; 11 | 12 | const defaultMocks: { [key: string]: any } = { 13 | getText: null, 14 | }; 15 | 16 | const mockInvoke = (mocks = defaultMocks) => { 17 | invoke.mockImplementation(async (key) => { 18 | if (mocks[key] instanceof Error) { 19 | throw mocks[key]; 20 | } 21 | 22 | return mocks[key]; 23 | }); 24 | }; 25 | 26 | describe('Admin page', () => { 27 | beforeEach(() => { 28 | invoke.mockReset(); 29 | }); 30 | 31 | it('renders app disconnected state', async () => { 32 | mockInvoke({ 33 | isAPIKeyConnected: { 34 | isConnected: false, 35 | }, 36 | }); 37 | const { findByText } = render(); 38 | expect(await findByText('API Token')).toBeDefined(); 39 | }); 40 | 41 | it('renders app connected state', async () => { 42 | mockInvoke({ 43 | isAPIKeyConnected: { 44 | isConnected: false, 45 | }, 46 | validateAndConnectAPIKey: { 47 | appId: 'abc', 48 | cloudId: '123', 49 | success: true, 50 | }, 51 | }); 52 | const { findByText } = render(); 53 | const submitButton = await findByText('Submit'); 54 | submitButton.click(); 55 | expect(await findByText('You are connected')).toBeDefined(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/manifest.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | compass:adminPage: 3 | - key: admin-page-ui 4 | resolver: 5 | function: admin-resolver 6 | resource: admin 7 | title: Statuspage Test 8 | icon: https://sp-compass-forge.s3.us-west-2.amazonaws.com/Statuspage_blue_80px.svg 9 | compass:dataProvider: 10 | - key: data-provider 11 | function: data-provider-fn 12 | callback: 13 | function: callback-fn 14 | domains: 15 | - "*.statuspage.io" 16 | - "*.status.atlassian.com" 17 | linkTypes: 18 | - other-link 19 | webtrigger: 20 | - key: handle-statuspage-event 21 | function: process-sp-incident-fn 22 | function: 23 | - key: admin-resolver 24 | handler: index.resolver 25 | - key: process-sp-incident-fn 26 | handler: index.processStatuspageIncidentEvent 27 | - key: data-provider-fn 28 | handler: index.dataProvider 29 | - key: callback-fn 30 | handler: index.callback 31 | app: 32 | id: ari:cloud:ecosystem::app/8eaba005-7b7c-4069-bcae-e84741d3a7b1 33 | resources: 34 | - key: admin 35 | path: ui/build 36 | tunnel: 37 | port: 3001 38 | permissions: 39 | scopes: 40 | - read:component:compass 41 | - storage:app 42 | - read:component:compass 43 | - write:component:compass 44 | - read:event:compass 45 | - write:event:compass 46 | - read:metric:compass 47 | - write:metric:compass 48 | external: 49 | fetch: 50 | backend: 51 | - "*.statuspage.io" 52 | - "*.status.atlassian.com" 53 | content: 54 | styles: 55 | - 'unsafe-inline' 56 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@atlaskit/button": "^16.1.6", 8 | "@atlaskit/form": "^8.5.6", 9 | "@atlaskit/css-reset": "^6.3.8", 10 | "@atlaskit/textfield": "^5.1.12", 11 | "@atlassian/forge-graphql": "^5.9.3", 12 | "@forge/bridge": "^2.1.3", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "styled-components": "^5.3.3" 16 | }, 17 | "devDependencies": { 18 | "@testing-library/jest-dom": "^5.16.2", 19 | "@testing-library/react": "^12.1.4", 20 | "@types/jest": "^27.4.1", 21 | "@types/node": "^14.14.31", 22 | "@types/react": "^17.0.40", 23 | "@types/react-dom": "^17.0.13", 24 | "@types/styled-components": "^5.1.24", 25 | "react-scripts": "^5.0.0", 26 | "typescript": "~4.5.5" 27 | }, 28 | "scripts": { 29 | "start": "SKIP_PREFLIGHT_CHECK=true BROWSER=none PORT=3001 react-scripts start", 30 | "build": "SKIP_PREFLIGHT_CHECK=true react-scripts build", 31 | "test": "SKIP_PREFLIGHT_CHECK=true react-scripts test", 32 | "pretest": "node -p \"JSON.stringify({...require('@forge/bridge/package.json'), main: 'out/index.js'}, null, 2)\" > tmp.json && mv tmp.json node_modules/@forge/bridge/package.json", 33 | "eject": "react-scripts eject" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /snippets/graphql/create-component-from-template/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will create a component from a template on your site. Note that this mutation will require an `@optIn(to: ["compass-beta"])` directive placed either on the `createComponentFromTemplate` field or on any parent (as done below). Refer to [createTemplate](../create-template/README.md) to create a template, or [createWebhook](../create-webhook/README.md) to create a webhook to receive a JSON payload after a component is created from a template. 4 | 5 | Replace `templateComponentId` and `createComponentDetails` below in the variables section with the ID of the template component you are using and component information, and execute the query. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 6 | 7 | 8 | ### Query 9 | 10 | ```graphql 11 | mutation createComponentFromTemplate($input: CreateCompassComponentFromTemplateInput!) { 12 | compass @optIn(to: ["compass-beta"]) { 13 | createComponentFromTemplate(input: $input) { 14 | success 15 | 16 | componentDetails { 17 | ...CompassComponentCore 18 | } 19 | 20 | errors { 21 | ...CommonMutationError 22 | } 23 | } 24 | } 25 | } 26 | 27 | 28 | ``` 29 | 30 | ### Query Variables 31 | 32 | ``` 33 | { 34 | "input": { 35 | "templateComponentId": "", 36 | "createComponentDetails": { 37 | "name": "", 38 | "typeId": "SERVICE" 39 | } 40 | } 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended', 'prettier'], 3 | settings: { 4 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 5 | 'import/resolver': { 6 | node: { 7 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 8 | }, 9 | }, 10 | 'import/external-module-folders': ['node_modules'], 11 | }, 12 | parser: '@typescript-eslint/parser', 13 | plugins: ['prettier', '@typescript-eslint'], 14 | rules: { 15 | 'max-len': [ 16 | 'warn', 17 | { 18 | code: 120, 19 | }, 20 | ], 21 | 'import/extensions': 'off', 22 | 'no-shadow': 'off', 23 | '@typescript-eslint/no-shadow': ['error'], 24 | 'no-restricted-syntax': 'off', 25 | 'no-underscore-dangle': 'off', 26 | 'no-await-in-loop': 'off', // https://softwareteams.atlassian.net/browse/COMPASS-2945 27 | 'import/no-extraneous-dependencies': [ 28 | 'error', 29 | { 30 | devDependencies: ['**/*.test.ts', '**/*.test.tsx', '**/__tests__/**/*'], 31 | }, 32 | ], 33 | 'import/no-unresolved': 'off', // https://softwareteams.atlassian.net/browse/COMPASS-2948 34 | 'react/react-in-jsx-scope': 'off', 35 | 'react/jsx-filename-extension': 'off', 36 | 'react/require-default-props': 'off', 37 | 'import/prefer-default-export': 'off', 38 | 'no-console': 'off', 39 | 'prettier/prettier': [ 40 | 'error', 41 | { 42 | singleQuote: true, 43 | trailingComma: 'all', 44 | tabWidth: 2, 45 | jsxSingleQuote: true, 46 | printWidth: 120, 47 | }, 48 | ], 49 | 'arrow-body-style': 'off', 50 | 'prefer-arrow-callback': 'off', 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /snippets/graphql/search-components/searchComponents.graphql: -------------------------------------------------------------------------------- 1 | query searchCompassComponents($cloudId: String!, $query: CompassSearchComponentQuery) { 2 | compass { 3 | searchComponents(cloudId: $cloudId, query: $query) { 4 | ... on CompassSearchComponentConnection { 5 | nodes { 6 | link 7 | component { 8 | name 9 | description 10 | typeId 11 | ownerId 12 | links { 13 | id 14 | type 15 | name 16 | url 17 | } 18 | labels { 19 | name 20 | } 21 | customFields { 22 | definition { 23 | id 24 | name 25 | } 26 | ...on CompassCustomBooleanField { 27 | booleanValue 28 | } 29 | ...on CompassCustomTextField { 30 | textValue 31 | } 32 | ...on CompassCustomNumberField { 33 | numberValue 34 | } 35 | ...on CompassCustomUserField { 36 | userValue { 37 | id 38 | name 39 | picture 40 | accountId 41 | canonicalAccountId 42 | accountStatus 43 | } 44 | } 45 | } 46 | } 47 | } 48 | pageInfo { 49 | hasNextPage 50 | endCursor 51 | } 52 | } 53 | ... on QueryError { 54 | message 55 | extensions { 56 | statusCode 57 | errorType 58 | } 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /snippets/graphql/get-metric-values-for-component/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will retrieve all metric sources and metric values for a given component. 4 | 5 | Replace `componentID` below in the variables section with a valid [Compass component ARI](https://developer.atlassian.com/cloud/compass/config-as-code/manage-components-with-config-as-code/#find-a-component-s-id) in your site and execute the query. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 6 | 7 | ### Query 8 | 9 | ```graphql 10 | query getMetricValue($componentID: ID!) { 11 | compass { 12 | component(id: $componentID) { 13 | ... on CompassComponent { 14 | metricSources(query: { first: 10 }) { 15 | ... on CompassComponentMetricSourcesConnection { 16 | nodes { 17 | id 18 | metricDefinition { 19 | name 20 | } 21 | 22 | values { 23 | ... on CompassMetricSourceValuesConnection { 24 | nodes { 25 | value 26 | 27 | timestamp 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | ### Query Headers 41 | 42 | ``` 43 | { 44 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 45 | } 46 | ``` 47 | 48 | ### Query Variables 49 | 50 | ``` 51 | { 52 | "componentID": "your-component-ari" 53 | # "componentID": "ari:cloud:compass:8c9fa0a4-58bf-4a52-a1c2-fb9d071abcbd:component/b17a5c71-52a9-4a98-a1a1-d3bdaba73178/01a7bf71-4cc2-4ab0-ad03-6aab38ec92ea" 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /snippets/graphql/create-metric-source/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will connect a metric to your component. 4 | 5 | Replace the following variables in the variables section below, and execute the query. 6 | 7 | `component-id` - The component ID of the component to connect a metric to 8 | 9 | `external-metric-source-id` - The identifier of your metric source in an external system, for example, a Bitbucket repository UUID. 10 | 11 | `metric-definition-id` - The metric definition ID of the metric to be added. Follow [createMetricDefinition](/snippets/graphql/create-metric-definitions/README.md) to create a metric definition if you do not already have an metric definition ID, or follow [getMetricDefinitions](/snippets/graphql/get-metric-definitions/README.md) to retrieve a predefined or existing metric definition ID. 12 | 13 | 14 | You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 15 | 16 | ### Query 17 | 18 | ```graphql 19 | mutation createMetricSource ($input:CompassCreateMetricSourceInput!){ 20 | compass { 21 | createMetricSource( 22 | input: $input 23 | ) { 24 | success 25 | createdMetricSource { 26 | title 27 | id 28 | metricDefinition { 29 | id 30 | } 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ### Query Headers 38 | 39 | ``` 40 | { 41 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 42 | } 43 | ``` 44 | 45 | ### Query Variables 46 | 47 | ``` 48 | { 49 | "input": { 50 | "componentId": "", 51 | "externalMetricSourceId": "", 52 | "metricDefinitionId": "" 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /snippets/graphql/get-metric-values-for-metric-definition/README.md: -------------------------------------------------------------------------------- 1 | This query can be used to get a paginateable list of metric values for metric sources with a specified metric definition. 2 | 3 | Replace `cloudId` and `metricDefinitionId` below in the variables section with the cloudId for your site and the metric definition ID, and execute the query. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 4 | ### Query 5 | 6 | ```graphql 7 | query getMetricDefinition ($cloudId: ID!, $metricDefinitionId: ID!){ 8 | compass { 9 | metricDefinition(cloudId: $cloudId, metricDefinitionId: $metricDefinitionId) { 10 | ... on CompassMetricDefinition { 11 | id 12 | name 13 | metricSources { 14 | ... on CompassMetricSourcesConnection { 15 | nodes { 16 | id 17 | values { 18 | ... on CompassMetricSourceValuesConnection { 19 | nodes { 20 | value 21 | timestamp 22 | } 23 | pageInfo { 24 | hasNextPage 25 | endCursor 26 | } 27 | } 28 | } 29 | } 30 | pageInfo { 31 | hasNextPage 32 | endCursor 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | ``` 42 | 43 | ### Query Headers 44 | 45 | ``` 46 | { 47 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 48 | } 49 | ``` 50 | 51 | ### Query Variables 52 | 53 | ``` 54 | { 55 | "cloudId": "your-cloud-id", 56 | "metricDefinitionId: "your-metric-definition-id" 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/entry/data-provider/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataProviderEventTypes, 3 | DataProviderResponse, 4 | DataProviderResult, 5 | BuiltinMetricDefinitions, 6 | } from '@atlassian/forge-graphql'; 7 | 8 | import { storage } from '@forge/api'; 9 | 10 | import { toDataProviderIncident } from '../../utils/statuspage-incident-transformers'; 11 | import { getPageCode, createWebhookSubscription, getPreviousIncidents } from '../../utils/statuspage-utils'; 12 | import { StatuspageIncident } from '../../types'; 13 | 14 | import { DataProviderPayload } from './types'; 15 | 16 | export const dataProvider = async (request: DataProviderPayload): Promise => { 17 | const pageCode = await getPageCode(request.url); 18 | 19 | // make a webhook subscription on the statuspage 20 | const apiKey = await storage.getSecret('manageAPIKey'); 21 | const email = await storage.getSecret('manageEmail'); 22 | 23 | if (apiKey === undefined || apiKey == null || email === undefined || email === null) { 24 | console.warn('API key or email not properly set up. Cannot process link.'); 25 | return null; 26 | } 27 | 28 | await createWebhookSubscription(pageCode, apiKey, email); 29 | 30 | // get previous incidents for this statuspage 31 | const backfilledIncidents = await getPreviousIncidents(request.url); 32 | 33 | const transformedIncidents = backfilledIncidents.map((incident: StatuspageIncident) => 34 | toDataProviderIncident(incident), 35 | ); 36 | 37 | const response = new DataProviderResponse(pageCode, { 38 | eventTypes: [DataProviderEventTypes.INCIDENTS], 39 | builtInMetricDefinitions: [ 40 | { 41 | name: BuiltinMetricDefinitions.MTTR_LAST_10, 42 | derived: true, 43 | }, 44 | ], 45 | customMetricDefinitions: [], 46 | }); 47 | 48 | return response.addIncidents(transformedIncidents).build(); 49 | }; 50 | -------------------------------------------------------------------------------- /snippets/scripts/components-forge-field-to-compass-components/README.md: -------------------------------------------------------------------------------- 1 | Use this script to copy values in your issues' Compass custom field to their Components field. This is so you can use the new Compass components integration with your company-managed Jira Software projects. 2 | 3 | ## Before you begin 4 | Make sure the projects you want to affect are switched to Compass components. Learn how: https://support.atlassian.com/jira-software-cloud/docs/switch-between-jira-and-compass-components/ 5 | In addition, we need you prepare the API token for your Atlassian account. Learn how: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/ 6 | 7 | ## Local environment setup 8 | Install Python3 9 | ```shell 10 | brew install python3 11 | ``` 12 | Install PIP3 command 13 | ```shell 14 | python3 -m pip install --upgrade pip 15 | ``` 16 | 17 | Ensure you have a working python and pip: 18 | ``` 19 | python3 --version 20 | python3 -m pip --version 21 | ``` 22 | 23 | Install requests module 24 | ```shell 25 | pip3 install -r requirements.txt 26 | ``` 27 | 28 | ## How to run locally 29 | First run file jiraProjectsInfo.py to collect your site's projects info. 30 | ```shell 31 | python3 ./jiraProjectsInfo.py 32 | ``` 33 | 34 | Please save the projects' keys. You can save keys by manually copying script output or redirecting the output to a file: 35 | ```shell 36 | python3 ./jiraProjectsInfo.py > project_keys.txt 37 | ``` 38 | 39 | or copying the output to clipboard: 40 | ```shell 41 | python3 ./jiraProjectsInfo.py | pbcopy 42 | ``` 43 | 44 | Second run file `migrateCompassCFToComponent.py` to migrate CompassCF to component field in all related issues in the project you want. 45 | ```shell 46 | python3 ./migrateCompassCFToComponent.py 47 | ``` 48 | 49 | ## Other Useful Links 50 | - https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-group-projects 51 | - https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-group-issues -------------------------------------------------------------------------------- /snippets/graphql/create-template/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will create a template on your site that can be used to quickly create new components. A template is a component with type template. Learn more about templates [here](https://developer.atlassian.com/cloud/compass/templates/about-templates/). Refer to [createComponentFromTemplate](../create-component-from-template/README.md) to create a component from this template, or [createWebhook](../create-webhook/README.md) to create a webhook to receive a JSON payload after a component is created using this template. 4 | 5 | Replace the following variables in the variables section below, and execute the query. 6 | 7 | `cloud-id` - The cloudId for your site and component information. 8 | 9 | `template-name` - The name of the template 10 | 11 | `repository-url` - The repository that hosts the template code which will be forked for components created from this template (currently only Github is supported). Required for template creation. 12 | 13 | Ensure that the typeId is "TEMPLATE". You may also specify any other `componentDetails` that you may want for the template such as owner team or description. 14 | 15 | ### Query 16 | 17 | ```graphql 18 | mutation createComponent($cloudId: ID!, $componentDetails: CreateCompassComponentInput!) { 19 | compass { 20 | createComponent(cloudId: $cloudId, input: $componentDetails) { 21 | success 22 | componentDetails { 23 | id 24 | name 25 | typeId 26 | } 27 | } 28 | } 29 | } 30 | 31 | ``` 32 | 33 | ### Query Headers 34 | 35 | ``` 36 | { 37 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 38 | } 39 | ``` 40 | 41 | ### Query Variables 42 | 43 | ``` 44 | { 45 | "cloudId": "", 46 | "componentDetails": { 47 | "name": "", 48 | "links": { 49 | "type": "REPOSITORY", 50 | "url": 51 | } 52 | "typeId": "TEMPLATE" 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /snippets/scripts/search-components/search-components.sh: -------------------------------------------------------------------------------- 1 | ## 2 | # This is a sample bash script that can be used to run the searchComponents query to retrieve 3 | # all components of type service. 4 | # Replace and with your own values and adjust the search critera (fieldFilters) as needed. 5 | ## 6 | query='query searchCompassComponents($cloudId: String!, $query: CompassSearchComponentQuery) { 7 | compass { 8 | searchComponents(cloudId: $cloudId, query: $query) { 9 | ... on CompassSearchComponentConnection { 10 | nodes { 11 | component { 12 | id 13 | } 14 | } 15 | pageInfo { 16 | hasNextPage 17 | endCursor 18 | } 19 | } 20 | } 21 | } 22 | }' 23 | query="$(echo $query)" 24 | 25 | variables='{ 26 | "cloudId": "", 27 | "query": { 28 | "fieldFilters": [ 29 | { 30 | "name": "type", 31 | "filter": { 32 | "eq":"SERVICE" 33 | } 34 | } 35 | ] 36 | } 37 | }' 38 | 39 | api_token="" 40 | email="" 41 | auth_header="${email}:${api_token}" 42 | encoded_auth_header=$(echo -n "${auth_header}" | base64) 43 | 44 | hasNextPage=true 45 | while [ "$hasNextPage" = true ] 46 | do response=$(curl https://api.atlassian.com/graphql \ 47 | -X POST \ 48 | -H "Content-Type: application/json" \ 49 | -H "Authorization: Basic $encoded_auth_header" \ 50 | -d "{ \"query\":\"$query\", \"variables\": $variables }") 51 | 52 | for id in $(echo "$response" | jq -r '.data.compass.searchComponents.nodes[].component.id'); do 53 | echo "$id" 54 | done 55 | 56 | hasNextPage=$(echo "$response" | jq -r '.data.compass.searchComponents.pageInfo.hasNextPage') 57 | if [ "$hasNextPage" = true ] 58 | then 59 | endCursor=$(echo "$response" | jq -r '.data.compass.searchComponents.pageInfo.endCursor') 60 | variables=$(echo "$variables" | jq --arg endCursor "$endCursor" '.query.after = $endCursor') 61 | fi 62 | done -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statuspage-example-app", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "author": "Atlassian", 6 | "license": "MIT", 7 | "private": true, 8 | "devDependencies": { 9 | "@forge/cli": "^4.1.0", 10 | "@typescript-eslint/eslint-plugin": "^5.14.0", 11 | "@typescript-eslint/parser": "^5.14.0", 12 | "@types/jest": "^27.4.1", 13 | "eslint": "^8.11.0", 14 | "eslint-config-airbnb-base": "^15.0.0", 15 | "eslint-config-prettier": "^8.5.0", 16 | "eslint-plugin-import": "^2.25.4", 17 | "eslint-plugin-jsx-a11y": "^6.5.1", 18 | "eslint-plugin-no-only-tests": "^2.6.0", 19 | "eslint-plugin-prettier": "^4.0.0", 20 | "eslint-plugin-react": "^7.29.4", 21 | "eslint-plugin-react-hooks": "^4.3.0", 22 | "husky": "^7.0.4", 23 | "jest": "^27.5.1", 24 | "jest-fetch-mock": "^3.0.3", 25 | "lint-staged": "^12.3.5", 26 | "prettier": "^2.5.1", 27 | "ts-jest": "^27.1.3", 28 | "typescript": "~4.5.5" 29 | }, 30 | "dependencies": { 31 | "@atlassian/forge-graphql": "^8.1.0", 32 | "@forge/api": "^2.6.0", 33 | "@forge/bridge": "^2.1.3", 34 | "@forge/resolver": "^1.4.2", 35 | "@forge/ui": "^1.1.0" 36 | }, 37 | "scripts": { 38 | "compile": "tsc --noEmit", 39 | "prepare": "husky install", 40 | "test": "jest", 41 | "lint": "yarn lint:eslint && yarn lint:prettier --check", 42 | "fix:prettier:and:lint": "yarn lint:prettier:fix && yarn lint:eslint:fix", 43 | "lint:eslint": "eslint '**/*.{tsx,js,ts}'", 44 | "lint:eslint:fix": "yarn lint:eslint --fix", 45 | "lint:prettier": "prettier '**/*.{tsx,js,ts}'", 46 | "lint:prettier:fix": "yarn lint:prettier --write", 47 | "ui:install": "cd ui && yarn install", 48 | "ui:start": "cd ui && yarn start", 49 | "ui:build": "cd ui && yarn build", 50 | "ui:test": "cd ui && yarn test --watchAll=false --passWithNoTests" 51 | }, 52 | "lint-staged": { 53 | "**/*.{tsx,js,ts}": [ 54 | "yarn run fix:prettier:and:lint" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/__tests__/contract/process-statuspage-event.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | import { mockAtlassianGraphQL, mockCreateEvent } from '../helpers/mock-atlassian-graphql'; 3 | /* eslint-disable import/first */ 4 | mockAtlassianGraphQL(); 5 | 6 | import processStatuspageEvent from '../../entry/webtriggers/process-statuspage-incident-event'; 7 | import { MOCK_STATUSPAGE_INCIDENT_UPDATE } from '../../mocks'; 8 | 9 | describe('Example webtrigger', () => { 10 | beforeEach(() => { 11 | jest.resetAllMocks(); 12 | jest.useFakeTimers(); 13 | jest.setSystemTime(Date.now()); 14 | }); 15 | 16 | test('makes request to Atlassian GraphQL Gateway to create incident event', async () => { 17 | await processStatuspageEvent( 18 | { 19 | body: JSON.stringify(MOCK_STATUSPAGE_INCIDENT_UPDATE), 20 | }, 21 | { 22 | installContext: 'test-site/site-id', 23 | }, 24 | ); 25 | 26 | expect(mockCreateEvent).toBeCalledTimes(1); 27 | expect(mockCreateEvent).toBeCalledWith({ 28 | cloudId: 'site-id', 29 | event: { 30 | incident: { 31 | externalEventSourceId: MOCK_STATUSPAGE_INCIDENT_UPDATE.page.id, 32 | displayName: MOCK_STATUSPAGE_INCIDENT_UPDATE.incident.name, 33 | description: MOCK_STATUSPAGE_INCIDENT_UPDATE.incident.incident_updates[0].body, 34 | lastUpdated: MOCK_STATUSPAGE_INCIDENT_UPDATE.incident.updated_at, 35 | updateSequenceNumber: Date.now(), 36 | url: MOCK_STATUSPAGE_INCIDENT_UPDATE.incident.shortlink, 37 | incidentProperties: { 38 | id: MOCK_STATUSPAGE_INCIDENT_UPDATE.incident.id, 39 | state: 'RESOLVED', 40 | severity: { 41 | label: 'critical', 42 | level: 'ONE', 43 | }, 44 | startTime: MOCK_STATUSPAGE_INCIDENT_UPDATE.incident.created_at, 45 | endTime: MOCK_STATUSPAGE_INCIDENT_UPDATE.incident.resolved_at, 46 | }, 47 | }, 48 | }, 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /snippets/scripts/convert-backstage-config/README.md: -------------------------------------------------------------------------------- 1 | # Backstage to Compass Config Conversion 2 | 3 | The `convert_to_compass.py` script is used to convert Backstage YAML config to Compass YAML config. Backstage usages can vary between organizations and teams. The script may not cover all the use cases. If you have custom requirements, you can update the `convert_handlers.py` and `convert_to_compass.py` scripts to handle them. For more information, see the [Structure and contents of a compass.yml file](https://developer.atlassian.com/cloud/compass/config-as-code/structure-and-contents-of-a-compass-yml-file/). 4 | 5 | ## Usage 6 | 7 | ```bash 8 | python convert_to_compass.py -o 9 | ``` 10 | 11 | The script takes the following arguments: 12 | - `path-to-backstage-config-file`: The path to the Backstage config file as a positional argument. 13 | - `-o`, `--output`: The path to the Compass config file as an optional argument. If not provided, the script will print the Compass config file to stdout. 14 | - `-dry`, `--dry-run`: If provided, the script will not write the Compass config file to the output path. It will only print the converted config to stdout. 15 | - `-h`, `--help`: Show the help message and exit. 16 | 17 | ## Running in bulk 18 | 19 | To run the script in a CI/CD pipeline or automated process, you can use the following command to put the converted Compass config files in the same directory as the Backstage config files: 20 | 21 | ```bash 22 | find . -name "backstage.yaml" | xargs -I {} sh -c 'python convert_to_compass.py {} -o "$(dirname {})/compass.yaml"' 23 | ``` 24 | 25 | ## Limitations 26 | 27 | The script has the following limitations: 28 | - It does not handle all the possible use cases of Backstage config. 29 | - Component ID has to be manually linked after YAML is used to create the model in Compass. 30 | - Similarly, dependency and ownership relationships have to be manually linked after the model is created in Compass. 31 | - Custom fields must exist in Compass before those can be used in the YAML config. -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/mocks.ts: -------------------------------------------------------------------------------- 1 | export const MOCK_STATUSPAGE_INCIDENT_UPDATE = { 2 | page: { 3 | id: 'test-page-code', 4 | }, 5 | incident: { 6 | name: 'test incident', 7 | status: 'resolved', 8 | created_at: '2023-01-20T20:49:36.249Z', 9 | updated_at: '2023-01-20T20:49:52.072Z', 10 | resolved_at: '2023-01-20T20:49:52.049Z', 11 | impact: 'critical', 12 | shortlink: 'https://stspg.io/test', 13 | scheduled_remind_prior: false, 14 | scheduled_auto_in_progress: false, 15 | scheduled_auto_completed: false, 16 | metadata: {}, 17 | started_at: '2023-01-20T20:49:36.244Z', 18 | id: 'test', 19 | page_id: 'page-id', 20 | incident_updates: [ 21 | { 22 | status: 'resolved', 23 | body: 'This incident has been resolved.', 24 | created_at: '2023-01-20T20:49:52.049Z', 25 | wants_twitter_update: false, 26 | updated_at: '2023-01-20T20:49:52.049Z', 27 | display_at: '2023-01-20T20:49:52.049Z', 28 | deliver_notifications: true, 29 | id: 'test', 30 | incident_id: 'test', 31 | affected_components: [Array], 32 | }, 33 | { 34 | status: 'investigating', 35 | body: 'We are currently investigating this issue.', 36 | created_at: '2023-01-20T20:49:36.315Z', 37 | wants_twitter_update: false, 38 | updated_at: '2023-01-20T20:49:36.315Z', 39 | display_at: '2023-01-20T20:49:36.315Z', 40 | deliver_notifications: true, 41 | id: 'test', 42 | incident_id: 'stest', 43 | affected_components: [Array], 44 | }, 45 | ], 46 | postmortem_ignored: false, 47 | postmortem_notified_subscribers: false, 48 | postmortem_notified_twitter: false, 49 | components: [ 50 | { 51 | status: 'operational', 52 | name: 'Component Name', 53 | created_at: '2020-08-13T18:56:54.359Z', 54 | updated_at: '2023-01-20T20:49:52.006Z', 55 | position: 4, 56 | showcase: true, 57 | id: 'test', 58 | page_id: 'test', 59 | }, 60 | ], 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /snippets/graphql/add-metric-to-components/README.md: -------------------------------------------------------------------------------- 1 | This following steps can be used to add a metric for a subset or all components. 2 | 3 | 1. Create or retrieve a metric definition. Follow [createMetricDefinition](/snippets/graphql/create-metric-definitions/README.md) to create a metric definition and save the `metric_definition_id`. To retrieve an existing metric definiton ID, follow [getMetricDefinitions](/snippets/graphql/get-metric-definitions/README.md) to retrieve a list of metric definitions on your site, or to add a predefined metric, grab the ID from [this list](https://developer.atlassian.com/cloud/compass/components/available-predefined-metrics/). 4 | 5 | 2. Retrieve the subset of components you would like to apply the metric definition to. Follow [searchComponents](/snippets/graphql/search-components/README.md) to search components based on various filters and retrieve `component_ids`. 6 | 7 | 3. For each component, call [createMetricSource](/snippets/graphql/create-metric-source/README.md) with the `metric_definition_id` to apply the metric definition. The `externalMetricSourceId` is a **unique** identifier of your metric source in an external system, for example, a Bitbucket repository UUID. 8 | 9 | ### Sample bash script 10 | ``` 11 | query='mutation createMetricSource($input:CompassCreateMetricSourceInput!) { 12 | compass { 13 | createMetricSource( input: $input ) { 14 | success 15 | createdMetricSource { 16 | title 17 | id 18 | metricDefinition { 19 | id 20 | } 21 | } 22 | } 23 | } 24 | }' 25 | query="$(echo $query)" 26 | 27 | for component_id in `component_ids` do 28 | variables='{ 29 | "input": { 30 | "componentId": "$component_id", 31 | "externalMetricSourceId": "", 32 | "metricDefinitionId": "" 33 | } 34 | }' 35 | variables="$(echo $variables)" 36 | 37 | curl https://api.atlassian.com/graphql \ 38 | -X POST \ 39 | -H "Content-Type: application/json" \ 40 | -H "Authorization: Basic " \ 41 | -H "X-ExperimentalApi: compass-beta, compass-prototype" \ 42 | -d "{ \"query\":\"$query\", \"variables\": $variables }" 43 | done 44 | ``` -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/README.md: -------------------------------------------------------------------------------- 1 | # Compass Metrics and Events Statuspage Example (Custom UI) 2 | 3 | This project demonstrates how the webtrigger, compass:adminPage, and compass:dataProvider [modules for Forge](https://developer.atlassian.com/platform/forge/manifest-reference/modules/index-compass/) can work together to ingest metrics and events from an external source (Statuspage) to Compass. It can be used as a companion to the [“Create a data provider app for events and metrics” tutorial](https://developer.atlassian.com/cloud/compass/integrations/create-a-data-provider-app/). This sample shows you how to build on the tutorial by making real API calls to a third party service, [Atlassian Statuspage](https://www.atlassian.com/software/statuspage). 4 | 5 | ## Getting Started 6 | 7 | Install the dependencies: 8 | 9 | ```bash 10 | nvm use 11 | yarn 12 | 13 | npm install -g @forge/cli # if you don't have it already 14 | ``` 15 | 16 | Set up the Custom UI Frontend 17 | 18 | ```bash 19 | yarn ui:install 20 | 21 | # build the frontend 22 | yarn ui:build 23 | 24 | # watch the frontend 25 | yarn ui:start 26 | ``` 27 | 28 | Set up the Forge App 29 | 30 | ```bash 31 | # login to Forge (will require an API token) 32 | forge login 33 | 34 | # register the app (this will change the app ID in the manifest) 35 | forge register 36 | 37 | # deploy the app 38 | forge deploy [-f] 39 | # -f, or --no-verify , allows you to include modules in your manifest that aren't officially published in Forge yet 40 | 41 | # install the app on your site 42 | forge install [--upgrade] 43 | # pick "Compass" and enter your site. <*.atlassian.net> 44 | # --upgrade will attempt to upgrade existing installations if the scopes or permissions have changed 45 | 46 | # run the tunnel which will listen for changes 47 | forge tunnel 48 | ``` 49 | 50 | ## Forge setup 51 | 52 | See [Set up Forge](https://developer.atlassian.com/platform/forge/set-up-forge/) for instructions to get set up. 53 | ### Notes 54 | 55 | - Use the `forge deploy` command when you want to persist code changes. 56 | - Use the `forge install` command when you want to install the app on a new site. 57 | - Once the app is installed on a site, the site picks up the new app changes you deploy without needing to rerun the install command. 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/types.ts: -------------------------------------------------------------------------------- 1 | type WebtriggerRequest = { 2 | body: string; 3 | }; 4 | 5 | type WebtriggerResponse = { 6 | body: string; 7 | statusCode: number; 8 | headers: Record; 9 | }; 10 | 11 | type ForgeTriggerContext = { 12 | installContext: string; 13 | }; 14 | 15 | type BaseStatuspageEvent = { 16 | meta: { 17 | unsubscribe: string; 18 | documentation: string; 19 | }; 20 | page: { 21 | id: string; 22 | status_indicator: string; 23 | status_description: string; 24 | }; 25 | }; 26 | 27 | type IncidentUpdate = { 28 | body: string; 29 | created_at: string; 30 | display_at: string; 31 | status: string; 32 | twitter_updated_at: string | null; 33 | updated_at: string; 34 | wants_twitter_update: boolean; 35 | id: string; 36 | incident_id: string; 37 | }; 38 | 39 | type ComponentUpdateEvent = BaseStatuspageEvent & { 40 | component_update: { 41 | created_at: string; 42 | new_status: string; 43 | old_status: string; 44 | id: string; 45 | component_id: string; 46 | }; 47 | component: { 48 | created_at: string; 49 | id: string; 50 | name: string; 51 | status: string; 52 | }; 53 | }; 54 | type StatuspageIncident = { 55 | backfilled: boolean; 56 | created_at: string; 57 | impact: string; 58 | impact_override: string; 59 | monitoring_at: string; 60 | postmortem_body: string; 61 | postmortem_body_last_updated_at: string; 62 | postmortem_ignored: boolean; 63 | postmortem_notified_subscribers: boolean; 64 | postmortem_notified_twitter: boolean; 65 | postmortem_published_at: string; 66 | resolved_at: string; 67 | scheduled_auto_transition: boolean; 68 | scheduled_for: string; 69 | scheduled_remind_prior: boolean; 70 | scheduled_reminded_at: string; 71 | scheduled_until: string; 72 | shortlink: string; 73 | status: string; 74 | updated_at: string; 75 | id: string; 76 | organization_id: string; 77 | incident_updates: IncidentUpdate[]; 78 | name: string; 79 | }; 80 | type IncidentUpdateEvent = BaseStatuspageEvent & { 81 | incident: StatuspageIncident; 82 | }; 83 | 84 | type StatuspageEvent = ComponentUpdateEvent | IncidentUpdateEvent; 85 | 86 | export { 87 | WebtriggerRequest, 88 | WebtriggerResponse, 89 | ForgeTriggerContext, 90 | IncidentUpdateEvent, 91 | StatuspageEvent, 92 | StatuspageIncident, 93 | }; 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Contributor Code of Conduct 2 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 3 | 4 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 5 | 6 | Examples of unacceptable behavior by participants include: 7 | 8 | The use of sexualized language or imagery 9 | Personal attacks 10 | Trolling or insulting/derogatory comments 11 | Public or private harassment 12 | Publishing other's private information, such as physical or electronic addresses, without explicit permission 13 | Submitting contributions or comments that you know to violate the intellectual property or privacy rights of others 14 | Other unethical or unprofessional conduct 15 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 16 | 17 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 18 | 19 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer. Complaints will result in a response and be reviewed and investigated in a way that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. 20 | 21 | This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available at http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import Form, { FormFooter, FormHeader } from '@atlaskit/form'; 3 | import ButtonGroup from '@atlaskit/button/button-group'; 4 | import LoadingButton from '@atlaskit/button/loading-button'; 5 | import Button from '@atlaskit/button/standard-button'; 6 | import { invoke } from '@forge/bridge'; 7 | import { FormWrapper, Centered } from './styles'; 8 | import { APITokenField } from './APITokenField'; 9 | import { EmailField } from './EmailField'; 10 | 11 | function App() { 12 | const [isConnected, setIsConnected] = useState(false); 13 | const [isLoading, setIsLoading] = useState(true); 14 | 15 | const onSubmit = (data: any) => { 16 | invoke<{ success: boolean }>('validateAndConnectAPIKey', data).then((respData) => { 17 | setIsConnected(respData.success); 18 | }); 19 | }; 20 | 21 | useEffect(() => { 22 | invoke<{ isConnected: boolean }>('isAPIKeyConnected').then((respData) => { 23 | setIsConnected(respData.isConnected); 24 | setIsLoading(false); 25 | }); 26 | }, []); 27 | 28 | if (isLoading) { 29 | return
Loading...
; 30 | } 31 | 32 | return isConnected ? ( 33 | 34 |
You are connected
35 | 44 |
45 | ) : ( 46 | 47 | 48 |
49 | {({ formProps, submitting }) => ( 50 | 51 | Connect to your Statuspage account. 52 |
Only active, paid public pages are supported at this time.
53 |
The email field will be used to confirm webhook subscriptions.
54 | 55 | 56 | 57 | 58 | 59 | Submit 60 | 61 | 62 | 63 | 64 | )} 65 | 66 |
67 |
68 | ); 69 | } 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /snippets/graphql/add-document-to-component/README.md: -------------------------------------------------------------------------------- 1 | This query can be used to add a document to a component 2 | https://developer.atlassian.com/cloud/compass/graphql/#mutations_addDocument 3 | https://developer.atlassian.com/cloud/compass/graphql/#queries_documentationCategories 4 | 5 | 6 | 7 | Replace cloudId, title, url, ComponentID and documentationCategoryId below in the variables section with the cloudId for your site, title and url of your document, componentID of your Compass component and the documentationcategory ID, and execute the query. You can use the GraphQL explorer to run this query and explore the Compass API further. 8 | 9 | NB: The addDocument mutation in Compass is an experimental feature, and you need to explicitly opt in by using the @optIn directive. 10 | 11 | First, you need to fetch the DocumentationCategoryID for the components. 12 | 13 | STEP 1: => Fetch DocumentationCategoryID for the component 14 | 15 | ##QUERY 16 | 17 | 18 | ```graphql 19 | query fetchDocumentationCategories($cloudId: ID!, $first: Int, $after: String) { 20 | compass { 21 | documentationCategories(cloudId: $cloudId, first: $first, after: $after) @optIn(to: "compass-beta") { 22 | nodes { 23 | id 24 | name 25 | } 26 | edges { 27 | node { 28 | id 29 | name 30 | } 31 | cursor 32 | } 33 | pageInfo { 34 | hasNextPage 35 | endCursor 36 | } 37 | } 38 | } 39 | } 40 | 41 | ##Query Variables 42 | 43 | ```graphql 44 | { 45 | "cloudId": "YOUR-CLOUD-ID", 46 | "first": 4, 47 | "after": null 48 | } 49 | 50 | 51 | From the above result, you will get the Document Category ID in the ARI format for the following types: 52 | 53 | Discover 54 | Contributor 55 | Maintainer 56 | Other 57 | 58 | Step 2: Use the following query to add a document to your Compass 59 | 60 | ##QUERY 61 | 62 | ```graphql 63 | mutation addDocument($input: CompassAddDocumentInput!) { 64 | compass { 65 | addDocument(input: $input) @optIn(to: "compass-beta") { 66 | success 67 | errors { 68 | message 69 | } 70 | documentDetails { 71 | id 72 | title 73 | url 74 | componentId 75 | documentationCategoryId 76 | } 77 | } 78 | } 79 | } 80 | 81 | ##QUERY VARIABLES 82 | 83 | ```graphql 84 | { 85 | "input": { 86 | "title": "title", 87 | "url": "your-doc-url", 88 | "componentId": "Your-component-id", 89 | "documentationCategoryId": "your-documentationcateoryid" 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-csv/jira_components_to_csv.py: -------------------------------------------------------------------------------- 1 | # Prereqs: pip/pip3 install -r requirements.txt 2 | 3 | import requests 4 | import datetime 5 | import pandas as pd 6 | 7 | # Replace with your email, API token, site URL, and Jira project keys 8 | email = "your-email" 9 | api_token = "your-api-token" 10 | site_url = "your-site-url" # desired format: my-cool-site.atlassian.net 11 | project_keys = ['your-project-key', 'another-project-key'] # List of Jira project keys or IDs to extract from 12 | 13 | auth = (email, api_token) 14 | 15 | all_data = [] 16 | 17 | if len(project_keys) > 100: 18 | print('Too many projects. Please specify no more than 100 projects.') 19 | else: 20 | # Iterate over the project keys 21 | for project_key in project_keys: 22 | # Construct the URL for the current project key 23 | url = f"https://{site_url}/rest/api/3/project/{project_key}/components" 24 | 25 | # Make the API request 26 | # API reference: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-components/#api-rest-api-3-project-projectidorkey-components-get 27 | response = requests.get(url, auth=auth) 28 | 29 | print() 30 | 31 | # If the request was successful, parse the JSON data to grab the project's Jira components 32 | if response.status_code == 200: 33 | data = response.json() 34 | for component in data: 35 | project_key_value = project_key 36 | component_name = component['name'] 37 | description = f"Jira component from the project {project_key_value}" 38 | 39 | all_data.append({ 40 | 'name': component_name, 41 | 'type': '', 42 | 'lifecycle stage': '', 43 | 'tier': '', 44 | 'description': description, 45 | 'labels': '', 46 | 'owner team': '', 47 | 'repositories': '' 48 | }) 49 | if len(all_data) == 1000: 50 | break 51 | else: 52 | print(f"Failed to get data for project: {project_key}") 53 | 54 | # Convert the data to a pandas DataFrame 55 | df = pd.DataFrame(all_data) 56 | 57 | # Write the DataFrame to an CSV file 58 | current_datetime = datetime.datetime.now().strftime("%Y%m%d%H%M%S") 59 | df.to_csv('output_component_{}.csv'.format(current_datetime), index=False) 60 | print('Written to output_component_{}.csv'.format(current_datetime)) 61 | -------------------------------------------------------------------------------- /snippets/scripts/search-components/search_components.py: -------------------------------------------------------------------------------- 1 | ## 2 | # This is a script that will retrieve all component IDs and write them to a file "component_ids.txt". 3 | # Make sure to replace api_token, email, and cloudId with your own values. 4 | ## 5 | import base64 6 | import requests 7 | import json 8 | 9 | url = "https://api.atlassian.com/graphql" 10 | api_token = "your-api-token" 11 | email = "your-email" 12 | auth_header = f"{email}:{api_token}" 13 | encoded_auth_header = base64.b64encode(auth_header.encode('utf-8')).decode('utf-8') 14 | headers = { 15 | "Content-Type": "application/json", 16 | "Accept": "application/json", 17 | "Authorization": f"Basic {encoded_auth_header}", 18 | } 19 | 20 | # Define the query and variables 21 | query = """ 22 | query searchCompassComponents($cloudId: String!, $query: CompassSearchComponentQuery) { 23 | compass { 24 | searchComponents(cloudId: $cloudId, query: $query) { 25 | ... on CompassSearchComponentConnection { 26 | nodes { 27 | component { 28 | id 29 | } 30 | } 31 | pageInfo { 32 | hasNextPage 33 | endCursor 34 | } 35 | } 36 | } 37 | } 38 | } 39 | """ 40 | 41 | variables = { 42 | "cloudId": "your-cloud-id", 43 | } 44 | 45 | filename = "component_ids.txt" 46 | with open(filename, 'w') as f: 47 | while True: 48 | # Prepare the data 49 | data = {"query": query, "variables": variables} 50 | 51 | # Send the request 52 | response = requests.post('https://api.atlassian.com/graphql', headers=headers, data=json.dumps(data)) 53 | 54 | # Parse the response 55 | response_data = response.json() 56 | if 'data' in response_data: 57 | ids = [node['component']['id'] for node in response_data['data']['compass']['searchComponents']['nodes']] 58 | for id in ids: 59 | f.write(id + '\n') 60 | 61 | # Check if there are more pages 62 | hasNextPage = response_data['data']['compass']['searchComponents']['pageInfo']['hasNextPage'] 63 | if hasNextPage: 64 | # Update the cursor 65 | endCursor = response_data['data']['compass']['searchComponents']['pageInfo']['endCursor'] 66 | variables['query'] = { 67 | "after": endCursor, 68 | } 69 | else: 70 | break 71 | else: 72 | print("Error:", response_data) 73 | break 74 | 75 | print(f"Component IDs have been written to {filename}.") -------------------------------------------------------------------------------- /snippets/scripts/bulk-delete-components/delete_components.py: -------------------------------------------------------------------------------- 1 | ## 2 | # This is a script that will delete all components from a file "component_ids.txt" or a list of component IDs specified in the script. 3 | # Make sure to replace api_token and email with your own values. 4 | ## 5 | 6 | import base64 7 | import requests 8 | 9 | dry_run = True 10 | 11 | url = "https://api.atlassian.com/graphql" 12 | api_token = "your-api-token" 13 | email = "your-email" 14 | auth_header = f"{email}:{api_token}" 15 | encoded_auth_header = base64.b64encode(auth_header.encode('utf-8')).decode('utf-8') 16 | headers = { 17 | "Content-Type": "application/json", 18 | "Accept": "application/json", 19 | "Authorization": f"Basic {encoded_auth_header}", 20 | } 21 | 22 | # List of component IDs 23 | # component_ids = [ 24 | # "id1", 25 | # "id2", 26 | # # Add more component IDs as needed 27 | # ] 28 | filename = "component_ids.txt" 29 | with open(filename, 'r') as f: 30 | component_ids = [line.strip() for line in f.readlines()] 31 | 32 | # GraphQL mutation template 33 | mutation_template = """ 34 | mutation DeleteComponent {{ 35 | compass {{ 36 | deleteComponent( 37 | input: {{id: "{component_id}"}} 38 | ) {{ 39 | deletedComponentId 40 | }} 41 | }} 42 | }} 43 | """ 44 | 45 | components_deleted = 0 46 | # Perform bulk deletion 47 | for component_id in component_ids: 48 | # Construct the GraphQL mutation 49 | mutation = mutation_template.format(component_id=component_id) 50 | 51 | if dry_run: 52 | # If dry run, just increment the counter 53 | components_deleted += 1 54 | else: 55 | # Send the GraphQL request 56 | response = requests.post(url, headers=headers, json={"query": mutation}) 57 | 58 | # Handle the response as needed 59 | data = response.json() 60 | deleted_component_id = data.get("data", {}).get("compass", {}).get("deleteComponent", {}).get("deletedComponentId") 61 | errors = data.get("data", {}).get("compass", {}).get("deleteComponent", {}).get("errors") 62 | 63 | if deleted_component_id: 64 | print(f"Component with ID {deleted_component_id} deleted successfully") 65 | components_deleted += 1 66 | else: 67 | print(f"Failed to delete component with ID {component_id}") 68 | if errors: 69 | for error in errors: 70 | print(f"Error message: {error.get('message')} Status code: {error.get('extensions', {}).get('statusCode')}") 71 | 72 | print(f"Number of components { 'to be deleted' if dry_run else 'deleted' }: {components_deleted}, dry_run: {dry_run}") 73 | -------------------------------------------------------------------------------- /snippets/scripts/compass-new-relic-importer/README.md: -------------------------------------------------------------------------------- 1 | ## Bootstrap your Compass Catalog from a New Relic Account 2 | 3 | This script 4 | 5 | 1. Gets all the apps within your New Relic account 6 | 2. Creates Compass Components for each of them 7 | 8 | ## Install 9 | 10 | You'll need node and npm to run this script 11 | 12 | ``` 13 | git clone https://github.com/atlassian-labs/compass-examples 14 | cd snippets/scripts/compass-new-relic-importer 15 | npm install 16 | ``` 17 | 18 | ## Credentials 19 | 20 | Create a `.env` file and fill in the blanks. This file should be ignored by git already since it is in the `.gitignore` 21 | 22 | ``` 23 | # Replace these with your NewRelic instance URL and API token 24 | NEW_RELIC_URL='' 25 | NEW_RELIC_API_TOKEN='' 26 | USER_EMAIL='' 27 | # https://id.atlassian.com/manage-profile/security/api-tokens 28 | TOKEN='' 29 | # Add your subdomain here - find it from the url - e.g. https://.atlassian.net 30 | TENANT_SUBDOMAIN='' 31 | # The UUID for your cloud site. This can be found in ARIs - look at the first uuid ari:cloud:compass:{cloud-uuid} 32 | CLOUD_ID='' 33 | ``` 34 | 35 | ## Do a dry run 36 | 37 | Preview what App the script will add to Compass. You don't need to wait until it's done - CTRL-C once you are comfortable. 38 | 39 | ``` 40 | DRY_RUN=1 node index.js 41 | ``` 42 | 43 | Output: 44 | 45 | ``` 46 | New component with external alias for https://one.newrelic.com/redirect/entity/ ... added test-app(dry-run) 47 | New component with external alias for https://one.newrelic.com/redirect/entity/ ... added test-app-1(dry-run) 48 | .... 49 | 50 | ``` 51 | 52 | By default, all apps will be added. If you notice any repositories that you don't want to import during the dry run, you can modify `index.js` to add filters accordingly. 53 | 54 | ```javascript 55 | for (const app of apps) { 56 | const { entityType, guid, name, permalink, tags } = app; 57 | 58 | const convertedLabels = newRelicTagsToCompassLabels(tags); 59 | const labelNames = [ 60 | "source:newrelic", 61 | `${formatTag("entitytype", entityType)}`, 62 | ...convertedLabels, 63 | ]; 64 | 65 | await putComponent( 66 | name, 67 | app.description || "", 68 | permalink, 69 | "new-relic", 70 | guid, 71 | JSON.stringify(labelNames) 72 | ); 73 | } 74 | ``` 75 | 76 | ## Ready to import? 77 | 78 | Remove `DRY_RUN=1` to run the CLI in write mode. 79 | 80 | ``` 81 | node index.js 82 | ``` 83 | 84 | If your connection is interrupted, or you want to re-run after the initial import you can. It checks for duplicates and skips them: 85 | 86 | ``` 87 | Already added https://one.newrelic.com/redirect/entity/ ... skipping 88 | ``` 89 | -------------------------------------------------------------------------------- /apps/sample-dataprovider-app-statuspage/src/utils/statuspage-incident-transformers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompassCreateIncidentEventInput, 3 | CompassIncidentEventState, 4 | CompassIncidentEventSeverityLevel, 5 | DataProviderIncidentEvent, 6 | } from '@atlassian/forge-graphql'; 7 | import { StatuspageIncident } from '../types'; 8 | 9 | function getState(status: string): CompassIncidentEventState { 10 | switch (status) { 11 | case 'investigating': 12 | return CompassIncidentEventState.Open; 13 | case 'identified': 14 | return CompassIncidentEventState.Open; 15 | case 'monitoring': 16 | return CompassIncidentEventState.Open; 17 | case 'in_progress': 18 | return CompassIncidentEventState.Open; 19 | case 'verifying': 20 | return CompassIncidentEventState.Open; 21 | case 'resolved': 22 | return CompassIncidentEventState.Resolved; 23 | case 'completed': 24 | return CompassIncidentEventState.Resolved; 25 | default: 26 | return CompassIncidentEventState.Resolved; 27 | } 28 | } 29 | function getSeverity(impact: string): CompassIncidentEventSeverityLevel { 30 | switch (impact) { 31 | case 'none': 32 | return CompassIncidentEventSeverityLevel.Five; 33 | case 'minor': 34 | return CompassIncidentEventSeverityLevel.Four; 35 | case 'major': 36 | return CompassIncidentEventSeverityLevel.Three; 37 | case 'critical': 38 | return CompassIncidentEventSeverityLevel.One; 39 | case 'maintenance': 40 | return CompassIncidentEventSeverityLevel.Four; 41 | default: 42 | return CompassIncidentEventSeverityLevel.Five; 43 | } 44 | } 45 | 46 | export function toIncidentEvent(incident: StatuspageIncident, pageCode: string): CompassCreateIncidentEventInput { 47 | const lastIncidentUpdate = incident.incident_updates[0]; 48 | return { 49 | externalEventSourceId: pageCode, 50 | displayName: incident.name, 51 | description: `${lastIncidentUpdate.body}`, 52 | lastUpdated: incident.updated_at, 53 | updateSequenceNumber: Date.now(), 54 | url: incident.shortlink, 55 | incidentProperties: { 56 | id: incident.id, 57 | state: getState(lastIncidentUpdate.status), 58 | severity: { 59 | label: incident.impact, 60 | level: getSeverity(incident.impact), 61 | }, 62 | startTime: incident.created_at, 63 | endTime: incident.resolved_at, 64 | }, 65 | }; 66 | } 67 | 68 | export function toDataProviderIncident(incident: StatuspageIncident): DataProviderIncidentEvent { 69 | const lastIncidentUpdate = incident.incident_updates[0]; 70 | return { 71 | displayName: incident.name, 72 | description: `${lastIncidentUpdate.body}`, 73 | lastUpdated: incident.updated_at, 74 | updateSequenceNumber: Date.now(), 75 | url: incident.shortlink, 76 | id: incident.id, 77 | state: getState(lastIncidentUpdate.status), 78 | severity: { 79 | label: incident.impact, 80 | level: getSeverity(incident.impact), 81 | }, 82 | startTime: incident.created_at, 83 | endTime: incident.resolved_at, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /snippets/scripts/compass-bitbucket-importer/README.md: -------------------------------------------------------------------------------- 1 | ## Bootstrap your Compass Catalog with from a Bitbucket Data Center Instance 2 | 3 | This script 4 | 1. Iterates over all your Bitbucket projects and all the repos within them 5 | 2. Creates Compass Components for each of them 6 | 3. Connects your Bitbucket repo by adding a Compass repository link to the created component 7 | 8 | 9 | ## Install 10 | 11 | You'll need node and npm to run this script 12 | 13 | ``` 14 | git clone https://github.com/atlassian-labs/compass-examples 15 | cd snippets/scripts/compass-bitbucket-importer 16 | npm install 17 | ``` 18 | 19 | ## Credentials 20 | Create a `.env` file and fill in the blanks. This file should be ignored by git already since it is in the `.gitignore` 21 | ``` 22 | # Replace these with your GitHub instance URL and access token (ex. 'bitbucket.test.com') 23 | BITBUCKET_URL='' 24 | #A Base64 encoded version of the following string `username:password` 25 | ACCESS_TOKEN='' 26 | USER_EMAIL='' 27 | # https://id.atlassian.com/manage-profile/security/api-tokens 28 | TOKEN='' 29 | # Add your subdomain here - find it from the url - e.g. https://.atlassian.net 30 | TENANT_SUBDOMAIN='' 31 | # The UUID for your cloud site. This can be found in ARIs - look at the first uuid ari:cloud:compass:{cloud-uuid} 32 | CLOUD_ID='' 33 | ``` 34 | ## Do a dry run 35 | Preview what Repos the script will add to Compass. You don't need to wait until it's done - CTRL-C once you are comfortable. 36 | ``` 37 | DRY_RUN=1 node index.js 38 | ``` 39 | 40 | Output: 41 | ``` 42 | New component for https://bitbucket.com/hyde.me/test_gb_less3_2 ... would be added hyde.me/test_gb_less3_2 (dry-run) 43 | New component for https://bitbucket.com/sachintomar009/reposetup ... would be added sachintomar009/reposetup (dry-run) 44 | New component for https://bitbucket.com/brianwilliamaldous/api-la ... would be added brianwilliamaldous/api-la (dry-run) 45 | New component for https://bitbucket.com/cloud-group3072118/timely ... would be added cloud-group3072118/timely (dry-run) 46 | .... 47 | 48 | ``` 49 | 50 | 51 | By default, all repos will be added. If you notice any repositories that you don't want to import during the dry run, you can modify `index.js` to add filters accordingly. 52 | 53 | ```javascript 54 | for (const repo of repos) { 55 | // need a filter, add one here! 56 | /* 57 | Example, skip is repo name is "hello-world" 58 | if (repo.name !== 'hello-world') 59 | */ 60 | if (true) { 61 | await putComponent(repo.name, repo.description || '', `https://${BITBUCKET_URL}/projects/${project.key}/repos/${repo.slug}`) 62 | } 63 | } 64 | ``` 65 | 66 | ## Ready to import? 67 | Remove `DRY_RUN=1` to run the CLI in write mode. 68 | ``` 69 | node index.js 70 | ``` 71 | 72 | If your connection is interrupted, or you want to re-run after the initial import you can. It checks for duplicates and skips them: 73 | 74 | ``` 75 | Already added https://bitbucket.com/VishnuprakashJ/home ... skipping 76 | ``` 77 | 78 | ## Next steps 79 | 80 | Once you're finished set up the on-prem webhook to link deployments, builds, etc to Compass. You only have to do this once (not once per Component) and Compass will associate the repo events with the Component they interact with. 81 | 82 | https://developer.atlassian.com/cloud/compass/components/create-incoming-webhooks/ -------------------------------------------------------------------------------- /snippets/scripts/compass-github-importer/README.md: -------------------------------------------------------------------------------- 1 | ## Bootstrap your Compass Catalog with from a Self-Hosted GitHub Instance 2 | 3 | This script 4 | 1. Iterates over all your GitHub organizations and all the repos within them 5 | 2. Can create incoming webhooks for each organization 6 | 2. Creates Compass Components for each of them 7 | 3. Connects your GitHub repo 8 | 9 | 10 | ## Install 11 | 12 | You'll need node and npm to run this script 13 | 14 | ``` 15 | git clone https://github.com/atlassian-labs/compass-examples 16 | cd snippets/scripts/compass-github-importer 17 | npm install 18 | ``` 19 | 20 | ## Credentials 21 | Create a `.env` file and fill in the blanks. This file should be ignored by git already since it is in the `.gitignore` 22 | ``` 23 | # Replace these with your GitHub instance URL and access token 24 | GITHUB_URL='' 25 | ACCESS_TOKEN='' 26 | USER_EMAIL='' 27 | # https://id.atlassian.com/manage-profile/security/api-tokens 28 | TOKEN='' 29 | # Add your subdomain here - find it from the url - e.g. https://.atlassian.net 30 | TENANT_SUBDOMAIN='' 31 | # The UUID for your cloud site. This can be found in ARIs - look at the first uuid ari:cloud:compass:{cloud-uuid} 32 | CLOUD_ID='' 33 | ``` 34 | ## Create incoming webhooks 35 | Create incoming webhooks in order to enable the ingestion of events and metrics from your GitHub instance 36 | ``` 37 | CREATE_WEBHOOKS=1 node index.js 38 | ``` 39 | Output: 40 | ``` 41 | New GitHub webhook for organization "myOrg1" created 42 | New GitHub webhook for organization "myOrg2" created 43 | New component for https://github.com/hyde.me/test_gb_less3_2 ... added hyde.me/test_gb_less3_2 44 | New component for https://github.com/sachintomar009/reposetup ... added sachintomar009/reposetup 45 | New component for https://github.com/brianwilliamaldous/api-la ... added brianwilliamaldous/api-la 46 | New component for https://github.com/cloud-group3072118/timely ... added cloud-group3072118/timely 47 | ``` 48 | ## Do a dry run 49 | Preview what Repos the script will add to Compass. You don't need to wait until it's done - CTRL-C once you are comfortable. 50 | ``` 51 | DRY_RUN=1 node index.js 52 | ``` 53 | 54 | Output: 55 | ``` 56 | New component for https://github.com/hyde.me/test_gb_less3_2 ... would be added hyde.me/test_gb_less3_2 (dry-run) 57 | New component for https://github.com/sachintomar009/reposetup ... would be added sachintomar009/reposetup (dry-run) 58 | New component for https://github.com/brianwilliamaldous/api-la ... would be added brianwilliamaldous/api-la (dry-run) 59 | New component for https://github.com/cloud-group3072118/timely ... would be added cloud-group3072118/timely (dry-run) 60 | .... 61 | 62 | ``` 63 | 64 | 65 | By default, all repos will be added. If you notice any repositories that you don't want to import during the dry run, you can modify `index.js` to add filters accordingly. 66 | 67 | ```javascript 68 | for (const repo of repos) { 69 | // need a filter, add one here! 70 | /* 71 | Example, skip is repo name is "hello-world" 72 | if (repo.name !== 'hello-world') 73 | */ 74 | if (true) { 75 | await putComponent(repo.name, repo.description || '', repo.html_url) 76 | } 77 | } 78 | ``` 79 | 80 | ## Ready to import? 81 | Remove `DRY_RUN=1` to run the CLI in write mode. 82 | ``` 83 | node index.js 84 | ``` 85 | 86 | If your connection is interrupted, or you want to re-run after the initial import you can. It checks for duplicates and skips them: 87 | 88 | ``` 89 | Already added https://github.com/VishnuprakashJ/home ... skipping 90 | ``` -------------------------------------------------------------------------------- /snippets/graphql/update-component-custom-field-value/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will update the value of a custom field for a given component 4 | 5 | Replace ``, ``, and `` below in the variables section. 6 | 7 | You can get a component's id by following the steps below: 8 | 1. In Compass, go to a component’s details page. Learn how to view a component's details 9 | 2. Select more actions (•••) then Copy component ID. 10 | 11 | Check the [graphQL example for getting custom field definitions for a given component](../get-component-custom-field-definitions/README.md) to get the relevant definition's id. 12 | 13 | Depending on what type the custom field's values are, choose the correct input that matches. 14 | 15 | You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 16 | 17 | Refer to `update-components.sh` for a sample bash script using this mutation. 18 | ### Mutation 19 | 20 | ```graphql 21 | mutation updateComponentCustomField($input: UpdateCompassComponentInput!) { 22 | compass { 23 | updateComponent(input: $input) { 24 | success 25 | errors { 26 | message 27 | extensions { 28 | statusCode 29 | errorType 30 | } 31 | } 32 | componentDetails { 33 | id 34 | name 35 | customFields { 36 | definition { 37 | id 38 | name 39 | } 40 | ...on CompassCustomBooleanField { 41 | booleanValue 42 | } 43 | ...on CompassCustomTextField { 44 | textValue 45 | } 46 | ...on CompassCustomNumberField { 47 | numberValue 48 | } 49 | ...on CompassCustomUserField { 50 | userValue { 51 | id 52 | name 53 | picture 54 | accountId 55 | canonicalAccountId 56 | accountStatus 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | ``` 66 | 67 | ### Mutation Headers 68 | 69 | ``` 70 | { 71 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 72 | } 73 | ``` 74 | 75 | ### Mutation Variables 76 | 77 | ##### Boolean Custom Field 78 | ``` 79 | { 80 | "input": { 81 | "id": "", 82 | "customFields": [{ 83 | "booleanField": { 84 | "definitionId": "", 85 | "booleanValue": "" 86 | } 87 | }] 88 | } 89 | } 90 | ``` 91 | 92 | ##### String / Text Custom Field 93 | ``` 94 | { 95 | "input": { 96 | "id": "", 97 | "customFields": [{ 98 | "textField": { 99 | "definitionId": "", 100 | "textValue": "" 101 | } 102 | }] 103 | } 104 | } 105 | ``` 106 | 107 | ##### Number Custom Field 108 | ``` 109 | { 110 | "input": { 111 | "id": "", 112 | "customFields": [{ 113 | "numberField": { 114 | "definitionId": "", 115 | "numberValue": "<5.0>" 116 | } 117 | }] 118 | } 119 | } 120 | ``` 121 | 122 | ##### User Custom Field 123 | ``` 124 | { 125 | "input": { 126 | "id": "", 127 | "customFields": [{ 128 | "userField": { 129 | "definitionId": "", 130 | "userIdValue": "" 131 | } 132 | }] 133 | } 134 | } 135 | ``` 136 | -------------------------------------------------------------------------------- /snippets/scripts/compass-gitlab-importer/README.md: -------------------------------------------------------------------------------- 1 | ## Bootstrap your Compass Catalog with from a Self-Hosted GitLab Instance 2 | 3 | This script 4 | 1. Iterates over all your GitLab projects 5 | 2. Can create incoming webhooks for each organization 6 | 3. Creates Compass Components for each of them 7 | 4. Connects your GitLab repo and Readme to that Component 8 | 9 | 10 | ## Install 11 | 12 | You'll need node and npm to run this script 13 | 14 | ``` 15 | git clone https://github.com/acunniffe/compass-gitlab 16 | cd compass-gitlab 17 | npm install 18 | ``` 19 | 20 | ## Credentials 21 | Create a `.env` file and fill in the blanks. This file should be ignored by git already since it is in the `.gitignore` 22 | ``` 23 | # Replace these with your GitLab instance URL and access token 24 | GITLAB_URL='' 25 | ACCESS_TOKEN='' 26 | USER_EMAIL='' 27 | # https://id.atlassian.com/manage-profile/security/api-tokens 28 | TOKEN='' 29 | # Add your subdomain here - find it from the url - e.g. https://.atlassian.net 30 | TENANT_SUBDOMAIN='' 31 | # The UUID for your cloud site. This can be found in ARIs - look at the first uuid ari:cloud:compass:{cloud-uuid} 32 | CLOUD_ID='' 33 | ``` 34 | ## Create incoming webhooks 35 | Create incoming webhooks in order to enable the ingestion of events and metrics from your GitLab instance 36 | ``` 37 | CREATE_WEBHOOKS=1 node index.js 38 | ``` 39 | Output: 40 | ``` 41 | New GitLab webhook for group "myGroup1" created 42 | New GitLab webhook for group "myGroup2" created 43 | New component for https://gitlab.com/hyde.me/test_gb_less3_2 ... added hyde.me/test_gb_less3_2 44 | New component for https://gitlab.com/sachintomar009/reposetup ... added sachintomar009/reposetup 45 | New component for https://gitlab.com/brianwilliamaldous/api-la ... added brianwilliamaldous/api-la 46 | New component for https://gitlab.com/cloud-group3072118/timely ... added cloud-group3072118/timely 47 | ``` 48 | ## Do a dry run 49 | Preview what Repos the script will add to Compass. You don't need to wait until it's done - CTRL-C once you are comfortable. 50 | ``` 51 | DRY_RUN=1 node index.js 52 | ``` 53 | 54 | Output: 55 | ``` 56 | New component for https://gitlab.com/hyde.me/test_gb_less3_2 ... would be added hyde.me/test_gb_less3_2 (dry-run) 57 | New component for https://gitlab.com/sachintomar009/reposetup ... would be added sachintomar009/reposetup (dry-run) 58 | New component for https://gitlab.com/brianwilliamaldous/api-la ... would be added brianwilliamaldous/api-la (dry-run) 59 | New component for https://gitlab.com/cloud-group3072118/timely ... would be added cloud-group3072118/timely (dry-run) 60 | .... 61 | 62 | ``` 63 | 64 | 65 | By default, all repos will be added. If you see some repos you don't want to import during the dry-run? Modify `index.js` to add some filters. There is a lot of metadata in the `project` variable. Read the API docs here: https://docs.gitlab.com/ee/api/projects.html 66 | 67 | ```javascript 68 | for (const project of projects) { 69 | 70 | // need a filter, add one here! 71 | /* 72 | Example, skip user's 'home' repos 73 | if (project.path !== 'home') 74 | */ 75 | if (true) { 76 | await putComponent(project.path_with_namespace, project.web_url, project.readme_url) 77 | } 78 | 79 | } 80 | 81 | ``` 82 | 83 | ## Ready to import? 84 | Remove `DRY_RUN=1` to run the CLI in write mode. 85 | ``` 86 | node index.js 87 | ``` 88 | 89 | If your connection is interrupted, or you want to re-run after the initial import you can. It checks for duplicates and skips them: 90 | 91 | ``` 92 | Already added https://gitlab.com/VishnuprakashJ/home ... skipping 93 | ``` -------------------------------------------------------------------------------- /snippets/graphql/search-components/README.md: -------------------------------------------------------------------------------- 1 | # How to use this 2 | 3 | This query will retrieve a paginate-able subset of components with various filters, fuzzy querying, and sorting. 4 | 5 | Replace `cloudId` below in the variables section with the cloudId for your site and execute the query.. You can use [the GraphQL explorer](https://developer.atlassian.com/cloud/compass/graphql/explorer/) to run this query and explore [the Compass API](https://developer.atlassian.com/cloud/compass/graphql/) further. 6 | 7 | Refer to `search-components.sh` for a sample bash script using this mutation. 8 | ### Query 9 | 10 | ```graphql 11 | query searchCompassComponents($cloudId: String!, $query: CompassSearchComponentQuery) { 12 | compass { 13 | searchComponents(cloudId: $cloudId, query: $query) { 14 | ... on CompassSearchComponentConnection { 15 | nodes { 16 | link 17 | component { 18 | name 19 | description 20 | typeId 21 | ownerId 22 | links { 23 | id 24 | type 25 | name 26 | url 27 | } 28 | labels { 29 | name 30 | } 31 | customFields { 32 | definition { 33 | id 34 | name 35 | } 36 | ...on CompassCustomBooleanField { 37 | booleanValue 38 | } 39 | ...on CompassCustomTextField { 40 | textValue 41 | } 42 | ...on CompassCustomNumberField { 43 | numberValue 44 | } 45 | ...on CompassCustomUserField { 46 | userValue { 47 | id 48 | name 49 | picture 50 | accountId 51 | canonicalAccountId 52 | accountStatus 53 | } 54 | } 55 | } 56 | } 57 | } 58 | pageInfo { 59 | hasNextPage 60 | endCursor 61 | } 62 | } 63 | ... on QueryError { 64 | message 65 | extensions { 66 | statusCode 67 | errorType 68 | } 69 | } 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | ### Query Headers 76 | 77 | ``` 78 | { 79 | "X-ExperimentalApi": ["compass-beta","compass-prototype"] 80 | } 81 | ``` 82 | 83 | ### Query Variables 84 | 85 | 86 | ##### Query with Component Type Filter 87 | ``` 88 | { 89 | "cloudId": "", 90 | "query": { 91 | "fieldFilters": [ 92 | { 93 | "name": "type", 94 | "filter": { 95 | "eq":"SERVICE" 96 | } 97 | } 98 | ] 99 | } 100 | } 101 | ``` 102 | 103 | ##### Query with Pagination and Component Type Filter 104 | ``` 105 | { 106 | "cloudId": "", 107 | "query": { 108 | "first": "<20>" 109 | "after": "" 110 | "fieldFilters": [ 111 | { 112 | "name": "type", 113 | "filter": { 114 | "eq":"SERVICE" 115 | } 116 | } 117 | ] 118 | } 119 | } 120 | ``` 121 | 122 | ##### Query with Fuzzy Text Search 123 | ``` 124 | { 125 | "cloudId": "", 126 | "query": { 127 | "query": "" 128 | } 129 | } 130 | ``` 131 | 132 | ##### Query Sorted by Name Descending 133 | ``` 134 | { 135 | "cloudId": "", 136 | "query": { 137 | "sort": { 138 | "name": "title", 139 | "order": "DESC" 140 | } 141 | } 142 | } 143 | ``` 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compass-examples 2 | 3 | [![Atlassian license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](CONTRIBUTING.md) 4 | 5 | > This repo is used for sharing example [Atlassian Forge apps](https://developer.atlassian.com/platform/forge/) & SDK code snippets, as well as code snippets for interacting with the [Atlassian Compass](https://www.atlassian.com/software/compass) API (GraphQL & REST). Compass was built from the ground up with extensibility in mind so you can build [the ultimate developer experience platform](https://www.youtube.com/watch?v=F92QM_3u0gw). Learn more about [building apps for Compass](https://developer.atlassian.com/cloud/compass/integrations/get-started-integrating-with-Compass/) using the Atlassian serverless app development platform Forge, our open-by-default [Compass APIs](https://developer.atlassian.com/cloud/compass/graphql/), and our [Compass Forge modules](https://developer.atlassian.com/platform/forge/manifest-reference/modules/index-compass/) that provide boilerplate to help you get started building faster. 6 | 7 | ## Examples 8 | 9 | > Details about each example found in this repo are below. Each directory contains a README with additional information/instructions about using each example. 10 | 11 | | Example | Description | 12 | | ------- | ----------- | 13 | | [dataProvider + events + metrics sample app](apps/sample-dataprovider-app-statuspage/) | A sample application that demonstrates how to use the [dataProvider module](https://developer.atlassian.com/platform/forge/manifest-reference/modules/compass-data-provider/) to create [event](https://developer.atlassian.com/cloud/compass/components/send-events-using-rest-api/) and [metric](https://developer.atlassian.com/cloud/compass/components/create-connect-and-view-component-metrics/) sources, then update events and metrics with a [webhook](https://developer.atlassian.com/platform/forge/manifest-reference/modules/web-trigger/). [Tutorial on creating a data provider app](https://developer.atlassian.com/cloud/compass/integrations/create-a-data-provider-app/).| 14 | | [GraphQL Snippets](snippets/graphql/) | A collection of various GraphQL queries and mutations that have been helpful for customers. | 15 | | [Forge GraphQL SDK Snippets](snippets/forge-graphql-sdk/) | A collection of various Forge GraphQL SDK usage examples. | 16 | | [GitLab app for Compass](https://github.com/atlassian-labs/gitlab-for-compass) | Not hosted in this repo but [the GitLab app for Compass is fully open source](https://github.com/atlassian-labs/gitlab-for-compass) and is a great "production ready" app to use as a reference.| 17 | 18 | ## Handy links 19 | 20 | - [Get started integrating with Compass](https://developer.atlassian.com/cloud/compass/integrations/get-started-integrating-with-Compass/) 21 | - [Get a Compass site](https://www.atlassian.com/try/cloud/signup?bundle=compass) for development and testing 22 | - [Compass Forge modules](https://developer.atlassian.com/platform/forge/manifest-reference/modules/index-compass/) 23 | - [GraphQL API](https://developer.atlassian.com/cloud/compass/graphql/#overview) 24 | - [Compass Forge SDK](https://www.npmjs.com/package/@atlassian/forge-graphql) 25 | 26 | 27 | ## Contributions 28 | 29 | Contributions are welcome! We're open to improving upon any existing examples and accepting new examples from the community that may help other Compass users. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. 30 | 31 | ## License 32 | 33 | Copyright (c) 2023 Atlassian US., Inc. 34 | Apache 2.0 licensed, see [LICENSE](LICENSE) file. 35 | 36 |
37 | 38 | [![With ❤️ from Atlassian](https://raw.githubusercontent.com/atlassian-internal/oss-assets/master/banner-cheers-light.png)](https://www.atlassian.com) 39 | -------------------------------------------------------------------------------- /snippets/scripts/components-forge-field-to-compass-components/jiraProjectsInfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import getpass 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | from requests.exceptions import HTTPError 7 | 8 | # Author: Huimin Pang 9 | 10 | DEFAULT_PAGE_SIZE = 50 11 | 12 | # Define Project Object 13 | class Project: 14 | def __init__(self, key, name, project_type_key, url): 15 | self.key = key 16 | self.name = name 17 | self.project_type_key = project_type_key 18 | self.url = url 19 | 20 | def print_project_info(self): 21 | print("Key: {}".format(self.key)) 22 | print("Name: {}".format(self.name)) 23 | print("Link: {}".format(self.url)) 24 | print("**********") 25 | 26 | 27 | # Utility Functions 28 | # Check whether the input is empty string or not 29 | def check_input(input_string, input_type): 30 | if not bool(input_string): 31 | print("Please provide a valid " + input_type + ".") 32 | quit() 33 | 34 | 35 | def get_software_projects_with_pagination(domain_name, user_name, api_token, start_at, max_results): 36 | auth = HTTPBasicAuth(user_name, api_token) 37 | headers = { 38 | "Accept": "application/json" 39 | } 40 | isLast = False 41 | projects_raw = [] 42 | offset = start_at 43 | 44 | print('Loading projects . . .') 45 | 46 | while not isLast: 47 | try: 48 | url = domain_name + f"/rest/api/3/project/search?type=software&startAt={offset}&maxResults={max_results}&orderBy=key&action=view" 49 | response = requests.get(url, auth=auth, headers=headers, timeout=1) 50 | 51 | if response.ok: 52 | response_json = response.json() 53 | isLast = response_json['isLast'] 54 | projects_raw += response_json['values'] 55 | offset += max_results 56 | projects_loaded = len(projects_raw) 57 | projects_total = response_json['total'] 58 | 59 | print(F"Loaded {projects_loaded} projects of {projects_total}") 60 | else: 61 | print(f"Unexpected HTTP response code {response.status_code} while retrieving projects for {domain_name}. Please try again.") 62 | quit() 63 | 64 | except HTTPError as http_err: 65 | print(f"Couldn't retrieve projects for {domain_name} due to an HTTP error: {http_err}. Please try again.") 66 | quit() 67 | except Exception as err: 68 | print(f"Couldn't retrieve projects for {domain_name} due to an error: {err}. Please try again.") 69 | quit() 70 | 71 | return projects_raw 72 | 73 | def parse_project_raw_result(projects_raw_json): 74 | projects_info = {} 75 | for project in projects_raw_json: 76 | key = project.get('key') 77 | name = project.get('name') 78 | project_type_key = project.get('projectTypeKey') 79 | url = project.get('self') 80 | projects_info[key] = Project(key, name, project_type_key, url) 81 | return projects_info 82 | 83 | 84 | def print_projects(projects): 85 | for key in projects: 86 | project = projects[key] 87 | project.print_project_info() 88 | 89 | 90 | def get_projects_by_type(projects, type): 91 | filtered_projects_raw_json = [project for project in projects if project["projectTypeKey"] == type] 92 | projects_info = parse_project_raw_result(filtered_projects_raw_json) 93 | return projects_info 94 | 95 | 96 | # Main Function 97 | def main(): 98 | # Get the list of software projects 99 | projects_raw = get_software_projects_with_pagination(DOMAIN_NAME, USER_NAME, API_TOKEN, 0, DEFAULT_PAGE_SIZE) 100 | 101 | project_count = len(projects_raw) 102 | print(f"Successfully retrieved {project_count} projects for {DOMAIN_NAME}.\n") 103 | 104 | if project_count > 0: 105 | projects_dict = parse_project_raw_result(projects_raw) 106 | print_projects(projects_dict) 107 | else: 108 | print(f"Your site doesn't have any project with software-type project.\n") 109 | quit() 110 | 111 | if __name__ == '__main__': 112 | # Ask user for the input 113 | DOMAIN_NAME = input("Enter your domain (example: https://acme.atlassian.net/):").strip() 114 | check_input(DOMAIN_NAME, "domain") 115 | USER_NAME = input("Enter your email address : ").strip() 116 | check_input(USER_NAME, "userName") 117 | API_TOKEN = getpass.getpass("Enter your API token: ").strip() 118 | check_input(API_TOKEN, "apiToken") 119 | print("\n") 120 | 121 | main() 122 | -------------------------------------------------------------------------------- /snippets/scripts/convert-backstage-config/convert_handlers.py: -------------------------------------------------------------------------------- 1 | # Field Handler Functions. For the Compass YAML structure, refer: 2 | # https://developer.atlassian.com/cloud/compass/config-as-code/structure-and-contents-of-a-compass-yml-file/ 3 | 4 | def handle_name(value): 5 | """ 6 | Convert the name value from Backstage to Compass format. 7 | 8 | :param value: The name value from Backstage. 9 | :return: The name value in Compass format. 10 | """ 11 | # Compass has a limit of 512 characters for the name 12 | return value.replace("\u0000", "").strip()[:512] 13 | 14 | 15 | def handle_description(value): 16 | """ 17 | Convert the description value from Backstage to Compass format. 18 | 19 | :param value: The description value from Backstage. 20 | :return: The description value in Compass format. 21 | """ 22 | # Compass has a limit of 4096 characters for the description 23 | return value.replace("\u0000", "").strip()[:4096] 24 | 25 | 26 | def handle_lifecycle(value): 27 | """ 28 | Convert the lifecycle value from Backstage to Compass format. 29 | Available values in Compass are Pre-Release, Active, Deprecated. 30 | 31 | :param value: The lifecycle value from Backstage. 32 | :return: The lifecycle value in Compass format. 33 | """ 34 | lifecycle_mappings = { 35 | "experimental": "Pre-Release", 36 | "beta": "Pre-Release", 37 | "production": "Active", 38 | "deprecated": "Deprecated", 39 | "end-of-life": "Deprecated" 40 | } 41 | 42 | # Implement your conversion logic here 43 | if value.lower() in lifecycle_mappings: 44 | return lifecycle_mappings[value.lower()] 45 | 46 | return value 47 | 48 | 49 | def handle_links(values): 50 | """ 51 | Convert the links from Backstage to Compass format. 52 | Refer to the Compass YAML link at the top for available link types. 53 | 54 | :param values: The links from Backstage. 55 | :return: The links in Compass format. 56 | """ 57 | link_type_mappings = { 58 | "source": "REPOSITORY", 59 | "docs": "DOCUMENT", 60 | "ci": "DASHBOARD", 61 | "cd": "DASHBOARD", 62 | "dashboard": "DASHBOARD", 63 | "logs": "DASHBOARD" 64 | } 65 | 66 | result = [] 67 | 68 | # Compass has a limit of 10 links per component 69 | for link in values[:10]: 70 | link_type = link_type_mappings.get(link["type"].lower(), "OTHER_LINK") 71 | link_name = link.get("title", None) 72 | link_url = link["url"] 73 | 74 | result.append({ 75 | "type": link_type, 76 | "name": link_name, 77 | "url": link_url 78 | }) 79 | 80 | return result 81 | 82 | 83 | def handle_tier(value): 84 | """ 85 | Convert the tier value from Backstage to Compass format. 86 | Available values in Compass are Tier 1 through Tier 4. 87 | 88 | :param value: The tier value from Backstage. 89 | :return: The tier value in Compass format. 90 | """ 91 | if value is None: 92 | return None 93 | 94 | # Implement your conversion logic here if needed 95 | tier_value = int(value) 96 | return max(1, min(4, tier_value)) 97 | 98 | 99 | def handle_type(value): 100 | """ 101 | Convert the type value from Backstage to Compass format. 102 | Refer to the Compass YAML link at the top for available component types. 103 | 104 | :param value: The type value from Backstage. 105 | :return: The type value in Compass format. 106 | """ 107 | component_type_mappings = { 108 | "service": "SERVICE", 109 | "website": "WEBSITE", 110 | "library": "LIBRARY", 111 | "sdk": "LIBRARY" 112 | } 113 | 114 | # Implement your conversion logic here if needed 115 | return component_type_mappings.get(value.lower(), "SERVICE") 116 | 117 | 118 | def handle_labels(value): 119 | """ 120 | Convert the labels from Backstage to Compass format. 121 | 122 | :param value: The list of tags from Backstage. 123 | :return: The labels in Compass format. 124 | """ 125 | # Tagging all components as imported from Backstage and truncating the labels to 40 characters 126 | value = [v[:40] for v in value if v is not None] + ["imported:backstage"] 127 | 128 | # Compass has a limit of 10 labels per component 129 | return value[:10] 130 | 131 | 132 | FIELD_HANDLERS = { 133 | "handle_name": handle_name, 134 | "handle_description": handle_description, 135 | "handle_lifecycle": handle_lifecycle, 136 | "handle_links": handle_links, 137 | "handle_tier": handle_tier, 138 | "handle_type": handle_type, 139 | "handle_labels": handle_labels, 140 | } 141 | -------------------------------------------------------------------------------- /snippets/scripts/convert-backstage-config/convert_to_compass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | In this script, the `MAPPINGS` dictionary defines a mapping from input paths in the Backstage YAML to output paths 4 | in the Compass YAML. Paths are specified as strings with dots separating each level of nesting. You can add more 5 | entries to this dictionary as needed. 6 | """ 7 | 8 | import yaml 9 | import argparse 10 | import sys 11 | import logging 12 | 13 | from functools import reduce 14 | from operator import getitem 15 | 16 | from convert_handlers import FIELD_HANDLERS 17 | 18 | # Mapping of Backstage YAML fields to Compass YAML fields - add more mappings here as needed 19 | MAPPINGS = { 20 | "metadata.name": {"path": "name", "handler": "handle_name"}, 21 | "metadata.description": {"path": "description", "handler": "handle_description"}, 22 | "metadata.labels.tier": {"path": "fields.tier", "handler": "handle_tier"}, 23 | "metadata.links": {"path": "links", "handler": "handle_links"}, 24 | "metadata.tags": {"path": "labels", "handler": "handle_labels"}, 25 | "spec.lifecycle": {"path": "fields.lifecycle", "handler": "handle_lifecycle"}, 26 | "spec.type": {"path": "typeId", "handler": "handle_type"}, 27 | } 28 | 29 | 30 | def load_yaml(file_path): 31 | """Load a YAML file from the given path.""" 32 | try: 33 | with open(file_path, 'r') as file: 34 | return yaml.safe_load(file) 35 | except Exception as e: 36 | print(f"Error loading YAML file: {e}", file=sys.stderr) 37 | sys.exit(1) 38 | 39 | 40 | def dump_yaml(data, file_path): 41 | """Dump a YAML file to the given path.""" 42 | try: 43 | with open(file_path, 'w') as file: 44 | yaml.dump(data, file) 45 | except Exception as e: 46 | print(f"Error dumping YAML file: {e}", file=sys.stderr) 47 | sys.exit(1) 48 | 49 | 50 | def create_parser(): 51 | """Create an argument parser for the script.""" 52 | parser = argparse.ArgumentParser(description='Convert Backstage YAML to Compass YAML.') 53 | parser.add_argument('input_file_path', type=str, help='Path to the input Backstage YAML file.') 54 | parser.add_argument('-o', '--output', type=str, help='Path to the output Compass YAML file.') 55 | parser.add_argument('-dry', '--dry-run', action='store_true', help='Run the script without writing any files.') 56 | return parser 57 | 58 | 59 | def map_properties(input_data): 60 | """Map properties from Backstage YAML to Compass YAML.""" 61 | if input_data.get("kind", None) != "Component": 62 | logging.error("The input file is not a valid Backstage Component YAML file.") 63 | raise ValueError("Invalid input file") 64 | 65 | # Start with default config data 66 | output_data = { 67 | "configVersion": 1, 68 | "fields": {}, 69 | "labels": ["imported:backstage"] 70 | } 71 | 72 | for input_path, output_info in MAPPINGS.items(): 73 | try: 74 | input_value = get_value_from_path(input_data, input_path.split('.')) 75 | 76 | if isinstance(output_info, dict): 77 | handler_name = output_info.get("handler", None) 78 | if handler_name: 79 | handler = FIELD_HANDLERS.get(handler_name, None) 80 | if handler is None: 81 | err = f"Handler '{handler_name}' is not implemented for input path '{input_path}'" 82 | raise NotImplementedError(err) 83 | 84 | input_value = handler(input_value) 85 | 86 | # Skip if input value is None 87 | if input_value is None: 88 | continue 89 | 90 | output_path = output_info.get("path", "").split('.') 91 | else: 92 | output_path = output_info.split('.') 93 | 94 | set_value_at_path(output_data, output_path, input_value) 95 | except Exception as e: 96 | logging.error(f"Error occurred while mapping properties: {str(e)}") 97 | raise e 98 | return output_data 99 | 100 | 101 | def get_value_from_path(data, path): 102 | """Get a value from a nested dictionary using a list of keys.""" 103 | try: 104 | return reduce(getitem, path, data) 105 | except KeyError: 106 | err = f"The input path '{path}' does not exist in the input data." 107 | logging.error(f"KeyError: {err}") 108 | raise KeyError(err) 109 | 110 | 111 | def set_value_at_path(data, path, value): 112 | """Set a value in a nested dictionary using a list of keys.""" 113 | for key in path[:-1]: 114 | data = data.setdefault(key, {}) 115 | data[path[-1]] = value 116 | 117 | 118 | if __name__ == "__main__": 119 | logging.basicConfig(level=logging.INFO) 120 | 121 | try: 122 | parser = create_parser() 123 | args = parser.parse_args() 124 | 125 | input_data = load_yaml(args.input_file_path) 126 | output_data = map_properties(input_data) 127 | 128 | if args.dry_run or args.output is None: 129 | print(yaml.dump(output_data)) 130 | else: 131 | dump_yaml(output_data, args.output) 132 | 133 | except Exception as e: 134 | logging.error(f"Error occurred: {str(e)}") 135 | sys.exit(1) 136 | -------------------------------------------------------------------------------- /snippets/scripts/jira-components-to-custom-field/jira_components_to_jira_custom_field.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import getpass 4 | import json 5 | import time 6 | 7 | import requests 8 | from requests.auth import HTTPBasicAuth 9 | from requests.exceptions import HTTPError 10 | 11 | 12 | class Issue: 13 | """ 14 | Issue represents a Jira issue and the fields we need to copy data. 15 | """ 16 | 17 | def __init__(self, id, components): 18 | """ 19 | :param id: Issue ID. 20 | :param components: Raw list of issues from Jira. 21 | """ 22 | self.id = id 23 | self.existing_components = [component["name"] for component in components] 24 | 25 | 26 | def check_input(input_string, input_type): 27 | """ 28 | Quick way to validate our input as strings. 29 | :param input_string: the raw input string. 30 | :param input_type: the type of input, e.g domainName. 31 | :return: None 32 | """ 33 | if not bool(input_string): 34 | print("Please provide a valid " + input_type + ".") 35 | quit() 36 | 37 | 38 | def get_issues_with_components_for_project( 39 | domain_name, user_name, api_token, start_at=0, max_results=100 40 | ): 41 | """ 42 | Calls Jira REST APIs to fetch a list of issues in a specific project whose component field is not empty. 43 | :param domain_name: Site URL. 44 | :param user_name: Usually your Atlassian email. 45 | :param api_token: API token generated for your site. 46 | :param start_at: Where we start pagination. 47 | :param max_results: The maximum number of issues to fetch in this call. 48 | :return: List 49 | """ 50 | url = ( 51 | domain_name + f"/rest/api/3/search?" 52 | f"jql=component%20is%20not%20EMPTY%20and%20project%20%3D%20{PROJECT_NAME}" 53 | f"&startAt={start_at}" 54 | f"&maxResults={max_results}" 55 | ) 56 | auth = HTTPBasicAuth(user_name, api_token) 57 | headers = {"Accept": "application/json"} 58 | 59 | try: 60 | response = requests.get(url, auth=auth, headers=headers) 61 | if response.ok: 62 | return response.json() 63 | else: 64 | response.raise_for_status() 65 | except HTTPError as e: 66 | print( 67 | f"Couldn't retrieve issues due to an HTTP error. Please try again." 68 | f"\nError: {e}" 69 | ) 70 | quit() 71 | except Exception as e: 72 | print( 73 | f"Couldn't retrieve issues due to an unknown error. Please try again." 74 | f"\nError: {e}" 75 | ) 76 | quit() 77 | 78 | 79 | def update_issue(domain_name, user_name, api_token, issue_id, cf_value): 80 | """ 81 | Sets a custom field for an issue_id. 82 | :param domain_name: Site URL. 83 | :param user_name: Usually your Atlassian email. 84 | :param api_token: API token generated for your site. 85 | :param issue_id: The issue to update. 86 | :param cf_value: The value to set on the custom field. 87 | :return: None 88 | """ 89 | url = ( 90 | domain_name 91 | + f"/rest/api/3/issue/{issue_id}?notifyUsers=false&overrideScreenSecurity=false&overrideEditableFlag=false&returnIssue=true" 92 | ) 93 | auth = HTTPBasicAuth(user_name, api_token) 94 | headers = {"Accept": "application/json", "Content-Type": "application/json"} 95 | 96 | data = { 97 | "fields": { 98 | NEW_CUSTOM_FIELD_ID: cf_value, 99 | } 100 | } 101 | 102 | try: 103 | response = requests.put(url, auth=auth, headers=headers, data=json.dumps(data)) 104 | if not response.ok: 105 | print( 106 | f"Couldn't copy issueId: {issue_id} issue's Jira component value to custom field." 107 | f"\nPlease try again." 108 | f"\nError: {response.text}" 109 | ) 110 | except Exception as e: 111 | print( 112 | f"Couldn't copy issueId: {issue_id} issue's Jira component value to new custom field due to an error." 113 | f"\nPlease try again." 114 | f"\nError: {e}" 115 | ) 116 | 117 | 118 | def main(): 119 | issues_with_components: list[Issue] = [] 120 | has_more = True 121 | start_at = 0 122 | page_size = 100 123 | 124 | while has_more is True: 125 | result = get_issues_with_components_for_project( 126 | DOMAIN_NAME, USER_NAME, API_TOKEN, start_at, page_size 127 | ) 128 | issues_with_components += [ 129 | Issue(issue["id"], issue["fields"]["components"]) 130 | for issue in result["issues"] 131 | ] 132 | start_at += page_size 133 | 134 | if result["total"] < start_at: 135 | has_more = False 136 | else: 137 | # Give API time to rest. 138 | time.sleep(0.5) 139 | 140 | # If there are no issues with components, we're done. 141 | if len(issues_with_components) == 0: 142 | print( 143 | f"The project: {PROJECT_NAME} doesn't have issues with Jira components set." 144 | ) 145 | quit() 146 | 147 | print( 148 | f"Successfully retrieved {len(issues_with_components)} issues in {PROJECT_NAME} project that have Jira " 149 | f"components set" 150 | ) 151 | 152 | # Update issues. 153 | for issue in issues_with_components: 154 | update_issue( 155 | DOMAIN_NAME, 156 | USER_NAME, 157 | API_TOKEN, 158 | issue.id, 159 | issue.existing_components, 160 | ) 161 | # Give time for API to rest. 162 | time.sleep(0.1) 163 | 164 | print(f"Successfully copied issues’ Jira component values to new custom field.\n") 165 | 166 | 167 | if __name__ == "__main__": 168 | DOMAIN_NAME = input("Enter your domain : ").strip() 169 | check_input(DOMAIN_NAME, "domain") 170 | USER_NAME = input("Enter your email address : ").strip() 171 | check_input(USER_NAME, "userName") 172 | API_TOKEN = getpass.getpass("Enter your API token: ").strip() 173 | check_input(API_TOKEN, "apiToken") 174 | PROJECT_NAME = input("Enter your project name (exact) : ").strip() 175 | check_input(PROJECT_NAME, "projectName") 176 | NEW_CUSTOM_FIELD_ID = input( 177 | "Enter the ID of the custom field you'd like data moved to : " 178 | ).strip() 179 | check_input(NEW_CUSTOM_FIELD_ID, "newCustomFieldId") 180 | print("\n") 181 | 182 | main() 183 | -------------------------------------------------------------------------------- /snippets/scripts/compass-bitbucket-importer/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const chalk = require('chalk') 3 | require('dotenv').config() 4 | 5 | const {BITBUCKET_URL, ACCESS_TOKEN, USER_EMAIL, TOKEN, TENANT_SUBDOMAIN, CLOUD_ID} = process.env 6 | const dryRun = process.env.DRY_RUN === "1" 7 | 8 | const ATLASSIAN_GRAPHQL_URL = `https://${TENANT_SUBDOMAIN}.atlassian.net/gateway/api/graphql`; 9 | 10 | function makeGqlRequest(query) { 11 | const header = btoa(`${USER_EMAIL}:${TOKEN}`); 12 | return fetch(ATLASSIAN_GRAPHQL_URL, { 13 | method: 'POST', 14 | headers: { 15 | Authorization: `Basic ${header}`, 16 | Accept: 'application/json', 17 | 'Content-Type': 'application/json', 18 | }, 19 | body: JSON.stringify(query), 20 | }).then((res) => res.json()); 21 | } 22 | 23 | // This function looks for a component with a certain name, but if it can't find it, it will create a component. 24 | async function putComponent(componentName, description, web_url) { 25 | const response = await makeGqlRequest({ 26 | query: ` 27 | query getComponent { 28 | compass @optIn(to: "compass-beta") { 29 | searchComponents(cloudId: "${CLOUD_ID}", query: { 30 | query: "${componentName}", 31 | first: 1 32 | }) { 33 | ... on CompassSearchComponentConnection { 34 | nodes { 35 | component { 36 | id 37 | name 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | `, 45 | }); 46 | const maybeResults = response?.data?.compass?.searchComponents?.nodes; 47 | 48 | if (!Array.isArray(maybeResults)) { 49 | console.error(`error fetching component: `, JSON.stringify(response)); 50 | throw new Error('Error fetching component'); 51 | } 52 | 53 | const maybeComponentAri = maybeResults.find( 54 | (r) => r.component?.name === componentName 55 | )?.component?.id; 56 | if (maybeComponentAri) { 57 | console.log(chalk.gray(`Already added ${web_url} ... skipping`)) 58 | return maybeComponentAri; 59 | } else { 60 | if (!dryRun) { 61 | const response = await makeGqlRequest({ 62 | query: ` 63 | mutation createComponent { 64 | compass @optIn(to: "compass-beta") { 65 | createComponent(cloudId: "${CLOUD_ID}", input: {name: "${componentName}", description: "${description}" typeId: "OTHER", links: [{type: REPOSITORY, name: "Repository", url: "${web_url}"}]}) { 66 | success 67 | componentDetails { 68 | id 69 | } 70 | } 71 | } 72 | }`, 73 | }); 74 | 75 | const maybeAri = 76 | response?.data.compass?.createComponent?.componentDetails?.id; 77 | const isSuccess = !!response?.data.compass?.createComponent?.success; 78 | if (!isSuccess || !maybeAri) { 79 | console.error(`error creating component: `, JSON.stringify(response)); 80 | throw new Error('Could not create component'); 81 | } 82 | console.log(chalk.green(`New component for ${web_url} ... added ${componentName}`)) 83 | return maybeAri; 84 | } else { 85 | console.log(chalk.yellow(`New component for ${web_url} ... would be added ${componentName} (dry-run)`)) 86 | return; 87 | } 88 | } 89 | } 90 | 91 | async function listAllProjects() { 92 | let instanceProjects = [] 93 | let page = 1; 94 | const perPage = 100; 95 | let isLastPage = false 96 | 97 | while(!isLastPage && page < 4) { 98 | try { 99 | const response = await axios.get(`https://${BITBUCKET_URL}/rest/api/latest/projects`, { 100 | params: { 101 | start: page, 102 | limit: perPage, 103 | }, 104 | headers: { 105 | 'Authorization': `Basic ${ACCESS_TOKEN}`, 106 | 'Accept': 'application/json', 107 | }, 108 | }); 109 | 110 | const projects = response.data.values; 111 | instanceProjects = [...projects, ...instanceProjects]; 112 | 113 | isLastPage = response.data.isLastPage 114 | 115 | page++; 116 | } catch (error) { 117 | console.error(`Error fetching projects on page ${page}:`, error); 118 | break; 119 | } 120 | } 121 | 122 | return instanceProjects; 123 | } 124 | 125 | async function listAllRepos() { 126 | const instanceProjects = await listAllProjects() 127 | let page = 1; 128 | const perPage = 100; 129 | 130 | for (const project of instanceProjects) { 131 | let isLastPage = false; 132 | while(!isLastPage) { 133 | try { 134 | // Fetch repos for the current page 135 | const response = await axios.get(`https://${BITBUCKET_URL}/rest/api/latest/projects/${project.key}/repos`, { 136 | params: { 137 | start: page, 138 | limit: perPage, 139 | }, 140 | headers: { 141 | 'Authorization': `Basic ${ACCESS_TOKEN}`, 142 | 'Accept': 'application/json', 143 | }, 144 | }); 145 | 146 | const repos = response.data.values; 147 | // Inner loop: Iterate over repos on the current page 148 | for (const repo of repos) { 149 | // need a filter, add one here! 150 | /* 151 | Example, skip is repo name is "hello-world" 152 | if (repo.name !== 'hello-world') 153 | */ 154 | if (true) { 155 | await putComponent(repo.name, repo.description || '', `https://${BITBUCKET_URL}/projects/${project.key}/repos/${repo.slug}`) 156 | } 157 | } 158 | 159 | //Check if we fetched all repos 160 | isLastPage = response.data.isLastPage; 161 | 162 | page++;//Fetch the next page 163 | } catch (error) { 164 | console.error(`Error fetching repositories for project: ${project.name} on page ${page}:`, error); 165 | break; 166 | } 167 | } 168 | } 169 | } 170 | 171 | listAllRepos() -------------------------------------------------------------------------------- /snippets/scripts/compass-github-importer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass-github-importer", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "compass-github-importer", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "chalk": "4.1.2", 14 | "dotenv": "^16.4.5" 15 | } 16 | }, 17 | "node_modules/ansi-styles": { 18 | "version": "4.3.0", 19 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/ansi-styles/-/ansi-styles-4.3.0.tgz", 20 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 21 | "license": "MIT", 22 | "dependencies": { 23 | "color-convert": "^2.0.1" 24 | }, 25 | "engines": { 26 | "node": ">=8" 27 | }, 28 | "funding": { 29 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 30 | } 31 | }, 32 | "node_modules/asynckit": { 33 | "version": "0.4.0", 34 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/asynckit/-/asynckit-0.4.0.tgz", 35 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 36 | "license": "MIT" 37 | }, 38 | "node_modules/axios": { 39 | "version": "1.7.7", 40 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/axios/-/axios-1.7.7.tgz", 41 | "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", 42 | "license": "MIT", 43 | "dependencies": { 44 | "follow-redirects": "^1.15.6", 45 | "form-data": "^4.0.0", 46 | "proxy-from-env": "^1.1.0" 47 | } 48 | }, 49 | "node_modules/chalk": { 50 | "version": "4.1.2", 51 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/chalk/-/chalk-4.1.2.tgz", 52 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 53 | "license": "MIT", 54 | "dependencies": { 55 | "ansi-styles": "^4.1.0", 56 | "supports-color": "^7.1.0" 57 | }, 58 | "engines": { 59 | "node": ">=10" 60 | }, 61 | "funding": { 62 | "url": "https://github.com/chalk/chalk?sponsor=1" 63 | } 64 | }, 65 | "node_modules/color-convert": { 66 | "version": "2.0.1", 67 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/color-convert/-/color-convert-2.0.1.tgz", 68 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 69 | "license": "MIT", 70 | "dependencies": { 71 | "color-name": "~1.1.4" 72 | }, 73 | "engines": { 74 | "node": ">=7.0.0" 75 | } 76 | }, 77 | "node_modules/color-name": { 78 | "version": "1.1.4", 79 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/color-name/-/color-name-1.1.4.tgz", 80 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 81 | "license": "MIT" 82 | }, 83 | "node_modules/combined-stream": { 84 | "version": "1.0.8", 85 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/combined-stream/-/combined-stream-1.0.8.tgz", 86 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 87 | "license": "MIT", 88 | "dependencies": { 89 | "delayed-stream": "~1.0.0" 90 | }, 91 | "engines": { 92 | "node": ">= 0.8" 93 | } 94 | }, 95 | "node_modules/delayed-stream": { 96 | "version": "1.0.0", 97 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/delayed-stream/-/delayed-stream-1.0.0.tgz", 98 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 99 | "license": "MIT", 100 | "engines": { 101 | "node": ">=0.4.0" 102 | } 103 | }, 104 | "node_modules/dotenv": { 105 | "version": "16.4.5", 106 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/dotenv/-/dotenv-16.4.5.tgz", 107 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 108 | "license": "BSD-2-Clause", 109 | "engines": { 110 | "node": ">=12" 111 | }, 112 | "funding": { 113 | "url": "https://dotenvx.com" 114 | } 115 | }, 116 | "node_modules/follow-redirects": { 117 | "version": "1.15.9", 118 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/follow-redirects/-/follow-redirects-1.15.9.tgz", 119 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 120 | "funding": [ 121 | { 122 | "type": "individual", 123 | "url": "https://github.com/sponsors/RubenVerborgh" 124 | } 125 | ], 126 | "license": "MIT", 127 | "engines": { 128 | "node": ">=4.0" 129 | }, 130 | "peerDependenciesMeta": { 131 | "debug": { 132 | "optional": true 133 | } 134 | } 135 | }, 136 | "node_modules/form-data": { 137 | "version": "4.0.1", 138 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/form-data/-/form-data-4.0.1.tgz", 139 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", 140 | "license": "MIT", 141 | "dependencies": { 142 | "asynckit": "^0.4.0", 143 | "combined-stream": "^1.0.8", 144 | "mime-types": "^2.1.12" 145 | }, 146 | "engines": { 147 | "node": ">= 6" 148 | } 149 | }, 150 | "node_modules/has-flag": { 151 | "version": "4.0.0", 152 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/has-flag/-/has-flag-4.0.0.tgz", 153 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 154 | "license": "MIT", 155 | "engines": { 156 | "node": ">=8" 157 | } 158 | }, 159 | "node_modules/mime-db": { 160 | "version": "1.52.0", 161 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/mime-db/-/mime-db-1.52.0.tgz", 162 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 163 | "license": "MIT", 164 | "engines": { 165 | "node": ">= 0.6" 166 | } 167 | }, 168 | "node_modules/mime-types": { 169 | "version": "2.1.35", 170 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/mime-types/-/mime-types-2.1.35.tgz", 171 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 172 | "license": "MIT", 173 | "dependencies": { 174 | "mime-db": "1.52.0" 175 | }, 176 | "engines": { 177 | "node": ">= 0.6" 178 | } 179 | }, 180 | "node_modules/proxy-from-env": { 181 | "version": "1.1.0", 182 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 183 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 184 | "license": "MIT" 185 | }, 186 | "node_modules/supports-color": { 187 | "version": "7.2.0", 188 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/supports-color/-/supports-color-7.2.0.tgz", 189 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 190 | "license": "MIT", 191 | "dependencies": { 192 | "has-flag": "^4.0.0" 193 | }, 194 | "engines": { 195 | "node": ">=8" 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /snippets/scripts/compass-gitlab-importer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass-gitlab-importer", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "compass-gitlab-importer", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "chalk": "4.1.2", 14 | "dotenv": "^16.4.5" 15 | } 16 | }, 17 | "node_modules/ansi-styles": { 18 | "version": "4.3.0", 19 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/ansi-styles/-/ansi-styles-4.3.0.tgz", 20 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 21 | "license": "MIT", 22 | "dependencies": { 23 | "color-convert": "^2.0.1" 24 | }, 25 | "engines": { 26 | "node": ">=8" 27 | }, 28 | "funding": { 29 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 30 | } 31 | }, 32 | "node_modules/asynckit": { 33 | "version": "0.4.0", 34 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/asynckit/-/asynckit-0.4.0.tgz", 35 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 36 | "license": "MIT" 37 | }, 38 | "node_modules/axios": { 39 | "version": "1.7.7", 40 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/axios/-/axios-1.7.7.tgz", 41 | "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", 42 | "license": "MIT", 43 | "dependencies": { 44 | "follow-redirects": "^1.15.6", 45 | "form-data": "^4.0.0", 46 | "proxy-from-env": "^1.1.0" 47 | } 48 | }, 49 | "node_modules/chalk": { 50 | "version": "4.1.2", 51 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/chalk/-/chalk-4.1.2.tgz", 52 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 53 | "license": "MIT", 54 | "dependencies": { 55 | "ansi-styles": "^4.1.0", 56 | "supports-color": "^7.1.0" 57 | }, 58 | "engines": { 59 | "node": ">=10" 60 | }, 61 | "funding": { 62 | "url": "https://github.com/chalk/chalk?sponsor=1" 63 | } 64 | }, 65 | "node_modules/color-convert": { 66 | "version": "2.0.1", 67 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/color-convert/-/color-convert-2.0.1.tgz", 68 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 69 | "license": "MIT", 70 | "dependencies": { 71 | "color-name": "~1.1.4" 72 | }, 73 | "engines": { 74 | "node": ">=7.0.0" 75 | } 76 | }, 77 | "node_modules/color-name": { 78 | "version": "1.1.4", 79 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/color-name/-/color-name-1.1.4.tgz", 80 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 81 | "license": "MIT" 82 | }, 83 | "node_modules/combined-stream": { 84 | "version": "1.0.8", 85 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/combined-stream/-/combined-stream-1.0.8.tgz", 86 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 87 | "license": "MIT", 88 | "dependencies": { 89 | "delayed-stream": "~1.0.0" 90 | }, 91 | "engines": { 92 | "node": ">= 0.8" 93 | } 94 | }, 95 | "node_modules/delayed-stream": { 96 | "version": "1.0.0", 97 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/delayed-stream/-/delayed-stream-1.0.0.tgz", 98 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 99 | "license": "MIT", 100 | "engines": { 101 | "node": ">=0.4.0" 102 | } 103 | }, 104 | "node_modules/dotenv": { 105 | "version": "16.4.5", 106 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/dotenv/-/dotenv-16.4.5.tgz", 107 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 108 | "license": "BSD-2-Clause", 109 | "engines": { 110 | "node": ">=12" 111 | }, 112 | "funding": { 113 | "url": "https://dotenvx.com" 114 | } 115 | }, 116 | "node_modules/follow-redirects": { 117 | "version": "1.15.9", 118 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/follow-redirects/-/follow-redirects-1.15.9.tgz", 119 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 120 | "funding": [ 121 | { 122 | "type": "individual", 123 | "url": "https://github.com/sponsors/RubenVerborgh" 124 | } 125 | ], 126 | "license": "MIT", 127 | "engines": { 128 | "node": ">=4.0" 129 | }, 130 | "peerDependenciesMeta": { 131 | "debug": { 132 | "optional": true 133 | } 134 | } 135 | }, 136 | "node_modules/form-data": { 137 | "version": "4.0.1", 138 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/form-data/-/form-data-4.0.1.tgz", 139 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", 140 | "license": "MIT", 141 | "dependencies": { 142 | "asynckit": "^0.4.0", 143 | "combined-stream": "^1.0.8", 144 | "mime-types": "^2.1.12" 145 | }, 146 | "engines": { 147 | "node": ">= 6" 148 | } 149 | }, 150 | "node_modules/has-flag": { 151 | "version": "4.0.0", 152 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/has-flag/-/has-flag-4.0.0.tgz", 153 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 154 | "license": "MIT", 155 | "engines": { 156 | "node": ">=8" 157 | } 158 | }, 159 | "node_modules/mime-db": { 160 | "version": "1.52.0", 161 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/mime-db/-/mime-db-1.52.0.tgz", 162 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 163 | "license": "MIT", 164 | "engines": { 165 | "node": ">= 0.6" 166 | } 167 | }, 168 | "node_modules/mime-types": { 169 | "version": "2.1.35", 170 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/mime-types/-/mime-types-2.1.35.tgz", 171 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 172 | "license": "MIT", 173 | "dependencies": { 174 | "mime-db": "1.52.0" 175 | }, 176 | "engines": { 177 | "node": ">= 0.6" 178 | } 179 | }, 180 | "node_modules/proxy-from-env": { 181 | "version": "1.1.0", 182 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 183 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 184 | "license": "MIT" 185 | }, 186 | "node_modules/supports-color": { 187 | "version": "7.2.0", 188 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/supports-color/-/supports-color-7.2.0.tgz", 189 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 190 | "license": "MIT", 191 | "dependencies": { 192 | "has-flag": "^4.0.0" 193 | }, 194 | "engines": { 195 | "node": ">=8" 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /snippets/scripts/compass-bitbucket-importer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass-bitbucket-importer", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "compass-bitbucket-importer", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "chalk": "4.1.2", 14 | "dotenv": "^16.4.5" 15 | } 16 | }, 17 | "node_modules/ansi-styles": { 18 | "version": "4.3.0", 19 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/ansi-styles/-/ansi-styles-4.3.0.tgz", 20 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 21 | "license": "MIT", 22 | "dependencies": { 23 | "color-convert": "^2.0.1" 24 | }, 25 | "engines": { 26 | "node": ">=8" 27 | }, 28 | "funding": { 29 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 30 | } 31 | }, 32 | "node_modules/asynckit": { 33 | "version": "0.4.0", 34 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/asynckit/-/asynckit-0.4.0.tgz", 35 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 36 | "license": "MIT" 37 | }, 38 | "node_modules/axios": { 39 | "version": "1.7.7", 40 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/axios/-/axios-1.7.7.tgz", 41 | "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", 42 | "license": "MIT", 43 | "dependencies": { 44 | "follow-redirects": "^1.15.6", 45 | "form-data": "^4.0.0", 46 | "proxy-from-env": "^1.1.0" 47 | } 48 | }, 49 | "node_modules/chalk": { 50 | "version": "4.1.2", 51 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/chalk/-/chalk-4.1.2.tgz", 52 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 53 | "license": "MIT", 54 | "dependencies": { 55 | "ansi-styles": "^4.1.0", 56 | "supports-color": "^7.1.0" 57 | }, 58 | "engines": { 59 | "node": ">=10" 60 | }, 61 | "funding": { 62 | "url": "https://github.com/chalk/chalk?sponsor=1" 63 | } 64 | }, 65 | "node_modules/color-convert": { 66 | "version": "2.0.1", 67 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/color-convert/-/color-convert-2.0.1.tgz", 68 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 69 | "license": "MIT", 70 | "dependencies": { 71 | "color-name": "~1.1.4" 72 | }, 73 | "engines": { 74 | "node": ">=7.0.0" 75 | } 76 | }, 77 | "node_modules/color-name": { 78 | "version": "1.1.4", 79 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/color-name/-/color-name-1.1.4.tgz", 80 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 81 | "license": "MIT" 82 | }, 83 | "node_modules/combined-stream": { 84 | "version": "1.0.8", 85 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/combined-stream/-/combined-stream-1.0.8.tgz", 86 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 87 | "license": "MIT", 88 | "dependencies": { 89 | "delayed-stream": "~1.0.0" 90 | }, 91 | "engines": { 92 | "node": ">= 0.8" 93 | } 94 | }, 95 | "node_modules/delayed-stream": { 96 | "version": "1.0.0", 97 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/delayed-stream/-/delayed-stream-1.0.0.tgz", 98 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 99 | "license": "MIT", 100 | "engines": { 101 | "node": ">=0.4.0" 102 | } 103 | }, 104 | "node_modules/dotenv": { 105 | "version": "16.4.5", 106 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/dotenv/-/dotenv-16.4.5.tgz", 107 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 108 | "license": "BSD-2-Clause", 109 | "engines": { 110 | "node": ">=12" 111 | }, 112 | "funding": { 113 | "url": "https://dotenvx.com" 114 | } 115 | }, 116 | "node_modules/follow-redirects": { 117 | "version": "1.15.9", 118 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/follow-redirects/-/follow-redirects-1.15.9.tgz", 119 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 120 | "funding": [ 121 | { 122 | "type": "individual", 123 | "url": "https://github.com/sponsors/RubenVerborgh" 124 | } 125 | ], 126 | "license": "MIT", 127 | "engines": { 128 | "node": ">=4.0" 129 | }, 130 | "peerDependenciesMeta": { 131 | "debug": { 132 | "optional": true 133 | } 134 | } 135 | }, 136 | "node_modules/form-data": { 137 | "version": "4.0.1", 138 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/form-data/-/form-data-4.0.1.tgz", 139 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", 140 | "license": "MIT", 141 | "dependencies": { 142 | "asynckit": "^0.4.0", 143 | "combined-stream": "^1.0.8", 144 | "mime-types": "^2.1.12" 145 | }, 146 | "engines": { 147 | "node": ">= 6" 148 | } 149 | }, 150 | "node_modules/has-flag": { 151 | "version": "4.0.0", 152 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/has-flag/-/has-flag-4.0.0.tgz", 153 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 154 | "license": "MIT", 155 | "engines": { 156 | "node": ">=8" 157 | } 158 | }, 159 | "node_modules/mime-db": { 160 | "version": "1.52.0", 161 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/mime-db/-/mime-db-1.52.0.tgz", 162 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 163 | "license": "MIT", 164 | "engines": { 165 | "node": ">= 0.6" 166 | } 167 | }, 168 | "node_modules/mime-types": { 169 | "version": "2.1.35", 170 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/mime-types/-/mime-types-2.1.35.tgz", 171 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 172 | "license": "MIT", 173 | "dependencies": { 174 | "mime-db": "1.52.0" 175 | }, 176 | "engines": { 177 | "node": ">= 0.6" 178 | } 179 | }, 180 | "node_modules/proxy-from-env": { 181 | "version": "1.1.0", 182 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 183 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 184 | "license": "MIT" 185 | }, 186 | "node_modules/supports-color": { 187 | "version": "7.2.0", 188 | "resolved": "https://packages.atlassian.com/api/npm/npm-remote/supports-color/-/supports-color-7.2.0.tgz", 189 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 190 | "license": "MIT", 191 | "dependencies": { 192 | "has-flag": "^4.0.0" 193 | }, 194 | "engines": { 195 | "node": ">=8" 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /snippets/scripts/compass-new-relic-importer/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | require("dotenv").config(); 3 | 4 | const { 5 | NEW_RELIC_URL, 6 | NEW_RELIC_API_TOKEN, 7 | USER_EMAIL, 8 | TOKEN, 9 | TENANT_SUBDOMAIN, 10 | CLOUD_ID, 11 | } = process.env; 12 | const dryRun = process.env.DRY_RUN === "1"; 13 | 14 | const ATLASSIAN_GRAPHQL_URL = `https://${TENANT_SUBDOMAIN}.atlassian.net/gateway/api/graphql`; 15 | 16 | function makeGqlRequest(query) { 17 | const header = btoa(`${USER_EMAIL}:${TOKEN}`); 18 | return fetch(ATLASSIAN_GRAPHQL_URL, { 19 | method: "POST", 20 | headers: { 21 | Authorization: `Basic ${header}`, 22 | Accept: "application/json", 23 | "Content-Type": "application/json", 24 | }, 25 | body: JSON.stringify(query), 26 | }).then((res) => res.json()); 27 | } 28 | 29 | function makeNewRelicGqlRequest(query) { 30 | return fetch(NEW_RELIC_URL, { 31 | method: "POST", 32 | headers: { 33 | "Api-Key": NEW_RELIC_API_TOKEN, 34 | "Content-type": "application/json", 35 | }, 36 | body: JSON.stringify(query), 37 | }).then((res) => res.json()); 38 | } 39 | 40 | const formatTag = (key, value) => { 41 | const k = key.toLowerCase().trim().replace(/\s+/, "-"); 42 | const v = value.toLowerCase().trim().replace(/\s+/, "-"); 43 | return `${k}:${v}`; 44 | }; 45 | 46 | const newRelicTagsToCompassLabels = (tags) => { 47 | const results = []; 48 | if (!tags || tags.length === 0) { 49 | return results; 50 | } 51 | // Tags need to be lowercase, contain no spaces, for API. 52 | tags 53 | .filter((tag) => tag.key && tag.values && tag.key.trim().length > 0) 54 | .filter((tag) => !tag.key.toLowerCase().includes("account")) 55 | .filter((tag) => !(tag.key.toLowerCase() === "guid")) 56 | .forEach((tag) => { 57 | tag.values.forEach((v) => { 58 | if (v && v.trim().length > 0) { 59 | results.push(formatTag(tag.key, v)); 60 | } 61 | }); 62 | }); 63 | 64 | // API requires labels to be shorter than 40 characters. 65 | return results.filter((t) => t.length < 40); 66 | }; 67 | 68 | // This function looks for a component with a certain name, but if it can't find it, it will create a component. 69 | async function putComponent( 70 | componentName, 71 | description, 72 | url, 73 | externalSource, 74 | externalId, 75 | labels 76 | ) { 77 | const response = await makeGqlRequest({ 78 | query: ` 79 | query componentByExternalAlias { 80 | compass { 81 | componentByExternalAlias(cloudId: "${CLOUD_ID}", externalSource: "${externalSource}", externalID: "${externalId}") { 82 | ... on CompassComponent { 83 | id 84 | name 85 | } 86 | ... on QueryError { 87 | message 88 | extensions { 89 | statusCode 90 | errorType 91 | } 92 | } 93 | } 94 | } 95 | } 96 | `, 97 | }); 98 | 99 | const maybeResults = response?.data?.compass?.componentByExternalAlias; 100 | 101 | if ( 102 | maybeResults?.message && 103 | maybeResults?.message !== "Component not found" 104 | ) { 105 | console.error( 106 | `error fetching component for app ${url} : `, 107 | JSON.stringify(response) 108 | ); 109 | throw new Error("Error fetching component"); 110 | } 111 | 112 | const maybeComponentAri = maybeResults?.id; 113 | 114 | if (maybeComponentAri) { 115 | console.log(chalk.gray(`Already added ${url} ... skipping`)); 116 | return maybeComponentAri; 117 | } else { 118 | if (!dryRun) { 119 | const response = await makeGqlRequest({ 120 | query: ` 121 | mutation createComponent { 122 | compass @optIn(to: "compass-beta") { 123 | createComponent(cloudId: "${CLOUD_ID}", input: {name: "${componentName}", description: "${description}" typeId: "SERVICE", labels: ${labels}, links: [{type: DASHBOARD, name: "New Relic", url: "${url}"}]}) { 124 | success 125 | componentDetails { 126 | id 127 | } 128 | } 129 | } 130 | }`, 131 | }); 132 | 133 | const maybeAri = 134 | response?.data.compass?.createComponent?.componentDetails?.id; 135 | const isSuccess = !!response?.data.compass?.createComponent?.success; 136 | if (!isSuccess || !maybeAri) { 137 | console.error(`error creating component: `, JSON.stringify(response)); 138 | throw new Error("Could not create component"); 139 | } 140 | 141 | const componentDetails = 142 | response?.data.compass?.createComponent.componentDetails; 143 | 144 | const createExternalAliasResponse = await makeGqlRequest({ 145 | query: ` 146 | mutation createCompassComponentExternalAlias { 147 | compass @optIn(to: "compass-beta") { 148 | createComponentExternalAlias(input: {componentId: "${componentDetails.id}", externalAlias: {externalSource:"${externalSource}", externalId:"${externalId}"}}) { 149 | success 150 | errors { 151 | message 152 | } 153 | } 154 | } 155 | }`, 156 | }); 157 | 158 | const isCreateExternalAliasSuccess = 159 | createExternalAliasResponse?.data.compass?.createComponentExternalAlias 160 | ?.success; 161 | if (!isCreateExternalAliasSuccess) { 162 | console.error( 163 | `error creating component's external alias: `, 164 | JSON.stringify(createExternalAliasResponse) 165 | ); 166 | throw new Error("Could not create component's external alias"); 167 | } 168 | 169 | console.log( 170 | chalk.green( 171 | `New component with external alias for ${url} ... added ${componentName}` 172 | ) 173 | ); 174 | 175 | return maybeAri; 176 | } else { 177 | console.log( 178 | chalk.yellow( 179 | `New component for ${url} ... would be added ${componentName} (dry-run)` 180 | ) 181 | ); 182 | return; 183 | } 184 | } 185 | } 186 | 187 | async function listAllApps() { 188 | const apps = []; 189 | 190 | const response = await makeNewRelicGqlRequest({ 191 | query: ` 192 | { 193 | actor { 194 | entitySearch(queryBuilder: {type: APPLICATION}) { 195 | count 196 | results { 197 | nextCursor 198 | entities { 199 | name 200 | entityType 201 | guid 202 | permalink 203 | ... on ApmApplicationEntityOutline { 204 | language 205 | } 206 | tags { 207 | key 208 | values 209 | } 210 | } 211 | } 212 | } 213 | } 214 | }`, 215 | }); 216 | 217 | let cursor = response?.data?.actor?.entitySearch?.results?.nextCursor; 218 | const entities = response?.data?.actor?.entitySearch?.results?.entities; 219 | apps.push(...entities); 220 | 221 | while (cursor) { 222 | const response = await makeNewRelicGqlRequest({ 223 | query: ` 224 | { 225 | actor { 226 | entitySearch(queryBuilder: {type: APPLICATION}) { 227 | count 228 | results (cursor: "${cursor}) { 229 | nextCursor 230 | entities { 231 | name 232 | entityType 233 | guid 234 | permalink 235 | ... on ApmApplicationEntityOutline { 236 | language 237 | } 238 | tags { 239 | key 240 | values 241 | } 242 | } 243 | } 244 | } 245 | } 246 | }`, 247 | }); 248 | 249 | const entites = response?.data?.actor?.entitySearch?.results?.entities; 250 | apps.push(...entites); 251 | cursor = response?.data?.actor?.entitySearch?.results?.nextCursor; 252 | } 253 | 254 | return apps; 255 | } 256 | 257 | async function importAllApps() { 258 | const apps = await listAllApps(); 259 | 260 | try { 261 | for (const app of apps) { 262 | const { entityType, guid, name, permalink, tags } = app; 263 | 264 | const convertedLabels = newRelicTagsToCompassLabels(tags); 265 | const labelNames = [ 266 | "source:newrelic", 267 | `${formatTag("entitytype", entityType)}`, 268 | ...convertedLabels, 269 | ]; 270 | 271 | await putComponent( 272 | name, 273 | app.description || "", 274 | permalink, 275 | "new-relic", 276 | guid, 277 | JSON.stringify(labelNames) 278 | ); 279 | } 280 | } catch (e) { 281 | console.error(`Error importing apps`, e); 282 | } 283 | } 284 | 285 | importAllApps(); 286 | -------------------------------------------------------------------------------- /snippets/scripts/compass-github-importer/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const chalk = require('chalk') 3 | require('dotenv').config() 4 | 5 | const {GITHUB_URL, ACCESS_TOKEN, USER_EMAIL, TOKEN, TENANT_SUBDOMAIN, CLOUD_ID} = process.env 6 | const dryRun = process.env.DRY_RUN === "1" 7 | const createWebhooks = process.env.CREATE_WEBHOOKS === "1" 8 | 9 | const ATLASSIAN_GRAPHQL_URL = `https://${TENANT_SUBDOMAIN}.atlassian.net/gateway/api/graphql`; 10 | 11 | function makeGqlRequest(query) { 12 | const header = btoa(`${USER_EMAIL}:${TOKEN}`); 13 | return fetch(ATLASSIAN_GRAPHQL_URL, { 14 | method: 'POST', 15 | headers: { 16 | Authorization: `Basic ${header}`, 17 | Accept: 'application/json', 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify(query), 21 | }).then((res) => res.json()); 22 | } 23 | 24 | // This function looks for a component with a certain name, but if it can't find it, it will create a component. 25 | async function putComponent(componentName, description, web_url) { 26 | const response = await makeGqlRequest({ 27 | query: ` 28 | query getComponent { 29 | compass @optIn(to: "compass-beta") { 30 | searchComponents(cloudId: "${CLOUD_ID}", query: { 31 | query: "${componentName}", 32 | first: 1 33 | }) { 34 | ... on CompassSearchComponentConnection { 35 | nodes { 36 | component { 37 | id 38 | name 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | `, 46 | }); 47 | const maybeResults = response?.data?.compass?.searchComponents?.nodes; 48 | 49 | if (!Array.isArray(maybeResults)) { 50 | console.error(`error fetching component for repo ${web_url} : `, JSON.stringify(response)); 51 | throw new Error('Error fetching component'); 52 | } 53 | 54 | const maybeComponentAri = maybeResults.find( 55 | (r) => r.component?.name === componentName 56 | )?.component?.id; 57 | if (maybeComponentAri) { 58 | console.log(chalk.gray(`Already added ${web_url} ... skipping`)) 59 | return maybeComponentAri; 60 | } else { 61 | if (!dryRun) { 62 | const response = await makeGqlRequest({ 63 | query: ` 64 | mutation createComponent { 65 | compass @optIn(to: "compass-beta") { 66 | createComponent(cloudId: "${CLOUD_ID}", input: {name: "${componentName}", description: "${description}" typeId: "SERVICE", links: [{type: REPOSITORY, name: "Repository", url: "${web_url}"}]}) { 67 | success 68 | componentDetails { 69 | id 70 | } 71 | } 72 | } 73 | }`, 74 | }); 75 | 76 | const maybeAri = 77 | response?.data.compass?.createComponent?.componentDetails?.id; 78 | const isSuccess = !!response?.data.compass?.createComponent?.success; 79 | if (!isSuccess || !maybeAri) { 80 | console.error(`error creating component: `, JSON.stringify(response)); 81 | throw new Error('Could not create component'); 82 | } 83 | console.log(chalk.green(`New component for ${web_url} ... added ${componentName}`)) 84 | return maybeAri; 85 | } else { 86 | console.log(chalk.yellow(`New component for ${web_url} ... would be added ${componentName} (dry-run)`)) 87 | return; 88 | } 89 | } 90 | } 91 | 92 | async function listAllOrgs() { 93 | let instanceOrganizations = [] 94 | let page = 1; 95 | const perPage = 100; // Maximum items per page as allowed by GitHub Enterprise Server API 96 | 97 | while(true) { 98 | try { 99 | const response = await axios.get(`${GITHUB_URL}/api/v3/organizations`, { 100 | params: { 101 | per_page: perPage, 102 | page: page, 103 | }, 104 | headers: { 105 | 'Authorization': `Bearer ${ACCESS_TOKEN}`, 106 | 'Accept': 'application/vnd.github+json', 107 | }, 108 | }); 109 | 110 | const orgs = response.data; 111 | instanceOrganizations = [...orgs, ...instanceOrganizations]; 112 | 113 | if(orgs.length < perPage) { 114 | break; 115 | } 116 | 117 | page++; 118 | } catch (error) { 119 | console.error(`Error fetching organizations on page ${page}:`, error); 120 | break; 121 | } 122 | } 123 | 124 | return instanceOrganizations; 125 | } 126 | 127 | async function createOrgWebhooks(orgs) { 128 | if(!dryRun) { 129 | for (const org of orgs) { 130 | const webhookName = `${org.login} webhook` 131 | const response = await makeGqlRequest({ 132 | query: ` 133 | mutation MyMutation { 134 | compass @optIn(to: "compass-beta"){ 135 | createIncomingWebhook(input: {cloudId: "${CLOUD_ID}", name: "${webhookName}", source: "github_enterprise_server"}) { 136 | success 137 | errors { 138 | message 139 | extensions { 140 | statusCode 141 | errorType 142 | } 143 | } 144 | webhookDetails { 145 | id 146 | } 147 | } 148 | } 149 | }`, 150 | }); 151 | 152 | const maybeId = response?.data.compass?.createIncomingWebhook?.webhookDetails?.id 153 | const isSuccess = response?.data.compass?.createIncomingWebhook?.success 154 | 155 | if (!isSuccess || !maybeId) { 156 | console.error(`error creating incoming webhook: `, JSON.stringify(response)); 157 | throw new Error('Could not create incoming webhook'); 158 | } 159 | 160 | console.log(chalk.green(`New compass incoming webhook for organization "${org.login}" created`)) 161 | 162 | const webhookId = maybeId.substring(maybeId.lastIndexOf('/') + 1) 163 | const webhookUrl = `https://${TENANT_SUBDOMAIN}.atlassian.net/gateway/api/compass/v1/webhooks/${webhookId}` 164 | 165 | 166 | const githubResponse = await axios.post( 167 | `${GITHUB_URL}/api/v3/orgs/${org.login}/hooks`, 168 | { 169 | name: 'web', 170 | config: { 171 | url: `${webhookUrl}`, 172 | content_type: 'json', 173 | }, 174 | events: ['deployment_status', 'workflow_run', 'push'] 175 | }, 176 | { 177 | headers: { 178 | 'Authorization': `Bearer ${ACCESS_TOKEN}`, 179 | 'Accept': 'application/vnd.github+json', 180 | }, 181 | } 182 | ); 183 | 184 | if (githubResponse.status !== 201) { 185 | console.error(`error creating github webhook: `, JSON.stringify(response)); 186 | throw new Error('Could not create github webhook'); 187 | } 188 | 189 | console.log(chalk.green(`New GitHub webhook for organization "${org.login}" created`)) 190 | } 191 | } else { 192 | for ( const org of orgs){ 193 | console.log(chalk.yellow(`New incoming webhook for organization "${org.login}" ... would be added (dry-run)`)); 194 | } 195 | return; 196 | } 197 | } 198 | 199 | async function listAllRepos() { 200 | const instanceOrganizations = await listAllOrgs() 201 | 202 | if(createWebhooks) { 203 | try { 204 | await createOrgWebhooks(instanceOrganizations) 205 | } catch (error) { 206 | console.error(`Error establishing webhooks for GitHub Organizations: `, error); 207 | } 208 | } 209 | 210 | let page = 1; 211 | const perPage = 100; // Maximum items per page as allowed by GitHub Enterprise Server API 212 | 213 | for (const org of instanceOrganizations) { 214 | while(true) { 215 | try { 216 | // Fetch repos for the current page 217 | const response = await axios.get(`${GITHUB_URL}/api/v3/orgs/${org.login}/repos`, { 218 | params: { 219 | per_page: perPage, 220 | page: page, 221 | }, 222 | headers: { 223 | 'Authorization': `Bearer ${ACCESS_TOKEN}`, 224 | 'Accept': 'application/vnd.github+json', 225 | }, 226 | }); 227 | 228 | const repos = response.data; 229 | 230 | // Inner loop: Iterate over repos on the current page 231 | for (const repo of repos) { 232 | // need a filter, add one here! 233 | /* 234 | Example, skip is repo name is "hello-world" 235 | if (repo.name !== 'hello-world') 236 | */ 237 | if (true) { 238 | await putComponent(repo.name, repo.description || '', repo.html_url) 239 | } 240 | } 241 | 242 | //Check if we fetched all repos 243 | if(repos.length < perPage) { 244 | break; //Break out of pagination loop 245 | } 246 | 247 | page++;//Fetch the next page 248 | } catch (error) { 249 | console.error(`Error fetching repositories for org: ${org.login} on page ${page}:`, error); 250 | break; 251 | } 252 | } 253 | } 254 | } 255 | 256 | listAllRepos() --------------------------------------------------------------------------------