fix(api): require treatments.readwrite on the v4 activity write actions #2317
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: "11.5.0" | |
| 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 QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - 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 | |
| # The scoped-DND rule classifier (and the optional rust/shadow alert engines) | |
| # load the nocturne_alerts cdylib at runtime. The API csproj packs it only when | |
| # crates/target/release contains it at publish time, so build it here or the | |
| # image ships without it and every rule classifies as undirected. | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-unknown-linux-gnu | |
| - name: Install aarch64 cross-linker | |
| run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu | |
| - name: Cache cargo | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| crates/target | |
| # Sources, not just the lockfile: actions/cache restores crates/target with mtimes | |
| # newer than the checkout, and cargo's path-package fingerprint is mtime-based, so a | |
| # lockfile-only key lets a source-only change reuse a stale cdylib and never re-save it. | |
| key: cargo-${{ runner.os }}-${{ hashFiles('crates/Cargo.lock', 'crates/**/*.rs', 'crates/**/Cargo.toml') }} | |
| - name: Build nocturne_alerts native library | |
| working-directory: crates | |
| env: | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc | |
| run: | | |
| cargo build --release -p nocturne-alerts-ffi | |
| cargo build --release -p nocturne-alerts-ffi --target aarch64-unknown-linux-gnu | |
| test -f target/release/libnocturne_alerts.so | |
| test -f target/aarch64-unknown-linux-gnu/release/libnocturne_alerts.so | |
| - 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:ContainerRuntimeIdentifiers="linux-x64;linux-arm64"' \ | |
| -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:ContainerRuntimeIdentifiers="linux-x64;linux-arm64"' \ | |
| -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,linux/arm64 \ | |
| ${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" | |
| manifest=$(docker buildx imagetools inspect "$ref") | |
| for platform in "linux/amd64" "linux/arm64"; do | |
| if ! grep -q "$platform" <<< "$manifest"; then | |
| echo "::error::$ref is missing the $platform platform" | |
| exit 1 | |
| fi | |
| done | |
| 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/main' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - 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 | |
| working-directory: src/Web/packages/glucose-chart | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| version=$(node -p "require('./package.json').version") | |
| if npm view "@nightscout/glucose-chart@${version}" version > /dev/null 2>&1; then | |
| echo "@nightscout/glucose-chart@${version} already published; skipping." | |
| else | |
| pnpm publish --access public --no-git-checks | |
| fi | |
| 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 | |
| # deploy/docker-compose/ is a tracked, committed bundle — regenerated by | |
| # scripts/publish-release.cs and committed ahead of tagging a release | |
| # (see that script's header comment), not produced fresh in CI. Attach | |
| # whatever is checked out at the tag. | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| gh release create "$VERSION" \ | |
| --generate-notes \ | |
| deploy/docker-compose/*.yaml \ | |
| deploy/docker-compose/.env.example | |