Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,45 @@ jobs:
# same surface. shellcheck is pre-installed on ubuntu-latest.
run: shellcheck --shell=sh scripts/install.sh

- name: Shell — shellcheck the release scripts
run: shellcheck scripts/version-sync.sh scripts/bump-version.sh scripts/release-lint.sh

# Release-readiness gate (scripts/release-lint.sh — the same checks the
# Release workflow's `version` job runs before publishing anything):
# - every PR/push: version coherence — version-sync.sh must be a no-op,
# so a hand-edited version in any single packaging site fails CI here
# instead of surfacing mid-release;
# - PRs that bump the workspace version (release/vX.Y.Z bump PRs): the
# full gate — CHANGELOG has a dated, non-empty section for the new
# version and the tag doesn't already exist.
release-readiness:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Lint release readiness
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
# Compare the workspace version against the PR base to detect a
# version bump. The shallow checkout doesn't have the base
# commit; fetch just that object.
git fetch --quiet --depth 1 origin "$BASE_SHA"
BASE_VERSION="$(git show "$BASE_SHA:Cargo.toml" | grep '^version = ' | head -1 | sed 's/version = "\(.*\)"/\1/')"
HEAD_VERSION="$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
if [ "$BASE_VERSION" != "$HEAD_VERSION" ]; then
echo "Version bump PR detected ($BASE_VERSION -> $HEAD_VERSION); running the full release gate."
bash scripts/release-lint.sh --tag-check
exit 0
fi
fi
bash scripts/release-lint.sh --sync-only

test:
strategy:
fail-fast: false
Expand Down
46 changes: 10 additions & 36 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,42 +64,16 @@ jobs:
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "Release version: $VERSION"

- name: Check tag does not exist at a different commit
env:
VERSION: ${{ steps.read.outputs.VERSION }}
run: |
# The checkout above is shallow and tagless, so local tag lookups
# prove nothing — ask the remote directly (stateless). Fail only
# when the tag exists at a DIFFERENT commit: a tag already at
# $GITHUB_SHA means a previous run of this workflow got as far as
# tagging, and re-running it (idempotent retry) must be allowed.
EXISTING_SHA="$(git ls-remote origin "refs/tags/v${VERSION}" | cut -f1)"
if [ -z "$EXISTING_SHA" ]; then
echo "Tag v${VERSION} does not exist yet."
elif [ "$EXISTING_SHA" = "$GITHUB_SHA" ]; then
echo "::notice::Tag v${VERSION} already exists at this commit; continuing as a retry of a previous run."
else
echo "::error::Tag v${VERSION} already exists at ${EXISTING_SHA} (this run is at ${GITHUB_SHA}). Bump the version in a PR first."
exit 1
fi

- name: Check CHANGELOG.md has release notes for version
env:
VERSION: ${{ steps.read.outputs.VERSION }}
run: |
if [ ! -f CHANGELOG.md ]; then
echo "::error::CHANGELOG.md does not exist at the repository root."
exit 1
fi
# A release requires an explicit `## [X.Y.Z]` / `## X.Y.Z` heading
# (notes written by hand in the version-bump PR).
if grep -qE "^## \[?${VERSION}\]?( |$)" CHANGELOG.md; then
echo "Found explicit CHANGELOG heading for ${VERSION}."
exit 0
fi
echo "::error::CHANGELOG.md has no release notes for ${VERSION}."
echo "::error::Add a \`## [${VERSION}] — $(date +%Y-%m-%d)\` heading with release notes before re-running."
exit 1
- name: Release-readiness gate
# scripts/release-lint.sh is the single source of truth for the
# version chores, shared with CI's release-readiness job (which runs
# it on the version-bump PR, so failures surface at PR time, not
# here). Checks: version coherence (version-sync.sh is a no-op),
# CHANGELOG has a non-empty section for this version, and — via
# --tag-check, asked of the remote since this checkout is shallow
# and tagless — the tag doesn't exist at a different commit (a tag
# already at $GITHUB_SHA is a retry of a previous run and passes).
run: bash scripts/release-lint.sh --tag-check

build:
needs: version
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Version Bump

# Opens the version-bump PR that precedes a release: runs
# scripts/bump-version.sh, which stamps the new version into every packaging
# site (scripts/version-sync.sh), rolls CHANGELOG.md's [Unreleased] notes into
# a dated `## [X.Y.Z]` section, and opens a `release/vX.Y.Z` PR.
#
# CAVEAT — PRs opened by this workflow's GITHUB_TOKEN do NOT trigger
# pull_request CI (GitHub suppresses events caused by that token, the same
# rule that shaped release.yml's one-workflow topology). To get CI on the PR:
# close and reopen it from the UI, or push any commit to the branch. Running
# `scripts/bump-version.sh <version> --pr` from a developer machine avoids
# the problem entirely and is the preferred path; this workflow exists so a
# bump can be started from the GitHub UI alone. See docs/releasing.md.

on:
workflow_dispatch:
inputs:
version:
description: 'New version (X.Y.Z, no leading v)'
required: true
type: string

permissions: {}

