Skip to content

Request review of planned action #1873

Description

@edburns

Successor: #1937

Plan: Unify publish.yml to include Java via workflow_call

Background

Investigation in #1872 revealed:

  • publish.yml is a fan-out/fan-in parallel workflow: a shared version job feeds four concurrent publish jobs (Node.js, .NET, Python, Rust), which all fan into a single github-release job. Java is entirely absent.
  • java-publish-maven.yml is a standalone workflow_dispatch-only workflow with its own preflight, publish, GitHub release, and site-deployment jobs.
  • Releasing a new SDK version currently requires two separate manual workflow dispatches, with no enforcement that they use compatible versions.

Version scheme analysis

Inspection of Maven Central (com.github:copilot-sdk-java) and the git tags revealed that pre-1.0.0 GA, Java used its own -java.N suffix versioning (e.g., 1.0.0-beta-10-java.5). At 1.0.0 GA this divergence ended. Post-1.0.0, Java versions are identical to cross-language versions (1.0.1, 1.0.4, 1.0.5, 1.0.6-preview.1). No version translation is needed for normal releases.

Why Java must retain its own independently dispatchable workflow

The primary reason Java must keep java-publish-maven.yml as an independently dispatchable workflow_dispatch workflow is that Java's release mechanics are fundamentally different from every other language in this repo:

  • Java uses mvn release:prepare + mvn release:perform, which commit version bumps directly to main and push git tags mid-workflow.
  • This requires an elevated JAVA_RELEASE_TOKEN (a PAT with contents:write and actions:write) that is distinct from the standard GITHUB_TOKEN used by other publish jobs.
  • It requires GPG signing secrets (JAVA_GPG_SECRET_KEY, JAVA_GPG_PASSPHRASE) for Maven Central artifact signing.
  • It includes a Maven Central username/password (JAVA_MAVEN_CENTRAL_USERNAME, JAVA_MAVEN_CENTRAL_PASSWORD) for Sonatype deployment.
  • It runs a preflight job to validate these secrets before any release work begins.
  • It updates documentation files and commits them to main as part of the release, with a rollback step on failure.
  • It triggers a separate site deployment on a standalone repo (github/copilot-sdk-java).

None of the other language publish jobs commit to the repository, push tags, or require elevated tokens. They simply build, pack, and upload to their respective package registries.

A secondary illustration of why independent dispatch must be preserved: 1.0.5-01 on Maven Central has no cross-language counterpart, demonstrating that Java-only patch releases are a real operational need.

Approach selected: Option 3 — Add workflow_call to java-publish-maven.yml and call it from publish.yml, with fan-in failure mitigation

Three options were evaluated:

Option 1 — Add Java jobs directly into publish.yml
Rejected. Embedding mvn release:prepare/perform (with its mid-workflow git commits, elevated token requirements, GPG secrets, and rollback logic) into publish.yml would make it fragile and difficult to maintain.

Option 2 — Decompose all languages into per-language workflow_call files
Deferred. This is architecturally cleanest but requires extracting four new workflow files from working code — high churn and high risk of regression. Better suited as a follow-up once unification is achieved.

Option 3 — Keep publish.yml and java-publish-maven.yml as-is; add workflow_call to the Java workflow and invoke it from publish.yml ✅ Selected.

This approach:

  • Makes the minimal change needed to achieve the stated goal (single dispatch for all languages)
  • Leaves all working workflow code intact
  • Preserves java-publish-maven.yml as an independently dispatchable workflow_dispatch workflow
  • Requires no version translation since post-1.0.0 versions are identical across languages
  • Adds workflow_call as an additive trigger, not a replacement

Fan-in failure mitigation

When publish-java is added to the needs list of github-release in publish.yml, a Java failure after NPM/NuGet/PyPI/Rust have already published would block the creation of the cross-language vX.Y.Z GitHub release, leaving other ecosystems without a corresponding release entry.

Mitigation: Use always() with explicit per-language status checks in the github-release job's if condition. Java is listed in needs (so it runs, its result is visible in the workflow summary, and the job waits for it to complete before proceeding) but its success is not required to create the GitHub release. A Java failure surfaces as a visible failure in the workflow without preventing the release entry from being created.

The relevant if expression is shown in Implementation step 4 below.

Implementation steps

All changes are in .github/workflows/java-publish-maven.yml and .github/workflows/publish.yml.

1. Add workflow_call trigger to java-publish-maven.yml

Add a workflow_call event alongside the existing workflow_dispatch. The inputs must mirror the workflow_dispatch inputs exactly so that inputs.* works for both trigger types:

