UID2-7069: harden shared_create_releases composite#233
Merged
Conversation
- Validate publish_platform input and fail fast on typos. - Collapse five Build X Changelog steps into one mikepenz step driven by a bash+jq template selector; replace the ternary fallback in Create Release.body with a single output reference. - NuGet workflow: parse package id from <id> in .nuspec instead of hardcoding UID2.Client; gate extraction on is_release. - Maven workflow: gate Extract Maven artifactId on steps.checkRelease.outputs.is_release; standardise on that source of truth at the composite call site and drop the unused env.IS_RELEASE. - PyPI workflow: gate Extract PyPI package name on is_release; replace grep|cut with python3 tomllib so single-quoted values and non-[project] sections appearing earlier in pyproject.toml are handled correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Restore the trailing space after "## Image reference to deploy:" in the Docker template (regression introduced by the heredoc rewrite — caught by a byte-for-byte parity smoke test against the original JSON-embedded templates; all five platforms now identical). - Comment the Maven job.name inline expression explaining why it can't reference steps.checkRelease.outputs.is_release (job-level evaluates pre-steps) — keep this in sync with the step gates if it ever changes. - Call out the remaining .nuspec filename hardcode in NuGet workflow as a known half-fix; switch <id> extraction from grep|sed to xmllint XPath with local-name() so namespace-declared nuspecs work too. - Note the Python 3.11+ tomllib dependency in the PyPI workflow. - Rename composite steps for clarity: "Build changelog config" → "Prepare changelog template", "Build Changelog" → "Build changelog". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the Python 3.11+ note in the PyPI workflow — call out that xmllint comes from libxml2-utils, preinstalled on ubuntu-latest, and how a minimal-runner fork would re-add it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Throwaway workflow on the PR branch. Three jobs:
1. validation guard rejects publish_platform=docker (lowercase) —
expected to fail; continue-on-error so the workflow itself passes.
2. validation guard accepts each canonical platform — matrix of 5.
3. template-prep bash + jq pipeline runs on real Linux for all 5
platforms; awk-extracts the case block live from action.yaml so
this stays in sync with the composite.
Deliberately does NOT run the composite with is_release: true —
delete_draft_releases would nuke every existing draft release in the
repo. The standalone template-prep job covers Linux-side rendering
without touching releases.
To be deleted along with this commit-set before merge — pattern
matches the UID2-7041 smoke commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A code comment in the Prepare-changelog-template run block literally
contained "${{ }}" (empty) as descriptive text. GitHub Actions' template
engine scans `run:` blocks for ${{...}} expressions and tried to
evaluate the empty one, failing with:
action.yaml (Line: 59, Col: 12): An expression was expected
Failed to load action.yaml
Caught by the smoke harness on the first run (which is exactly what
the harness exists for — local YAML linters parse the file fine
because YAML syntax is valid; only the GHA template pass rejects it).
Rewrite the comment to describe the GHA expression form without
embedding the syntax verbatim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub Actions' contains() is case-insensitive — so the previous expression `!contains(fromJSON(["Docker","Maven","PyPI","NuGet","iOS"]), inputs.publish_platform)` returned false for `publish_platform: docker`, silently skipping the validate step. The exact typo the ticket asked this guard to catch. Caught on the smoke run (rejects-invalid-platform job completed silently with no error output — composite no-op'd through, fell back to is_release==false skipping everything else). Switch to a bash `case` statement on a $PLATFORM env var. Bash case is case-sensitive by default. The step now runs unconditionally; the case either matches and exits 0 silently, or hits the wildcard and fails with the documented error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add three jobs to the smoke harness that exercise the new Extract bash
logic from each publish workflow against real consumer-repo fixtures,
without actually publishing:
- extract_maven_artifactid: checks out IABTechLab/uid2-attestation-aws
and runs `mvn help:evaluate -Dexpression=project.artifactId`. Asserts
the result is `attestation-aws`.
- extract_pypi_name: checks out IABTechLab/uid2-client-python and runs
the new `python3 -c "import tomllib; ..."`. Asserts the result is
`uid2_client` (with underscore — NOT `uid2-client`, which is exactly
the kind of drift the in-line comment was missing).
- extract_nuget_id: checks out IABTechLab/uid2-client-net and runs the
new `xmllint --xpath` with local-name(). Asserts the result is
`UID2.Client`.
These complement the existing composite smoke (validation + template
render) by covering the publish-workflow code paths that the composite
smoke can't reach.
Deleted along with the rest of the smoke harness before merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier comment claimed xmllint was preinstalled on ubuntu-latest runners. The Path C smoke against uid2-client-net just disproved that: ubuntu-latest now ships without libxml2-utils, so the Extract NuGet package id step fails with `xmllint: command not found`. Add an explicit `sudo apt-get install -y libxml2-utils` step in the NuGet workflow (gated on is_release, same as the Extract step) and mirror it in the smoke harness so both code paths match. Total install overhead: ~3-5s per release run, negligible vs. the full publish flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an opt-in smoke job that exercises the composite with is_release:
true — the only path NOT covered by the existing smoke jobs (which
all use is_release: false to avoid the destructive Delete Draft
Releases side effect).
Design:
1. workflow_dispatch only (manual trigger), gated on a boolean input
`run_destructive_e2e` so it never fires accidentally on push.
2. Pre-step: snapshot every existing draft release (id, name, tag,
body, prerelease) to a JSON file.
3. Pre-step: tag HEAD as v0.0.0-smoke so mikepenz has a real toTag
to resolve. fromTag is the newest existing v-prefixed tag in the
repo.
4. Run composite with publish_platform: Maven (single platform —
the other 4 share identical composite plumbing).
5. Post-step: assert the smoke draft body contains the Maven-
specific marker `<artifactId>smoke-pkg</artifactId>`.
6. if: always() cleanup: delete the smoke draft, recreate every
snapshotted draft from the JSON file, delete the smoke tag.
Closes the remaining residual gap: GHA $GITHUB_OUTPUT plumbing in a
composite step, mikepenz consuming the new configurationJson string,
softprops interpolating body via steps.changelog.outputs.changelog.
Manual trigger:
gh workflow run smoke-UID2-7069.yaml \
--ref jn-UID2-7069-harden-shared-create-releases \
-f run_destructive_e2e=true
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous run picked the newest tag (v3.85) as fromTag and HEAD as
toTag. HEAD is the PR branch with direct branch commits (no merged
PRs since v3.85), so mikepenz fell back to its empty_template ("- no
changes") and discarded the platform template entirely — body
verification failed.
Pick the 5th-newest SemVer tag instead. There are guaranteed merged
PRs in range, so mikepenz takes the template path and substitutes
#{{CHANGELOG}} / #{{UNCATEGORIZED}} as intended. Also log the merge
count up-front so future failures are easier to diagnose.
Note: mikepenz collapsing the entire template to "- no changes" on
empty diffs is pre-existing behavior — not a regression in this PR.
A real release ALWAYS has merged PRs between consecutive tags, so
this edge case doesnt surface in production.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UID2-7069: drop smoke harness; runs captured
Smoke harness served its purpose — caught three real PR bugs (empty
${{ }} in run-block comment, case-insensitive contains() in validation
guard, xmllint not preinstalled on ubuntu-latest) plus verified the
full is_release=true path end-to-end via snapshot+restore against the
real composite.
Runs captured:
- 26142464874: composite no-op path (5 platforms validation + 5
template renders on Linux) — green
- 26143046210: Path C extract steps against real consumer fixtures
(uid2-attestation-aws, uid2-client-python, uid2-client-net) — green
- 26143611716: destructive end-to-end (composite with is_release=true,
publish_platform=Maven, snapshot+restore of draft releases, body
verification against expected marker) — green
Matches the UID2-7041 cleanup convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
Three documentation-only comments added per the latest review pass: - Placeholder substitution order is silently order-dependent. Note in action.yaml that __TAGS__/__IMAGE_TAG__/__REPO__/__VERSION__ are reserved tokens and that current inputs come from trusted CI sources. - Heredoc indentation relies on YAML literal-block-scalar dedent so the EOF markers end up flush-left. Spell that out so a future contributor doesnt re-indent the heredoc body and break it. - PyPI extraction assumes PEP 621 layout (project.name); Poetrys [tool.poetry] would raise KeyError. No current PyPI consumer uses Poetry, so leaving the assumption in place with a note. Also clarified that the validate-publish_platform step intentionally runs on Snapshot builds too — caught typos should be loud, not gated on is_release. Also fixed an outdated example in the PyPI comment (uid2-client-python publishes as `uid2_client` with underscore, not `uid2-client` — caught by the Path C smoke earlier in the PR). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BehnamMozafari
approved these changes
May 21, 2026
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.
Summary
Code-review follow-ups from PR #229 (UID2-6762). Closes UID2-7069.
publish_platformat the top ofshared_create_releases— a typo no longer silently produces a draft release with an empty body. Implemented as a bashcase(case-sensitive); the originalcontains(fromJSON([...]))expression was case-insensitive and letdockerlowercase slip through.Prepare changelog templateshell step picks the platform template (bash + jq + placeholder substitution); a singlemikepenz/release-changelog-builder-actionconsumes the output via${{ steps.changelog_config.outputs.json }};Create Release.bodyreferences the one resulting output. Future template/SHA tweaks happen in one place.UID2.Clientas the package id — parsed from<id>in the.nuspecviaxmllint(explicitly installed viaapt-get install -y libxml2-utils— it is NOT preinstalled onubuntu-latest), gated onis_release. The.nuspecfilename is still hardcoded; that remains as a documented follow-up.Extract Maven artifactIdgated onsteps.checkRelease.outputs.is_release(no longer runs on Snapshot builds); same step output used at the composite call site; unusedenv.IS_RELEASEremoved. The inlinedis_releaseboolean atjobs.release.nameis documented in-file since job-level expressions cannot reference step outputs.Extract PyPI package namegated onis_releaseand rewritten withpython3 tomllib— handles single- and double-quoted values, ignores unrelatedname =keys in earlier tables, fails fast if[project].nameis missing.Verification — all run on real GHA Linux (no aspirational checkboxes)
A TEMP smoke harness landed on this branch and was deleted in
32e4920after capturing three runs. Coverage:Composite no-op path (
is_release: false) — run 26142464874Validate publish_platformrejectsdocker(lowercase typo) withUnsupported publish_platform 'docker'+ exit 1Validate publish_platformaccepts each canonical platform: Docker, Maven, PyPI, NuGet, iOSPrepare changelog templatebash + jq produces validconfigurationJsonon real Linux for all 5 platforms (case block extracted live fromaction.yamlto guarantee parity with the composite)Extract steps against real consumer fixtures — run 26143046210
mvn help:evaluate -Dexpression=project.artifactIdonIABTechLab/uid2-attestation-aws/attestation-aws/pom.xml→attestation-awspython3 tomllibonIABTechLab/uid2-client-python/pyproject.toml→uid2_client(with underscore — exactly the kind of drift the new parser handles correctly)xmllint --xpath '...local-name()...'onIABTechLab/uid2-client-net/UID2.Client.nuspec→UID2.ClientDestructive composite end-to-end (
is_release: true) — run 26143611716publish_platform: Maven,is_release: true,fromTag: v2.3.2(123 PRs in range so mikepenz takes the template path),toTag: v0.0.0-smoke(temporary tag created at HEAD)Build changelog(mikepenz) successfully consumed${{ steps.changelog_config.outputs.json }}and substituted#{{CHANGELOG}}/#{{UNCATEGORIZED}}into the Maven templateDelete Draft Releases+Create Releaseran in sequence; resulting draft body contained<artifactId>smoke-pkg</artifactId>— proving the full plumbing chain (template prep → mikepenz → softprops body interpolation)v0.0.0-smokeBugs caught by the smoke and fixed before merge
fc5ff51${{ }}in a code comment inside arun:block. GHA's template engine scansrun:blocks for${{...}}and tried to evaluate the empty expression — composite action YAML failed to load on every invocation. Localyaml.safe_loadparsed it fine because YAML itself is valid; only the GHA template pass rejected it.776f73eValidate publish_platformwas using!contains(fromJSON('["Docker",...]'), inputs.publish_platform). GHA'scontains()is case-insensitive — so passingdockerlowercase returnedtrue, the!flipped tofalse, the validation step was silently skipped, and the bad input flowed through to mikepenz. The exact typo the guard was meant to catch. Rewrote as a bashcasestatement (case-sensitive by default).4277ce4xmllintwas preinstalled onubuntu-latest. It isn't (as of 2026 runner images). Added an explicitsudo apt-get install -y libxml2-utilsstep gated onis_release. Total overhead ~3-5s per release.Without the smoke, every one of these would have shipped to production and broken the next real release.
Known limitations (documented in-file, not blocking)
.nuspecfilename in the NuGet workflow is still hardcoded asUID2.Client.nuspec. Fix is package-id only; filename remains single-tenant. Follow-up tracked.empty_template(default- no changes) when there are zero merged PRs in thefromTag..toTagrange. Pre-existing behavior — not introduced by this PR — but worth a follow-up to set a sensibleempty_templatethat preserves the platform install snippet.🤖 Generated with Claude Code