jobs:
bump:
runs-on: ubuntu-latest
permissions:
contents: write # push the release/vX.Y.Z branch
pull-requests: write # open the bump PR
steps:
- name: Checkout
# Intentionally persists credentials: bump-version.sh pushes the
# release branch with this workflow's GITHUB_TOKEN.
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Open the bump PR
env:
# Never interpolate the input into the script text (zizmor
# template-injection); bump-version.sh validates it is a plain
# X.Y.Z version before doing anything.
VERSION: ${{ inputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
bash scripts/bump-version.sh "$VERSION" --pr

- name: Remind about the CI caveat
run: |
echo "::notice title=Bump PR opened::pull_request CI does not run on PRs opened with GITHUB_TOKEN — close/reopen the PR (or push to its branch) to trigger the checks before merging."
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ history. For full per-release detail, see the
[GitHub releases page](https://github.com/SocketDev/socket-patch/releases).

The `Release` workflow refuses to publish a version that does not appear
in this file — see `.github/workflows/release.yml` (`version` job).
in this file — see `scripts/release-lint.sh` (run by the `version` job in
`.github/workflows/release.yml` and by CI on version-bump PRs). Bump PRs
are opened by `scripts/bump-version.sh`, which rolls `[Unreleased]` over
into the new version's section — see docs/releasing.md.

## [Unreleased]

Expand Down Expand Up @@ -44,6 +47,18 @@ in this file — see `.github/workflows/release.yml` (`version` job).

### Added

- **Version-bump automation + release-readiness gate.**
`scripts/bump-version.sh <X.Y.Z> --pr` performs the whole bump chore —
stamps every packaging site via `version-sync.sh`, rolls `[Unreleased]`
into a dated `## [X.Y.Z]` CHANGELOG section, and opens the `release/vX.Y.Z`
PR (also dispatchable from the Actions tab as the **Version Bump**
workflow). A new `release-readiness` CI job runs `scripts/release-lint.sh`
on every PR: version-coherence always (version-sync must be a no-op, so a
hand-edited version in any one packaging site fails CI), plus the full
gate — non-empty CHANGELOG section, no pre-existing tag — on PRs that bump
the workspace version. The `Release` workflow's `version` job now runs the
same script, so the publish gate and the PR gate cannot drift. Playbook:
docs/releasing.md.
- **Maven Central and NuGet distribution.** Two new install channels for the
CLI. Maven Central: `dev.socket:socket-patch`, a dependency-free launcher
jar — run via `java -jar` (fetch it with `mvn dependency:copy`) or in one
Expand Down
65 changes: 65 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Releasing socket-patch

One release = one version-bump PR + one dispatch of the **Release** workflow.
Every ecosystem package (crates.io, npm, PyPI, RubyGems ×2, Packagist, Maven
Central, NuGet) publishes from that single dispatch.

## 1. Open the version-bump PR

From a developer machine (preferred — CI runs on the PR normally):

```sh
scripts/bump-version.sh 3.4.0 --pr
```

This stamps `3.4.0` into every packaging site (`scripts/version-sync.sh`),
rolls `CHANGELOG.md`'s `[Unreleased]` notes into a dated `## [3.4.0]` section
(it refuses to run if `[Unreleased]` is empty — write the notes first), and
opens a `release/v3.4.0` PR whose body carries the rolled-over notes.

Alternatively, dispatch the **Version Bump** workflow from the Actions tab
(input: the new version). Caveat: a PR opened by a workflow's `GITHUB_TOKEN`
does not trigger `pull_request` CI — close/reopen the PR (or push any commit
to its branch) to kick the checks.

CI's `release-readiness` job runs the full release gate on the bump PR
(`scripts/release-lint.sh`): version coherence across all packaging sites,
a non-empty CHANGELOG section for the new version, and no pre-existing tag.
On every *other* PR the same job runs the coherence check only, so a
hand-edited version in any single site fails CI immediately.

## 2. Merge, then dispatch **Release**

Actions → **Release** → Run workflow (on the default branch). Optionally run
once with `dry-run: true` — that builds all 14 targets but skips tagging and
publishing.

The real run: re-verifies the release gate → builds the matrix → creates and
pushes `v<version>` → creates the GitHub release with `SHA256SUMS` → fans out
to all registries in parallel (OIDC everywhere except Maven Central, which
has no trusted-publishing option and uses the portal token + GPG key from the
`maven-central` environment).

## 3. Approve npm (the one manual step)

The npm job *stages* rather than publishes. Approve with 2FA — **platform
packages first, then `@socketsecurity/socket-patch`** — via the link in the
run's step summary, so optionalDependencies resolution never sees the main
package without its binaries. The launcher channels (gem, composer, maven,
nuget) go live without human action: they fetch binaries from the GitHub
release at run time.

## If a job fails mid-release

Fix the cause and use **"Re-run failed jobs"** on the same run. Every job is
idempotent: the tag re-push is a no-op, the GitHub release re-uploads with
`--clobber`, and each registry job probes for an already-published version
and skips it. A partial release never requires deleting tags or re-bumping.

## One-time registry setup

Environments, trusted publishers, the `dev.socket` namespace claim, the GPG
key, and the nuget.org policy are listed in the checklist of
[PR #138](https://github.com/SocketDev/socket-patch/pull/138). Until a
registry's credentials exist, its job skips with a `::notice` instead of
failing the release.
Loading
Loading