name: release-publish # What both release pipelines do once they know which commit to cut and what to # call it. ci.yml calls this on every green master run to advance the patch; # release.yml calls it by hand to promote the last release to a minor or major. # Neither holds a copy of these steps, because two copies of a publish drift and # the drift only shows up on a release. on: workflow_call: inputs: bump: description: Which part of MAJOR.MINOR.PATCH to advance type: string default: patch sha: description: >- The commit to release. Empty means the commit the last release was cut from, which is what promoting a run of patches to a milestone does. type: string default: "" secrets: NPM_TOKEN: required: true permissions: contents: read jobs: # Nothing is published until the tag is pushed, so a version that cannot be # tagged never reaches a registry. npm is the half of a release that cannot be # taken back and a tag is the half that can. tag: name: Tag runs-on: ubuntu-latest # The two pipelines count their version off the same tags, so they must not # resolve one at the same time. This sits on the job rather than on either # caller because a caller's group covers only its own runs, and ci's release # runs under ci's group. Queued rather than cancelled: a promotion landing # first simply means the next merge counts its patch off the milestone. concurrency: group: release-tag cancel-in-progress: false permissions: contents: write outputs: version: ${{ steps.next.outputs.version }} tag: ${{ steps.next.outputs.tag }} previous_tag: ${{ steps.next.outputs.previous_tag }} steps: - uses: actions/checkout@v7 with: # The version is counted off the tags, so the tags have to be here. fetch-depth: 0 - name: Resolve the version id: next run: .github/scripts/next-version.sh env: BUMP: ${{ inputs.bump }} # A promotion re-cuts the commit that is already released, so it needs no # ci run of its own: that commit is only tagged because ci went green on # it. A repository with nothing released yet has nothing to promote, and # saying so beats tagging whatever master happens to be. - name: Tag the commit run: | target="$SHA" if [ -z "$target" ]; then if [ -z "$RELEASED_TAG" ]; then echo "release: nothing has been released yet, so there is no release to promote" >&2 exit 1 fi target="$RELEASED_TAG^{commit}" echo "release: promoting $RELEASED_TAG to $TAG" fi git -c user.name='github-actions[bot]' \ -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ tag -a "$TAG" "$target" -m "$TAG" git push origin "refs/tags/$TAG" env: SHA: ${{ inputs.sha }} TAG: ${{ steps.next.outputs.tag }} RELEASED_TAG: ${{ steps.next.outputs.released_tag }} npm: name: Release (npm) needs: tag runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: ref: ${{ needs.tag.outputs.tag }} # `npm ci` below runs dependency lifecycle scripts, and no step in # this job needs the git credential afterwards. persist-credentials: false - name: Set up Node 22 uses: actions/setup-node@v7 with: node-version: "22" registry-url: "https://registry.npmjs.org" cache: npm cache-dependency-path: pkg/spec/package-lock.json - name: Install dependencies working-directory: pkg/spec run: npm ci # The repo keeps package.json at 0.0.0-dev. The tags are the record of # what has been released, and a version committed to master would be a # second record to hold in step with them. - name: Stamp the version working-directory: pkg/spec run: npm version "$VERSION" --no-git-tag-version --allow-same-version env: VERSION: ${{ needs.tag.outputs.version }} # A publish that landed and then failed on its way out leaves npm holding # the version, and re-running the job must not be red for it. The registry # is asked rather than the tags: only npm knows what npm has. Only stdout # decides, because `npm view` on a version that does not exist is empty on # some npm releases and an error on others, and an unreachable registry # must end in a publish that fails loudly rather than a skip that reads as # success. - name: Ask npm whether this version is already published id: published run: | if [ -n "$(npm view "@sanderling/spec@$VERSION" version 2>/dev/null || true)" ]; then echo "npm already has @sanderling/spec@$VERSION, nothing to publish" echo "publish=false" >> "$GITHUB_OUTPUT" else echo "publish=true" >> "$GITHUB_OUTPUT" fi env: VERSION: ${{ needs.tag.outputs.version }} - name: Publish @sanderling/spec to npm if: steps.published.outputs.publish == 'true' working-directory: pkg/spec run: npm publish --access public # The publish credential is scoped to the one step that publishes rather # than to the job, so no other step runs with it in reach. env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} cli: name: Release (cli) needs: tag runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v7 with: ref: ${{ needs.tag.outputs.tag }} # GoReleaser reads the tag history for its changelog. fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v7 with: go-version-file: go.mod cache: true - name: Set up JDK 17 uses: actions/setup-java@v5 with: distribution: temurin java-version: "17" - name: Set up Android SDK uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - name: Cache Gradle uses: actions/cache@v6 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | gradle-${{ runner.os }}- - name: Build sidecar JAR run: make sidecar # GoReleaser reaches back to the release before this one on its own, which # is right for a patch and wrong for a milestone: a promotion tags a commit # that is already tagged, so the notes would cover the single merge that # produced the last patch. GORELEASER_PREVIOUS_TAG moves that boundary back # to the last release at this one's level, and an empty value leaves # GoReleaser on its own default, which is what a patch passes. - name: Publish the sanderling CLI to GitHub Releases uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_PREVIOUS_TAG: ${{ needs.tag.outputs.previous_tag }}