├── .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 |
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 | [](LICENSE) [](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 | [](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()
--------------------------------------------------------------------------------