on:
  workflow_dispatch:
    inputs:
      releaseVersion:
        description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT"
        required: false
        type: string
      developmentVersion:
        description: "Next development version (e.g., 1.0.1-SNAPSHOT). If empty, increments patch version"
        required: false
        type: string
      prerelease:
        description: "Is this a prerelease?"
        type: boolean
        required: false
        default: false
  workflow_call:
    inputs:
      releaseVersion:
        description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT"
        required: false
        type: string
      developmentVersion:
        description: "Next development version (e.g., 1.0.1-SNAPSHOT). If empty, increments patch version"
        required: false
        type: string
      prerelease:
        description: "Is this a prerelease?"
        type: boolean
        required: false
        default: false

2. Replace github.event.inputs.* with inputs.* in java-publish-maven.yml

The inputs.* context works for both workflow_dispatch and workflow_call; github.event.inputs.* only works for workflow_dispatch. Globally replace every occurrence of github.event.inputs. with inputs. throughout the file. Verify no instances remain after the substitution.

3. Add a publish-java job to publish.yml

Add the following job. Note if: github.event.inputs.dist-tag != 'unstable' matches the guard used on all other non-Node publish jobs:

publish-java:
  name: Publish Java SDK
  if: github.event.inputs.dist-tag != 'unstable'
  needs: version
  uses: ./.github/workflows/java-publish-maven.yml
  with:
    releaseVersion: ${{ needs.version.outputs.version }}
    prerelease: ${{ github.event.inputs.dist-tag == 'prerelease' }}
  secrets: inherit

4. Update github-release in publish.yml to use always() with explicit status checks

Replace the existing needs and if on the github-release job with the following. Java is present in needs so it runs and is awaited, but only the four original languages are required to succeed for the release to be created:

github-release:
  name: Create GitHub Release
  needs: [version, publish-nodejs, publish-dotnet, publish-python, publish-rust, publish-java]
  if: |
    always() &&
    github.ref == 'refs/heads/main' &&
    github.event.inputs.dist-tag != 'unstable' &&
    needs.version.result == 'success' &&
    needs.publish-nodejs.result == 'success' &&
    needs.publish-dotnet.result == 'success' &&
    needs.publish-python.result == 'success' &&
    needs.publish-rust.result == 'success'

The always() call is required. Without it, GitHub Actions automatically skips the job when any job in needs fails — which would defeat the mitigation.

5. Verify inputs.* compatibility

After making the substitutions in step 2, confirm that every former github.event.inputs.* reference has been replaced and that no github.event.inputs string remains in java-publish-maven.yml.

6. Test

  • Dispatch publish.yml with dist-tag: unstable and confirm the publish-java job is skipped (matching the existing behaviour for publish-dotnet, publish-rust, publish-python).
  • Inspect the workflow graph to confirm publish-java appears in the dependency chain and that github-release lists it in needs.

What is explicitly preserved

  • java-publish-maven.yml remains independently dispatchable via workflow_dispatch, preserving Java's fundamentally different release mechanics as a self-contained unit.
  • The Java workflow's own github-release and deploy-site jobs remain within java-publish-maven.yml; they are not moved to publish.yml.
  • All existing secrets and token requirements for the Java workflow are unchanged (secrets: inherit passes them through).
  • A Java publish failure does not block the cross-language vX.Y.Z GitHub release entry from being created for the other four language SDKs.

