diff --git a/.github/workflows/nightly-dry-run.yml b/.github/workflows/nightly-dry-run.yml index eecaf54e29..4d2b838bc7 100644 --- a/.github/workflows/nightly-dry-run.yml +++ b/.github/workflows/nightly-dry-run.yml @@ -1,26 +1,91 @@ name: nightly-dry-run -# Manual preview of the nightly version-computation plan (cross-test gate + +# Manual preview of the release version-computation plan (cross-test gate + # git-cliff oracle, capped at minor, with the not-major assertion). Calls the -# same release-gate-plan reusable workflow the real nightly uses, so it +# same release-gate-plan reusable workflow the real release uses, so it # exercises identical logic. Has NO publish jobs — it structurally cannot # publish/tag anything. Use this to verify pipeline changes before they ship. +# +# release-type: +# nightly — preview the gated nightly plan (cross-test must be green). +# dev — preview the GATELESS dev plan AND the per-SDK resolved dev +# versions (e.g. node-sdk 6.1.0-dev.). This is the dry-run for +# the dev path: it runs the exact oracle + resolve-sdk-version the +# real dev release uses, minus every publish/tag step. on: workflow_dispatch: inputs: ref: - description: "Ref to compute the nightly plan for" + description: "Ref to compute the release plan for" required: false type: string default: main + release-type: + description: "Which plan to preview" + required: false + type: choice + options: + - nightly + - dev + default: nightly jobs: plan: uses: ./.github/workflows/release-gate-plan.yml with: ref: ${{ inputs.ref || github.ref_name }} + # dev previews the gateless path (no cross-test gate), exactly like the + # real dev release. nightly keeps the gate. + gate: ${{ inputs.release-type != 'dev' }} secrets: inherit + + # Preview the per-SDK versions a dev release would publish, using the same + # `resolve-sdk-version --release-type dev` the real release jobs call. Prints + # only — no manifest writes, no npm publish, no tags. + preview-dev-versions: + needs: [plan] + if: inputs.release-type == 'dev' && needs.plan.outputs.nothing-pending != 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + # Check out the exact SHA the plan oracle resolved against (not the + # branch HEAD), so the previewed short-sha + manifest match what a + # real dev run on this commit would compute — no drift if commits + # land between the plan job and this one. + ref: ${{ needs.plan.outputs.sha }} + fetch-depth: 0 + - uses: ./.github/actions/setup-release-tools + - name: Resolve dev versions per SDK + env: + PENDING_VERSION: ${{ needs.plan.outputs.version }} + PENDING_KIND: ${{ needs.plan.outputs.kind }} + run: | + set -euo pipefail + if [ -z "$PENDING_VERSION" ] || [ -z "$PENDING_KIND" ]; then + echo "::error::plan produced empty pending version/kind; cannot preview dev versions" + exit 1 + fi + { + echo "## Dev dry-run — resolved versions" + echo "" + echo "Pending libxmtp bump: \`$PENDING_VERSION\` (kind: $PENDING_KIND)" + echo "" + echo "| SDK | dev version |" + echo "| --- | --- |" + } >> "$GITHUB_STEP_SUMMARY" + for SDK in node-sdk browser-sdk node-bindings wasm-bindings ios android libxmtp; do + V=$(xmtp-release resolve-sdk-version \ + --sdk "$SDK" --release-type dev \ + --pending-version "$PENDING_VERSION" \ + --pending-kind "$PENDING_KIND") + echo "| $SDK | \`$V\` |" >> "$GITHUB_STEP_SUMMARY" + echo "::notice::dev $SDK -> $V" + done + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "No publishes/tags occur in a dry-run." >> "$GITHUB_STEP_SUMMARY" + summary: needs: [plan] if: always() @@ -29,7 +94,7 @@ jobs: - name: Print plan result run: | { - echo "## Nightly dry-run plan" + echo "## Release dry-run plan (${{ inputs.release-type || 'nightly' }})" echo "" echo "- gate-skip: \`${{ needs.plan.outputs.gate-skip }}\`" echo "- nothing-pending: \`${{ needs.plan.outputs.nothing-pending }}\`" diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b7edba3afc..33c40a6054 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -98,13 +98,21 @@ jobs: - name: Determine NPM tag id: npm-tag + env: + VERSION: ${{ inputs.version }} run: | - if [[ "${{ inputs.version }}" == *"nightly"* ]]; then - echo "tag=nightly" >> $GITHUB_OUTPUT - elif [[ "${{ inputs.version }}" == *"dev"* || "${{ inputs.version }}" == *"rc"* ]]; then - echo "tag=prerelease" >> $GITHUB_OUTPUT + set -euo pipefail + # Each prerelease channel gets its own dist-tag so consumers can pin + # @nightly / @dev / @rc independently. Check nightly first (a nightly + # string contains neither "dev" nor "rc"). A plain release -> latest. + if [[ "$VERSION" == *"nightly"* ]]; then + echo "tag=nightly" >> "$GITHUB_OUTPUT" + elif [[ "$VERSION" == *"-dev"* ]]; then + echo "tag=dev" >> "$GITHUB_OUTPUT" + elif [[ "$VERSION" == *"-rc"* ]]; then + echo "tag=rc" >> "$GITHUB_OUTPUT" else - echo "tag=latest" >> $GITHUB_OUTPUT + echo "tag=latest" >> "$GITHUB_OUTPUT" fi - name: Update npm to latest diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml index c04adb6cb9..1775a69e19 100644 --- a/.github/workflows/release-android.yml +++ b/.github/workflows/release-android.yml @@ -47,7 +47,14 @@ jobs: if [ "${{ inputs.release-type }}" = "rc" ]; then VERSION=$(xmtp-release compute-version --sdk android --release-type rc --rc-number ${{ inputs.rc-number }}) elif [ "${{ inputs.release-type }}" = "dev" ]; then - VERSION=$(xmtp-release compute-version --sdk android --release-type dev) + if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then + echo "::error::dev resolve called with empty pending version/kind" + exit 1 + fi + VERSION=$(xmtp-release resolve-sdk-version \ + --sdk android --release-type dev \ + --pending-version "${{ inputs.pending-version }}" \ + --pending-kind "${{ inputs.pending-kind }}") elif [ "${{ inputs.release-type }}" = "nightly" ]; then # Belt-and-suspenders: release.yml already skips this whole job when # the gate isn't green or there are no conventional commits diff --git a/.github/workflows/release-browser-sdk.yml b/.github/workflows/release-browser-sdk.yml index 949e4c6d38..71d82ddc1d 100644 --- a/.github/workflows/release-browser-sdk.yml +++ b/.github/workflows/release-browser-sdk.yml @@ -53,7 +53,14 @@ jobs: if [ "${{ inputs.release-type }}" = "rc" ]; then VERSION=$(xmtp-release compute-version --sdk browser-sdk --release-type rc --rc-number ${{ inputs.rc-number }}) elif [ "${{ inputs.release-type }}" = "dev" ]; then - VERSION=$(xmtp-release compute-version --sdk browser-sdk --release-type dev) + if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then + echo "::error::dev resolve called with empty pending version/kind" + exit 1 + fi + VERSION=$(xmtp-release resolve-sdk-version \ + --sdk browser-sdk --release-type dev \ + --pending-version "${{ inputs.pending-version }}" \ + --pending-kind "${{ inputs.pending-kind }}") elif [ "${{ inputs.release-type }}" = "nightly" ]; then if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then echo "::error::nightly resolve called with empty pending version/kind" diff --git a/.github/workflows/release-gate-plan.yml b/.github/workflows/release-gate-plan.yml index 94733aea5e..70e21a9939 100644 --- a/.github/workflows/release-gate-plan.yml +++ b/.github/workflows/release-gate-plan.yml @@ -1,9 +1,12 @@ name: release-gate-plan -# Reusable: cross-test gate + version oracle (the nightly "plan"). Computes the +# Reusable: cross-test gate + version oracle (the release "plan"). Computes the # pending libxmtp {version,kind}, caps at minor, asserts not-major, prints the -# plan. Does NO publishing. Called by release.yml (real nightly) and -# nightly-dry-run.yml (manual preview) so both run identical logic. +# plan. Does NO publishing. Used for BOTH nightly and dev: nightly runs the +# cross-test gate (gate: true, the default); dev runs it gateless (gate: false) +# since the cross-test gate is a nightly-only correctness gate. Called by +# release.yml (real nightly + dev) and nightly-dry-run.yml (manual preview) so +# all paths run identical version-computation logic. on: workflow_call: @@ -11,6 +14,11 @@ on: ref: required: true type: string + gate: + required: false + type: boolean + default: true + description: "Run the nightly cross-test gate. dev builds pass false to skip it." outputs: sha: value: ${{ jobs.gate.outputs.sha }} @@ -53,8 +61,14 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} SHA: ${{ steps.sha.outputs.sha }} + GATE_ENABLED: ${{ inputs.gate }} run: | set -euo pipefail + if [ "$GATE_ENABLED" != "true" ]; then + echo "gate-skip=false" >> "$GITHUB_OUTPUT" + echo "::notice::Cross-test gate disabled for this run (e.g. dev build); proceeding." + exit 0 + fi gh api "repos/${REPO}/actions/workflows/cross-test.yml/runs?head_sha=${SHA}&status=completed" > /tmp/cross-runs.json RESULT=$(xmtp-release cross-test-gate --input=/tmp/cross-runs.json --sha "$SHA") PASS=$(printf '%s' "$RESULT" | jq -r '.pass') @@ -143,7 +157,7 @@ jobs: echo "kind=$KIND" >> "$GITHUB_OUTPUT" # Human-readable release plan. { - echo "## Nightly release plan" + echo "## Release plan" echo "" echo "- libxmtp: \`$VERSION\` (bump: $KIND, baseline: $LAST)" echo "" diff --git a/.github/workflows/release-ios.yml b/.github/workflows/release-ios.yml index 829fbdd626..7e8eca952d 100644 --- a/.github/workflows/release-ios.yml +++ b/.github/workflows/release-ios.yml @@ -55,7 +55,14 @@ jobs: if [ "${{ inputs.release-type }}" = "rc" ]; then VERSION=$(xmtp-release compute-version --sdk ios --release-type rc --rc-number ${{ inputs.rc-number }}) elif [ "${{ inputs.release-type }}" = "dev" ]; then - VERSION=$(xmtp-release compute-version --sdk ios --release-type dev) + if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then + echo "::error::dev resolve called with empty pending version/kind" + exit 1 + fi + VERSION=$(xmtp-release resolve-sdk-version \ + --sdk ios --release-type dev \ + --pending-version "${{ inputs.pending-version }}" \ + --pending-kind "${{ inputs.pending-kind }}") elif [ "${{ inputs.release-type }}" = "nightly" ]; then # Belt-and-suspenders: release.yml already skips this whole job when # the gate isn't green or there are no conventional commits diff --git a/.github/workflows/release-node-sdk.yml b/.github/workflows/release-node-sdk.yml index 98ec2d3bf1..ec4187d861 100644 --- a/.github/workflows/release-node-sdk.yml +++ b/.github/workflows/release-node-sdk.yml @@ -53,7 +53,14 @@ jobs: if [ "${{ inputs.release-type }}" = "rc" ]; then VERSION=$(xmtp-release compute-version --sdk node-sdk --release-type rc --rc-number ${{ inputs.rc-number }}) elif [ "${{ inputs.release-type }}" = "dev" ]; then - VERSION=$(xmtp-release compute-version --sdk node-sdk --release-type dev) + if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then + echo "::error::dev resolve called with empty pending version/kind" + exit 1 + fi + VERSION=$(xmtp-release resolve-sdk-version \ + --sdk node-sdk --release-type dev \ + --pending-version "${{ inputs.pending-version }}" \ + --pending-kind "${{ inputs.pending-kind }}") elif [ "${{ inputs.release-type }}" = "nightly" ]; then if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then echo "::error::nightly resolve called with empty pending version/kind" diff --git a/.github/workflows/release-node.yml b/.github/workflows/release-node.yml index cbdbe0a281..dc12da933c 100644 --- a/.github/workflows/release-node.yml +++ b/.github/workflows/release-node.yml @@ -47,7 +47,14 @@ jobs: if [ "${{ inputs.release-type }}" = "rc" ]; then VERSION=$(xmtp-release compute-version --sdk node-bindings --release-type rc --rc-number ${{ inputs.rc-number }}) elif [ "${{ inputs.release-type }}" = "dev" ]; then - VERSION=$(xmtp-release compute-version --sdk node-bindings --release-type dev) + if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then + echo "::error::dev resolve called with empty pending version/kind" + exit 1 + fi + VERSION=$(xmtp-release resolve-sdk-version \ + --sdk node-bindings --release-type dev \ + --pending-version "${{ inputs.pending-version }}" \ + --pending-kind "${{ inputs.pending-kind }}") elif [ "${{ inputs.release-type }}" = "nightly" ]; then # Belt-and-suspenders: release.yml already skips this whole job when # the gate isn't green or there are no conventional commits diff --git a/.github/workflows/release-wasm.yml b/.github/workflows/release-wasm.yml index 60279d3455..1bf06e5f93 100644 --- a/.github/workflows/release-wasm.yml +++ b/.github/workflows/release-wasm.yml @@ -50,7 +50,14 @@ jobs: if [ "${{ inputs.release-type }}" = "rc" ]; then VERSION=$(xmtp-release compute-version --sdk wasm-bindings --release-type rc --rc-number ${{ inputs.rc-number }}) elif [ "${{ inputs.release-type }}" = "dev" ]; then - VERSION=$(xmtp-release compute-version --sdk wasm-bindings --release-type dev) + if [ -z "${{ inputs.pending-version }}" ] || [ -z "${{ inputs.pending-kind }}" ]; then + echo "::error::dev resolve called with empty pending version/kind" + exit 1 + fi + VERSION=$(xmtp-release resolve-sdk-version \ + --sdk wasm-bindings --release-type dev \ + --pending-version "${{ inputs.pending-version }}" \ + --pending-kind "${{ inputs.pending-kind }}") elif [ "${{ inputs.release-type }}" = "nightly" ]; then # Belt-and-suspenders: release.yml already skips this whole job when # the gate isn't green or there are no conventional commits diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c814bdca2a..4bc27b06a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -155,10 +155,15 @@ jobs: # nothing-pending. plan: needs: [validate] - if: needs.validate.outputs.release-type == 'nightly' && needs.validate.outputs.skip != 'true' + if: >- + (needs.validate.outputs.release-type == 'nightly' || needs.validate.outputs.release-type == 'dev') + && needs.validate.outputs.skip != 'true' uses: ./.github/workflows/release-gate-plan.yml with: ref: ${{ needs.validate.outputs.ref }} + # dev builds preview the next version off main but must NOT be gated on + # cross-test (that's a nightly correctness gate). nightly keeps the gate. + gate: ${{ needs.validate.outputs.release-type == 'nightly' }} secrets: inherit release-ios: @@ -167,7 +172,7 @@ jobs: !cancelled() && !contains(needs.*.result, 'failure') && needs.validate.outputs.ios == 'true' && needs.validate.outputs.skip != 'true' && needs.validate.outputs.dry-run != 'true' - && (needs.validate.outputs.release-type != 'nightly' + && ((needs.validate.outputs.release-type != 'nightly' && needs.validate.outputs.release-type != 'dev') || (needs.plan.outputs.gate-skip != 'true' && needs.plan.outputs.nothing-pending != 'true')) uses: ./.github/workflows/release-ios.yml with: @@ -184,7 +189,7 @@ jobs: !cancelled() && !contains(needs.*.result, 'failure') && needs.validate.outputs.android == 'true' && needs.validate.outputs.skip != 'true' && needs.validate.outputs.dry-run != 'true' - && (needs.validate.outputs.release-type != 'nightly' + && ((needs.validate.outputs.release-type != 'nightly' && needs.validate.outputs.release-type != 'dev') || (needs.plan.outputs.gate-skip != 'true' && needs.plan.outputs.nothing-pending != 'true')) uses: ./.github/workflows/release-android.yml with: @@ -200,7 +205,7 @@ jobs: if: >- !cancelled() && !contains(needs.*.result, 'failure') && needs.validate.outputs.node-sdk == 'true' && needs.validate.outputs.skip != 'true' - && (needs.validate.outputs.release-type != 'nightly' + && ((needs.validate.outputs.release-type != 'nightly' && needs.validate.outputs.release-type != 'dev') || (needs.plan.outputs.gate-skip != 'true' && needs.plan.outputs.nothing-pending != 'true')) uses: ./.github/workflows/release-node-sdk.yml with: @@ -217,7 +222,7 @@ jobs: if: >- !cancelled() && !contains(needs.*.result, 'failure') && needs.validate.outputs.browser-sdk == 'true' && needs.validate.outputs.skip != 'true' - && (needs.validate.outputs.release-type != 'nightly' + && ((needs.validate.outputs.release-type != 'nightly' && needs.validate.outputs.release-type != 'dev') || (needs.plan.outputs.gate-skip != 'true' && needs.plan.outputs.nothing-pending != 'true')) uses: ./.github/workflows/release-browser-sdk.yml with: