From 8187b27b34ef25618d205e004f76b11d402eb1ba Mon Sep 17 00:00:00 2001 From: michalharakal Date: Sun, 20 Sep 2026 19:30:19 +0200 Subject: [PATCH] ci(publish): manual dry run; docs(gitflow): dry-run and green-before-tag gates publish.yml gains workflow_dispatch: a manually started run executes the whole release pipeline (native matrix, Android SDK + NDK, signing) but ends in publishToMavenLocal and is never given the Maven Central credentials, so it cannot upload. The real publish step now runs only on tag pushes. GITFLOW.adoc documents what the 0.56.0 release taught: dry-run the publish on the release branch, merge the release PR with a merge commit, wait until develop CI on that merge commit has finished green, tag that merge commit (matching what 0.54.0 and 0.56.0 actually did), and what to do when a publish run fails (move the tag only if nothing was uploaded; otherwise cut a patch release). --- .github/workflows/publish.yml | 47 ++++++++++++++++++++++ GITFLOW.adoc | 73 ++++++++++++++++++++++++++++------- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0784ff7e..d946d164 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,10 +32,17 @@ name: release # target" — see SKaiNET PR #577). Linux ARM64 consumers fall back # cleanly to the Panama priority-50 provider. +# Dry run: "Run workflow" (workflow_dispatch) on any branch executes the SAME pipeline — native +# matrix, Android SDK + NDK setup, signing — but ends in `publishToMavenLocal` on the runner +# instead of `publish`. It is never given the Maven Central credentials, so it cannot upload. +# Run it on the release branch before tagging: this workflow is otherwise only ever exercised by +# the tag push itself, which is how a broken runner setup step surfaced mid-release in 0.56.0. + on: push: tags: - '**' + workflow_dispatch: # Set default permission for all jobs to none. Publishing authenticates to Maven # Central and signs with GPG through repository secrets, which are independent of @@ -261,6 +268,8 @@ jobs: echo "SKAINET_KERNELS_MACOS_DIR=$MACOS_DIR" >> "$GITHUB_ENV" - name: Publish to MavenCentral + # Only a tag push publishes. A manual run takes the dry-run step below instead. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') run: | ./gradlew publish --no-configuration-cache --stacktrace \ -PskainetKernelsX64Dir="$SKAINET_KERNELS_X64_DIR" \ @@ -273,3 +282,41 @@ jobs: ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + + # Dry run (workflow_dispatch): same Gradle invocation, same signing, but into the runner's + # ~/.m2 — and deliberately WITHOUT mavenCentralUsername/Password in the environment. + - name: Dry run — publish to Maven Local (no upload) + if: github.event_name == 'workflow_dispatch' + run: | + ./gradlew publishToMavenLocal --no-configuration-cache --stacktrace \ + -PskainetKernelsX64Dir="$SKAINET_KERNELS_X64_DIR" \ + -PskainetKernelsArm64Dir="$SKAINET_KERNELS_ARM64_DIR" \ + -PskainetKernelsIosArm64Dir="$SKAINET_KERNELS_IOS_DIR" \ + -PskainetKernelsIosSimulatorArm64Dir="$SKAINET_KERNELS_IOS_SIM_DIR" \ + -PskainetKernelsMacosArm64Dir="$SKAINET_KERNELS_MACOS_DIR" + env: + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + + - name: Dry run — summarize what would have been published + if: github.event_name == 'workflow_dispatch' + run: | + set -euo pipefail + VERSION="$(grep -E '^VERSION_NAME=' gradle.properties | cut -d= -f2)" + REPO="$HOME/.m2/repository/sk/ainet" + MODULES="$(find "$REPO" -type d -name "$VERSION" | wc -l | tr -d ' ')" + POMS="$(find "$REPO" -path "*/$VERSION/*.pom" | wc -l | tr -d ' ')" + SIGS="$(find "$REPO" -path "*/$VERSION/*.asc" | wc -l | tr -d ' ')" + { + echo "## Publish dry run — nothing was uploaded" + echo "" + echo "| | |" + echo "|---|---|" + echo "| Ref | \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA::8}\` |" + echo "| Version | \`${VERSION}\` |" + echo "| Modules | ${MODULES} |" + echo "| POMs | ${POMS} |" + echo "| Signatures (.asc) | ${SIGS} |" + } >> "$GITHUB_STEP_SUMMARY" + test "$MODULES" -gt 0 || { echo "No modules were published to Maven Local" >&2; exit 1; } + test "$SIGS" -gt 0 || { echo "No .asc signatures produced — signing did not run" >&2; exit 1; } diff --git a/GITFLOW.adoc b/GITFLOW.adoc index 8694fdf6..d43be901 100644 --- a/GITFLOW.adoc +++ b/GITFLOW.adoc @@ -69,8 +69,8 @@ GitFlow is a branching model for Git that defines specific branch types and thei == Workflow Diagram NOTE: This diagram shows the conceptual GitFlow shape (tag on `main` after merging the release -branch in). SKaiNET's actual sequence tags the release branch's own commit *before* touching -`main` — see <<_release_process,Release Process>> below for the real order and why. +branch in). SKaiNET's actual sequence tags the release PR's merge commit on `develop` *before* +touching `main` — see <<_release_process,Release Process>> below for the real order and why. [mermaid] ifdef::env-github[[source,mermaid]] @@ -167,10 +167,20 @@ git branch -d feature/lstm-layers === Release Process This is the sequence actually used from 0.51.0 onward — it differs from a textbook GitFlow release -in one important way: the tag lives on the *release branch's own commit*, independent of `main`, -because the Maven Central publish workflow triggers on the tag push (`on: push: tags: '**'` in -`.github/workflows/publish.yml`), not on anything landing on `main`. `main` is updated *after* a -successful publish, as an explicit last step — never before, and never as a side effect of tagging. +in one important way: the tag lives on `develop` — on the merge commit of the release PR — +independent of `main`, because the Maven Central publish workflow triggers on the tag push +(`on: push: tags: '**'` in `.github/workflows/publish.yml`), not on anything landing on `main`. +`main` is updated *after* a successful publish, as an explicit last step — never before, and never +as a side effect of tagging. + +Two gates guard the tag, because a tag push publishes and a Maven Central release cannot be taken +back: + +* **Dry run before the release PR merges.** The publish workflow only ever runs on a tag push, so + ordinary CI can never tell you it is broken. Run it by hand on the release branch first (see the + steps below): same pipeline, no upload. +* **Green before tagging.** Tag only a commit whose `develop` workflows have *finished* green. + A release PR that was green is not the same thing as its merge commit being green. . Create a release branch from `develop`: + @@ -192,28 +202,61 @@ git checkout -b release/1.2.0 ** `gradle.properties` (`VERSION_NAME`) — its own commit, last, matching `release: X.Y.Z` ** Run final tests; fix any release-blocking bugs on the branch +. Dry-run the publish workflow on the release branch and wait for it to go green: ++ +[source,bash] +---- +git push origin release/1.2.0 +gh workflow run publish.yml --ref release/1.2.0 +gh run watch "$(gh run list --workflow publish.yml --branch release/1.2.0 --limit 1 --json databaseId -q '.[0].databaseId')" +---- ++ +A manually started run (`workflow_dispatch`) executes the whole release pipeline — native matrix, +Android SDK + NDK setup, signing — but ends in `publishToMavenLocal` on the runner and is never +given the Maven Central credentials, so it cannot upload. Its job summary lists the version and how +many modules, POMs and signatures it produced. This is the only way to find a broken runner setup +step *before* the tag exists (0.56.0 found one after). + . Open a PR from the release branch to `develop` and merge it once CI is green — this is what actually brings the version bump back into the integration branch: + [source,bash] ---- -git push origin release/1.2.0 gh pr create --base develop --head release/1.2.0 --title "Release 1.2.0" -# wait for CI to go green, then merge +# wait for CI to go green, then merge with a MERGE COMMIT (not squash, not rebase) +---- + +. Wait until every workflow on that merge commit has finished green on `develop` — the merge + commit is what gets tagged, and it has its own CI run: ++ +[source,bash] +---- +git fetch origin +MERGE_SHA="$(git rev-parse origin/develop)" # the "Merge pull request #… from …/release/1.2.0" commit +gh run list --commit "$MERGE_SHA" --json workflowName,status,conclusion \ + -q '.[] | "\(.workflowName): \(.conclusion // .status)"' +# every line must say "success" before you continue ---- -. Tag the release branch's own `release: X.Y.Z` commit directly — not a commit on `main`, which - doesn't have this release yet: +. Tag that merge commit with an annotated tag — not a commit on `main`, which doesn't have this + release yet, and not the release branch's `release: X.Y.Z` commit, which lacks whatever else + landed on `develop` alongside it: + [source,bash] ---- -git tag -a 1.2.0 -m "SKaiNET 1.2.0 — " +git tag -a 1.2.0 "$MERGE_SHA" -m "SKaiNET 1.2.0 — " git push origin 1.2.0 ---- + Pushing the tag triggers the publish workflow. **Wait for it to go green before the next step** — a red publish run means the tag exists but nothing actually shipped to Maven Central; do not treat tagging alone as "released." ++ +If the publish run fails: check Maven Central first. If *nothing* was uploaded under the version, +fix the cause on `develop` and move the tag to the new commit (delete and re-create it; tell anyone +who fetched it to `git fetch --tags --force`) — the workflow file that runs is the one at the +tagged commit, so re-running the old tag cannot pick up a workflow fix. If *anything* was uploaded, +the version is burned: leave the tag alone and cut a patch release. . Once the publish workflow succeeds, merge `main` up to the release — the step every release before 0.51.0 skipped: @@ -221,7 +264,7 @@ tagging alone as "released." [source,bash] ---- git fetch origin -git push origin :refs/heads/main # fast-forward; main has no protection to route around +git push origin "$MERGE_SHA":refs/heads/main # the tagged commit; fast-forward, main has no protection to route around ---- + This is a manual step by design (see the `main` Branch section above) — not automated on publish @@ -307,8 +350,10 @@ git branch -d hotfix/1.2.1 * Block merges if tests fail === Publishing -* A pushed tag (any branch, in practice always the `release/*` branch's own commit) is what - triggers the Maven Central publish workflow — not landing on `main`. +* A pushed tag (in practice always the release PR's merge commit on `develop`) is what triggers + the Maven Central publish workflow — not landing on `main`. +* The same workflow started by hand (`workflow_dispatch`) is a dry run: full pipeline into Maven + Local on the runner, never given the Maven Central credentials. * `main` is a read-only mirror of "what's been published," updated manually after the fact (see the Release Process above); it has no deploy step of its own. * `develop` is where CI runs on every push/PR; there is no separate staging deploy.