Activity

  1. self-assigned this
    on Jul 1, 2026
  2. added theissue type on Jul 1, 2026
  3. github-actions commented on Jul 1, 2026

    @github-actions
    Contributor

    👋 Thanks for opening this issue! Unfortunately, I wasn't able to automatically classify it into one of the standard categories (bug, enhancement, question, or documentation).

    Reason: The issue title "Request review of planned action" with no body appears to be a meta or process request rather than a bug report, feature request, usage question, or documentation issue. This falls outside the scope of automated classification.

    A human maintainer will review this issue shortly.

    Generated by Issue Classification Agent for issue #1873 · sonnet46 303.9K · ◷

  4. edburns commented on Jul 7, 2026

    @edburns
    CollaboratorAuthor

    @SteveSandersonMS wrote:

    Seems like a fairly minimal change so it sounds OK to me.

    My one concern is that, if Java's release process is so much more complex with all these extra moving parts (and it is not good that it involves mutating the main branch), to what extent will it be prone to failure?

     Add publish-java to the needs list of the github-release job so the GitHub release is not created until all languages (including Java) have published successfully.

    This means if the Java publish fails, we will have shipped packages to NPM, NuGet, etc, but then we'll fail to create a corresponding GitHub release entry and release notes, leaving other package ecosystems in a bad state. I know the same is true for others too (e.g., if the PyPI publish fails, as it has done sometimes, we end up with a corresponding problem).

    There might not be much we can do given there's no atomic commit across multiple package ecosystems, but if it's a thing that actually happens in practice, we could change the Java publish flow so that it just creates the publishable artifact within the GitHub release, and then triggers an independent continuation job that deploys it to Maven. If the latter fails, we can retry it any number of times without it breaking things for the other languages, and without blocking the production of a GitHub release in the first place.

    Fine to try it as-is first though.

  5. brunoborges commented on Jul 7, 2026

    @brunoborges
    Collaborator

    I think it is OK to move away from maven-release-plugin, but in the short term, we should stick to it because it works, it is proven, and it is automated. Later on, we can look into a new CI release process to accommodate the monorepo process.

  6. edburns commented on Jul 7, 2026

    @edburns
    CollaboratorAuthor

    Analysis of SteveSanderson's Mitigation

    The Core Observation (It's Correct But Hard)

    His instinct is sound: the thing that must not fail is the GitHub release creation. Maven Central deployment is a distribution step that can be retried; a missing vX.Y.Z release entry cannot easily be patched after the fact.

    However, implementing his mitigation properly is much harder than it appears because of how mvn release:prepare + mvn release:perform work as a coupled unit.

    ────────────────────

    Why Decoupling Is Non-Trivial

    release:prepare does:

    1. Removes -SNAPSHOT, commits version bump to main
    2. Creates and pushes the java/vX.Y.Z git tag
    3. Bumps to next -SNAPSHOT, commits to main
    4. Writes release.properties to the working directory

    release:perform does:

    1. Reads release.properties to find the tag
    2. Checks out that tag into target/checkout/
    3. Builds and GPG-signs all artifacts
    4. Deploys signed JARs/POMs to Maven Central via the Sonatype API

    These two phases are designed to run in the same workspace. If you split them across workflow jobs or across workflows, release.properties must be passed as an artifact, or release:perform must be replaced with a manual equivalent (git checkout java/vX.Y.Z && mvn deploy -Prelease).

    ────────────────────

    What "Decouple" Would Actually Require

    SteveSanderson describes: "just creates the publishable artifact within the GitHub release, and then triggers an independent continuation job that deploys it to Maven."

    This means a 3-phase architecture:

    ┌─────────────────────┬──────────────────────────────────────────────────────────┬───────────────────────────────┐
    │ Phase               │ Does                                                     │ Blocking?                     │
    ├─────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────┤
    │ Phase 1 — prepare   │ release:prepare (commits, tags), build JARs, attach to   │ Blocks github-release in      │
    │                     │ GitHub release                                           │ publish.yml                   │
    ├─────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────┤
    │ Phase 2 — GitHub    │ publish.yml's github-release creates vX.Y.Z              │ Can run after Phase 1         │
    │ release             │                                                          │                               │
    ├─────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────┤
    │ Phase 3 — deploy    │ Download JARs from GitHub release, mvn deploy to Maven   │ Independently retryable       │
    │                     │ Central                                                  │                               │
    └─────────────────────┴──────────────────────────────────────────────────────────┴───────────────────────────────┘
    

    Problems:

    • The current workflow never materializes JARs as GitHub release assets — Maven Central gets them directly from release:perform. Adding a "build and attach" step requires producing signed JARs before deployment, which partially duplicates what release:perform does internally.
    • Phase 3 would need GPG signing to happen at artifact-creation time (Phase 1), and then Maven Central would receive pre-signed artifacts. This is doable but requires restructuring the Maven build profile.
    • This is a significant rework of java-publish-maven.yml — at minimum 3–4 new steps, artifact upload/download, and a new independently-dispatchable perform-only workflow.

    ────────────────────

    The Status Quo Risk (Already Accepted for Other Languages)

    The exact same fan-in failure already exists today:

    • If PyPI upload fails, publish.yml's github-release is already blocked (it's in needs)
    • The team has accepted this

    Java's risk is slightly higher because:

    1. release:prepare mutates main before Maven Central succeeds — so a Java failure leaves main with a version bump but no deployed artifact
    2. The rollback step (revert + release:rollback) helps but adds its own failure mode

    However: Java already has the rollback mechanism, and java-publish-maven.yml is independently dispatchable — a failed Java publish can be retried without re-running the other four languages.

    ────────────────────

    My Recommendation

    Proceed with Option 3 as planned (SteveSanderson's "fine to try it as-is first"), with one small tactical addition:

    In publish.yml, make github-release use if: always() with explicit status checks rather than strict dependency failure propagation:

    github-release:
      needs: [version, publish-nodejs, publish-dotnet, publish-python, publish-rust, publish-java]
      if: |
        github.ref == 'refs/heads/main' &&
        github.event.inputs.dist-tag != 'unstable' &&
        needs.publish-nodejs.result == 'success' &&
        needs.publish-dotnet.result == 'success' &&
        needs.publish-python.result == 'success' &&
        needs.publish-rust.result == 'success'
        # Java is intentionally NOT required here
    

    This means:

    • Java runs in parallel and its failure is surfaced in the workflow summary
    • But it does not block the cross-language vX.Y.Z GitHub release
    • Java's own github-release job (inside java-publish-maven.yml) still creates the java/vX.Y.Z release if Java succeeds

    The full SteveSanderson decoupling is the right long-term architecture but is a ~2-sprint refactor that should be a follow-up issue. It's premature to block the unification work on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions