diff --git a/.changeset/clever-ties-burn.md b/.changeset/clever-ties-burn.md new file mode 100644 index 0000000000..cdd861e069 --- /dev/null +++ b/.changeset/clever-ties-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Prevent `.set()` to execute a request to the StorageClient if the user is `guest` diff --git a/.changeset/quiet-dancers-jog.md b/.changeset/quiet-dancers-jog.md new file mode 100644 index 0000000000..2cca69103a --- /dev/null +++ b/.changeset/quiet-dancers-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-gcalendar': patch +--- + +Fixed loader showing when user not signed in diff --git a/.changeset/renovate-5da63a4.md b/.changeset/renovate-5da63a4.md new file mode 100644 index 0000000000..6d27c9e99f --- /dev/null +++ b/.changeset/renovate-5da63a4.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Updated dependency `@asyncapi/react-component` to `1.0.0-next.43`. diff --git a/.changeset/silly-rules-join.md b/.changeset/silly-rules-join.md new file mode 100644 index 0000000000..88d3898317 --- /dev/null +++ b/.changeset/silly-rules-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Initializes a git repository when creating an app using @packages/create-app diff --git a/.github/labeler.yml b/.github/labeler.yml index bfba5eb321..587f29efa3 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -9,7 +9,7 @@ search: - plugins/search/**/* - plugins/search-*/**/* - packages/search-*/**/* -docs-like-code: +techdocs: - plugins/techdocs/**/* - plugins/techdocs-*/**/* - packages/techdocs-*/**/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c6ac696e9..64067188bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -63,7 +63,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -178,7 +178,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 03105b753a..467c817baf 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -8,7 +8,7 @@ jobs: cron: runs-on: ubuntu-latest steps: - - uses: backstage/actions/cron@v0.5.5 + - uses: backstage/actions/cron@v0.5.6 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index a4664288da..476e5dd10b 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -26,7 +26,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 64e5fe813a..f3736d93a2 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -67,7 +67,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -145,7 +145,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index 0a8b243255..95e91dda30 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -10,4 +10,4 @@ jobs: if: github.repository == 'backstage/backstage' steps: - name: Issue sync - uses: backstage/actions/issue-sync@v0.5.5 + uses: backstage/actions/issue-sync@v0.5.6 diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index 4b1048da9b..e822209e03 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -35,7 +35,7 @@ jobs: const prNumber = artifact.name.slice('pr_number-'.length) console.log(`::set-output name=pr-number::${prNumber}`); - - uses: backstage/actions/re-review@v0.5.5 + - uses: backstage/actions/re-review@v0.5.6 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 3fbac53400..e13928ccee 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,7 +18,7 @@ jobs: if: github.repository == 'backstage/backstage' && ( github.event.pull_request || github.event.issue.pull_request ) steps: - name: PR sync - uses: backstage/actions/pr-sync@v0.5.5 + uses: backstage/actions/pr-sync@v0.5.6 with: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index c37f8c578e..779284818f 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -20,7 +20,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index ae65e90ac0..46a6227f2c 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 0eedfd9d6b..3353a84f3b 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -41,13 +41,18 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Configure Git + run: | + git config --global user.email noreply@backstage.io + git config --global user.name 'GitHub e2e user' + - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index e62a9040a6..4ed406f52e 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -39,7 +39,7 @@ jobs: run: yarn build - name: Install mkdocs & techdocs-core - run: python -m pip install mkdocs-techdocs-core + run: python -m pip install mkdocs-techdocs-core==1.1.6 mkdocs==1.3.1 - name: techdocs-cli e2e test working-directory: packages/techdocs-cli diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 3bc41a9eeb..4e8025f7d5 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -39,6 +39,11 @@ jobs: - uses: actions/checkout@v3 + - name: Configure Git + run: | + git config --global user.email noreply@backstage.io + git config --global user.name 'GitHub e2e user' + - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: diff --git a/.github/workflows/verify_kubernetes.yml b/.github/workflows/verify_kubernetes.yml index 6b8d527609..e64ddec586 100644 --- a/.github/workflows/verify_kubernetes.yml +++ b/.github/workflows/verify_kubernetes.yml @@ -28,7 +28,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 63e857f19a..cac94ff682 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -35,7 +35,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: storybook yarn install diff --git a/contrib/scripts/upgrade-backstage-app/README.md b/contrib/scripts/upgrade-backstage-app/README.md new file mode 100644 index 0000000000..ea8b0a0e2f --- /dev/null +++ b/contrib/scripts/upgrade-backstage-app/README.md @@ -0,0 +1,30 @@ +# Upgrade Backstage App + +## Overview + +The script upgrades the version of Backstage created via the create-app plugin. It lets you resolve conflicts iteratively with your own merge [tool](https://www.git-scm.com/docs/git-mergetool). + +## Requirements + +You will need to have the following tools in your shell environment: + +- git +- curl +- jq + +## Usage + +Here's how to use the script: + +1. download the script +2. copy it in the root of your app +3. bootstrap a git repository (you may already have done so): + +```bash +git init +git add . +git commit -m "initial commit" +``` + +4. run `sh upgrade-backstage-app.sh [optional-backstage-version]` +5. resolve any conflicts iteratively diff --git a/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh b/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh new file mode 100755 index 0000000000..d68d9e31c4 --- /dev/null +++ b/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh @@ -0,0 +1,15 @@ +#!/bin/sh +CURRENT_VERSION="v$(cat backstage.json | jq -r '.version')" +TARGET_VERSION=${1:-"$(curl -s https://api.github.com/repos/backstage/backstage/releases/latest | jq -r '.tag_name')"} +CREATE_APP_CURRENT_VERSION=$(curl -s https://raw.githubusercontent.com/backstage/backstage/$CURRENT_VERSION/packages/create-app/package.json | jq -r '.version') +CREATE_APP_TARGET_VERSION=$(curl -s https://raw.githubusercontent.com/backstage/backstage/$TARGET_VERSION/packages/create-app/package.json | jq -r '.version') + +if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then + echo "Already up to date" +else + echo "Attempting upgrade from Backstage $CURRENT_VERSION (create-app $CREATE_APP_CURRENT_VERSION) to $TARGET_VERSION ($CREATE_APP_TARGET_VERSION)" + rm -rf .upgrade && mkdir .upgrade + curl -s https://raw.githubusercontent.com/backstage/upgrade-helper-diff/master/diffs/$CREATE_APP_CURRENT_VERSION..$CREATE_APP_TARGET_VERSION.diff > .upgrade/upgrade.diff + git apply -3 .upgrade/upgrade.diff + git mergetool +fi diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 0af58bc911..48305e266c 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -34,6 +34,7 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "chalk": "^4.0.0", + "command-exists": "^1.2.9", "commander": "^9.1.0", "fs-extra": "10.1.0", "handlebars": "^4.7.3", @@ -43,6 +44,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/inquirer": "^8.1.3", "@types/node": "^16.11.26", diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 8e7b3bae7d..ec8d15ead1 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -32,6 +32,8 @@ const promptMock = jest.spyOn(inquirer, 'prompt'); const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask'); const templatingMock = jest.spyOn(tasks, 'templatingTask'); const checkAppExistsMock = jest.spyOn(tasks, 'checkAppExistsTask'); +const initGitRepositoryMock = jest.spyOn(tasks, 'initGitRepository'); +const readGitConfig = jest.spyOn(tasks, 'readGitConfig'); const createTemporaryAppFolderMock = jest.spyOn( tasks, 'createTemporaryAppFolderTask', @@ -47,7 +49,7 @@ describe('command entrypoint', () => { }); }); - afterAll(() => { + afterEach(() => { mockFs.restore(); }); @@ -56,6 +58,11 @@ describe('command entrypoint', () => { name: 'MyApp', dbType: 'PostgreSQL', }); + readGitConfig.mockResolvedValue({ + name: 'git-user', + email: 'git-email', + defaultBranch: 'git-default-branch', + }); }); afterEach(() => { @@ -67,6 +74,7 @@ describe('command entrypoint', () => { await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); expect(createTemporaryAppFolderMock).toHaveBeenCalled(); + expect(initGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(moveAppMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); @@ -76,6 +84,7 @@ describe('command entrypoint', () => { const cmd = { path: 'myDirectory' } as unknown as Command; await createApp(cmd); expect(checkPathExistsMock).toHaveBeenCalled(); + expect(initGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); }); @@ -85,4 +94,11 @@ describe('command entrypoint', () => { await createApp(cmd); expect(buildAppMock).not.toHaveBeenCalled(); }); + + it('should not call `initGitRepository` when `gitConfig` is undefined', async () => { + const cmd = {} as unknown as Command; + readGitConfig.mockResolvedValue({}); + await createApp(cmd); + expect(initGitRepositoryMock).not.toHaveBeenCalled(); + }); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 7cf589e5f8..182e043565 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -28,8 +28,12 @@ import { createTemporaryAppFolderTask, moveAppTask, templatingTask, + initGitRepository, + readGitConfig, } from './lib/tasks'; +const DEFAULT_BRANCH = 'master'; + export default async (opts: OptionValues): Promise => { /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); @@ -73,6 +77,8 @@ export default async (opts: OptionValues): Promise => { Task.log('Creating the app...'); try { + const gitConfig = await readGitConfig(); + if (opts.path) { // Template directly to specified path @@ -80,7 +86,10 @@ export default async (opts: OptionValues): Promise => { await checkPathExistsTask(appDir); Task.section('Preparing files'); - await templatingTask(templateDir, opts.path, answers); + await templatingTask(templateDir, opts.path, { + ...answers, + defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, + }); } else { // Template to temporary location, and then move files @@ -91,12 +100,20 @@ export default async (opts: OptionValues): Promise => { await createTemporaryAppFolderTask(tempDir); Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers); + await templatingTask(templateDir, tempDir, { + ...answers, + defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, + }); Task.section('Moving to final location'); await moveAppTask(tempDir, appDir, answers.name); } + if (gitConfig?.name && gitConfig?.email) { + Task.section('Initializing git repository'); + await initGitRepository(appDir); + } + if (!opts.skipInstall) { Task.section('Building the app'); await buildAppTask(appDir); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index decc89fbd4..b918fc0180 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -17,7 +17,8 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; import child_process from 'child_process'; -import path from 'path'; +import path, { resolve as resolvePath } from 'path'; +import os from 'os'; import { Task, buildAppTask, @@ -26,8 +27,12 @@ import { createTemporaryAppFolderTask, moveAppTask, templatingTask, + initGitRepository, + readGitConfig, } from './tasks'; +const commandExists = jest.fn(); + jest.spyOn(Task, 'log').mockReturnValue(undefined); jest.spyOn(Task, 'error').mockReturnValue(undefined); jest.spyOn(Task, 'section').mockReturnValue(undefined); @@ -36,6 +41,12 @@ jest .mockImplementation((_a, _b, taskFunc) => taskFunc()); jest.mock('child_process'); +jest.mock( + 'command-exists', + () => + (...args: any[]) => + commandExists(...args), +); // By mocking this the filesystem mocks won't mess with reading all of the package.jsons jest.mock('./versions', () => ({ @@ -87,6 +98,14 @@ jest.mock('./versions', () => ({ })); describe('tasks', () => { + const mockExec = child_process.exec as unknown as jest.MockedFunction< + ( + command: string, + options: any, + callback: (error: null, stdout: any, stderr: any) => void, + ) => void + >; + beforeEach(() => { mockFs({ 'projects/my-module.ts': '', @@ -100,6 +119,7 @@ describe('tasks', () => { }); afterEach(() => { + mockExec.mockRestore(); mockFs.restore(); }); @@ -174,12 +194,6 @@ describe('tasks', () => { describe('buildAppTask', () => { it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { const mockChdir = jest.spyOn(process, 'chdir'); - const mockExec = child_process.exec as unknown as jest.MockedFunction< - ( - command: string, - callback: (error: null, stdout: string, stderr: string) => void, - ) => void - >; // requires callback implementation to support `promisify` wrapper // https://stackoverflow.com/a/60579617/10044859 @@ -273,4 +287,99 @@ describe('tasks', () => { ).toContain('sqlite3"'); }); }); + + describe('readGitConfig', () => { + const tmpDir = resolvePath(os.tmpdir(), 'git-temp-dir'); + + it('should return git config if git package is installed and git credentials are set', async () => { + mockExec.mockImplementation((_command, _options, callback) => { + callback(null, { stdout: 'main' }, 'standard error'); + }); + + commandExists.mockResolvedValue(true); + + const gitConfig = await readGitConfig(); + + expect(gitConfig).toBeTruthy(); + expect(gitConfig).toEqual({ + name: 'main', + email: 'main', + defaultBranch: 'main', + }); + expect(mockExec).toHaveBeenCalledWith( + 'git config user.name', + { cwd: tmpDir }, + expect.any(Function), + ); + expect(mockExec).toHaveBeenCalledWith( + 'git config user.email', + { cwd: tmpDir }, + expect.any(Function), + ); + expect(mockExec).toHaveBeenCalledWith( + 'git init', + { cwd: tmpDir }, + expect.any(Function), + ); + expect(mockExec).toHaveBeenCalledWith( + 'git commit --allow-empty -m "Initial commit"', + { cwd: tmpDir }, + expect.any(Function), + ); + }); + + it('should return false if git package is not installed', async () => { + commandExists.mockResolvedValue(false); + + const gitConfig = await readGitConfig(); + + expect(gitConfig).toEqual({}); + }); + + it('should return false if git package is installed but git credentials are not set', async () => { + mockExec.mockImplementation((_command, _options, callback) => { + callback(null, { stdout: null }, 'standard error'); + }); + + commandExists.mockResolvedValue(true); + + const gitConfig = await readGitConfig(); + + expect(gitConfig).toEqual({}); + expect(mockExec).toHaveBeenCalledWith( + 'git config user.name', + { cwd: tmpDir }, + expect.any(Function), + ); + expect(mockExec).toHaveBeenCalledWith( + 'git config user.email', + { cwd: tmpDir }, + expect.any(Function), + ); + }); + }); + + describe('initGitRepository', () => { + it('should initialize a git repository at the given path', async () => { + const destinationDir = 'tmp/mockApp/'; + + mockExec.mockImplementation((_command, callback) => { + callback(null, { stdout: 'main' }, 'standard error'); + }); + + await initGitRepository(destinationDir); + + expect(mockExec).toHaveBeenCalledTimes(2); + expect(mockExec).toHaveBeenNthCalledWith( + 1, + 'git init', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 2, + 'git commit --allow-empty -m "Initial commit"', + expect.any(Function), + ); + }); + }); }); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 1650432969..1e30f9d33a 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -28,10 +28,18 @@ import { import { exec as execCb } from 'child_process'; import { packageVersions } from './versions'; import { promisify } from 'util'; +import commandExists from 'command-exists'; +import os from 'os'; const TASK_NAME_MAX_LENGTH = 14; const exec = promisify(execCb); +export type GitConfig = { + name?: string; + email?: string; + defaultBranch?: string; +}; + export class Task { static log(name: string = '') { process.stdout.write(`${chalk.green(name)}\n`); @@ -236,3 +244,77 @@ export async function moveAppTask( }); }); } + +/** + * Read git configs by creating a temp folder and initializing a repo + * + * @throws if `exec` fails + */ +export async function readGitConfig(): Promise { + const tempDir = resolvePath(os.tmpdir(), 'git-temp-dir'); + + const runCmd = (cmd: string) => + exec(cmd, { cwd: tempDir }).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); + }); + + const isGitAvailable = await commandExists('git').catch(() => false); + + if (!isGitAvailable) return {}; + + try { + await fs.mkdir(tempDir); + + const [gitUsername, gitEmail] = await Promise.all([ + runCmd('git config user.name'), + runCmd('git config user.email'), + ]); + + const gitCredentials = Boolean( + gitUsername.stdout?.trim() && gitEmail.stdout?.trim(), + ); + + if (!gitCredentials) return {}; + + await runCmd('git init'); + await runCmd('git commit --allow-empty -m "Initial commit"'); + + const gitDefaultBranch = await runCmd( + 'git branch --format="%(refname:short)"', + ); + + return { + name: gitUsername.stdout?.trim(), + email: gitEmail.stdout?.trim(), + defaultBranch: gitDefaultBranch.stdout?.trim(), + }; + } catch (error) { + throw new Error(`Failed to read git config, ${error}`); + } finally { + await fs.rm(tempDir, { recursive: true }); + } +} + +/** + * Initializes a git repository in the destination folder + * + * @param dir - source path to initialize git repository in + * @throws if `exec` fails + */ +export async function initGitRepository(dir: string) { + const runCmd = (cmd: string) => + exec(cmd).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); + }); + + await Task.forItem('init', 'git repository', async () => { + process.chdir(dir); + + await runCmd('git init'); + await runCmd('git commit --allow-empty -m "Initial commit"'); + }); +} diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index dd4d937db0..c5810d8880 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -16,7 +16,7 @@ "clean": "backstage-cli repo clean", "test": "backstage-cli repo test", "test:all": "backstage-cli repo test --coverage", - "lint": "backstage-cli repo lint --since origin/master", + "lint": "backstage-cli repo lint --since origin/{{defaultBranch}}", "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", "create-plugin": "backstage-cli create-plugin --scope internal", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index c41a69c2d7..bfbb63c7f0 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@asyncapi/react-component": "1.0.0-next.42", + "@asyncapi/react-component": "1.0.0-next.43", "@backstage/catalog-model": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", diff --git a/plugins/gcalendar/src/components/CalendarCard/CalendarCard.tsx b/plugins/gcalendar/src/components/CalendarCard/CalendarCard.tsx index a5575bc190..0a9b517293 100644 --- a/plugins/gcalendar/src/components/CalendarCard/CalendarCard.tsx +++ b/plugins/gcalendar/src/components/CalendarCard/CalendarCard.tsx @@ -50,10 +50,13 @@ export const CalendarCard = () => { useAsync(async () => signIn(true), [signIn]); - const { isLoading: isCalendarLoading, data: calendars = [] } = - useCalendarsQuery({ - enabled: isSignedIn, - }); + const { + isLoading: isCalendarLoading, + isFetching: isCalendarFetching, + data: calendars = [], + } = useCalendarsQuery({ + enabled: isSignedIn, + }); const primaryCalendarId = calendars.find(c => c.primary === true)?.id; const defaultSelectedCalendars = primaryCalendarId ? [primaryCalendarId] : []; const [storedCalendars, setStoredCalendars] = useStoredCalendars( @@ -69,6 +72,11 @@ export const CalendarCard = () => { timeZone: date.zoneName, }); + const showLoader = + (isCalendarLoading && isCalendarFetching) || + isEventLoading || + !isInitialized; + return ( { calendars={calendars} selectedCalendars={storedCalendars} setSelectedCalendars={setStoredCalendars} - disabled={isCalendarLoading || !isSignedIn} + disabled={ + (isCalendarFetching && isCalendarLoading) || !isSignedIn + } /> ) : ( @@ -114,8 +124,8 @@ export const CalendarCard = () => { }} > - {(isCalendarLoading || isEventLoading || !isInitialized) && ( - + {showLoader && ( + )} diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index 0ebd96d19c..a421c413dc 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -38,6 +38,10 @@ describe('Persistent Storage API', () => { const mockIdentityApi: Partial = { getCredentials: async () => ({ token: 'a-token' }), }; + const mockIdentityApiFallback: Partial = { + // This API recreates the guest mode, where the WebStorage is used as fallback + getCredentials: async () => ({}), + }; const createPersistentStorage = ( args?: Partial<{ @@ -56,6 +60,23 @@ describe('Persistent Storage API', () => { }); }; + const createPersistentStorageFallback = ( + args?: Partial<{ + fetchApi: FetchApi; + discoveryApi: DiscoveryApi; + errorApi: ErrorApi; + namespace?: string; + }>, + ): StorageApi => { + return UserSettingsStorage.create({ + errorApi: mockErrorApi, + fetchApi: new MockFetchApi(), + discoveryApi: mockDiscoveryApi, + identityApi: mockIdentityApiFallback as IdentityApi, + ...args, + }); + }; + afterEach(() => { jest.resetAllMocks(); }); @@ -124,6 +145,33 @@ describe('Persistent Storage API', () => { await storage.set('my-key', dummyValue); }); + it('should fallback set when user not logged in', async () => { + const storage = createPersistentStorageFallback(); + + const selectedKeyNextHandler = jest.fn(); + const dummyValue = 'my-value'; + + await new Promise(resolve => { + storage.observe$('my-key').subscribe({ + next: snapshot => { + selectedKeyNextHandler(snapshot); + if (snapshot.presence === 'present') { + resolve(); + } + }, + }); + + storage.set('my-key', dummyValue); + }); + + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'my-key', + value: dummyValue, + presence: 'present', + }); + }); + it('should subscribe to key changes when setting a new value', async () => { const storage = createPersistentStorage(); diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts index 8905b4aeea..ef4d3ead5d 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts @@ -118,6 +118,7 @@ export class UserSettingsStorage implements StorageApi { if (!(await this.isSignedIn())) { await this.fallback.set(key, data); this.notifyChanges({ key, presence: 'present', value: data }); + return; } const fetchUrl = await this.getFetchUrl(key); diff --git a/yarn.lock b/yarn.lock index 975da92d56..432159f3e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -286,12 +286,12 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:^1.15.0": - version: 1.15.0 - resolution: "@asyncapi/parser@npm:1.15.0" +"@asyncapi/parser@npm:^1.17.0": + version: 1.17.0 + resolution: "@asyncapi/parser@npm:1.17.0" dependencies: "@apidevtools/json-schema-ref-parser": ^9.0.6 - "@asyncapi/specs": ^2.14.0 + "@asyncapi/specs": ^3.2.0 "@fmvilas/pseudo-yaml-ast": ^0.3.1 ajv: ^6.10.1 js-yaml: ^3.13.1 @@ -299,17 +299,17 @@ __metadata: lodash.clonedeep: ^4.5.0 node-fetch: ^2.6.0 tiny-merge-patch: ^0.1.2 - checksum: e9435265b4671f6a4c2451e054751ea637dd466b1f6b6848622781fbf6fa9fc93c292ed8791bf38816f63cd9c352f09727ab2d34f76d62f8f21c084b0125445b + checksum: 6ac0bd3c9c90abf331cfdfb054dab047cf0324ee803633c2abc6dad96b12c64d033e4fb94907906adfb521ba16e9b23083b2a6acc7719ccd22154a7867046a95 languageName: node linkType: hard -"@asyncapi/react-component@npm:1.0.0-next.42": - version: 1.0.0-next.42 - resolution: "@asyncapi/react-component@npm:1.0.0-next.42" +"@asyncapi/react-component@npm:1.0.0-next.43": + version: 1.0.0-next.43 + resolution: "@asyncapi/react-component@npm:1.0.0-next.43" dependencies: "@asyncapi/avro-schema-parser": ^0.3.0 "@asyncapi/openapi-schema-parser": ^2.0.0 - "@asyncapi/parser": ^1.15.0 + "@asyncapi/parser": ^1.17.0 highlight.js: ^10.7.2 isomorphic-dompurify: ^0.13.0 marked: ^4.0.14 @@ -319,14 +319,14 @@ __metadata: peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: eba4aaf344aabc1889085df7f94d14d51e2449db06cabc70c60b9d5b0b2fb9ccec6aa89cbedf9d45c28deca99e1e4ab2d4399b0a2756e9969d4691f304088a77 + checksum: d19adc2b6e674633457da28d38636df4cf9891f4038827b52f2737699d064bcf0d3be707e9f3284852d1bb507f3f36df9ec3bb02fd6f901cfda7a1f49ea9c5da languageName: node linkType: hard -"@asyncapi/specs@npm:^2.14.0": - version: 2.14.0 - resolution: "@asyncapi/specs@npm:2.14.0" - checksum: 066c23c493df54c44c319433bdcf8482a3acd584e32c0073e6a9f5b167d61bde23a252621be2b28bbaf1466636f6cafaab570795de403f0c671358784d4b12ed +"@asyncapi/specs@npm:^3.2.0": + version: 3.2.0 + resolution: "@asyncapi/specs@npm:3.2.0" + checksum: 09971262aefc8844ab3e7c0c3652711862ac562dd5d614f23b496185690430a81df8e50eddba657f4141e0fd9548ef622fe6c20f4e3dec8054be23f774798335 languageName: node linkType: hard @@ -3687,11 +3687,13 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" + "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/inquirer": ^8.1.3 "@types/node": ^16.11.26 "@types/recursive-readdir": ^2.2.0 chalk: ^4.0.0 + command-exists: ^1.2.9 commander: ^9.1.0 fs-extra: 10.1.0 handlebars: ^4.7.3 @@ -4066,7 +4068,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-api-docs@workspace:plugins/api-docs" dependencies: - "@asyncapi/react-component": 1.0.0-next.42 + "@asyncapi/react-component": 1.0.0-next.43 "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" @@ -8554,8 +8556,8 @@ __metadata: linkType: hard "@google-cloud/storage@npm:^6.0.0": - version: 6.5.1 - resolution: "@google-cloud/storage@npm:6.5.1" + version: 6.5.2 + resolution: "@google-cloud/storage@npm:6.5.2" dependencies: "@google-cloud/paginator": ^3.0.7 "@google-cloud/projectify": ^3.0.0 @@ -8575,7 +8577,7 @@ __metadata: retry-request: ^5.0.0 teeny-request: ^8.0.0 uuid: ^8.0.0 - checksum: 5ae81ae5e00afeb73ae07a00772af5d5fa8f2ceacc4723d4e6e24f05ca9bc7cd22885acee3cb75c15068fe85f20b81719c1634b2c2ac0d05552172a229b34ea5 + checksum: 31e8abea01e5160434a23c11c7219c83469c97a0c3bed9dc29d5ff1441ba1f42aa75a99df6e0b95db1501cf4b7f4c896c79b13bb597a35ddd2924408b584f802 languageName: node linkType: hard @@ -11249,8 +11251,8 @@ __metadata: linkType: hard "@octokit/auth-app@npm:^4.0.0": - version: 4.0.5 - resolution: "@octokit/auth-app@npm:4.0.5" + version: 4.0.6 + resolution: "@octokit/auth-app@npm:4.0.6" dependencies: "@octokit/auth-oauth-app": ^5.0.0 "@octokit/auth-oauth-user": ^2.0.0 @@ -11262,7 +11264,7 @@ __metadata: lru-cache: ^6.0.0 universal-github-app-jwt: ^1.0.1 universal-user-agent: ^6.0.0 - checksum: 26da7c1b279be68be17572897652091257a5ef7666fa5ac4c79dd42f2717aed560b117089c44ad766b4f90a87348572356ac3571c417fef385fdf31794281023 + checksum: 342fece6db4470ee489e710af8aa14ffd3a89e666815a15fadbabe3a382932a714d5bece24375f0ca39f3310f9d10bd28bc348210282cbc6701d27dd9b2ff7ed languageName: node linkType: hard @@ -13710,11 +13712,11 @@ __metadata: linkType: hard "@types/inquirer@npm:^8.1.3": - version: 8.2.3 - resolution: "@types/inquirer@npm:8.2.3" + version: 8.2.4 + resolution: "@types/inquirer@npm:8.2.4" dependencies: "@types/through": "*" - checksum: 2978e749fbda061b85a56331a267f6c7f40380c17bce1b0f8b0bd1d86c88820e3bd7df4985c633647652b571f7ffd6b97b929cabe8b7d61259d354229e4c6c5b + checksum: 3a46231faac1e5df4863a2d501617b3f25e9481ea8a966a9b866bafef01fcc151606d9f25cb66f90492dd0d1d7af7c675a2314f50db85a7f2aeed265d93eb412 languageName: node linkType: hard @@ -13906,11 +13908,11 @@ __metadata: linkType: hard "@types/ldapjs@npm:^2.2.0": - version: 2.2.2 - resolution: "@types/ldapjs@npm:2.2.2" + version: 2.2.4 + resolution: "@types/ldapjs@npm:2.2.4" dependencies: "@types/node": "*" - checksum: e5da993f06240251dad1e095564c722124ed08d915781fb3046f7933cde94b5d2681030ab9f48d4aa5d72b655b501bfd6f2ad4ea41d557d0e1a08d574ed058bf + checksum: 3f240809927e1292380e5977579d7edffb6e5e41b35510c7091b51da19e7387d4ebff0f254c0b145f3cf5d5485b6bb39c77335d857d092e6f4e9ce5a6682a872 languageName: node linkType: hard @@ -15428,12 +15430,12 @@ __metadata: linkType: hard "@yarnpkg/parsers@npm:^3.0.0-rc.4": - version: 3.0.0-rc.20 - resolution: "@yarnpkg/parsers@npm:3.0.0-rc.20" + version: 3.0.0-rc.21 + resolution: "@yarnpkg/parsers@npm:3.0.0-rc.21" dependencies: js-yaml: ^3.10.0 tslib: ^2.4.0 - checksum: a0dfc2d4d824322ae779363870ad7705b42161ad5e90c992404bf5dc0454a046a6f04144303c9bb047239e480b882b4105272127f2ba080b865180276c46deda + checksum: c0741ef01089a7d452dfb75b8c24a82ddcc3e218dac22b794a06f2e50c7070378c460f63f63fb7ed0f62f634114e448e3d85a75ccd4db84e1526242ae9b4a7d5 languageName: node linkType: hard @@ -18862,9 +18864,9 @@ __metadata: linkType: hard "core-js@npm:^3.6.5": - version: 3.25.2 - resolution: "core-js@npm:3.25.2" - checksum: e93c6c645d44d98973efb07750975552ad405f080f5a563a99972ff6b2c5c6ee25705f55accd363f5dccd51e9e5f56be25e2be6c14a7294da65763e0e5659c02 + version: 3.25.3 + resolution: "core-js@npm:3.25.3" + checksum: 26ca0a5e575e6da03dc30099f48bc00db50a1c0a3f81299165f111cb163869ae453de5e345f2cb7bc8be85a9adcee11fca94f0fbdbdf6d483c68e1c55a9efc5a languageName: node linkType: hard @@ -23821,8 +23823,8 @@ __metadata: linkType: hard "google-auth-library@npm:^8.0.0": - version: 8.5.1 - resolution: "google-auth-library@npm:8.5.1" + version: 8.5.2 + resolution: "google-auth-library@npm:8.5.2" dependencies: arrify: ^2.0.0 base64-js: ^1.3.0 @@ -23833,7 +23835,7 @@ __metadata: gtoken: ^6.1.0 jws: ^4.0.0 lru-cache: ^6.0.0 - checksum: 0d80d7e136e672055e44fdb44f9e13843a554cc531a01deb1b0da6b78427778785faa7377ed85d840208bb61878cd7d69902172886476bd49ea67e645729570a + checksum: 5ab2904f5da3c119a7c241a1d5a11640468a7da58dfcec8a9cad181cc2723e6662b3c65997906069852d9fa066ba3e4b7f14e11cbb80d541e320b66ead6777dc languageName: node linkType: hard @@ -27840,8 +27842,8 @@ __metadata: linkType: hard "ldapjs@npm:^2.2.0": - version: 2.3.2 - resolution: "ldapjs@npm:2.3.2" + version: 2.3.3 + resolution: "ldapjs@npm:2.3.3" dependencies: abstract-logging: ^2.0.0 asn1: ^0.2.4 @@ -27851,7 +27853,7 @@ __metadata: once: ^1.4.0 vasync: ^2.2.0 verror: ^1.8.1 - checksum: 4eb7b18839d8fe1d8acc0ba5d2aa71aefcb25ecb6f4f9582299d73faf546984326590c44366f8b148d3a4276707006b1aaffffdc548b3c4ca753e720403da4a5 + checksum: 791cf22812805db3888dfc8aa8e4db8337e67387b318522c9965d40e678ecdb64950a6ea5930083590ec3377038620a3e63f90e985173ae6aca40e7f485290e0 languageName: node linkType: hard