Repository navigation
v10.5.2c - Update CI and pages + docker hub delivery - #27
Merged
Merged
Conversation
Publishes the three things that were missing around the 10.5.2 tag. GitHub Release (release.yml). A tag existed but no release page, so nothing announced the release. The body is header.md + docs/WhatsNew-v<version>.md + GitHub's auto-generated PR notes. It refuses to publish when the tag and <Version> disagree: publish-nuget packs from the csproj, not the tag, so a mismatch ships a nupkg whose metadata lies about its own version. That is exactly the silent-skip failure 10.5.2 hit. GitHub Pages (pages.yml + site/index.html). Hand-written static landing: header, versions, quick links to the key docs. No docs toolchain -- a landing page does not justify adding Python to a .NET repo. The version is injected from the same csproj that drives `dotnet pack`, so the page cannot advertise an unpublished version, and the build fails loudly on a leftover placeholder. Docker Hub cards (dockerhub-metadata.yml + dockerhub/*.md). Both repos showed empty cards because `docker push` writes no metadata. The overviews now live in the repo and are PATCHed onto both repositories on every v* tag using the existing DOCKERHUB_TOKEN. Deliberately a separate workflow: a metadata failure must never turn a successful image publish into a red run. Description and categories go in separate calls so a taxonomy rejection costs the categories, not the overview. README (EN + RU). Adds Docker Hub, pulls, Release and Pages badges plus a second CI gate. Installation was a single `dotnet add package Delibera.Core` line while three packages and two images are published; it is now a three-way choice, with the localhost-binding warning placed where users will actually hit it. Also fixes the runtime-smoke step in publish-docker.yml, left over from the earlier session: both images declare ENTRYPOINT, so `docker run IMAGE dotnet --version` appended to it and booted the web host instead of printing a version, hanging the job for 20+ minutes. It also asked `dotnet --version`, which needs an SDK that a runtime image does not carry -- the fix would have failed a perfectly good image. Now uses --entrypoint with --list-runtimes, asserts the AspNetCore runtime is present, checks the published payload layout, and is bounded by `timeout` plus a job-level timeout-minutes that did not exist before. Verified locally against the real published 10.5.2 images: `--list-runtimes` prints Microsoft.AspNetCore.App 10.0.12 (exit 0), `node --version` prints v22.23.3 (exit 0), and /app/Delibera.Server.dll and /app/.output/server/index.mjs are both present. Site and release-body assembly were simulated locally: zero unsubstituted placeholders in either output.
Replaces the static landing page from the previous commit with what was actually asked for: the Web UI itself, statically exported to GitHub Pages via `nuxt generate`. Two real defects surfaced by running the export rather than reading the docs, both of which would have shipped a broken site behind a green build: - Output location. Nuxt 4 emits to `.output/public`, not `dist`. GitHub's own sample workflow still uploads ./dist because it predates Nuxt 4, and copying it verbatim uploads nothing. - No prerendered routes. With no explicit list, `nuxt generate` emitted only the 200.html and 404.html SPA fallbacks and nothing for the real pages — a deploy that is a blank site. The workflow now asserts index.html exists instead of trusting the exit code. useDebates resolves its API base from NUXT_PUBLIC_DELIBERA_API_BASE instead of a hardcoded `/api/delibera`. Empty by default, so the container path is byte-for-byte unchanged: same-origin BFF, and the browser still never touches the API origin. The variable exists because a static export has no Nitro server and therefore no BFF route. It is read from import.meta.env rather than useRuntimeConfig() because useDebates() is also called from useDebateStream's start() and poll timer, which run after setup has unwound and Vue's current instance is gone. Not changed, deliberately: Delibera.Server still registers no CORS policy. Widening CORS on an unauthenticated API is an owner decision, so the static build deploys with API calls 404ing until that is made deliberately and an origin is set. That state is documented in the workflow, in nuxt.config.ts and on the Docker Hub card. Also adds the missing `generate` script to package.json — the workflow calls it and the manifest did not define it. Verified locally: nuxt typecheck exit 0, vitest 34/34 across 4 files, nuxt generate exit 0 producing index.html (6691 bytes) and debates/new/index.html.
…nable Both found by validating the workflows rather than reading them. release.yml asked `gh release view --json url,tagName,isLatest`. `isLatest` is not one of the fields `gh release view` exposes — it belongs to `gh release list` (https://cli.github.com/manual/gh_release_view). The command fails with "Unknown JSON field", and it fails AFTER the release has been created, so a successful publish would have been reported as a red run. Now queries url,tagName,isDraft,isPrerelease. dockerhub-metadata.yml discarded the response from the categories PATCH into /dev/null, so a rejected slug produced a warning that said nothing about what to change. Docker Hub echoes the accepted values in the error body; it is now captured and printed. The description write is unaffected either way — that part was already a separate, fatal call. Categories are keyed by slug, not by the display name shown in the Docker Hub UI ("Machine learning & AI" and friends), so the slugs are the part most likely to need editing. They are inline and documented for that reason. Validation: all five workflow files parse under YamlDotNet with no tabs, and all 25 extracted `run:` bodies pass `bash -n` under Git Bash 5.2.
Captures the order that actually matters, written after this cycle hit every silent failure mode it describes. The two worth stating plainly, because neither raises an error: - Tagging does not set the package version. publish-nuget packs from the csproj, so a tag ahead of the csproj packs the previous version and --skip-duplicate discards it without a word. The job is green and nothing ships. release.yml now fails loudly on that mismatch, but only for the release page, and only after the packages are gone. - Docker Hub cards stay empty unless the two repositories exist and DOCKERHUB_USERNAME/DOCKERHUB_TOKEN are set. Missing secrets fail in zero seconds, which is how you tell them apart from a wrong token. Also records the pre-tag check that actually catches the first one -- pack the package and confirm the filename -- and the Pages source toggle, which the API cannot create.
`Generate PR notes` runs `gh api`, but `GH_TOKEN` was set only on the final step. A GitHub-hosted runner has no ambient credentials, so the unauthenticated call failed, `set -e` aborted the job, and the release was never created — a 100% failure rate on the one workflow whose whole purpose is publishing the release. GH_TOKEN now sits at job level, covering every step. Also makes the generate-notes call explicit JSON via `--input -` instead of `-f`. `gh api -f` sends a form-encoded body; this endpoint wants application/json, and the mismatch returns a 415 that reads like a permissions problem rather than a content-type one. And makes note generation non-fatal. The PR list is appended after the WhatsNew document, so losing it must not cost the release. Revalidated: all five workflows parse under YamlDotNet, all 25 extracted `run:` bodies pass `bash -n`.
The `tag` input was declared under `workflow_dispatch` but never mapped into the environment, so the resolve step fell through to $GITHUB_REF. For a dispatch that is the BRANCH, giving tag="main", which the `v*` guard rejects — the run died on "Refusing to release a non-version tag: main". That broke exactly the backfill path: creating the release page for the existing v10.5.2 tag without re-cutting it. The workflow looked correct and the failure mode was a message about a branch. Also pins the checkout to the tag being released, so the version cross-check reads the csproj at that tag rather than whatever is on the branch. Verified by running the resolution logic against four scenarios: dispatch with tag input -> OK, releasing v10.5.2 (Delibera.Core 10.5.2) tag push -> OK, releasing v10.5.2 dispatch with blank input -> refused: refs/heads/main dispatch naming v10.5.3 -> MISMATCH caught: tag says 10.5.3, csproj 10.5.2 Revalidated: all five workflows parse under YamlDotNet, all 25 `run:` bodies pass `bash -n`.
The GitHub Pages build of the Web UI is a static export: no Nitro server, so the same-origin /api/delibera/** BFF route does not exist there. The site deployed and rendered, but every request 404ed — a "Pages" that shows nothing is not Pages support. This closes that gap. Delibera:Server:Cors:AllowedOrigins registers a named policy, and Program.cs calls UseCors only when that list is non-empty. Off by default, and the default is the load-bearing part. The Web UI in the container talks to the API through its own BFF and never needs this. The absent Access-Control-Allow-Origin is also what stops an arbitrary page from reading a response: without CORS a cross-origin fetch still fires the request, it just cannot see the answer. Wiring this on by default would have added a browser-side CSRF vector to an API that is unauthenticated, unthrottled, and spends real credits per debate. Origins are matched exactly. There is deliberately no SetIsOriginAllowed(_ => true) path. AllowCredentials together with "*" is rejected at startup with a message naming the config key, instead of by the framework with one that names nothing. Two naming details worth keeping: the option type is CorsGateOptions rather than CorsOptions so it cannot shadow Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions in any file importing both namespaces, and a whitespace-only origin counts as unconfigured — otherwise an unset compose variable would register a policy matching the literal string " " while IsCorsEnabled reported true, and UseCors would run against a policy that can never match. 8 new tests, including that the default path registers no CORS service at all rather than an empty policy. Two of them failed first run because the assertion resolving IOptions<CorsOptions> assumed AddCors had been called — the failure was the point, since it shows nothing is registered, and the helper now reports that instead. Documented in both READMEs, appsettings.json, deploy/docker-compose.hub.yml (DELIBERA_CORS_ORIGIN) and a new CHANGELOG [Unreleased] section. Verified: build 0 errors / 0 warnings under -warnaserror; 628/628 tests (493 Core + 125 Server + 10 gRPC).
The HTTP-level tests found a startup crash in the CORS wiring added earlier. Program.cs decided whether to call UseCors by asking the configuration whether origins were configured, while the policy itself was registered inside AddDeliberaServer from that same configuration. Two reads of the same section disagreed, and the host died before serving anything: System.InvalidOperationException : Unable to resolve service for type 'Microsoft.AspNetCore.Cors.Infrastructure.ICorsService' while attempting to activate 'Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware' UseCors against a policy that was never registered is not a degraded mode, it is a dead process. The gate now asks whether AddCors actually ran, by testing for ICorsService — which is registered by AddCors and by nothing else, so the answer cannot disagree with what is actually in the container. The configuration-reading helper is gone rather than left as a second source of truth waiting to drift. Adds CorsPolicyBehaviourTests: five tests driving the real pipeline through WebApplicationFactory. The DI tests said what was registered; these say what a browser would see. Default host emits no Access-Control-Allow-Origin for a cross-origin call, a configured origin gets its own origin echoed back rather than a wildcard, an unlisted origin gets nothing, the preflight is answered with Allow-Methods, and X-Correlation-Id is exposed so the id that finds a log entry stays reachable from JavaScript. Worth noting for anyone extending these: this host is built by CreateSlimBuilder, which captures its configuration before WebApplicationFactory's ConfigureAppConfiguration delegates run. Injecting settings with ConfigureAppConfiguration leaves AddDeliberaServer blind to them and the gate silently stays off. UseSetting reaches builder.Configuration. Verified: build 0 errors / 0 warnings under -warnaserror; 633/633 tests (493 Core + 130 Server + 10 gRPC).
The backfill sections told you to dispatch release and dockerhub-metadata for an already-pushed tag. That advice cannot be followed while those workflows only exist on a feature branch: workflow_dispatch is resolved against the default branch, so the workflow has no Actions entry and the button is not there at all. This is not hypothetical. Right now the tags are on Docker Hub with real pull counts, every pipeline is green, and both overviews are empty, because nothing in the tag-push path writes metadata on its own. Adds the one command that settles it before promising a backfill. Verified: curl .../actions/workflows lists only publish-docker and publish-nuget, which is the shape of a branch that has not merged yet.
Two things found by checking the release notes of the actions this workflow depends on, not by reading the file. deploy-pages was pinned at v4 while v5 has been out since September, and v5 is the version GitHub's own Pages sample pairs with the v3 artifact uploader. v5.0.1 only adds backoff and jitter to the deployment poll and still reads upload-artifact@v4 containers, so v3 artifacts remain a supported input and there is no reason to move the upload major as well and re-test a tarball format that cannot be reproduced here. setup-node keyed its cache on package-lock.json while the step above it detected the package manager. Those agree today because the WebUI has exactly one lockfile, so this is latent rather than broken. It is the wrong kind of agreement though: add pnpm tomorrow and a pnpm cache keyed on a file that does not exist never invalidates, restoring a stale node_modules under a green build with no signal that anything is wrong. The detector now emits the lockfile it actually found. Verified: all five workflows parse; the five pages run-blocks pass bash -n; the detect step was executed against the real tree and emits manager=npm, lockfile=package-lock.json, command=ci. Release notes checked: configure-pages v6.0.0 (node 24, no functional change), upload-pages-artifact v5.0.0 (upload-artifact 7 plus include-hidden-files), deploy-pages v5.0.1.
A dispatch checks out the TAG, not main. The release header is authored on main, so it is absent from every tag cut before it landed, and the assembly step copied it unconditionally: cp .github/release-notes/header.md "$body" under set -euo pipefail. That is a hard stop at the last step before the release is written, and the error it prints is "cp: cannot stat" — which points at the filesystem, not at the fact that the backfill this dispatch exists for can never work as written. Confirmed by running it, not by reading it. A shallow clone at v10.5.2 reproduces the missing file exactly; git cat-file says header.md is absent from that tag while docs/WhatsNew-v10.5.2.md is present, and the version cross-check passes (Delibera.Core.csproj says 10.5.2 at the tag), so the job would sail through every guard and then fail on the copy. fetch_note now prefers the checked-out ref, falls back to origin/<default> via a depth-1 fetch, and warns instead of failing when a document is missing everywhere. A release page without the header still beats no release page. The fallback is only useful once the header reaches main, which is the same reason this workflow cannot be dispatched at all before PR #27 merges — worth knowing before you spend an hour on a red run. Verified by execution against a simulated post-merge remote: a clone at v10.5.2 with main carrying the header produces an 11063-byte body, opens with "# Delibera 10.5.2", zero unsubstituted {{VERSION}}, exit 0.
bash -n proves a run: block parses. It cannot prove the block finds the files it reads, because on a dispatch the checkout is the TAG: anything authored on main after that tag was cut is simply absent, and the block still parses. Clone shallow at the tag and execute the step against the clone. That is what turned "cp of a missing file" from a claim into a reproduction.
techbuzzz
force-pushed
the
feat/v10.5.2c
branch
from
October 10, 2026 06:35
b101061 to
1f82f74
Compare
NUXT_PUBLIC_DELIBERA_API_BASE never reached the bundle, so a GitHub Pages build configured with it still called its own origin and every request 404'd. Nothing would have said so: pages.yml was green from start to finish. The reason is that Nuxt substitutes an import.meta.env.NUXT_PUBLIC_* read only when the name corresponds to a key of runtimeConfig.public. deliberaApiBase was never declared, so the read was left alone and the variable silently discarded - no warning, no non-zero exit. Measured rather than assumed: a generate with the variable set produced zero occurrences of it anywhere in .output/public. Declaring the key fixes it and costs nothing in the container, where the empty default keeps the client on the same-origin BFF exactly as before. Verified in both directions - with the variable the origin appears in 4 places in the export, without it in none. pages.yml now checks the artefact instead of trusting the exit code: if an origin is configured and appears nowhere in the export, the deploy fails naming the config key. Preview mode (nothing configured) is a deliberate no-op. Tested all three ways: present passes, absent fails with the key named, unset prints the preview notice and exits 0. The reading of import.meta.env stays. useDebateStream calls this from a start callback and a poll timer, long after setup has unwound, where a useRuntimeConfig() lookup would depend on a Vue instance that is gone - a note worth keeping next to the key it depends on. Web UI tests 34/34 (npm run build first: the SSE suite loads .output/server/index.mjs, which `nuxt generate` does not produce).
techbuzzz
force-pushed
the
feat/v10.5.2c
branch
from
October 10, 2026 06:35
1f82f74 to
2186b2d
Compare
pages.yml runs `nuxt generate` and two assertions about the export. None of that happens on a pull request, so a change that breaks the static build cannot fail CI - it can only fail after the merge that turns it into a deploy. Writes the reproduction as plain shell rather than a script: the repo has no shell scripts and no place to put one, and these are commands a maintainer runs by hand anyway. The last line is the one that earns the section. `grep -rqF` on the export, not the exit code of the build - an undeclared NUXT_PUBLIC_* name is dropped silently, so the build reports success while the deployed site calls its own origin. That is exactly how the API-base bug survived a green pipeline. Also records why `npm run build` is needed before `npm test`: the SSE suite loads .output/server/index.mjs, which a static export does not produce, and reports a failed suite that reads like a broken test. Verified by running the section top to bottom: 6 routes prerendered, index.html and debates/new/index.html present, unset build 0 hits, configured build bakes the origin in.
Two corrections, both found by checking the claim instead of repeating it. The order. pages.yml runs on every push to main, so merging it into a repository with no Pages site puts a red run on main immediately: "Get Pages site failed ... Error: Not Found". The site is not created by the workflow and `enablement: true` cannot create it either — that is a repository administration operation, and the automatic GITHUB_TOKEN can never administer its own repository no matter what `permissions:` says. So the Source dropdown is clicked BEFORE the merge, not after. The 404. The runbook said "the API returns 404 until this is set", and earlier notes here repeated it. It does not distinguish the two cases: an unauthenticated GET answers 404 whether or not Pages is configured, while the same repository at the same moment reports "has_pages": true in its metadata. The earlier reading of that 404 as "Pages is off" was wrong, and it is recorded here so nobody repeats it. What actually settles it: the Source dropdown, and has_pages. What does not: an anonymous /pages call.
Last correction on this, because the previous one was still wrong. has_pages: true is not evidence that a Pages site exists. The account has never published one: https://techbuzzz.github.io/Delibera/ and the account root both answer 404 from GitHub.com's own servers, while the repository metadata reads has_pages: true at the same moment. Two probes that look authoritative and are not: GET /repos/OWNER/REPO/pages unauthenticated -> 404, ambiguous has_pages: true -> capability, not a site Replaces both with the check GitHub itself serves: does the URL answer 200? A 404 there, straight from GitHub.com, is conclusive. Only the Source dropdown says whether the site will accept deploys.
The goal asked whether publishing can ride the PR. Pages already does:
pages.yml runs on every push to main. The two workflows that still
required a manual dispatch are now on the same footing.
release.yml also runs on push to main and refreshes the page for the
newest v* tag reachable from HEAD (checkout is now depth 0, because the
default shallow clone has no tags and git describe finds nothing).
dockerhub-metadata.yml runs on push to main, filtered to the paths that
can change a card.
The part that needed care is what a mismatch means. Two triggers, two
meanings, deliberately not unified:
v* tag pushed / dispatch naming a tag -> mismatch FAILS. That tag
exists to publish a specific version; the csproj disagreeing with it
is the exact bug that once shipped a nupkg whose metadata lied about
itself.
merge to main -> mismatch exits 0 with a notice. A version bump with
no tag yet is an ordinary intermediate state, and failing there would
put a red run on main for something nobody did wrong.
Verified by running the extracted step against a real clone, all six
modes: merge resolves v10.5.2 and releases it (exit 0); tag push and
dispatch both go explicit; a dispatch naming an unmerged tag fails loudly;
a main with the version bumped to 10.5.3 exits 0, writes no outputs and
says the release will happen at v10.5.3; the same tree on a tag push
still fails. The safety net from the original bug survives.
NuGet is untouched and stays that way: publish-nuget triggers on tag
pushes only, so merging a PR cannot republish anything.
Runbook updated to say dispatch is now a repair tool, not a step.
configure-pages with static_site_generator: nuxt cannot configure this project. It rewrites the generator config file rather than exporting anything, supports only .js/.cjs/.mjs, and our nuxt.config.ts is TypeScript - so it warns and injects nothing. The keys it would inject (router.base, target) are Nuxt 3 names that Nuxt 4 has moved or removed. So the input is dropped and the base path comes from the step's own base_path output, passed to the build as NUXT_APP_BASE_URL. Nuxt reads that into app.baseURL: documents stay at the artifact root, asset URLs gain the prefix, which is what Pages expects. A third gate joins the two existing ones. A Pages project site is served from /<repo>, so an export whose asset URLs are root-relative deploys successfully and renders unstyled over a client bundle that 404s - no failed step, no visible error, just a broken site. The gate reads the base back out of the emitted HTML. Recorded here because I got it wrong first: I "found" a serious bug where setting a base produced redirect stubs instead of documents, and started rewriting nuxt.config.ts around it. The bug was in my test harness. Git Bash rewrites an environment value that looks like a POSIX path when it exports it to a child process: NUXT_APP_BASE_URL=/Delibera reached the build as "C:/Program Files/Git/Delibera". A base that malformed produces exactly the stub layout I mistook for a Nuxt defect. Re-measured from PowerShell, where the value passes through untouched, on a pristine nuxt.config.ts: documents at the artifact root, payload baseURL "/Delibera", assets under /Delibera/_nuxt/. nuxt.config.ts is therefore unchanged by this commit. Verified with the exact environment CI sets - both variables together: export gate PASS, base gate PASS (/Delibera/_nuxt/StatusBadge.*.css), API-base gate PASS, zero extension-less stub files.
Git Bash exports an environment value that looks like a POSIX path as a Windows path: NUXT_APP_BASE_URL=/Delibera reaches the build as "C:/Program Files/Git/Delibera". A malformed base does not error. It produces .output/public/index and debates/new containing "Redirecting..." instead of documents - no index.html anywhere, green build, and every route 404s on deploy. That is indistinguishable from a Nuxt bug by inspection, and I acted on it for a cycle before checking the variable itself. PowerShell passes the value through untouched; that is where the real behaviour was measured.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Description
Related Issues
Type of Change
Checklist
dotnet build --configuration Release)README.md,QuickStart.md)Additional Notes