Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
233cad3
Initial plan
Copilot Nov 5, 2025
a1e89e1
feat: add automated release pipeline with GitHub Actions
Copilot Nov 5, 2025
4ea43c4
feat: add version verification and secret validation to release workflow
Copilot Nov 5, 2025
0c0c935
fix: improve prerelease detection and version verification in workflow
Copilot Nov 5, 2025
eba52c8
refactor: move permissions to job level for least privilege
Copilot Nov 5, 2025
fd75af4
security: add workflow-level permissions lock and disable bundler cache
Copilot Nov 5, 2025
96a34a4
refactor: split workflow into separate jobs for better modularity
Copilot Nov 5, 2025
a1f6483
Update .github/workflows/release.yml
jkowalleck Dec 4, 2025
f851a5c
Update .github/workflows/release.yml
jkowalleck Dec 4, 2025
5a95290
Update .github/workflows/release.yml
jkowalleck Dec 4, 2025
5fc0fd3
refactor: replace 3rd party release action with GitHub CLI
Copilot Dec 4, 2025
b89b7e1
Merge latest master into PR branch
Copilot May 30, 2026
7b6238e
Remove Gemfile.lock from repository tracking
Copilot May 30, 2026
a1cc25f
Enable RubyGems trusted publishing
Copilot May 30, 2026
44f31e2
Clarify trusted publishing documentation
Copilot May 30, 2026
2459eb0
Update release workflow for trusted publishing and job dependency
Copilot May 30, 2026
187650f
Update CONTRIBUTING with automated release process
Copilot May 30, 2026
6fccf2f
Update gem release workflow to RubyGems trusted publishing pattern
Copilot May 30, 2026
1f9577a
Document required release environment in workflow
Copilot May 30, 2026
2f751db
Restore full release pipeline and RubyGems trusted publish job
Copilot May 30, 2026
3341a14
Fix GitHub release artifact paths in workflow
Copilot May 30, 2026
819daa2
Make GitHub release artifact discovery path-agnostic
Copilot May 30, 2026
5585ee5
Harden GitHub release asset collection command
Copilot May 30, 2026
1aa20e7
Scope release asset lookup to downloaded artifact path
Copilot May 30, 2026
35584f7
Require single gem and checksum artifact for GitHub release
Copilot May 30, 2026
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
89 changes: 89 additions & 0 deletions .github/RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Release Process

This repository uses an automated release pipeline via GitHub Actions.

## How It Works

The release workflow (`.github/workflows/release.yml`) is triggered when a version tag is pushed to the repository. The workflow:

