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
30 changes: 30 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,38 @@ env:
REGISTRY_IMAGE: cipherstash/proxy

jobs:
verify-release:
name: Verify release metadata
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin action to commit SHA and disable credential persistence.

Two security concerns:

  1. The action reference @v4 is not pinned to an immutable commit SHA, violating the security policy flagged by static analysis. Mutable tags can be updated maliciously.
  2. Missing persist-credentials: false means the GitHub token persists in .git/config and could leak through uploaded artifacts.
🔒 Proposed fix to pin action and disable credential persistence
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      with:
+        persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 24-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 24, Update the workflow step that uses
actions/checkout@v4 to reference an immutable commit SHA instead of the mutable
tag and add persist-credentials: false to the checkout step; specifically locate
the checkout usage (the line containing actions/checkout@v4) and replace the tag
with the pinned commit SHA (actions/checkout@<commit-sha>) and add the
persist-credentials: false entry under that step to ensure credentials are not
left in .git/config.


# Only enforced for numbered releases. On push/PR/workflow_dispatch this
# job is a no-op so it can still gate the build matrix below.
- name: Check version + changelog match the release tag
if: github.event_name == 'release'
run: |
tag='${{ github.event.release.tag_name }}'
version="${tag#v}"

Comment on lines +31 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate tag format before use to prevent potential code injection.

Directly interpolating github.event.release.tag_name into shell context without validation could allow code injection if the tag contains shell metacharacters (e.g., quotes, semicolons). While GitHub's tag restrictions reduce this risk, defense-in-depth suggests validating the expected vX.Y.Z format before using the value.

🛡️ Proposed fix to validate tag format
       run: |
         tag='${{ github.event.release.tag_name }}'
+        # Validate tag format before using it
+        if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
+          echo "::error::Release tag must match vX.Y.Z format (with optional pre-release suffix), got: $tag"
+          exit 1
+        fi
         version="${tag#v}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tag='${{ github.event.release.tag_name }}'
version="${tag#v}"
tag='${{ github.event.release.tag_name }}'
# Validate tag format before using it
if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Release tag must match vX.Y.Z format (with optional pre-release suffix), got: $tag"
exit 1
fi
version="${tag#v}"
🧰 Tools
🪛 zizmor (1.25.2)

[error] 31-31: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 31 - 33, Validate the incoming
tag value before using it: before computing version from tag (the variable named
tag and the expansion version="${tag#v}"), add a check that tag matches the
strict vX.Y.Z pattern (e.g., starting with "v" followed by numeric
major.minor.patch) and fail the job with a clear error if it does not; only
strip the leading "v" and set version when the regex check passes to avoid using
untrusted content in the shell context.

cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
if [ "$cargo_version" != "$version" ]; then
echo "::error::Cargo.toml workspace version ($cargo_version) does not match release tag $tag. Bump the version in a prepare-release PR before tagging."
exit 1
fi

# Fixed-string match so dots in the version aren't treated as regex wildcards.
if ! grep -qF "## [$version]" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no '## [$version]' section. Add release notes in a prepare-release PR before tagging."
exit 1
fi

echo "OK: tag $tag matches Cargo.toml version and CHANGELOG has a [$version] section."
Comment on lines +19 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add explicit permissions block to follow least-privilege principle.

The verify-release job inherits default permissions, granting broader access than necessary. Limit permissions to only what's required: reading repository contents.

🔒 Proposed fix to restrict permissions
 verify-release:
   name: Verify release metadata
   runs-on: ubuntu-latest
   timeout-minutes: 5
+  permissions:
+    contents: read
   steps:
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 24-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 19-46: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[error] 31-31: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 19 - 46, The verify-release job
currently inherits default permissions; add an explicit permissions block for
the job named verify-release that restricts permissions to only what’s needed to
read the repository (e.g., set contents: read) so the workflow follows
least-privilege; place the permissions: block directly under the verify-release
job definition to limit access for the steps that check Cargo.toml and
CHANGELOG.md.


build:
name: Build binaries + Docker images
needs: verify-release
strategy:
fail-fast: false
matrix:
Expand Down
38 changes: 30 additions & 8 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,29 +735,51 @@ Note: not all errors do this at the moment, and we will change over time.

