diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 56b39b5..0fdf58b 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -5,7 +5,7 @@ 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: '🚀 Features' @@ -32,14 +32,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 435e914..40e25fe 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/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + with: + config-name: release-drafter.yml + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index efcd1c0..5106233 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/ospo-reusable-workflows/.github/workflows/pr-title.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b981e30..e1999f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,26 +13,31 @@ 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 + uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + with: + publish: true + release-config-name: release-drafter.yml + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + release_image: + needs: release + permissions: + contents: read + packages: write + id-token: write + attestations: write + uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + with: + image-name: ${{ github.repository }} + full-tag: ${{ needs.release.outputs.full-tag }} + short-tag: ${{ needs.release.outputs.short-tag }} + create-attestation: true + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + image-registry: ghcr.io + image-registry-username: ${{ github.actor }} + image-registry-password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4c0fe90..2133055 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: 'Upload artifact' - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 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@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 with: sarif_file: results.sarif diff --git a/Dockerfile b/Dockerfile index fce5cf2..e3a43a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,13 @@ ENV NEXT_TELEMETRY_DISABLED 1 RUN npm run build FROM node:22-alpine@sha256:c06bea602e410a3321622c7782eb35b0afb7899d9e28300937ebf2e521902555 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." + RUN apk add --no-cache git WORKDIR /app diff --git a/package-lock.json b/package-lock.json index 842a412..1dafc2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@trpc/server": "10.45.2", "fast-safe-stringify": "2.1.1", "fuse.js": "7.0.0", - "next": "14.2.15", + "next": "14.2.23", "next-auth": "4.24.11", "octokit": "3.2.1", "probot": "13.4.1", @@ -32,7 +32,7 @@ "superjson": "2.2.2", "tempy": "1.0.1", "tslog": "4.9.3", - "undici": "6.21.0", + "undici": "6.21.1", "zod": "3.23.8" }, "devDependencies": { @@ -2401,9 +2401,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", - "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==" + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", + "integrity": "sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==" }, "node_modules/@next/eslint-plugin-next": { "version": "15.0.4", @@ -2443,9 +2443,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", - "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz", + "integrity": "sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==", "cpu": [ "arm64" ], @@ -2458,9 +2458,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", - "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz", + "integrity": "sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==", "cpu": [ "x64" ], @@ -2473,9 +2473,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", - "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz", + "integrity": "sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==", "cpu": [ "arm64" ], @@ -2488,9 +2488,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", - "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz", + "integrity": "sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==", "cpu": [ "arm64" ], @@ -2503,9 +2503,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", - "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz", + "integrity": "sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==", "cpu": [ "x64" ], @@ -2518,9 +2518,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", - "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz", + "integrity": "sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==", "cpu": [ "x64" ], @@ -2533,9 +2533,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", - "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz", + "integrity": "sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==", "cpu": [ "arm64" ], @@ -2548,9 +2548,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", - "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", + "integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", "cpu": [ "ia32" ], @@ -2563,9 +2563,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", - "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz", + "integrity": "sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==", "cpu": [ "x64" ], @@ -7146,9 +7146,10 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -7169,7 +7170,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -7184,6 +7185,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/cookie": { @@ -11053,9 +11058,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -11092,11 +11097,11 @@ } }, "node_modules/next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", - "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", + "integrity": "sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==", "dependencies": { - "@next/env": "14.2.15", + "@next/env": "14.2.23", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -11111,15 +11116,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.15", - "@next/swc-darwin-x64": "14.2.15", - "@next/swc-linux-arm64-gnu": "14.2.15", - "@next/swc-linux-arm64-musl": "14.2.15", - "@next/swc-linux-x64-gnu": "14.2.15", - "@next/swc-linux-x64-musl": "14.2.15", - "@next/swc-win32-arm64-msvc": "14.2.15", - "@next/swc-win32-ia32-msvc": "14.2.15", - "@next/swc-win32-x64-msvc": "14.2.15" + "@next/swc-darwin-arm64": "14.2.23", + "@next/swc-darwin-x64": "14.2.23", + "@next/swc-linux-arm64-gnu": "14.2.23", + "@next/swc-linux-arm64-musl": "14.2.23", + "@next/swc-linux-x64-gnu": "14.2.23", + "@next/swc-linux-x64-musl": "14.2.23", + "@next/swc-win32-arm64-msvc": "14.2.23", + "@next/swc-win32-ia32-msvc": "14.2.23", + "@next/swc-win32-x64-msvc": "14.2.23" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -11718,9 +11723,10 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -14043,9 +14049,10 @@ } }, "node_modules/undici": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", - "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", "engines": { "node": ">=18.17" } diff --git a/package.json b/package.json index 6d2c328..9e694bf 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@trpc/server": "10.45.2", "fast-safe-stringify": "2.1.1", "fuse.js": "7.0.0", - "next": "14.2.15", + "next": "14.2.23", "next-auth": "4.24.11", "octokit": "3.2.1", "probot": "13.4.1", @@ -42,7 +42,7 @@ "superjson": "2.2.2", "tempy": "1.0.1", "tslog": "4.9.3", - "undici": "6.21.0", + "undici": "6.21.1", "zod": "3.23.8" }, "devDependencies": { diff --git a/src/server/repos/controller.ts b/src/server/repos/controller.ts index 981fd89..f541e60 100644 --- a/src/server/repos/controller.ts +++ b/src/server/repos/controller.ts @@ -28,6 +28,10 @@ export const createMirrorHandler = async ({ }: { input: CreateMirrorSchema }) => { + // Use to track mirror creation for cleanup in case of errors + let privateOctokit + let newRepo + try { reposApiLogger.info('createMirror', { input: input }) @@ -41,7 +45,7 @@ export const createMirrorHandler = async ({ const contributionOctokit = octokitData.contribution.octokit const contributionAccessToken = octokitData.contribution.accessToken - const privateOctokit = octokitData.private.octokit + privateOctokit = octokitData.private.octokit const privateInstallationId = octokitData.private.installationId const privateAccessToken = octokitData.private.accessToken @@ -79,95 +83,85 @@ export const createMirrorHandler = async ({ } }) - try { - const forkData = await contributionOctokit.rest.repos.get({ - owner: input.forkRepoOwner, - repo: input.forkRepoName, - }) + const forkData = await contributionOctokit.rest.repos.get({ + owner: input.forkRepoOwner, + repo: input.forkRepoName, + }) - // Now create a temporary directory to clone the repo into - const tempDir = temporaryDirectory() - - const options: Partial = { - config: [ - `user.name=pma[bot]`, - // We want to use the private installation ID as the email so that we can push to the private repo - `user.email=${privateInstallationId}+pma[bot]@users.noreply.github.com`, - // Disable any global git hooks to prevent potential interference when running the app locally - 'core.hooksPath=/dev/null', - ], - } - const git = simpleGit(tempDir, options) - const remote = generateAuthUrl( - contributionAccessToken, - input.forkRepoOwner, - input.forkRepoName, - ) + // Now create a temporary directory to clone the repo into + const tempDir = temporaryDirectory() + + const options: Partial = { + config: [ + `user.name=pma[bot]`, + // We want to use the private installation ID as the email so that we can push to the private repo + `user.email=${privateInstallationId}+pma[bot]@users.noreply.github.com`, + // Disable any global git hooks to prevent potential interference when running the app locally + 'core.hooksPath=/dev/null', + ], + } + const git = simpleGit(tempDir, options) + const remote = generateAuthUrl( + contributionAccessToken, + input.forkRepoOwner, + input.forkRepoName, + ) - await git.clone(remote, tempDir) + await git.clone(remote, tempDir) - // Get the organization custom properties - const orgCustomProps = - await privateOctokit.rest.orgs.getAllCustomProperties({ - org: privateOrg, - }) - - // Creates custom property fork in the org if it doesn't exist - if ( - !orgCustomProps.data.some( - (prop: { property_name: string }) => prop.property_name === 'fork', - ) - ) { - await privateOctokit.rest.orgs.createOrUpdateCustomProperty({ - org: privateOrg, - custom_property_name: 'fork', - value_type: 'string', - }) - } + // Get the organization custom properties + const orgCustomProps = + await privateOctokit.rest.orgs.getAllCustomProperties({ + org: privateOrg, + }) - // This repo needs to be created in the private org - const newRepo = await privateOctokit.rest.repos.createInOrg({ - name: input.newRepoName, + // Creates custom property fork in the org if it doesn't exist + if ( + !orgCustomProps.data.some( + (prop: { property_name: string }) => prop.property_name === 'fork', + ) + ) { + await privateOctokit.rest.orgs.createOrUpdateCustomProperty({ org: privateOrg, - private: true, - description: `Mirror of ${input.forkRepoOwner}/${input.forkRepoName}`, - custom_properties: { - fork: `${input.forkRepoOwner}/${input.forkRepoName}`, - }, + custom_property_name: 'fork', + value_type: 'string', }) + } - const defaultBranch = forkData.data.default_branch + // This repo needs to be created in the private org + newRepo = await privateOctokit.rest.repos.createInOrg({ + name: input.newRepoName, + org: privateOrg, + private: true, + description: `Mirror of ${input.forkRepoOwner}/${input.forkRepoName}`, + custom_properties: { + fork: `${input.forkRepoOwner}/${input.forkRepoName}`, + }, + }) - // Add the mirror remote - const upstreamRemote = generateAuthUrl( - privateAccessToken, - newRepo.data.owner.login, - newRepo.data.name, - ) - await git.addRemote('upstream', upstreamRemote) - await git.push('upstream', defaultBranch) + const defaultBranch = forkData.data.default_branch - // Create a new branch on both - await git.checkoutBranch(input.newBranchName, defaultBranch) - await git.push('origin', input.newBranchName) + // Add the mirror remote + const upstreamRemote = generateAuthUrl( + privateAccessToken, + newRepo.data.owner.login, + newRepo.data.name, + ) + await git.addRemote('upstream', upstreamRemote) + await git.push('upstream', defaultBranch) - reposApiLogger.info('Mirror created', { - org: newRepo.data.owner.login, - name: newRepo.data.name, - }) + // Create a new branch on both + await git.checkoutBranch(input.newBranchName, defaultBranch) + await git.push('origin', input.newBranchName) - return { - success: true, - data: newRepo.data, - } - } catch (error) { - // Clean up the private mirror repo made - await privateOctokit.rest.repos.delete({ - owner: privateOrg, - repo: input.newRepoName, - }) + reposApiLogger.info('Mirror created', { + org: newRepo.data.owner.login, + name: newRepo.data.name, + }) - throw error + return { + success: true, + data: newRepo.data, } } catch (error) { reposApiLogger.error('Error creating mirror', { error }) @@ -178,6 +172,18 @@ export const createMirrorHandler = async ({ (error as Error)?.message ?? 'An error occurred' + if (privateOctokit && newRepo) { + try { + // Clean up the private mirror repo made + await privateOctokit.rest.repos.delete({ + owner: newRepo.data.owner.login, + repo: input.newRepoName, + }) + } catch (deleteError) { + reposApiLogger.error('Failed to delete mirror', { deleteError }) + } + } + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message, diff --git a/test/server/repos.test.ts b/test/server/repos.test.ts index f8e3303..ae383b2 100644 --- a/test/server/repos.test.ts +++ b/test/server/repos.test.ts @@ -199,9 +199,18 @@ describe('Repos router', () => { om.mockFunctions.rest.orgs.get.mockResolvedValue(fakeOrg) om.mockFunctions.rest.repos.get.mockResolvedValueOnce(repoNotFound) om.mockFunctions.rest.repos.get.mockResolvedValueOnce(fakeMirrorRepo) - om.mockFunctions.rest.repos.delete.mockResolvedValue({}) + stubbedGit.clone.mockResolvedValue({}) + om.mockFunctions.rest.orgs.getAllCustomProperties.mockResolvedValue({ + data: [{ fork: 'test' }], + }) + om.mockFunctions.rest.repos.createInOrg.mockResolvedValue({ + data: { owner: { login: 'github' } }, + }) - stubbedGit.clone.mockRejectedValue(new Error('clone error')) + // error after repo creation so that cleanup can be tested + stubbedGit.addRemote.mockRejectedValue(new Error('error adding remote')) + + om.mockFunctions.rest.repos.delete.mockResolvedValue({}) await caller .createMirror({ @@ -213,7 +222,7 @@ describe('Repos router', () => { newRepoName: 'test', }) .catch((error) => { - expect(error.message).toEqual('clone error') + expect(error.message).toEqual('error adding remote') }) expect(configSpy).toHaveBeenCalledTimes(1) @@ -239,9 +248,19 @@ describe('Repos router', () => { om.mockFunctions.rest.orgs.get.mockResolvedValue(fakeOrg) om.mockFunctions.rest.repos.get.mockResolvedValueOnce(repoNotFound) om.mockFunctions.rest.repos.get.mockResolvedValueOnce(fakeMirrorRepo) + stubbedGit.clone.mockResolvedValue({}) + om.mockFunctions.rest.orgs.getAllCustomProperties.mockResolvedValue({ + data: [{ fork: 'test' }], + }) + om.mockFunctions.rest.repos.createInOrg.mockResolvedValue({ + data: { owner: { login: 'github-test' } }, + }) om.mockFunctions.rest.repos.delete.mockResolvedValue({}) - stubbedGit.clone.mockRejectedValue(new Error('clone error')) + // error after repo creation so that cleanup can be tested + stubbedGit.addRemote.mockRejectedValue(new Error('error adding remote')) + + om.mockFunctions.rest.repos.delete.mockResolvedValue({}) await caller .createMirror({ @@ -253,7 +272,7 @@ describe('Repos router', () => { newRepoName: 'test', }) .catch((error) => { - expect(error.message).toEqual('clone error') + expect(error.message).toEqual('error adding remote') }) expect(configSpy).toHaveBeenCalledTimes(1)