diff --git a/.dockerignore b/.dockerignore index 05edb62..696548d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,8 @@ -.git -.yarn/cache -.yarn/install-state.gz +dist-types node_modules -packages/*/src +packages/*/dist packages/*/node_modules -plugins -*.local.yaml +packages/backend/.env +plugins/*/dist +plugins/*/node_modules +*.local.yaml \ No newline at end of file diff --git a/.github/workflows/docker-publish-dev.yml b/.github/workflows/docker-publish-dev.yml new file mode 100644 index 0000000..a4a04dd --- /dev/null +++ b/.github/workflows/docker-publish-dev.yml @@ -0,0 +1,91 @@ +name: "Build and Publish Docker Demo TEST" + +on: + workflow_dispatch: + +env: + REGISTRY_IMAGE_BASE: estoesmoises/stackoverflow-backstage-demo + IMAGE_TAG: test + +jobs: + build-multi-arch: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + outputs: type=image,push-by-digest=true,name=${{ env.REGISTRY_IMAGE_BASE }},push=true + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build-multi-arch + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create manifest list and push with test tag + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create -t ${{ env.REGISTRY_IMAGE_BASE }}:${{ env.IMAGE_TAG }} \ + $(printf '${{ env.REGISTRY_IMAGE_BASE }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE_BASE }}:${{ env.IMAGE_TAG }} \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..3c5af57 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,109 @@ +name: "Build and Publish Docker Demo" + +on: + push: + tags: + - "v*" + +env: + REGISTRY_IMAGE: estoesmoises/stackoverflow-backstage-demo + +jobs: + build-multi-arch: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,push-by-digest=true,name=${{ env.REGISTRY_IMAGE }},push=true + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build-multi-arch + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ github.ref_type == 'tag' }} + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f7f0771 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,121 @@ +# DO NOT USE THIS IN PRODUCTION +# +# This Dockerfile builds a Docker image for a pre-configured Backstage instance +# with the Stack Overflow plugins installed. It’s designed as a quick way to try +# out the integration without having to set it up yourself. +# +# This is not intended for production use. + +# Stage 1 - Create yarn install skeleton layer +FROM node:20-bookworm-slim AS packages + +WORKDIR /app +COPY backstage.json package.json yarn.lock ./ +COPY .yarn ./.yarn +COPY .yarnrc.yml ./ + +COPY packages packages + +# Comment this out if you don't have any internal plugins +COPY plugins plugins + +RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ + +# Stage 2 - Install dependencies and build packages +FROM node:20-bookworm-slim AS build + +# Set Python interpreter for `node-gyp` to use +ENV PYTHON=/usr/bin/python3 + +# Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install -y --no-install-recommends python3 g++ build-essential && \ + rm -rf /var/lib/apt/lists/* + +# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, +# in which case you should also move better-sqlite3 to "devDependencies" in package.json. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev && \ + rm -rf /var/lib/apt/lists/* + +USER node +WORKDIR /app + +COPY --from=packages --chown=node:node /app . + +RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ + yarn install --immutable + +COPY --chown=node:node . . + +RUN yarn tsc +RUN yarn --cwd packages/backend build + +RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \ + && tar xzf packages/backend/dist/skeleton.tar.gz -C packages/backend/dist/skeleton \ + && tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle + +# Stage 3 - Build the actual backend image and install production dependencies +FROM node:20-bookworm-slim + +# Set Python interpreter for `node-gyp` to use +ENV PYTHON=/usr/bin/python3 + +# Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install -y --no-install-recommends python3 g++ build-essential && \ + rm -rf /var/lib/apt/lists/* + +# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, +# in which case you should also move better-sqlite3 to "devDependencies" in package.json. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev && \ + rm -rf /var/lib/apt/lists/* + +# From here on we use the least-privileged `node` user to run the backend. +USER node + +# This should create the app dir as `node`. +# If it is instead created as `root` then the `tar` command below will +# fail: `can't create directory 'packages/': Permission denied`. +# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) +# so the app dir is correctly created as `node`. +WORKDIR /app + +# Copy the install dependencies from the build stage and context +COPY --from=build --chown=node:node /app/.yarn ./.yarn +COPY --from=build --chown=node:node /app/.yarnrc.yml ./ +COPY --from=build --chown=node:node /app/backstage.json ./ +COPY --from=build --chown=node:node /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton/ ./ + +# Note: The skeleton bundle only includes package.json files -- if your app has +# plugins that define a `bin` export, the bin files need to be copied as well to +# be linked in node_modules/.bin during yarn install. + +RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ + yarn workspaces focus --all --production && rm -rf "$(yarn cache clean)" + +# Copy the built packages from the build stage +COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./ + +# Copy any other files that we need at runtime +COPY --chown=node:node app-config*.yaml ./ + +# This will include the examples, if you don't need these simply remove this line +COPY --chown=node:node examples ./examples + +# This switches many Node.js dependencies to production mode. +ENV NODE_ENV=development + +# This disables node snapshot for Node 20 to work with the Scaffolder +ENV NODE_OPTIONS="--no-node-snapshot" + +CMD ["node", "packages/backend", "--config", "app-config.docker-local.yaml"] \ No newline at end of file diff --git a/app-config.docker-local.yaml b/app-config.docker-local.yaml new file mode 100644 index 0000000..9508a5e --- /dev/null +++ b/app-config.docker-local.yaml @@ -0,0 +1,131 @@ +# DO NOT USE THIS IN PRODUCTION. +# +# This app-config is used to run a development environment with Docker. +# It resembles the app-config.yaml file which is not intended for production use. + +app: + title: Scaffolded Backstage App + baseUrl: http://localhost:7007 + +organization: + name: My Company + +stackoverflow: + baseUrl: ${STACK_OVERFLOW_INSTANCE_URL} + # teamName: ${STACK_OVERFLOW_TEAM_NAME} + + apiAccessToken: ${STACK_OVERFLOW_API_ACCESS_TOKEN} + # The API Access Token is used for the Questions' collator, a no-expiry, read-only token is recommended. + + clientId: ${STACK_OVERFLOW_CLIENT_ID} + # The clientid must be for an API Application with read-write access. + + redirectUri: ${STACK_OVERFLOW_REDIRECT_URI} + # If no redirectUri is specified this will return to https:///stack-overflow-teams + +backend: + # Used for enabling authentication, secret is shared by all backend plugins + # See https://backstage.io/docs/auth/service-to-service-auth for + # information on the format + # auth: + # keys: + # - secret: ${BACKEND_SECRET} + baseUrl: http://localhost:7007 + listen: + port: 7007 + # Uncomment the following host directive to bind to specific interfaces + # host: 127.0.0.1 + csp: + connect-src: ["'self'", 'http:', 'https:'] + # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference + # Default Helmet Content-Security-Policy values can be removed by setting the key to false + cors: + origin: http://localhost:7007 + methods: [GET, HEAD, PATCH, POST, PUT, DELETE] + credentials: true + # This is for local development only, it is not recommended to use this in production + # The production database configuration is stored in app-config.production.yaml + database: + client: better-sqlite3 + connection: ':memory:' + # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir + +integrations: + github: + - host: github.com + # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information + # about setting up the GitHub integration here: https://backstage.io/docs/integrations/github/locations#configuration + token: ${GITHUB_TOKEN} + ### Example for how to add your GitHub Enterprise instance using the API: + # - host: ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: ${GHE_TOKEN} + +proxy: + ### Example for how to add a proxy endpoint for the frontend. + ### A typical reason to do this is to handle HTTPS and CORS for internal services. + # endpoints: + # '/test': + # target: 'https://example.com' + # changeOrigin: true + +# Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach +techdocs: + builder: 'local' # Alternatives - 'external' + generator: + runIn: 'docker' # Alternatives - 'local' + publisher: + type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. + +auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + providers: + # See https://backstage.io/docs/auth/guest/provider + guest: {} + +scaffolder: + # see https://backstage.io/docs/features/software-templates/configuration for software template options + +catalog: + import: + entityFilename: catalog-info.yaml + pullRequestBranchName: backstage-integration + rules: + - allow: [Component, System, API, Resource, Location] + locations: + # Local example data, file locations are relative to the backend process, typically `packages/backend` + - type: file + target: ../../examples/entities.yaml + + # Local example template + - type: file + target: ../../examples/template/template.yaml + rules: + - allow: [Template] + + # Local example organizational data + - type: file + target: ../../examples/org.yaml + rules: + - allow: [User, Group] + + ## Uncomment these lines to add more example data + # - type: url + # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml + + ## Uncomment these lines to add an example org + # - type: url + # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml + # rules: + # - allow: [User, Group] + +kubernetes: + # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options + +# see https://backstage.io/docs/permissions/getting-started for more on the permission framework +permission: + # setting this to `false` will disable permissions + enabled: true diff --git a/app-config.production.yaml b/app-config.production.yaml index 5d426f5..8fd2375 100644 --- a/app-config.production.yaml +++ b/app-config.production.yaml @@ -2,6 +2,19 @@ app: # Should be the same as backend.baseUrl when using the `app-backend` plugin. baseUrl: http://localhost:7007 +stackoverflow: + baseUrl: ${STACK_OVERFLOW_INSTANCE_URL} + # teamName: ${STACK_OVERFLOW_TEAM_NAME} + + apiAccessToken: ${STACK_OVERFLOW_API_ACCESS_TOKEN} + # The API Access Token is used for the Questions' collator, a no-expiry, read-only token is recommended. + + clientId: ${STACK_OVERFLOW_CLIENT_ID} + # The clientid must be for an API Application with read-write access. + + redirectUri: ${STACK_OVERFLOW_REDIRECT_URI} + # If no redirectUri is specified this will return to https:///stack-overflow-teams + backend: # Note that the baseUrl should be the URL that the browser and other clients # should use when communicating with the backend, i.e. it needs to be diff --git a/plugins/search-backend-module-stack-overflow-teams-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts b/plugins/search-backend-module-stack-overflow-teams-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts index 6951c8a..d3a2c8e 100644 --- a/plugins/search-backend-module-stack-overflow-teams-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts +++ b/plugins/search-backend-module-stack-overflow-teams-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts @@ -60,7 +60,6 @@ export type StackOverflowQuestionsRequestParams = { * @public */ export type StackOverflowQuestionsCollatorFactoryOptions = { - baseUrl: string; apiAccessToken?: string; teamName?: string; requestParams?: StackOverflowQuestionsRequestParams; @@ -88,7 +87,7 @@ export class StackOverflowQuestionsCollatorFactory private forceOriginUrl = (baseUrl: string): string => `${new URL(baseUrl).origin}`; - private constructor(options: StackOverflowQuestionsCollatorFactoryOptions) { + private constructor(options: StackOverflowQuestionsCollatorFactoryOptions & { baseUrl: string }) { this.baseUrl = this.forceOriginUrl(options.baseUrl); this.apiAccessToken = options.apiAccessToken; this.teamName = options.teamName;