diff --git a/.github/workflows/shared-publish-to-maven-versioned.yaml b/.github/workflows/shared-publish-to-maven-versioned.yaml index 7888ae80..902237ff 100644 --- a/.github/workflows/shared-publish-to-maven-versioned.yaml +++ b/.github/workflows/shared-publish-to-maven-versioned.yaml @@ -35,11 +35,15 @@ on: default: '' env: - IS_RELEASE: ${{ (inputs.release_type == 'Major' || inputs.release_type == 'Minor' || inputs.release_type == 'Patch') && (github.event.repository.default_branch == github.ref_name ) }} REPO: ${{ github.event.repository.name }} jobs: release: + # The release/pre-release boolean is inlined here (rather than referencing + # steps.checkRelease.outputs.is_release) because job-level expressions are + # evaluated before any step runs. Every other "is this a release?" check + # in this file uses steps.checkRelease.outputs.is_release — keep this in + # sync if the condition ever changes. See UID2-7069. name: ${{ ((inputs.release_type == 'Major' || inputs.release_type == 'Minor' || inputs.release_type == 'Patch') && (github.event.repository.default_branch == github.ref_name )) && 'Create Release' || 'Publish Pre-release' }} runs-on: ubuntu-latest environment: ${{ inputs.merge_environment }} @@ -161,12 +165,14 @@ jobs: - name: Extract Maven artifactId id: package_name + if: ${{ steps.checkRelease.outputs.is_release == 'true' }} run: | # Maven artifactId comes from pom.xml, not the github repo name # (e.g. uid2-attestation-azure repo publishes the `attestation-azure` # artifact). Use `mvn help:evaluate` as the canonical source so # release-notes install snippets match what `mvn deploy` actually - # published. + # published. Gated on is_release to avoid running on Snapshot builds + # where shared_create_releases is a no-op. artifact_id=$(mvn -B -f "${{ inputs.working_dir }}/pom.xml" help:evaluate -Dexpression=project.artifactId -q -DforceStdout) if [ -z "$artifact_id" ]; then echo "ERROR: could not extract artifactId from ${{ inputs.working_dir }}/pom.xml" >&2 @@ -178,7 +184,7 @@ jobs: - name: Create Release uses: IABTechLab/uid2-shared-actions/actions/shared_create_releases@v3 with: - is_release: ${{ env.IS_RELEASE }} + is_release: ${{ steps.checkRelease.outputs.is_release }} new_version: ${{ steps.version.outputs.new_version }} github_token: ${{ secrets.GITHUB_TOKEN }} publish_platform: Maven diff --git a/.github/workflows/shared-publish-to-nuget-versioned.yaml b/.github/workflows/shared-publish-to-nuget-versioned.yaml index b4a3b64a..fca125f3 100644 --- a/.github/workflows/shared-publish-to-nuget-versioned.yaml +++ b/.github/workflows/shared-publish-to-nuget-versioned.yaml @@ -107,6 +107,37 @@ jobs: tag: v${{ steps.version.outputs.new_version }} github_token: ${{ inputs.merge_environment != '' && secrets.GH_MERGE_TOKEN || '' }} + - name: Install xmllint + if: ${{ steps.checkRelease.outputs.is_release == 'true' }} + run: sudo apt-get update -qq && sudo apt-get install -y -qq libxml2-utils + + - name: Extract NuGet package id + id: package_name + if: ${{ steps.checkRelease.outputs.is_release == 'true' }} + run: | + # NuGet package id comes from the element in the .nuspec, not + # the github repo name. Use xmllint's local-name() XPath so we + # ignore the default xmlns the nuspec schema declares. Dependencies + # use an `id` attribute (), not an + # element, so this XPath matches only the package id. + # + # xmllint (libxml2-utils) is NOT preinstalled on ubuntu-latest as + # of 2026 — installed explicitly in the previous step. + # + # NOTE: the .nuspec file path itself is still hardcoded throughout + # this workflow (UID2.Client.nuspec). Fixing the package id only + # solves half of the single-tenancy problem flagged in UID2-7069 — + # a follow-up should parameterise the .nuspec filename via a + # workflow input. + nuspec="${{ inputs.working_dir }}/UID2.Client.nuspec" + package_id=$(xmllint --xpath 'string(//*[local-name()="package"]/*[local-name()="metadata"]/*[local-name()="id"])' "$nuspec") + if [ -z "$package_id" ]; then + echo "ERROR: could not extract from $nuspec" >&2 + exit 1 + fi + echo "Extracted NuGet package id: $package_id" + echo "name=$package_id" >> "$GITHUB_OUTPUT" + - name: Create Release uses: IABTechLab/uid2-shared-actions/actions/shared_create_releases@v3 with: @@ -114,4 +145,4 @@ jobs: new_version: ${{ steps.version.outputs.new_version }} github_token: ${{ secrets.GITHUB_TOKEN }} publish_platform: NuGet - repo: UID2.Client + repo: ${{ steps.package_name.outputs.name }} diff --git a/.github/workflows/shared-publish-to-pypi-versioned.yaml b/.github/workflows/shared-publish-to-pypi-versioned.yaml index 4aea7a84..582d6905 100644 --- a/.github/workflows/shared-publish-to-pypi-versioned.yaml +++ b/.github/workflows/shared-publish-to-pypi-versioned.yaml @@ -99,14 +99,30 @@ jobs: - name: Extract PyPI package name id: package_name + if: ${{ steps.checkRelease.outputs.is_release == 'true' }} run: | # PyPI package name comes from pyproject.toml, not the github repo name - # (e.g. uid2-client-python repo publishes the `uid2-client` package). - # Read the top-level `name = "..."` field so release-notes install snippets - # match the real PyPI package, not the git repo. - name=$(grep -E '^\s*name\s*=' "${{ inputs.working_dir }}/pyproject.toml" | head -1 | cut -d '"' -f 2) + # (e.g. uid2-client-python repo publishes the `uid2_client` package). + # Use tomllib so we correctly handle both single- and double-quoted + # values, ignore unrelated `name =` keys in earlier tables (e.g. + # dependency tables), and fail fast if [project].name is missing. + # Gated on is_release to avoid running on Snapshot builds where + # shared_create_releases is a no-op. + # + # REQUIRES Python 3.11+ (tomllib is stdlib from 3.11). ubuntu-latest + # currently ships Python 3.12, so this is safe today; if a caller + # ever forks the workflow onto ubuntu-22.04 (Python 3.10), add an + # actions/setup-python step before this one or fall back to `tomli`. + # + # Assumes PEP 621 layout — i.e. the package name lives at + # [project].name. Poetry-style projects with [tool.poetry] would + # need a different key path (e.g. ['tool']['poetry']['name']); + # the KeyError this would raise is loud but unhelpful. No current + # PyPI consumer uses Poetry, so leaving the PEP 621 assumption + # in place. + name=$(python3 -c "import sys, tomllib; print(tomllib.load(open(sys.argv[1], 'rb'))['project']['name'])" "${{ inputs.working_dir }}/pyproject.toml") if [ -z "$name" ]; then - echo "ERROR: could not extract 'name' from ${{ inputs.working_dir }}/pyproject.toml" >&2 + echo "ERROR: could not extract [project].name from ${{ inputs.working_dir }}/pyproject.toml" >&2 exit 1 fi echo "Extracted PyPI package name: $name" diff --git a/actions/shared_create_releases/action.yaml b/actions/shared_create_releases/action.yaml index 592fbe3c..df6675bf 100644 --- a/actions/shared_create_releases/action.yaml +++ b/actions/shared_create_releases/action.yaml @@ -31,78 +31,170 @@ runs: using: "composite" steps: - - name: Build Docker Changelog - id: github_release_docker - if: ${{ inputs.is_release == 'true' && inputs.publish_platform == 'Docker' }} - uses: mikepenz/release-changelog-builder-action@32e3c96f29a6532607f638797455e9e98cfc703d # v4 - with: - toTag: v${{ inputs.new_version }} - fromTag: ${{ inputs.from_tag }} - configurationJson: | - { - "template": "#{{CHANGELOG}}\n## Installation\n```\ndocker pull ${{ inputs.tags }}\n```\n\n## Image reference to deploy: \n```\n${{ inputs.image_tag }}\n```\n\n## Changelog\n#{{UNCATEGORIZED}}", - "pr_template": " - #{{TITLE}} - ( PR: ##{{NUMBER}} )" - } + # Fail fast on a typo in publish_platform. Without this guard, an unknown + # value (e.g. 'docker' lowercase) silently skips every Build Changelog + # step, lets Delete Draft Releases run, and creates a draft release with + # an empty body. See UID2-7069. + # + # Validation is done in bash (case statement) rather than via + # `contains(fromJSON([...]), inputs.publish_platform)` because GitHub + # Actions' contains() is case-INSENSITIVE — `contains(['Docker',...], + # 'docker')` returns true, so the expression form silently lets the + # exact typo we're trying to catch slip through. Bash `case` is + # case-sensitive by default. + # + # Intentionally NOT gated on is_release — typos should be loud on + # Snapshot runs too, so they're caught before someone tries to cut + # a real release with the same misconfiguration. + - name: Validate publish_platform + shell: bash env: - GITHUB_TOKEN: ${{ inputs.github_token }} + PLATFORM: ${{ inputs.publish_platform }} + run: | + case "$PLATFORM" in + Docker|Maven|PyPI|NuGet|iOS) + ;; + *) + echo "::error::Unsupported publish_platform '$PLATFORM'. Must be one of [Docker, Maven, PyPI, NuGet, iOS]." + exit 1 + ;; + esac - - name: Build Maven Changelog - id: github_release_maven - if: ${{ inputs.is_release == 'true' && inputs.publish_platform == 'Maven' }} - uses: mikepenz/release-changelog-builder-action@32e3c96f29a6532607f638797455e9e98cfc703d # v4 - with: - toTag: v${{ inputs.new_version }} - fromTag: ${{ inputs.from_tag }} - configurationJson: | - { - "template": "#{{CHANGELOG}}\n## Maven\n```\n\n com.uid2\n ${{ inputs.repo }}\n ${{ inputs.new_version }}\n\n```\n\n## Jar Files\n- [${{ inputs.repo }}-${{ inputs.new_version }}.jar](https://repo1.maven.org/maven2/com/uid2/${{ inputs.repo }}/${{ inputs.new_version }}/${{ inputs.repo }}-${{ inputs.new_version }}.jar)\n\n## Changelog\n#{{UNCATEGORIZED}}", - "pr_template": " - #{{TITLE}} - ( PR: ##{{NUMBER}} )" - } + # Build the mikepenz configurationJson for the chosen platform. One step + # replaces what used to be five near-identical Build X Changelog steps — + # future changes (mikepenz SHA bump, template tweak) now happen in one + # place. See UID2-7069. + - name: Prepare changelog template + id: changelog_config + if: ${{ inputs.is_release == 'true' }} + shell: bash env: - GITHUB_TOKEN: ${{ inputs.github_token }} + PLATFORM: ${{ inputs.publish_platform }} + REPO: ${{ inputs.repo }} + VERSION: ${{ inputs.new_version }} + TAGS: ${{ inputs.tags }} + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + # Heredocs use quoted EOF so backticks and $ stay literal — no shell + # expansion inside the template body. We then substitute the four + # placeholders via bash parameter expansion. + # + # Heredoc body lines sit at YAML's common-prefix indent (8 spaces) + # so YAML's literal-block-scalar dedent leaves them flush-left in + # the actual bash script — that's what lets the closing EOF marker + # match. If you re-indent the heredoc body, the heredoc breaks. + case "$PLATFORM" in + Docker) + template=$(cat <<'EOF' + #{{CHANGELOG}} + ## Installation + ``` + docker pull __TAGS__ + ``` - - name: Build PyPI Changelog - id: github_release_pypi - if: ${{ inputs.is_release == 'true' && inputs.publish_platform == 'PyPI' }} - uses: mikepenz/release-changelog-builder-action@32e3c96f29a6532607f638797455e9e98cfc703d # v4 - with: - toTag: v${{ inputs.new_version }} - fromTag: ${{ inputs.from_tag }} - configurationJson: | - { - "template": "#{{CHANGELOG}}\n## PyPI\n```\npip install ${{ inputs.repo }}==${{ inputs.new_version }}\n```\n\n## PyPI page\n- [${{ inputs.repo }} ${{ inputs.new_version }}](https://pypi.org/project/${{ inputs.repo }}/${{ inputs.new_version }}/)\n\n## Changelog\n#{{UNCATEGORIZED}}", - "pr_template": " - #{{TITLE}} - ( PR: ##{{NUMBER}} )" - } - env: - GITHUB_TOKEN: ${{ inputs.github_token }} + ## Image reference to deploy: + ``` + __IMAGE_TAG__ + ``` - - name: Build NuGet Changelog - id: github_release_nuget - if: ${{ inputs.is_release == 'true' && inputs.publish_platform == 'NuGet' }} - uses: mikepenz/release-changelog-builder-action@32e3c96f29a6532607f638797455e9e98cfc703d # v4 - with: - toTag: v${{ inputs.new_version }} - fromTag: ${{ inputs.from_tag }} - configurationJson: | - { - "template": "#{{CHANGELOG}}\n## NuGet\n```\ndotnet add package ${{ inputs.repo }} --version ${{ inputs.new_version }}\n```\n\n## NuGet page\n- [${{ inputs.repo }} ${{ inputs.new_version }}](https://www.nuget.org/packages/${{ inputs.repo }}/${{ inputs.new_version }})\n\n## Changelog\n#{{UNCATEGORIZED}}", - "pr_template": " - #{{TITLE}} - ( PR: ##{{NUMBER}} )" - } - env: - GITHUB_TOKEN: ${{ inputs.github_token }} + ## Changelog + #{{UNCATEGORIZED}} + EOF + ) + ;; + Maven) + template=$(cat <<'EOF' + #{{CHANGELOG}} + ## Maven + ``` + + com.uid2 + __REPO__ + __VERSION__ + + ``` + + ## Jar Files + - [__REPO__-__VERSION__.jar](https://repo1.maven.org/maven2/com/uid2/__REPO__/__VERSION__/__REPO__-__VERSION__.jar) + + ## Changelog + #{{UNCATEGORIZED}} + EOF + ) + ;; + PyPI) + template=$(cat <<'EOF' + #{{CHANGELOG}} + ## PyPI + ``` + pip install __REPO__==__VERSION__ + ``` - - name: Build iOS Changelog - id: github_release_ios - if: ${{ inputs.is_release == 'true' && inputs.publish_platform == 'iOS' }} + ## PyPI page + - [__REPO__ __VERSION__](https://pypi.org/project/__REPO__/__VERSION__/) + + ## Changelog + #{{UNCATEGORIZED}} + EOF + ) + ;; + NuGet) + template=$(cat <<'EOF' + #{{CHANGELOG}} + ## NuGet + ``` + dotnet add package __REPO__ --version __VERSION__ + ``` + + ## NuGet page + - [__REPO__ __VERSION__](https://www.nuget.org/packages/__REPO__/__VERSION__) + + ## Changelog + #{{UNCATEGORIZED}} + EOF + ) + ;; + iOS) + template=$(cat <<'EOF' + #{{CHANGELOG}} + ## Changelog + #{{UNCATEGORIZED}} + EOF + ) + ;; + esac + + # Placeholder tokens (__TAGS__, __IMAGE_TAG__, __REPO__, __VERSION__) + # are reserved — no input value should contain them. Substitution is + # order-dependent: if e.g. $TAGS expanded to literal `__REPO__`, the + # next line would re-substitute it. All inputs currently come from + # trusted CI sources (docker/metadata-action outputs, semver), so + # this is theoretical. Keep the tokens unique-looking if you add + # more. + template="${template//__TAGS__/$TAGS}" + template="${template//__IMAGE_TAG__/$IMAGE_TAG}" + template="${template//__REPO__/$REPO}" + template="${template//__VERSION__/$VERSION}" + + # Compact JSON (single line) keeps the step output value on one line — + # simpler to pass into mikepenz's configurationJson input via a + # GitHub Actions expression (no multi-line $GITHUB_OUTPUT delimiter + # needed downstream). + config=$(jq -nc \ + --arg t "$template" \ + --arg p ' - #{{TITLE}} - ( PR: ##{{NUMBER}} )' \ + '{template: $t, pr_template: $p}') + + echo "json=$config" >> "$GITHUB_OUTPUT" + + - name: Build changelog + id: changelog + if: ${{ inputs.is_release == 'true' }} uses: mikepenz/release-changelog-builder-action@32e3c96f29a6532607f638797455e9e98cfc703d # v4 with: toTag: v${{ inputs.new_version }} fromTag: ${{ inputs.from_tag }} - configurationJson: | - { - "template": "#{{CHANGELOG}}\n## Changelog\n#{{UNCATEGORIZED}}", - "pr_template": " - #{{TITLE}} - ( PR: ##{{NUMBER}} )" - } + configurationJson: ${{ steps.changelog_config.outputs.json }} env: GITHUB_TOKEN: ${{ inputs.github_token }} @@ -115,5 +207,5 @@ runs: uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: name: v${{ inputs.new_version }} - body: ${{ inputs.publish_platform == 'Docker' && steps.github_release_docker.outputs.changelog || inputs.publish_platform == 'Maven' && steps.github_release_maven.outputs.changelog || inputs.publish_platform == 'PyPI' && steps.github_release_pypi.outputs.changelog || inputs.publish_platform == 'NuGet' && steps.github_release_nuget.outputs.changelog || steps.github_release_ios.outputs.changelog }} + body: ${{ steps.changelog.outputs.changelog }} draft: true