diff --git a/.github/RELEASE.md b/.github/RELEASE.md new file mode 100644 index 0000000..1c28694 --- /dev/null +++ b/.github/RELEASE.md @@ -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..` (e.g., `v1.2.0`) +- **Prereleases**: `v..-` (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-.gem`) - The built Ruby gem +2. **SHA-512 Checksum** (`cyclonedx-ruby-.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. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fc94cd4 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7157a53..a2e75a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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"` 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` +#### 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