1. **Runs tests** - Ensures all tests pass before releasing
2. **Builds the gem** - Creates the `.gem` package using `bundle exec rake build`
3. **Generates checksums** - Creates SHA-512 checksums for the gem package
4. **Publishes to RubyGems** - Uses RubyGems trusted publishing (OIDC) and pushes the gem to [rubygems.org](https://rubygems.org)
5. **Creates GitHub Release** - Publishes a release on GitHub with the gem and checksums as artifacts, only after RubyGems publish succeeds

## Triggering a Release

### For Stable Releases

1. Update the version in `lib/cyclonedx/ruby/version.rb`
2. Update `CHANGELOG.md` with the changes
3. Commit the changes: `git commit -am "🔖 Prepare release v1.2.0"`
4. Create and push a tag: `git tag v1.2.0 && git push origin v1.2.0`

### For Prereleases

Prereleases follow the same process but use a tag with a prerelease identifier:

- Alpha: `git tag v1.3.0-alpha.1 && git push origin v1.3.0-alpha.1`
- Beta: `git tag v1.3.0-beta.1 && git push origin v1.3.0-beta.1`
- Release Candidate: `git tag v1.3.0-rc.1 && git push origin v1.3.0-rc.1`

Prereleases are automatically detected by the workflow and marked as "prerelease" on GitHub.

## Version Tag Format

- **Stable releases**: `v<MAJOR>.<MINOR>.<PATCH>` (e.g., `v1.2.0`)
- **Prereleases**: `v<MAJOR>.<MINOR>.<PATCH>-<PRERELEASE>` (e.g., `v1.3.0-alpha.1`)

The version in the tag must match the version in `lib/cyclonedx/ruby/version.rb`.

## RubyGems Trusted Publishing Setup

The release workflow uses RubyGems trusted publishing via GitHub Actions OIDC.
No `RUBYGEMS_API_KEY` repository secret is required.

Before the first release, configure a trusted publisher for the `cyclonedx-ruby` gem on [rubygems.org](https://rubygems.org) and point it at this repository (`CycloneDX/cyclonedx-ruby-gem`):

1. Open the gem's trusted publishing settings on RubyGems.org.
2. Add a GitHub Actions trusted publisher for this repository:
- Owner: `CycloneDX`
- Repository: `cyclonedx-ruby-gem`
- Workflow file: `.github/workflows/release.yml`
3. Save the publisher configuration on RubyGems.org.

**Note**: The RubyGems publishing job only runs on the official repository (`CycloneDX/cyclonedx-ruby-gem`) to prevent accidental publishes from forks.

## Release Artifacts

Each release includes the following artifacts:

1. **Gem Package** (`cyclonedx-ruby-<version>.gem`) - The built Ruby gem
2. **SHA-512 Checksum** (`cyclonedx-ruby-<version>.gem.sha512`) - Checksum for verification

These artifacts are attached to the GitHub Release and can be downloaded for verification.

## Monitoring Releases

- **GitHub Actions**: Check the [Actions tab](https://github.com/CycloneDX/cyclonedx-ruby-gem/actions) for workflow runs
- **GitHub Releases**: View all releases in the [Releases section](https://github.com/CycloneDX/cyclonedx-ruby-gem/releases)
- **RubyGems**: Check [rubygems.org/gems/cyclonedx-ruby](https://rubygems.org/gems/cyclonedx-ruby) for published versions

## Troubleshooting

### Release workflow fails on tests

The workflow will not create a release if tests fail. Fix the failing tests and push a new commit, then create the tag again.

### Gem fails to publish to RubyGems

Check that:
- Trusted publishing is configured for `CycloneDX/cyclonedx-ruby-gem` on RubyGems.org
- The workflow has permission to request an OIDC token
- The gem version doesn't already exist on RubyGems (versions cannot be overwritten)

If RubyGems publishing fails, the GitHub Release job will not run.

### Prerelease not detected correctly

The workflow detects prereleases by checking if the version matches the exact pattern `MAJOR.MINOR.PATCH` (e.g., `1.2.3`). Any version that includes additional characters after the patch version (e.g., `1.2.3-alpha.1`, `1.2.3.rc1`, `1.2.3-beta`) is automatically marked as a prerelease.
163 changes: 163 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
name: Release

run-name: Release ${{ github.ref_name }}

on:
push:
tags:
- 'v*'

permissions: {}

jobs:
validate:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.VERSION }}
prerelease: ${{ steps.prerelease.outputs.PRERELEASE }}
steps:
- uses: actions/checkout@v5

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: false

- name: Install dependencies
run: bundle install --jobs 4 --retry 3

- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

- name: Verify version matches code
run: |
TAG_VERSION="${{ steps.version.outputs.VERSION }}"
CODE_VERSION=$(bundle exec ruby -r ./lib/cyclonedx/ruby/version.rb -e 'puts Cyclonedx::Ruby::VERSION')
echo "Tag version: $TAG_VERSION"
echo "Code version: $CODE_VERSION"
if [ "$TAG_VERSION" != "$CODE_VERSION" ]; then
echo "::error::Version mismatch! Tag is v$TAG_VERSION but code version is $CODE_VERSION"
exit 1
fi

- name: Determine if prerelease
id: prerelease
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
# A version is a prerelease if it contains anything after the patch version
# e.g., 1.2.3-alpha.1, 1.2.3.rc1, 1.2.3-beta are all prereleases
# but 1.2.3 is a stable release
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "PRERELEASE=false" >> $GITHUB_OUTPUT
echo "Detected stable release: $VERSION"
else
echo "PRERELEASE=true" >> $GITHUB_OUTPUT
echo "Detected prerelease: $VERSION"
fi

test:
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: false

- name: Install dependencies
run: bundle install --jobs 4 --retry 3

- name: Run tests
run: bundle exec rake test

build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: false

- name: Install dependencies
run: bundle install --jobs 4 --retry 3

- name: Build gem
run: bundle exec rake build

- name: Generate checksum
run: bundle exec rake build:checksum

- name: List build artifacts
run: ls -lh pkg/ checksums/

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: gem-package
path: |
pkg/*.gem
checksums/*.sha512

release-rubygems:
needs: [validate, build]
runs-on: ubuntu-latest
if: github.repository == 'CycloneDX/cyclonedx-ruby-gem'
permissions:
contents: write
id-token: write
# If you configured a GitHub environment on RubyGems, you must use it here.
environment: release
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: ruby

- uses: rubygems/release-gem@v1

release-github:
needs: [validate, build, release-rubygems]
runs-on: ubuntu-latest
# GitHub release creation requires contents:write to create releases
permissions:
contents: write
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: gem-package
path: release-assets

- name: Create GitHub Release
run: |
PRERELEASE_FLAG=""
mapfile -t GEM_FILES < <(find release-assets -type f -name '*.gem')
mapfile -t CHECKSUM_FILES < <(find release-assets -type f -name '*.sha512')
if [ "${#GEM_FILES[@]}" -ne 1 ] || [ "${#CHECKSUM_FILES[@]}" -ne 1 ]; then
echo "::error::Expected exactly one .gem and one .sha512 from build artifact download"
exit 1
fi
if [ "${{ needs.validate.outputs.prerelease }}" = "true" ]; then
PRERELEASE_FLAG="--prerelease"
fi
gh release create "${{ github.ref_name }}" \
"${GEM_FILES[@]}" "${CHECKSUM_FILES[@]}" \
--title "${{ github.ref_name }}" \
--generate-notes \
$PRERELEASE_FLAG
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 changes: 12 additions & 30 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,36 +88,18 @@ Made with [contributors-img][🖐contrib-rocks].

### To release a new version:

#### Automated process

Coming Soon!

#### Manual process

1. Run `bin/setup && bin/rake` as a "test, coverage, & linting" sanity check
2. Update the version number in `version.rb`, and ensure `CHANGELOG.md` reflects changes
3. Run `bin/setup && bin/rake` again as a secondary check, and to update `Gemfile.lock`
4. Run `git commit -am "🔖 Prepare release v<VERSION>"` to commit the changes
5. Run `git push` to trigger the final CI pipeline before release, and merge PRs
- NOTE: Remember to [check the build][🧪build].
6. Run `export GIT_TRUNK_BRANCH_NAME="$(git remote show origin | grep 'HEAD branch' | cut -d ' ' -f5)" && echo $GIT_TRUNK_BRANCH_NAME`
7. Run `git checkout $GIT_TRUNK_BRANCH_NAME`
8. Run `git pull origin $GIT_TRUNK_BRANCH_NAME` to ensure latest trunk code
9. Optional for older Bundler (< 2.7.0): Set `SOURCE_DATE_EPOCH` so `rake build` and `rake release` use the same timestamp and generate the same checksums
- If your Bundler is >= 2.7.0, you can skip this; builds are reproducible by default.
- Run `export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH`
- If the echo above has no output, then it didn't work.
- Note: `zsh/datetime` module is needed, if running `zsh`.
- In older versions of `bash` you can use `date +%s` instead, i.e. `export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH`
10. Run `bundle exec rake build`
11. Run `bundle exec rake release` which will create a git tag for the version,
push git commits and tags, and push the `.gem` file to the gem host configured in the gemspec.
12. Run `bin/gem_checksums` (more context [1][🔒️rubygems-checksums-pr], [2][🔒️rubygems-guides-pr])
to create SHA-256 and SHA-512 checksums. This functionality is provided by the `stone_checksums`
[gem][💎stone_checksums].
- The script automatically commits but does not push the checksums
13. Sanity check the SHA256, comparing with the output from the `bin/gem_checksums` command:
- `sha256sum pkg/<gem name>-<version>.gem`
#### Automated release process

Releases are automated via `.github/workflows/release.yml` and are triggered by pushing a version tag (`v*`).

1. Update the version in `lib/cyclonedx/ruby/version.rb` and update `CHANGELOG.md`.
2. Run `bin/setup && bin/rake` locally as a sanity check.
3. Commit and merge the release preparation changes.
4. Create and push the release tag (for example, `v1.2.3`).
5. Monitor the release workflow in [GitHub Actions][🧪build].

The workflow validates the version, runs tests, builds the gem, generates checksums, publishes the gem to RubyGems via trusted publishing (OIDC), and then creates the GitHub release.
The `release-github` job depends on `release-rubygems`, so a GitHub release is only created after a successful RubyGems publish.

[📜src-gh]: https://github.com/CycloneDX/cyclonedx-ruby-gem
[🧪build]: https://github.com/CycloneDX/cyclonedx-ruby-gem/actions
Expand Down