diff --git a/.env.example b/.env.example index 3ac2f0fb..c76b75cb 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ APP_ID=12345 GITHUB_CLIENT_ID=Iv1.12345def GITHUB_CLIENT_SECRET=clientsecret -# Private key for the GitHub App +# Private key for the GitHub App (base64 encoded or raw) PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nYOUR KEY HERE WITH \n INCLUDED=\n-----END RSA PRIVATE KEY-----\n # Auth configs @@ -27,3 +27,33 @@ NODE_ENV=development # Used for GHEC configs, where private mirrors are kept in a different org PUBLIC_ORG= PRIVATE_ORG= + +# GitHub Enterprise (GHE.com Data Residency / GHES) configuration. +# Leave unset to use the github.com defaults shown below. For GHE/GHES, set +# each custom URL explicitly. +# GITHUB_SERVER_URL= +# GITHUB_API_URL= +# GITHUB_GRAPHQL_URL= + +# Committer email domain used on sync commits. Defaults to +# `users.noreply.github.com`. Set explicitly for GHE/GHES; the exact value +# depends on the instance configuration. +# GITHUB_USER_EMAIL_DOMAIN= + +# Used to skip branch protection creation if organization level branch protections are used instead +SKIP_BRANCH_PROTECTION_CREATION= + +# Used to create mirrors with internal visibility instead of private for use in GitHub Enterprise Cloud organizations +CREATE_MIRRORS_WITH_INTERNAL_VISIBILITY= + +# Used for syncing logic to avoid having EMU users listed as committer +DELETE_INTERNAL_MERGE_COMMITS_ON_SYNC= + +# Used to configure the timeout for syncing a mirror before the task gets backgrounded (default is 30 seconds) +MIRROR_SYNC_TIMEOUT_MS= + +# Used to configure the number of commits to push at a time when syncing a mirror (default is 1000) +MIRROR_PUSH_CHUNK_SIZE= + +# Used to disable mirror deletion through private mirrors. Hides the delete action in the UI and rejects direct API calls. +DISABLE_MIRROR_DELETION= diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 704b33a0..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": [ - "next/core-web-vitals", - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-type-checked" - ], - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "parserOptions": { - "project": ["./tsconfig.json"] - }, - "root": true, - "rules": { - // Most of the problems this rule catches are false positives from libs - "@typescript-eslint/no-misused-promises": "off" - } -} diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index eda42751..01aaf5cd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,5 +2,5 @@ blank_issues_enabled: false contact_links: - name: Ask a question - url: https://github.com/github/private-mirrors/discussions/new + url: https://github.com/github-community-projects/private-mirrors/discussions/new?category=q-a about: Ask a question or start a discussion diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5c6b55ff..0b8a12f9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,10 +18,6 @@ examples: "feat: add new logger" or "fix: remove unused imports" ### Author/Contributor - [ ] If documentation is needed for this change, has that been included in this pull request +- [ ] run `npm run format` and fix any formatting issues that have been introduced - [ ] run `npm run lint` and fix any linting issues that have been introduced - [ ] run `npm run test` and run tests -- [ ] If publishing new data to the public (scorecards, security scan results, code quality results, live dashboards, etc.), please request review from `@jeffrey-luszcz` - -### Reviewer - -- [ ] Label as either `bug`, `documentation`, `enhancement`, `infrastructure`, `maintenance`, or `breaking` diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 56b39b58..72c6e23c 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -5,9 +5,12 @@ template: | # Changelog $CHANGES - See details of [all code changes](https://github.com/github-community-projects/private-mirrors/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release + See details of [all code changes](https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release categories: + - title: '💥 Breaking Changes' + labels: + - 'breaking' - title: '🚀 Features' labels: - 'feature' @@ -32,14 +35,18 @@ version-resolver: major: labels: - 'breaking' + - 'major' minor: labels: - 'enhancement' - - 'fix' + - 'feature' + - 'minor' patch: labels: + - 'fix' - 'documentation' - 'maintenance' + - 'patch' default: patch autolabeler: - label: 'automation' diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 435e914f..009e628b 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -12,13 +12,10 @@ permissions: jobs: main: permissions: - contents: write + contents: read pull-requests: write - name: Auto label pull requests - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 # pin@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml + uses: github-community-projects/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 + with: + config-name: release-drafter.yml + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dae67439..ec7f68d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,14 @@ name: Run Build on: - - push - - workflow_dispatch - - pull_request + push: + branches: + - main + workflow_dispatch: + pull_request: + +permissions: + contents: read jobs: build: @@ -11,10 +16,18 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' - name: Install Dependencies - run: npm install + run: npm ci --ignore-scripts - name: Run Build run: NEXT_TELEMETRY_DISABLED=1 npm run build diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7032cff4..a7432ddf 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,9 +1,15 @@ name: Build Docker Image on: - - push - - workflow_dispatch - - pull_request + push: + branches: + - main + workflow_dispatch: + pull_request: + +permissions: + contents: read + packages: write jobs: docker-build: @@ -11,7 +17,27 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Get Git commit timestamps + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV + + - name: Validate build configuration + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + call: check - name: Build Docker image - run: docker build -t private-mirrors . + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + push: false + tags: private-mirrors + cache-from: type=gha + cache-to: type=gha,mode=max + env: + SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..d9cf23c5 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,98 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + schedule: + - cron: '38 18 * * *' + push: + branches: [ "main" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "main" ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e66a198d..735965a5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,9 +1,14 @@ name: Run Lint on: - - push - - workflow_dispatch - - pull_request + push: + branches: + - main + workflow_dispatch: + pull_request: + +permissions: + contents: read jobs: lint: @@ -11,10 +16,18 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' - name: Install Dependencies - run: npm install + run: npm ci --ignore-scripts - name: Run Lint run: npm run lint diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index efcd1c0d..ff3e6f9e 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -6,6 +6,7 @@ on: pull_request_target: types: - opened + - reopened - edited - synchronize @@ -15,27 +16,9 @@ permissions: jobs: main: permissions: + contents: read pull-requests: read statuses: write - name: Validate PR title - runs-on: ubuntu-latest - steps: - - uses: amannn/action-semantic-pull-request@40166f00814508ec3201fc8595b393d451c8cd80 # pin@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - # Configure which types are allowed (newline-delimited). - # From: https://github.com/commitizen/conventional-commit-types/blob/master/index.json - # listing all below - types: | - build - chore - ci - docs - feat - fix - perf - refactor - revert - style - test + uses: github-community-projects/ospo-reusable-workflows/.github/workflows/pr-title.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b981e303..c05d4272 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,26 +13,20 @@ permissions: contents: read jobs: - create_release: - # release if - # manual deployment OR - # merged to main and labelled with release labels - if: | - (github.event_name == 'workflow_dispatch') || - (github.event.pull_request.merged == true && - (contains(github.event.pull_request.labels.*.name, 'breaking') || - contains(github.event.pull_request.labels.*.name, 'feature') || - contains(github.event.pull_request.labels.*.name, 'vuln') || - contains(github.event.pull_request.labels.*.name, 'release'))) - runs-on: ubuntu-latest + release: permissions: - contents: write - pull-requests: read - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 # pin@v6 - id: release-drafter - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml - publish: true + contents: write # Create release and push tags + pull-requests: read # Read PR labels for release-drafter + packages: write # Push container image to ghcr.io + id-token: write # Federate for artifact attestation + attestations: write # Generate build provenance attestations + discussions: write # Create release announcement discussion + uses: github-community-projects/ospo-reusable-workflows/.github/workflows/release.yaml@e92cb6053ace495fe40a5f185988557afcdcecbc # v1.0.1 + with: + publish: true + release-config-name: release-drafter.yml + image-name: ${{ github.repository }} + create-attestation: true + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + image-registry-password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4c0fe902..220053d0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -30,18 +30,18 @@ jobs: persist-credentials: false - name: 'Run analysis' - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif publish_results: true - name: 'Upload artifact' - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif retention-days: 5 - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 + uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 with: sarif_file: results.sarif diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 00000000..27bad43e --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,33 @@ +name: Check formatting and style + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + format: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install Dependencies + run: npm ci --ignore-scripts + + - name: Run format + run: npm run format diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eaa481e4..c1863c0e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,20 +1,37 @@ -name: Run Jest Tests +name: Run Tests on: - - push - - workflow_dispatch - - pull_request + push: + branches: + - main + workflow_dispatch: + pull_request: + +permissions: + contents: read jobs: test: + name: Test on Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22, 24] steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' - name: Install Dependencies - run: npm install + run: npm ci --ignore-scripts - - name: Run Jest Tests - run: npm test + - name: Run Tests + run: npm run test diff --git a/.gitignore b/.gitignore index 4f47a718..e505b749 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ build .env.* !.env.example .DS_Store +next-env.d.ts +tsconfig.tsbuildinfo diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100755 new mode 100644 index 36af2198..c27d8893 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx lint-staged +lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 00000000..18de9841 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +npm test \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..04520074 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.14.1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fce5cf26..9c0163d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,33 @@ -FROM node:22-alpine@sha256:c06bea602e410a3321622c7782eb35b0afb7899d9e28300937ebf2e521902555 AS deps +FROM node:24-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json ./ -RUN npm install --omit=dev +RUN npm ci -FROM node:22-alpine@sha256:c06bea602e410a3321622c7782eb35b0afb7899d9e28300937ebf2e521902555 AS builder +FROM node:24-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -ENV NEXT_TELEMETRY_DISABLED 1 +ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build +RUN npm prune --omit=dev + +FROM node:24-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b AS runner +LABEL maintainer="@github" \ + org.opencontainers.image.url="https://github.com/github-community-projects/private-mirrors" \ + org.opencontainers.image.source="https://github.com/github-community-projects/private-mirrors" \ + org.opencontainers.image.documentation="https://github.com/github-community-projects/private-mirrors" \ + org.opencontainers.image.vendor="GitHub Community Projects" \ + org.opencontainers.image.description="A GitHub App that allows you to contribute upstream using private mirrors of public projects." -FROM node:22-alpine@sha256:c06bea602e410a3321622c7782eb35b0afb7899d9e28300937ebf2e521902555 AS runner RUN apk add --no-cache git WORKDIR /app -ENV NODE_ENV production -ENV NEXT_TELEMETRY_DISABLED 1 +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs @@ -32,6 +40,6 @@ USER nextjs EXPOSE 3000 -ENV PORT 3000 +ENV PORT=3000 CMD ["npm", "start"] diff --git a/README.md b/README.md index 00d77a62..4e950c86 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ For a video overview, check out [this short presentation and demo video](https:/ > > This app is still a work in progress and is pre-1.0 _public beta_. We are actively working on improving it with beta testers, and if you're interested in using it for your organization, [we'd love to hear from you](https://github.com/github-community-projects/private-mirrors/issues/new)! -## Background +## Problem Statement Enterprises struggle with how to let their developers contribute to open source projects. Most are not opposed, in principle, to contributing back to the projects they rely upon. Many are enthusiastic about becoming better open source citizens, and understand the reputational and technical benefits that working in open source can accrue to the business. However, real and perceived security concerns make this process difficult at best and impossible at worst for companies. @@ -61,7 +61,7 @@ The app uses an intermediary public fork to merge the private mirror into, and t ## Hosting -You'll need to self-host the app. See the section on [Developing](#developing) for more information. +You'll need to self-host the app. See the section on [Developing](docs/developing.md) for more information. This app was created with the idea of self-hosting in mind and can be deployed to any hosting provider that supports Next.js/Docker. @@ -78,9 +78,9 @@ docker run --env-file=.env -p 3000:3000 private-mirrors docker compose up ``` -We recommend using Node 20.x or higher, though any Node LTS version >18 should work. +This project supports Node.js LTS releases; the latest LTS version is preferred. See the `engines` field in [package.json](./package.json) for the exact supported range. -Once it's running, you'll need to create a GitHub App and configure it to point to your deployment. See the [Developing — GitHub App](#github-app) section for more information. +Once it's running, you'll need to create a GitHub App and configure it to point to your deployment. See the [Developing — GitHub App](docs/developing.md#github-app) section for more information. ## Integrating the App into GHEC @@ -97,60 +97,47 @@ PRIVATE_ORG=name-of-your-ghec-org # Where your private mirrors will be creat The authentication of the UI will still need to be a user's github.com user, but the app will be able to create forks and mirrors in the GHEC instance. -## Usage - -Once the app is installed, follow this document on [Using the Private Mirrors App](docs/using-the-app.md) to get the repository fork and mirrors set up for work. +## Integrating the App into GHE.com (Data Residency) or GHES -## Developing +The app also supports GitHub Enterprise Cloud with Data Residency (`*.ghe.com`) and GitHub Enterprise Server. Configure the server, REST API, and GraphQL API URLs explicitly for your environment. -### Environment - -Create a new `.env` file from the `.env.example` file +Set the following environment variables in addition to the GHEC variables above: ```sh -cp .env.example .env +# Base URL of your GHE instance (no trailing slash). +# GHE.com Data Residency: https://.ghe.com +# GHES: https://ghes.example.com +GITHUB_SERVER_URL=https://acme.ghe.com + +# REST API and GraphQL URLs for the same GitHub host. +GITHUB_API_URL=https://api.acme.ghe.com +GITHUB_GRAPHQL_URL=https://api.acme.ghe.com/graphql + +# Committer email domain used on sync commits. Defaults to `users.noreply.github.com`. +# Set explicitly for GHE/GHES (value depends on instance configuration), e.g.: +# users.noreply.acme.ghe.com +# users.noreply.ghes.example.com +GITHUB_USER_EMAIL_DOMAIN=users.noreply.acme.ghe.com ``` -### GitHub App - -1. Create a new GitHub App [here](https://github.com/settings/apps/new) -2. There's an App manifest in the repo that lays out all the permissions and webhook events needed and can be found [here](./app.yml). -3. Copy all the secrets, credentials, and IDs into the `.env` file - -### Webapp - -This is a webapp built with Next.js. You can find the Next.js documentation [here](https://nextjs.org/docs). - -#### Install dependencies - -```sh -npm i -``` +Notes: -#### Start the application +- The OAuth App / GitHub App, organizations, members and forks must all live on the same GHE instance. +- GitHub configuration is read at runtime and safely passed from the server to client-side hooks and UI links. No duplicate `NEXT_PUBLIC_*` variables or Docker build arguments are required. +- If these variables are unset, the app uses `github.com`, `api.github.com`, and `users.noreply.github.com` defaults. +- The local webhook relay (`npm run webhook`) uses `github-app-webhook-relay-polling` against the GitHub App hook deliveries endpoint. It is best-effort on GHE; in production, use real webhook deliveries configured directly on your GitHub App. -```sh -npm run dev -``` - -You should be up and running on [http://localhost:3000](http://localhost:3000)! - -#### Testing Webhooks - -Webhooks are an important part of this application, they listen for events that happen to your organization and trigger the app to do things like create branch protections or sync code between forks. - -We have our own webhook proxy that you can use to test webhooks locally. You will need to set `PUBLIC_ORG` (and `PRIVATE_ORG` if you want to test with a different organization) in your `.env` file to your GitHub organization name. - -### GitHub Requirements - -We recommend that you have a dedicated GitHub organization for your contributions. This will allow you to keep your contributions separate from your organization's daily operations. +## Usage -> We have added support for GitHub Enterprise Managed Users (EMUs) and GitHub Enterprise Cloud (GHEC) in the app. If you are using GitHub Enterprise, you will need to make sure that the app is installed on your GitHub Enterprise instance. +Once the app is installed, follow this document on [Using the Private Mirrors App](docs/using-the-app.md) to get the repository fork and mirrors set up for work. -Permissions: +## Further Reading -- The GitHub App must be installed on the organization(s) you plan on contributing from -- Currently, any member of the organization can access the app and create additional private mirror repositories +- [Contributing with Confidence: Capital One's open source contribution workflows](https://www.youtube.com/watch?v=boWJs4lASfY) - Talk about PMA by @ahpook and @riley-kohler at GitHub Universe 2024 +- [Developing](docs/developing.md) +- [Using the App](docs/using-the-app.md) +- [Architecture](docs/architecture.md) +- [Attribution Flow](docs/attribution-flow.md) ## License diff --git a/babel.config.testing.js b/babel.config.testing.js deleted file mode 100644 index d4f6648f..00000000 --- a/babel.config.testing.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { presets: ['@babel/preset-env'] } diff --git a/docs/attribution-flow.md b/docs/attribution-flow.md new file mode 100644 index 00000000..cd46b854 --- /dev/null +++ b/docs/attribution-flow.md @@ -0,0 +1,84 @@ +# Attribution Flow + +## Overview + +Enterprise Managed Users (EMU) are a [feature of GitHub Enterprise Cloud](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/understanding-iam-for-enterprises/about-enterprise-managed-users) which provides a "walled garden" user account that cannot interact with public repositories on github.com, including filing issues, commenting on discussions, and raising pull requests. Large organizations use EMUs to provide a tighter degree of control over their user accounts, but this control can come at the cost of participation in open source communities. + +One of the goals of the Private Mirrors App is to enable contributions from EMUs, and this doc explains how to do it. + +## How Attributions Work + +Git commits under the hood are associated with email addresses. GitHub makes a convenient association between email addresses and user accounts for purposes of attribution, contribution graphs, etc, but it's email underneath all that. So in the case where a contribution is crossing user accounts and especially across EMU boundaries, as long as there is some association between the public github.com user and an email address, the attributions will be linked up automatically. + +For contributions which originate on a private mirror and PMA syncs to the public fork, this linkage can go in either direction: + +- the public github.com account can have the EMU account's email address added as a secondary address, and commits made with that email will be attributed to the user. **or** +- a user inside the EMU boundary can configure their git client to commit with an address associated with their public account. :arrow_left: + +We recommend the second option because it does not expose any internal information, namely the user's email address, to the public contribution graph. + +Sometimes stakeholders will raise the idea of funneling all contributions through a "role" account like `@bigcorp-opensource`, but we strongly discourage this. It's both bad for maintainers (people want contributions from humans not corporations) and for contributors (the attribution of their work to the open source world is often a primary driver for wanting to contribute in the first place). + +## Configuring git-config + +So, to ensure that contributions made by an EMU are properly attributed to their public GitHub account, the user needs to configure their local git-config to use an email address associated with their public account. This should be done at the repository level, when they are working in a private mirror managed by PMA, rather than as a global configuration. + +```sh +git config --local user.email "your-public-email@example.com" +``` + +## Example + +Here is an example of how the attribution flow works: + +1. An EMU user configures their local git-config to use the email address associated with their public GitHub account. +2. The user makes a contribution to a private mirror repository. +3. The contribution is reviewed and merged into the private mirror's default branch. +4. The Private Mirrors App automatically syncs the private mirror to the public fork. +5. The contribution is now visible in the public fork and is attributed to the user's public GitHub account. +6. The user can then switch to their public account and open a pull request from the public fork to the upstream repository, and the contribution will be properly attributed to their public identity. + +### Automatically Configuring Git (Advanced) + +Git config supports a [conditional includes feature](https://git-scm.com/docs/git-config#_conditional_includes) to automatically include configuration conditionally based on metadata of the repository locally. Email, a [key for signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits), and other helpful settings can be configured automatically with this feature. It helps prevent mistakes from forgetting to configure settings within each repository. + +To configure conditional includes, start by creating a file in the same user directory where your `.gitconfig` is stored. Add the following content to the file: + +```ini +[user] + # replace this with your email address + email = your-public-email@example.com + # remove this option if not used + signingKey = +``` + +This file can be named anything, but for this example, we'll call it `.gitconfig-pma`. + +Then, add configuration to your `.gitconfig` to conditionally use the `.gitconfig-pma` file when in a repository used for contributions through PMA: + +```ini +# Include config based on the remote HTTPS URL of the repository +# Replace the github.com remote URL below with the remote URL used for private mirror contributions +[includeIf "hasconfig:remote.*.url:https://github.com/**"] + path = .gitconfig-pma + +# Include config based on the remote git URL of the repository +# Replace the github.com remote URL below with the remote URL used for private mirror contributions +[includeIf "hasconfig:remote.*.url:git@github.com*/**"] + path = .gitconfig-pma + +# Include config based on directory in which the repository resides +# Replace the **/pma/** directory example below with the directory used for private mirror contributions +[includeIf "gitdir:**/pma/**/.git"] + path = .gitconfig-pma +``` + +Choose one or more of the above options, based on your needs; not all configuration approaches may be needed. + +With this configured, git should automatically set the user email and any other settings defined in the conditional config within matching repositories. You can verify the settings are applied by running `git config get