Skip to content

fix(oauth): real app identifiers in known-client directory; fix Tray … #1188

fix(oauth): real app identifiers in known-client directory; fix Tray …

fix(oauth): real app identifiers in known-client directory; fix Tray … #1188

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