Add ability to alter the api base url for the sentry scaffolder
Signed-off-by: InsidersByte <6938575+InsidersByte@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-sentry': minor
|
||||
---
|
||||
|
||||
Add ability to configure the API Base URL
|
||||
@@ -36,6 +36,14 @@ scaffolder:
|
||||
token: ${SENTRY_TOKEN}
|
||||
```
|
||||
|
||||
You can optional override the default Sentry API Base URL (https://sentry.io/api/0) in your `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
scaffolder:
|
||||
sentry:
|
||||
apiBaseUrl: API-BASE-URL
|
||||
```
|
||||
|
||||
After that you can use the action in your template:
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -18,6 +18,7 @@ export function createSentryCreateProjectAction(options: {
|
||||
slug?: string | undefined;
|
||||
platform?: string | undefined;
|
||||
authToken?: string | undefined;
|
||||
apiBaseUrl?: string | undefined;
|
||||
},
|
||||
{
|
||||
[x: string]: any;
|
||||
@@ -33,6 +34,7 @@ export function createSentryFetchDSNAction(options: {
|
||||
organizationSlug: string;
|
||||
projectSlug: string;
|
||||
authToken?: string | undefined;
|
||||
apiBaseUrl?: string | undefined;
|
||||
},
|
||||
{
|
||||
dsn?: string | undefined;
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('sentry:project:create action', () => {
|
||||
slug?: string;
|
||||
platform?: string;
|
||||
authToken?: string;
|
||||
apiBaseUrl?: string;
|
||||
}> =>
|
||||
createMockActionContext({
|
||||
workspacePath: './dev/proj',
|
||||
@@ -279,4 +280,47 @@ describe('sentry:project:create action', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`should ${examples[3].description}`, async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
let input;
|
||||
try {
|
||||
input = yaml.parse(examples[3].example).steps[0].input;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse YAML:', error);
|
||||
}
|
||||
|
||||
const action = createSentryCreateProjectAction(createScaffolderConfig());
|
||||
const actionContext = getActionContext();
|
||||
|
||||
worker.use(
|
||||
http.post(
|
||||
`${input.apiBaseUrl || 'https://sentry.io/api/0'}/teams/${
|
||||
input.organizationSlug
|
||||
}/${input.teamSlug}/projects/`,
|
||||
async ({ request }) => {
|
||||
expect(request.headers.get('Authorization')).toBe(
|
||||
`Bearer d16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf95`,
|
||||
);
|
||||
expect(request.headers.get('Content-Type')).toBe(`application/json`);
|
||||
await expect(request.json()).resolves.toEqual({
|
||||
name: 'Scaffolded project A',
|
||||
});
|
||||
return new HttpResponse(JSON.stringify({ id: 'mock-id' }), {
|
||||
status: 201,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...actionContext,
|
||||
input: {
|
||||
...actionContext.input,
|
||||
...input,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,4 +100,24 @@ export const examples: TemplateExample[] = [
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Creates a Sentry project with a custom API base URL.',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'create-sentry-project',
|
||||
action: 'sentry:project:create',
|
||||
name: 'Create a Sentry project with custom API base URL.',
|
||||
input: {
|
||||
organizationSlug: 'my-org',
|
||||
teamSlug: 'team-a',
|
||||
name: 'Scaffolded project A',
|
||||
apiBaseUrl: 'https://custom.sentry.io/api/0',
|
||||
authToken:
|
||||
'd16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf95',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('sentry:project:create action', () => {
|
||||
slug?: string;
|
||||
platform?: string;
|
||||
authToken?: string;
|
||||
apiBaseUrl?: string;
|
||||
}> =>
|
||||
createMockActionContext({
|
||||
workspacePath: './dev/proj',
|
||||
@@ -245,4 +246,64 @@ describe('sentry:project:create action', () => {
|
||||
new InputError(`Sentry Response was: OUCH`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a Sentry project with custom apiBaseUrl.', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const action = createSentryCreateProjectAction(createScaffolderConfig());
|
||||
const actionContext = getActionContext();
|
||||
actionContext.input = {
|
||||
...actionContext.input,
|
||||
apiBaseUrl: 'https://custom.sentry.io/api/0',
|
||||
};
|
||||
|
||||
worker.use(
|
||||
http.post(
|
||||
`https://custom.sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`,
|
||||
async ({ request }) => {
|
||||
expect(request.headers.get('Authorization')).toBe(
|
||||
`Bearer ${actionContext.input.authToken}`,
|
||||
);
|
||||
expect(request.headers.get('Content-Type')).toBe(`application/json`);
|
||||
await expect(request.json()).resolves.toEqual({
|
||||
name: actionContext.input.name,
|
||||
});
|
||||
return HttpResponse.json({ id: 'mock-id' }, { status: 201 });
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(actionContext);
|
||||
});
|
||||
|
||||
it('should create a Sentry project with apiBaseUrl from config.', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const action = createSentryCreateProjectAction(
|
||||
createScaffolderConfig({
|
||||
sentry: {
|
||||
apiBaseUrl: 'https://config.sentry.io/api/0',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const actionContext = getActionContext();
|
||||
|
||||
worker.use(
|
||||
http.post(
|
||||
`https://config.sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`,
|
||||
async ({ request }) => {
|
||||
expect(request.headers.get('Authorization')).toBe(
|
||||
`Bearer ${actionContext.input.authToken}`,
|
||||
);
|
||||
expect(request.headers.get('Content-Type')).toBe(`application/json`);
|
||||
await expect(request.json()).resolves.toEqual({
|
||||
name: actionContext.input.name,
|
||||
});
|
||||
return HttpResponse.json({ id: 'mock-id' }, { status: 201 });
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(actionContext);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,11 +67,25 @@ export function createSentryCreateProjectAction(options: { config: Config }) {
|
||||
'authenticate via bearer auth token. Requires scope: project:write',
|
||||
})
|
||||
.optional(),
|
||||
apiBaseUrl: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'Optional base URL for the Sentry API. e.g. https://sentry.io/api/0',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { organizationSlug, teamSlug, name, slug, platform, authToken } =
|
||||
ctx.input;
|
||||
const {
|
||||
organizationSlug,
|
||||
teamSlug,
|
||||
name,
|
||||
slug,
|
||||
platform,
|
||||
authToken,
|
||||
apiBaseUrl,
|
||||
} = ctx.input;
|
||||
|
||||
const body: any = {
|
||||
name: name,
|
||||
@@ -93,11 +107,16 @@ export function createSentryCreateProjectAction(options: { config: Config }) {
|
||||
throw new InputError(`No valid sentry token given`);
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
apiBaseUrl ||
|
||||
config.getOptionalString('scaffolder.sentry.apiBaseUrl') ||
|
||||
'https://sentry.io/api/0';
|
||||
|
||||
const { result } = await ctx.checkpoint({
|
||||
key: `create.project.${organizationSlug}.${teamSlug}`,
|
||||
fn: async () => {
|
||||
const response = await fetch(
|
||||
`https://sentry.io/api/0/teams/${organizationSlug}/${teamSlug}/projects/`,
|
||||
`${baseUrl}/teams/${organizationSlug}/${teamSlug}/projects/`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('sentry:fetch:dsn action', () => {
|
||||
organizationSlug: string;
|
||||
projectSlug: string;
|
||||
authToken?: string;
|
||||
apiBaseUrl?: string;
|
||||
}> =>
|
||||
createMockActionContext({
|
||||
workspacePath: './dev/proj',
|
||||
@@ -147,4 +148,50 @@ describe('sentry:fetch:dsn action', () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it(`should ${examples[2].description}`, async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
let input;
|
||||
try {
|
||||
input = yaml.parse(examples[2].example).steps[0].input;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse YAML:', error);
|
||||
}
|
||||
|
||||
const action = createSentryFetchDSNAction(createScaffolderConfig());
|
||||
const actionContext = getActionContext(null);
|
||||
|
||||
worker.use(
|
||||
http.get(
|
||||
`${input.apiBaseUrl || 'https://sentry.io/api/0'}/projects/${
|
||||
input.organizationSlug
|
||||
}/${input.projectSlug}/keys/`,
|
||||
async ({ request }) => {
|
||||
expect(request.headers.get('Authorization')).toBe(
|
||||
`Bearer b14711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d5df97`,
|
||||
);
|
||||
expect(request.headers.get('Content-Type')).toBe(`application/json`);
|
||||
return HttpResponse.json(
|
||||
[{ dsn: { public: 'https://abcdef1234567890@sentry.io/67890' } }],
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler({
|
||||
...actionContext,
|
||||
input: {
|
||||
...actionContext.input,
|
||||
...input,
|
||||
},
|
||||
});
|
||||
expect(actionContext.output).toHaveBeenCalledWith(
|
||||
'dsn',
|
||||
'https://abcdef1234567890@sentry.io/67890',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,4 +54,23 @@ export const examples: TemplateExample[] = [
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Fetch the DSN for a Sentry project with custom API base URL.',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'fetch-sentry-dsn',
|
||||
action: 'sentry:fetch:dsn',
|
||||
name: 'Fetch DSN with custom API base URL',
|
||||
input: {
|
||||
organizationSlug: 'my-org',
|
||||
projectSlug: 'my-project',
|
||||
apiBaseUrl: 'https://sentry.io/api/0/custom',
|
||||
authToken:
|
||||
'b14711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d5df97',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -41,6 +41,7 @@ describe('sentry:fetch:dsn action', () => {
|
||||
organizationSlug: string;
|
||||
projectSlug: string;
|
||||
authToken?: string;
|
||||
apiBaseUrl?: string;
|
||||
}> =>
|
||||
createMockActionContext({
|
||||
workspacePath: './dev/proj',
|
||||
@@ -196,4 +197,66 @@ describe('sentry:fetch:dsn action', () => {
|
||||
new InputError('No public DSN found in project keys'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch DSN with custom apiBaseUrl.', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const action = createSentryFetchDSNAction(createScaffolderConfig());
|
||||
const actionContext = getActionContext();
|
||||
actionContext.input = {
|
||||
...actionContext.input,
|
||||
apiBaseUrl: 'https://custom.sentry.io/api/0',
|
||||
};
|
||||
const mockDSN = 'https://test@sentry.io/123';
|
||||
|
||||
worker.use(
|
||||
http.get(
|
||||
`https://custom.sentry.io/api/0/projects/${actionContext.input.organizationSlug}/${actionContext.input.projectSlug}/keys/`,
|
||||
async ({ request }) => {
|
||||
expect(request.headers.get('Authorization')).toBe(
|
||||
`Bearer ${actionContext.input.authToken}`,
|
||||
);
|
||||
expect(request.headers.get('Content-Type')).toBe(`application/json`);
|
||||
return HttpResponse.json([{ dsn: { public: mockDSN } }], {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(actionContext);
|
||||
expect(actionContext.output).toHaveBeenCalledWith('dsn', mockDSN);
|
||||
});
|
||||
|
||||
it('should fetch DSN with apiBaseUrl from config.', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const action = createSentryFetchDSNAction(
|
||||
createScaffolderConfig({
|
||||
sentry: {
|
||||
apiBaseUrl: 'https://config.sentry.io/api/0',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const actionContext = getActionContext();
|
||||
const mockDSN = 'https://test@sentry.io/123';
|
||||
|
||||
worker.use(
|
||||
http.get(
|
||||
`https://config.sentry.io/api/0/projects/${actionContext.input.organizationSlug}/${actionContext.input.projectSlug}/keys/`,
|
||||
async ({ request }) => {
|
||||
expect(request.headers.get('Authorization')).toBe(
|
||||
`Bearer ${actionContext.input.authToken}`,
|
||||
);
|
||||
expect(request.headers.get('Content-Type')).toBe(`application/json`);
|
||||
return HttpResponse.json([{ dsn: { public: mockDSN } }], {
|
||||
status: 200,
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(actionContext);
|
||||
expect(actionContext.output).toHaveBeenCalledWith('dsn', mockDSN);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,13 @@ export function createSentryFetchDSNAction(options: { config: Config }) {
|
||||
'authenticate via bearer auth token. Requires one of the following scopes: project:admin, project:read, project:write',
|
||||
})
|
||||
.optional(),
|
||||
apiBaseUrl: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'Optional base URL for the Sentry API. e.g. https://sentry.io/api/0',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
output: {
|
||||
dsn: z =>
|
||||
@@ -62,7 +69,8 @@ export function createSentryFetchDSNAction(options: { config: Config }) {
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { organizationSlug, projectSlug, authToken } = ctx.input;
|
||||
const { organizationSlug, projectSlug, authToken, apiBaseUrl } =
|
||||
ctx.input;
|
||||
|
||||
const token = authToken
|
||||
? authToken
|
||||
@@ -72,8 +80,13 @@ export function createSentryFetchDSNAction(options: { config: Config }) {
|
||||
throw new InputError(`No valid sentry token given`);
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
apiBaseUrl ||
|
||||
config.getOptionalString('scaffolder.sentry.apiBaseUrl') ||
|
||||
'https://sentry.io/api/0';
|
||||
|
||||
const response = await fetch(
|
||||
`https://sentry.io/api/0/projects/${organizationSlug}/${projectSlug}/keys/`,
|
||||
`${baseUrl}/projects/${organizationSlug}/${projectSlug}/keys/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user