fix(oauth): allow Dynamic Client Registration without redirect_uris (… #1185
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Publish with Aspire | |
| on: | |
| push: | |
| branches: [master, main] | |
| tags: ["v*"] | |
| pull_request: | |
| branches: [master, main] | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_REPOSITORY: ${{ github.repository }} | |
| DOTNET_VERSION: "10.0.x" | |
| NODE_VERSION: "24" | |
| PNPM_VERSION: "10" | |
| jobs: | |
| build-and-push: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Install Aspire CLI | |
| run: dotnet tool install --global aspire.cli | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| cache-dependency-path: src/Web/pnpm-lock.yaml | |
| - name: Install web dependencies | |
| working-directory: src/Web | |
| run: pnpm install --frozen-lockfile | |
| - name: Build bridge package | |
| working-directory: src/Web/packages/bridge | |
| run: pnpm run build | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Determine version tag | |
| id: version | |
| run: | | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| VERSION="pr-${{ github.event.pull_request.number }}-${GITHUB_SHA::7}" | |
| DOTNET_TAGS="$VERSION" | |
| DOCKER_TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-web:${VERSION}" | |
| elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION=${GITHUB_REF#refs/tags/v} | |
| DOTNET_TAGS="${VERSION}" | |
| DOCKER_TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-web:${VERSION},${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-web:latest" | |
| elif [[ "${{ github.ref }}" == refs/heads/master ]] || [[ "${{ github.ref }}" == refs/heads/main ]]; then | |
| VERSION=latest | |
| DOTNET_TAGS="$VERSION" | |
| DOCKER_TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-web:${VERSION}" | |
| else | |
| SAFE_REF=$(echo "${GITHUB_REF_NAME}" | tr '/_' '-' | tr -cd '[:alnum:].-') | |
| VERSION="${SAFE_REF}-${GITHUB_SHA::7}" | |
| DOTNET_TAGS="$VERSION" | |
| DOCKER_TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-web:${VERSION}" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "dotnet_tags=$DOTNET_TAGS" >> $GITHUB_OUTPUT | |
| echo "docker_tags=$DOCKER_TAGS" >> $GITHUB_OUTPUT | |
| - name: Restore .NET dependencies | |
| run: dotnet restore | |
| # Generate API client, Zod schemas, and remote functions for the web build. | |
| # IMPORTANT: Must run BEFORE dotnet publish (publish marks outputs as | |
| # up-to-date, causing post-build generation targets to be skipped). | |
| - name: Generate API client for web build | |
| working-directory: src/API/Nocturne.API | |
| run: dotnet build -c Release | |
| - name: Verify generated API client files | |
| run: | | |
| GENERATED_DIR="src/Web/packages/app/src/lib/api/generated" | |
| MISSING=() | |
| # Spot-check a representative set of generated remote files | |
| for f in passkeys patientRecords chartDatas profiles alerts; do | |
| if [ ! -f "$GENERATED_DIR/${f}.generated.remote.ts" ]; then | |
| MISSING+=("$f") | |
| fi | |
| done | |
| if [ ${#MISSING[@]} -gt 0 ]; then | |
| echo "::error::Missing generated remote files: ${MISSING[*]}" | |
| ls -la "$GENERATED_DIR/"*.generated.remote.ts 2>/dev/null || echo "No generated remote files found" | |
| exit 1 | |
| fi | |
| # Verify total count is reasonable (currently ~58 files; fail if below 40) | |
| REMOTE_COUNT=$(find "$GENERATED_DIR" -name "*.generated.remote.ts" | wc -l) | |
| echo "Generated API client files found ($REMOTE_COUNT remote files)" | |
| if [ "$REMOTE_COUNT" -lt 40 ]; then | |
| echo "::error::Expected at least 40 generated remote files but found only $REMOTE_COUNT" | |
| exit 1 | |
| fi | |
| - name: Build and push Nocturne.API container | |
| if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) | |
| working-directory: src/API/Nocturne.API | |
| run: | | |
| dotnet publish \ | |
| -c Release \ | |
| -p:PublishProfile=DefaultContainer \ | |
| -p:ContainerRegistry=${{ env.REGISTRY }} \ | |
| -p:ContainerRepository=${{ env.IMAGE_REPOSITORY }}/nocturne-api \ | |
| "-p:ContainerImageTags=${{ steps.version.outputs.dotnet_tags }}" \ | |
| "-p:ContainerGitCommit=${GITHUB_SHA}" \ | |
| "-p:ContainerBuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| - name: Build and push Nocturne.Services.Demo container | |
| if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) | |
| working-directory: src/Services/Nocturne.Services.Demo | |
| run: | | |
| dotnet publish \ | |
| -c Release \ | |
| -p:PublishProfile=DefaultContainer \ | |
| -p:ContainerRegistry=${{ env.REGISTRY }} \ | |
| -p:ContainerRepository=${{ env.IMAGE_REPOSITORY }}/nocturne-demo \ | |
| "-p:ContainerImageTags=${{ steps.version.outputs.dotnet_tags }}" | |
| - name: Tag dotnet containers as latest | |
| if: startsWith(github.ref, 'refs/tags/v') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) | |
| run: | | |
| for image in nocturne-api nocturne-demo; do | |
| docker buildx imagetools create \ | |
| --tag ${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/${image}:latest \ | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/${image}:${{ steps.version.outputs.version }} | |
| done | |
| - name: Build and push Nocturne.Web container | |
| if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) | |
| run: | | |
| IFS=',' read -ra TAGS <<< "${{ steps.version.outputs.docker_tags }}" | |
| TAG_ARGS="" | |
| for t in "${TAGS[@]}"; do | |
| TAG_ARGS+=" --tag ${t}" | |
| done | |
| docker buildx build \ | |
| --platform linux/amd64 \ | |
| ${TAG_ARGS} \ | |
| --file Dockerfile.web \ | |
| --push \ | |
| . | |
| - name: Verify published image manifests | |
| if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) | |
| run: | | |
| set -euo pipefail | |
| images=( | |
| "nocturne-api" | |
| "nocturne-demo" | |
| "nocturne-web" | |
| ) | |
| for image in "${images[@]}"; do | |
| ref="${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/${image}:${{ steps.version.outputs.version }}" | |
| echo "Verifying manifest: $ref" | |
| docker buildx imagetools inspect "$ref" >/dev/null | |
| done | |
| - name: Collect image manifest status for PR comment | |
| id: image_status | |
| if: always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) | |
| run: | | |
| set +e | |
| images=( | |
| "nocturne-api" | |
| "nocturne-demo" | |
| "nocturne-web" | |
| ) | |
| { | |
| echo "results<<EOF" | |
| for image in "${images[@]}"; do | |
| ref="${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/${image}:${{ steps.version.outputs.version }}" | |
| if docker buildx imagetools inspect "$ref" >/dev/null 2>&1; then | |
| echo "${image}|published|${ref}" | |
| else | |
| echo "${image}|missing|${ref}" | |
| fi | |
| done | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Summarize published image tags | |
| if: always() | |
| run: | | |
| { | |
| echo "### Container Image Tags" | |
| echo "" | |
| echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-api:${{ steps.version.outputs.version }}\`" | |
| echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-demo:${{ steps.version.outputs.version }}\`" | |
| echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}/nocturne-web:${{ steps.version.outputs.version }}\`" | |
| echo "" | |
| if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then | |
| echo "> Publishing is skipped for fork PRs." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upsert PR comment with published images | |
| if: always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) | |
| uses: actions/github-script@v7 | |
| env: | |
| VERSION_TAG: ${{ steps.version.outputs.version }} | |
| REGISTRY: ${{ env.REGISTRY }} | |
| IMAGE_REPOSITORY: ${{ env.IMAGE_REPOSITORY }} | |
| IMAGE_RESULTS: ${{ steps.image_status.outputs.results }} | |
| with: | |
| script: | | |
| const marker = "<!-- nocturne-pr-images -->"; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const prNumber = context.payload.pull_request.number; | |
| const tag = process.env.VERSION_TAG; | |
| const registry = process.env.REGISTRY; | |
| const imageRepo = process.env.IMAGE_REPOSITORY; | |
| const imageResultsRaw = process.env.IMAGE_RESULTS || ""; | |
| const parsedResults = imageResultsRaw | |
| .split("\n") | |
| .map((l) => l.trim()) | |
| .filter(Boolean) | |
| .map((line) => { | |
| const [image, status, ref] = line.split("|"); | |
| return { image, status, ref }; | |
| }); | |
| const images = parsedResults.length > 0 | |
| ? parsedResults | |
| : [ | |
| { | |
| image: "nocturne-api", | |
| status: "missing", | |
| ref: `${registry}/${imageRepo}/nocturne-api:${tag}`, | |
| }, | |
| { | |
| image: "nocturne-demo", | |
| status: "missing", | |
| ref: `${registry}/${imageRepo}/nocturne-demo:${tag}`, | |
| }, | |
| { | |
| image: "nocturne-web", | |
| status: "missing", | |
| ref: `${registry}/${imageRepo}/nocturne-web:${tag}`, | |
| }, | |
| ]; | |
| const lines = []; | |
| lines.push(marker); | |
| lines.push("## Preview Container Images"); | |
| lines.push(""); | |
| lines.push(`Published for commit \`${context.sha.substring(0, 7)}\` with tag \`${tag}\`.`); | |
| lines.push(""); | |
| lines.push("| Image | Status | Package | URI |"); | |
| lines.push("|---|---|---|---|"); | |
| for (const { image, status, ref } of images) { | |
| const packageName = encodeURIComponent(`${repo}/${image}`); | |
| const packageUrl = `https://github.com/${owner}/${repo}/pkgs/container/${packageName}`; | |
| const statusText = status === "published" | |
| ? "✅ Published" | |
| : "❌ Image not generated by build. Check CI logs"; | |
| lines.push(`| \`${image}\` | ${statusText} | [package](${packageUrl}) | \`${ref}\` |`); | |
| } | |
| lines.push(""); | |
| lines.push("_This comment is updated on each push to this PR._"); | |
| const body = lines.join("\n"); | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| per_page: 100, | |
| } | |
| ); | |
| const existing = comments.find( | |
| (c) => c.user?.type === "Bot" && c.body && c.body.includes(marker) | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| body, | |
| }); | |
| } | |
| publish-glucose-chart: | |
| needs: [build-and-push] | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/master' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| registry-url: 'https://npm.pkg.github.com' | |
| scope: '@nightscout' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| working-directory: src/Web | |
| - name: Publish @nightscout/glucose-chart | |
| run: pnpm publish --access public --no-git-checks | |
| working-directory: src/Web/packages/glucose-chart | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| publish-ui: | |
| needs: [build-and-push] | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/master' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| registry-url: 'https://npm.pkg.github.com' | |
| scope: '@nocturne' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| working-directory: src/Web | |
| - name: Publish @nocturne/ui | |
| run: pnpm publish --access public --no-git-checks | |
| working-directory: src/Web/packages/ui | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| publish-cms: | |
| needs: [build-and-push] | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/master' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| registry-url: 'https://npm.pkg.github.com' | |
| scope: '@nocturne' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| working-directory: src/Web | |
| - name: Publish @nocturne/cms | |
| run: pnpm publish --access public --no-git-checks | |
| working-directory: src/Web/packages/cms | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| create-release: | |
| needs: [build-and-push] | |
| runs-on: ubuntu-latest | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Install Aspire CLI | |
| run: dotnet tool install --global aspire.cli | |
| - name: Restore .NET dependencies | |
| run: dotnet restore | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| cache-dependency-path: src/Web/pnpm-lock.yaml | |
| - name: Install web dependencies | |
| working-directory: src/Web | |
| run: pnpm install --frozen-lockfile | |
| - name: Build bridge package | |
| working-directory: src/Web/packages/bridge | |
| run: pnpm run build | |
| - name: Build bot package | |
| working-directory: src/Web/packages/bot | |
| run: pnpm run build | |
| - name: Build Aspire AppHost | |
| run: dotnet build -c Debug src/Aspire/Nocturne.Aspire.Host/Nocturne.Aspire.Host.csproj | |
| - name: Generate production compose bundle | |
| run: dotnet run scripts/publish-release.cs ./release-output | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| gh release create "$VERSION" \ | |
| --generate-notes \ | |
| ./release-output/docker-compose.yaml \ | |
| ./release-output/.env.example | |