Releases are published via GitHub Actions when a GitHub release is created.

### Using mise (recommended)
A release happens in two steps: first a **prepare-release PR** that records the
version and notes, then **tagging** that merged commit.

### 1. Prepare-release PR

Open a PR against `main` that:

1. Bumps `version` under `[workspace.package]` in the root `Cargo.toml` (and runs
`cargo update --workspace` so `Cargo.lock` matches).
2. Adds a `## [X.Y.Z]` section to `CHANGELOG.md` describing the user-facing changes.

Both are required: the release tooling (and the release workflow) will refuse to
publish a tag whose version isn't reflected in `Cargo.toml` and `CHANGELOG.md`.

### 2. Cut the release (recommended)

Once the prepare-release PR is merged, from an up-to-date `main`:

```bash
mise run release v2.1.9
mise run release vX.Y.Z
```

This will:
This verifies you're on a clean, in-sync `main`, that `Cargo.toml` and
`CHANGELOG.md` already describe `X.Y.Z`, and that the tag doesn't already exist.
If those checks pass it will:
1. Create a git tag for the version
2. Push the tag to origin
3. Create a GitHub release with auto-generated notes
4. Trigger the release workflow which builds and publishes Docker images

The release workflow re-runs the same version/changelog check (the
`verify-release` job) before building, so a mismatched tag fails fast without
publishing anything.

### Manual release

If you need more control over the release process:
If you need more control, the steps the task automates are:

```bash
# Create and push the tag
git tag v2.1.9
git push origin v2.1.9
# Create and push the tag (from the merged prepare-release commit on main)
git tag vX.Y.Z
git push origin vX.Y.Z

# Create the GitHub release
gh release create v2.1.9 --generate-notes
gh release create vX.Y.Z --generate-notes
```

### Re-releasing a version
Expand Down
50 changes: 47 additions & 3 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -700,15 +700,59 @@ mise --env tls run proxy:down
"""

[tasks.release]
description = "Create a GitHub release"
description = "Create a GitHub release (run after the prepare-release PR is merged)"
run = """
#!/usr/bin/env bash
set -euo pipefail

VERSION="${1:?Version required, e.g., mise run release v2.1.9}"
VERSION="${1:?Version required, e.g., mise run release v2.2.2}"

echo "Creating release $VERSION..."
# Tag must be vMAJOR.MINOR.PATCH with an optional pre-release suffix.
if [[ ! "$VERSION" =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
echo "error: version must look like v2.2.2 (got '$VERSION')" >&2
exit 1
fi
BARE="${VERSION#v}"

# Releases are cut from a clean, up-to-date main.
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [ "$BRANCH" != "main" ]; then
echo "error: releases must be cut from main (currently on '$BRANCH')" >&2
exit 1
fi
if [ -n "$(git status --porcelain)" ]; then
echo "error: working tree is not clean; commit or stash changes first" >&2
exit 1
fi
git fetch origin main --quiet
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
echo "error: local main is not in sync with origin/main; pull first" >&2
exit 1
fi

# The prepare-release PR must have bumped the version and added release notes.
CARGO_VERSION="$(sed -n 's/^version = "\\(.*\\)"/\\1/p' Cargo.toml | head -1)"
if [ "$CARGO_VERSION" != "$BARE" ]; then
echo "error: Cargo.toml workspace version ($CARGO_VERSION) does not match $VERSION" >&2
echo " bump the version in a prepare-release PR and merge it before releasing" >&2
exit 1
fi
# Fixed-string match so dots in the version aren't treated as regex wildcards.
if ! grep -qF "## [$BARE]" CHANGELOG.md; then
echo "error: CHANGELOG.md has no '## [$BARE]' section" >&2
echo " add release notes in a prepare-release PR and merge it before releasing" >&2
exit 1
fi
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
echo "error: tag $VERSION already exists locally" >&2
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "error: tag $VERSION already exists on origin" >&2
exit 1
fi

echo "Releasing $VERSION (verified version + changelog)..."
git tag "$VERSION"
git push origin "$VERSION"
gh release create "$VERSION" --generate-notes
Expand Down
Loading