diff --git a/.github/workflows/_publish-dar-packages.yaml b/.github/workflows/_publish-dar-packages.yaml index 158c80b..9f45744 100644 --- a/.github/workflows/_publish-dar-packages.yaml +++ b/.github/workflows/_publish-dar-packages.yaml @@ -48,6 +48,11 @@ on: required: false type: boolean default: false + release_tag: + description: 'Draft release tag to attach nupkgs to (e.g. v0.4.0-preview.4). Empty = v.' + required: false + type: string + default: '' secrets: COUNTERS_TOKEN: description: 'Token with Variables: read & write on this repo (used to read/persist the counter store).' @@ -190,12 +195,12 @@ jobs: run: .github/scripts/persist-counters.sh - name: Attach packages to draft release (best-effort, never fails) - if: env.DRY_RUN != 'true' env: GH_TOKEN: ${{ github.token }} + RELEASE_TAG_INPUT: ${{ inputs.release_tag }} run: | set -uo pipefail - TAG="v${CODEGEN_CS_VERSION}" + TAG="${RELEASE_TAG_INPUT:-v${CODEGEN_CS_VERSION}}" if gh release view "$TAG" >/dev/null 2>&1; then shopt -s nullglob files=("$NUPKG_DIR"/*.nupkg) diff --git a/.github/workflows/_release-lifecycle.yaml b/.github/workflows/_release-lifecycle.yaml new file mode 100644 index 0000000..581f135 --- /dev/null +++ b/.github/workflows/_release-lifecycle.yaml @@ -0,0 +1,84 @@ +name: Release lifecycle (reusable) + +on: + workflow_call: + inputs: + action: + description: "'bootstrap' (ensure a draft prerelease exists) or 'finalize' (flip the draft to published)." + required: true + type: string + tag: + description: 'Release tag, including the leading v (e.g. v0.4.0-preview.4).' + required: true + type: string + name: + description: 'Release title. Empty = the tag.' + required: false + type: string + default: '' + notes: + description: 'Draft notes used when bootstrap creates a new draft.' + required: false + type: string + default: 'Draft release assembled by CI. Assets are attached automatically across the release pipeline; publish manually once complete.' + outputs: + release_url: + description: 'HTML URL of the affected release.' + value: ${{ jobs.lifecycle.outputs.release_url }} + created: + description: "'true' when bootstrap created a new draft; 'false' when it already existed." + value: ${{ jobs.lifecycle.outputs.created }} + +jobs: + lifecycle: + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + release_url: ${{ steps.run.outputs.release_url }} + created: ${{ steps.run.outputs.created }} + steps: + - name: Run ${{ inputs.action }} for ${{ inputs.tag }} + id: run + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + ACTION: ${{ inputs.action }} + TAG: ${{ inputs.tag }} + NAME: ${{ inputs.name }} + NOTES: ${{ inputs.notes }} + run: | + set -euo pipefail + TITLE="${NAME:-$TAG}" + case "$ACTION" in + bootstrap) + if gh release view "$TAG" >/dev/null 2>&1; then + echo "created=false" >> "$GITHUB_OUTPUT" + echo "draft $TAG already exists" + else + gh release create "$TAG" --draft --prerelease --verify-tag --title "$TITLE" --notes "$NOTES" + echo "created=true" >> "$GITHUB_OUTPUT" + echo "created draft $TAG" + fi + ;; + finalize) + if ! gh release view "$TAG" >/dev/null 2>&1; then + echo "::error::cannot finalize: release $TAG does not exist" + exit 1 + fi + is_draft="$(gh release view "$TAG" --json isDraft --jq '.isDraft')" + if [ "$is_draft" != "true" ]; then + echo "::warning::release $TAG is already published; nothing to finalize" + else + gh release edit "$TAG" --draft=false + echo "published $TAG" + fi + echo "created=false" >> "$GITHUB_OUTPUT" + ;; + *) + echo "::error::unknown action '$ACTION' (want bootstrap|finalize)" + exit 1 + ;; + esac + url="$(gh release view "$TAG" --json url --jq '.url')" + echo "release_url=${url}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-oci-codegen-cs.yaml b/.github/workflows/build-oci-codegen-cs.yaml index ccb7cd7..6117eb8 100644 --- a/.github/workflows/build-oci-codegen-cs.yaml +++ b/.github/workflows/build-oci-codegen-cs.yaml @@ -31,10 +31,6 @@ on: type: boolean default: true - push: - tags: - - 'v*' - concurrency: group: build-oci-codegen-cs-${{ inputs.version || github.ref_name }} cancel-in-progress: false @@ -670,7 +666,7 @@ jobs: merge-multiple: true path: bundles - - name: Attach bundles to draft release (never publish) + - name: Attach bundles to draft release (upload only; never create/publish) env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} @@ -678,8 +674,8 @@ jobs: set -euo pipefail TAG="${GITHUB_REF_NAME}" if ! gh release view "$TAG" >/dev/null 2>&1; then - gh release create "$TAG" --draft --prerelease --verify-tag --title "$TAG" \ - --notes "Draft release assembled by CI. OCI bundle + NuGet packages are attached automatically; attach Splice/Daml.Finance packages, then publish manually." + echo "no draft release $TAG (orchestrator bootstrap should have created it); skipping attach" + exit 0 fi shopt -s nullglob files=(bundles/*.tar.gz) diff --git a/.github/workflows/finalize-release.yaml b/.github/workflows/finalize-release.yaml new file mode 100644 index 0000000..3e4a077 --- /dev/null +++ b/.github/workflows/finalize-release.yaml @@ -0,0 +1,18 @@ +name: Finalize Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version whose draft release to publish (no leading v, e.g. 0.4.0-preview.4).' + required: true + type: string + +jobs: + finalize: + permissions: + contents: write + uses: ./.github/workflows/_release-lifecycle.yaml + with: + action: finalize + tag: v${{ inputs.version }} diff --git a/.github/workflows/publish-daml-finance.yaml b/.github/workflows/publish-daml-finance.yaml index 9d0c5c0..7c1246f 100644 --- a/.github/workflows/publish-daml-finance.yaml +++ b/.github/workflows/publish-daml-finance.yaml @@ -1,6 +1,48 @@ name: Publish Daml.Finance DAR Packages on: + workflow_call: + inputs: + daml_finance_version: + description: 'Daml.Finance SDK release version (digital-asset/daml-finance sdk/ tag).' + required: false + type: string + default: '2.10.0' + codegen_cs_version: + description: 'Version of ghcr.io/peacefulstudio/dpm-codegen-cs to pull. Empty = Directory.Build.props .' + required: false + type: string + default: '' + damlc_version: + description: 'damlc component version listed under daml.yaml components.' + required: false + type: string + default: '3.4.11' + runtime_version: + description: 'Daml.Runtime NuGet version referenced from generated csprojs. Empty = Directory.Build.props .' + required: false + type: string + default: '' + package_license: + description: 'SPDX license expression emitted into generated csprojs.' + required: false + type: string + default: 'Apache-2.0' + dry_run: + description: 'Build + pack + verify but skip the NuGet push AND the DAML_FINANCE_RELEASE_COUNTERS write-back.' + required: false + type: boolean + default: false + release_tag: + description: 'Draft release tag to attach nupkgs to. Empty = v.' + required: false + type: string + default: '' + secrets: + RELEASE_COUNTERS_TOKEN: + required: true + NUGET_USER: + required: true workflow_dispatch: inputs: daml_finance_version: @@ -60,6 +102,7 @@ jobs: runtime_version: ${{ inputs.runtime_version }} package_license: ${{ inputs.package_license }} dry_run: ${{ inputs.dry_run }} + release_tag: ${{ inputs.release_tag }} secrets: COUNTERS_TOKEN: ${{ secrets.RELEASE_COUNTERS_TOKEN }} NUGET_USER: ${{ secrets.NUGET_USER }} diff --git a/.github/workflows/publish-splice.yaml b/.github/workflows/publish-splice.yaml index 02130d0..cd921cb 100644 --- a/.github/workflows/publish-splice.yaml +++ b/.github/workflows/publish-splice.yaml @@ -1,6 +1,47 @@ name: Publish Splice DAR Packages on: + workflow_call: + inputs: + splice_version: + description: 'Splice version (decentralized-canton-sync release tag, e.g. 0.6.11). No default — always pass the version explicitly.' + required: true + type: string + codegen_cs_version: + description: 'Version of ghcr.io/peacefulstudio/dpm-codegen-cs to pull. Empty = Directory.Build.props .' + required: false + type: string + default: '' + damlc_version: + description: 'damlc component version listed under daml.yaml components.' + required: false + type: string + default: '3.4.11' + runtime_version: + description: 'Daml.Runtime NuGet version referenced from generated csprojs. Empty = Directory.Build.props .' + required: false + type: string + default: '' + package_license: + description: 'SPDX license expression emitted into generated csprojs.' + required: false + type: string + default: 'Apache-2.0' + dry_run: + description: 'Build + pack + verify but skip the NuGet push AND the SPLICE_RELEASE_COUNTERS write-back.' + required: false + type: boolean + default: false + release_tag: + description: 'Draft release tag to attach nupkgs to. Empty = v.' + required: false + type: string + default: '' + secrets: + RELEASE_COUNTERS_TOKEN: + required: true + NUGET_USER: + required: true workflow_dispatch: inputs: splice_version: @@ -52,6 +93,7 @@ jobs: runtime_version: ${{ inputs.runtime_version }} package_license: ${{ inputs.package_license }} dry_run: ${{ inputs.dry_run }} + release_tag: ${{ inputs.release_tag }} secrets: COUNTERS_TOKEN: ${{ secrets.RELEASE_COUNTERS_TOKEN }} NUGET_USER: ${{ secrets.NUGET_USER }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 303c5ee..93797b1 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,9 +1,28 @@ name: Publish NuGet Packages on: - push: - tags: - - 'v*' + workflow_call: + inputs: + version: + description: 'Version to publish (no leading v). Required when called from the orchestrator.' + required: true + type: string + dry_run: + description: 'Pack + attach to the draft but skip the nuget.org push.' + required: false + type: boolean + default: false + outputs: + version: + description: 'The version that was packed/published.' + value: ${{ jobs.publish.outputs.version }} + published: + description: "'true' when packages were pushed to nuget.org; 'false' on dry_run or skip." + value: ${{ jobs.publish.outputs.published }} + secrets: + NUGET_USER: + description: 'nuget.org username for OIDC trusted publishing (NuGet/login).' + required: false workflow_dispatch: inputs: version_override: @@ -18,6 +37,9 @@ jobs: permissions: contents: write id-token: write + outputs: + version: ${{ steps.ver.outputs.version }} + published: ${{ steps.ver.outputs.published }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -27,45 +49,78 @@ jobs: id: ver env: VERSION_OVERRIDE: ${{ inputs.version_override }} + VERSION_INPUT: ${{ inputs.version }} + DRY_RUN_INPUT: ${{ inputs.dry_run }} run: | set -euo pipefail - if [ -n "${VERSION_OVERRIDE}" ]; then + if [ -n "${VERSION_OVERRIDE:-}" ]; then RESOLVED="${VERSION_OVERRIDE}" - elif [ "${GITHUB_REF_TYPE}" = "tag" ]; then - RESOLVED="${GITHUB_REF_NAME#v}" - if ! [[ "${RESOLVED}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z.-]+)?$ ]]; then - echo "::warning::tag '${GITHUB_REF_NAME}' is not a release version; skipping publish" - echo "should_publish=false" >> "$GITHUB_OUTPUT" - exit 0 - fi + elif [ -n "${VERSION_INPUT:-}" ]; then + RESOLVED="${VERSION_INPUT}" else - echo "::warning::no version_override and not a tag — skipping publish" + echo "::warning::no version_override and no workflow_call version — skipping publish" echo "should_publish=false" >> "$GITHUB_OUTPUT" + echo "published=false" >> "$GITHUB_OUTPUT" exit 0 fi - echo "Publishing version: ${RESOLVED}" + if ! [[ "${RESOLVED}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::warning::'${RESOLVED}' is not a release version; skipping publish" + echo "should_publish=false" >> "$GITHUB_OUTPUT" + echo "published=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Publishing version: ${RESOLVED} (dry_run=${DRY_RUN_INPUT:-false})" echo "version=${RESOLVED}" >> "$GITHUB_OUTPUT" echo "should_publish=true" >> "$GITHUB_OUTPUT" + if [ "${DRY_RUN_INPUT:-false}" = "true" ]; then + echo "published=false" >> "$GITHUB_OUTPUT" + else + echo "published=true" >> "$GITHUB_OUTPUT" + fi + - name: Setup .NET (dry-run pack path only) + if: steps.ver.outputs.should_publish == 'true' && inputs.dry_run == true + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.0.0 + with: + dotnet-version: '10.0.x' - name: NuGet login (OIDC -> short-lived key) id: login - if: steps.ver.outputs.should_publish == 'true' + if: steps.ver.outputs.should_publish == 'true' && inputs.dry_run != true uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 with: user: ${{ secrets.NUGET_USER }} - name: Build, test, pack & push - if: steps.ver.outputs.should_publish == 'true' + if: steps.ver.outputs.should_publish == 'true' && inputs.dry_run != true uses: peacefulstudio/github-actions/.github/actions/csharp-publish@v1 with: api-key: ${{ steps.login.outputs.NUGET_API_KEY }} version_override: ${{ steps.ver.outputs.version }} - - name: Attach packages to draft release (never publish) - if: steps.ver.outputs.should_publish == 'true' && github.event_name == 'push' && github.ref_type == 'tag' + - name: Build & pack only (dry-run — no push) + if: steps.ver.outputs.should_publish == 'true' && inputs.dry_run == true env: - GH_TOKEN: ${{ github.token }} + PACKAGE_VERSION: ${{ steps.ver.outputs.version }} run: | set -euo pipefail - TAG="${GITHUB_REF_NAME}" - gh release view "$TAG" >/dev/null 2>&1 || gh release create "$TAG" --draft --prerelease --verify-tag --title "$TAG" --notes "Draft release assembled by CI. OCI bundle + NuGet packages are attached automatically; attach Splice/Daml.Finance packages, then publish manually." || true - shopt -s nullglob - files=(output/nuget/*.nupkg output/nuget/*.snupkg) - [ ${#files[@]} -gt 0 ] && gh release upload "$TAG" "${files[@]}" --clobber || echo "no packages to attach" + dotnet restore + dotnet build --configuration Release --no-restore -p:Version="$PACKAGE_VERSION" -p:ContinuousIntegrationBuild=true + dotnet pack --configuration Release --no-build \ + -p:PackageVersion="$PACKAGE_VERSION" \ + -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg \ + -o "$GITHUB_WORKSPACE/output/nuget" + - name: Attach packages to draft release (upload only; never create/publish) + if: steps.ver.outputs.should_publish == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -uo pipefail + TAG="v${{ steps.ver.outputs.version }}" + if gh release view "$TAG" >/dev/null 2>&1; then + shopt -s nullglob + files=(output/nuget/*.nupkg output/nuget/*.snupkg) + if [ ${#files[@]} -gt 0 ]; then + gh release upload "$TAG" "${files[@]}" --clobber || echo "upload failed for $TAG; continuing" + else + echo "no packages under output/nuget to attach" + fi + else + echo "no draft release $TAG; skipping attach (bootstrap creates it in the orchestrator)" + fi diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..34c1645 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,165 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (no leading v, e.g. 0.4.0-preview.4). For a rehearsal, push a throwaway tag v first.' + required: true + type: string + dry_run: + description: 'Rehearsal: pack + attach to the draft, skip nuget.org push + counter bumps + GHCR push.' + required: false + type: boolean + default: false + codegen_cs_version: + description: 'dry_run only: an already-published dpm-codegen-cs image version the DAR legs pull. Empty = .' + required: false + type: string + default: '' + splice_version: + description: 'Splice source version to package (e.g. 0.6.11). Empty on tag push = repo var SPLICE_VERSION.' + required: false + type: string + default: '' + daml_finance_version: + description: 'Daml.Finance source version to package. Empty on tag push = repo var DAML_FINANCE_VERSION or 2.10.0.' + required: false + type: string + default: '' + +permissions: {} + +jobs: + resolve: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.r.outputs.version }} + tag: ${{ steps.r.outputs.tag }} + dry_run: ${{ steps.r.outputs.dry_run }} + codegen_cs_version: ${{ steps.r.outputs.codegen_cs_version }} + splice_version: ${{ steps.r.outputs.splice_version }} + daml_finance_version: ${{ steps.r.outputs.daml_finance_version }} + steps: + - name: Resolve version + mode + source versions + id: r + env: + VERSION_INPUT: ${{ inputs.version }} + DRY_RUN_INPUT: ${{ inputs.dry_run }} + CODEGEN_CS_INPUT: ${{ inputs.codegen_cs_version }} + SPLICE_INPUT: ${{ inputs.splice_version }} + FINANCE_INPUT: ${{ inputs.daml_finance_version }} + SPLICE_VAR: ${{ vars.SPLICE_VERSION }} + FINANCE_VAR: ${{ vars.DAML_FINANCE_VERSION }} + run: | + set -euo pipefail + if [ -n "${VERSION_INPUT:-}" ]; then + VERSION="${VERSION_INPUT}" + elif [ "${GITHUB_REF_TYPE}" = "tag" ]; then + VERSION="${GITHUB_REF_NAME#v}" + else + echo "::error::no version input and not a tag"; exit 1 + fi + if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::'${VERSION}' is not a release version"; exit 1 + fi + DRY="${DRY_RUN_INPUT:-false}" + CGV="${CODEGEN_CS_INPUT:-}"; [ -z "$CGV" ] && CGV="${VERSION}" + SPLICE="${SPLICE_INPUT:-}"; [ -z "$SPLICE" ] && SPLICE="${SPLICE_VAR:-}" + FINANCE="${FINANCE_INPUT:-}"; [ -z "$FINANCE" ] && FINANCE="${FINANCE_VAR:-}"; [ -z "$FINANCE" ] && FINANCE="2.10.0" + if [ -z "$SPLICE" ]; then + echo "::error::no Splice source version — pass splice_version or set repo var SPLICE_VERSION"; exit 1 + fi + { + echo "version=${VERSION}" + echo "tag=v${VERSION}" + echo "dry_run=${DRY}" + echo "codegen_cs_version=${CGV}" + echo "splice_version=${SPLICE}" + echo "daml_finance_version=${FINANCE}" + } >> "$GITHUB_OUTPUT" + + draft-bootstrap: + needs: resolve + permissions: + contents: write + uses: ./.github/workflows/_release-lifecycle.yaml + with: + action: bootstrap + tag: ${{ needs.resolve.outputs.tag }} + + runtime-publish: + needs: [resolve, draft-bootstrap] + permissions: + contents: write + id-token: write + uses: ./.github/workflows/publish.yaml + with: + version: ${{ needs.resolve.outputs.version }} + dry_run: ${{ fromJSON(needs.resolve.outputs.dry_run) }} + secrets: inherit + + oci-build: + needs: [resolve, runtime-publish] + permissions: + contents: write + packages: write + id-token: write + uses: ./.github/workflows/build-oci-codegen-cs.yaml + with: + version: ${{ needs.resolve.outputs.version }} + push: ${{ !fromJSON(needs.resolve.outputs.dry_run) }} + + splice-publish: + needs: [resolve, oci-build] + permissions: + contents: write + id-token: write + packages: read + uses: ./.github/workflows/publish-splice.yaml + with: + splice_version: ${{ needs.resolve.outputs.splice_version }} + codegen_cs_version: ${{ needs.resolve.outputs.codegen_cs_version }} + dry_run: ${{ fromJSON(needs.resolve.outputs.dry_run) }} + release_tag: ${{ needs.resolve.outputs.tag }} + secrets: inherit + + daml-finance-publish: + needs: [resolve, oci-build] + permissions: + contents: write + id-token: write + packages: read + uses: ./.github/workflows/publish-daml-finance.yaml + with: + daml_finance_version: ${{ needs.resolve.outputs.daml_finance_version }} + codegen_cs_version: ${{ needs.resolve.outputs.codegen_cs_version }} + dry_run: ${{ fromJSON(needs.resolve.outputs.dry_run) }} + release_tag: ${{ needs.resolve.outputs.tag }} + secrets: inherit + + complete: + needs: [resolve, runtime-publish, oci-build, splice-publish, daml-finance-publish] + if: always() + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Summarise draft + env: + DRY: ${{ needs.resolve.outputs.dry_run }} + TAG: ${{ needs.resolve.outputs.tag }} + run: | + { + echo "## Release draft ${TAG} assembled (dry_run=${DRY})" + echo "" + echo "- runtime-publish: ${{ needs.runtime-publish.result }}" + echo "- oci-build: ${{ needs.oci-build.result }}" + echo "- splice-publish: ${{ needs.splice-publish.result }}" + echo "- daml-finance-publish: ${{ needs.daml-finance-publish.result }}" + echo "" + echo "Draft is NOT published. Run **Finalize Release** (workflow_dispatch) to publish." + } >> "$GITHUB_STEP_SUMMARY"