Skip to content

Commit eed5d05

Browse files
authored
Merge pull request #15 from TwilightCoders/develop
πŸš€ Add Manual Release Workflow with Enhanced Test Coverage Replaces automatic gem publishing with user-controlled manual workflows and significantly improves test coverage from 77% to 96%. πŸ”§ Key Changes - Manual Release Control: User-triggered workflows with explicit confirmation instead of automatic publishing - SHA-Based Versioning: Unique versions for development (.dev.SHA) and release candidate (.rc.SHA) builds - Artifact-Based Builds: CI builds and stores gems for later publishing decisions - Multi-Target Publishing: Support for GitHub Packages, RubyGems.org, or both βœ… Test Coverage Improvements - Added comprehensive specs for edge cases, error conditions, and timeouts - Improved coverage from 77% to 96% with previously untested methods - Clean test output with suppressed intentional warnings πŸ”’ Safety Features - Manual confirmation required (type "confirm" to proceed) - Shows gem details and contents before publishing - 30-day artifact retention for audit trails - Protected against accidental overwrites 🎯 Workflow Before: Push β†’ Auto-publish After: Push β†’ Build artifact β†’ Manual review β†’ Confirm β†’ Publish
2 parents 9c33cbf + 0140c8c commit eed5d05

File tree

6 files changed

+512
-35
lines changed

6 files changed

+512
-35
lines changed

β€Ž.github/workflows/ci.ymlβ€Ž

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [main, develop, 'release/**']
5+
branches: [main, develop, release/**]
66
pull_request:
77
branches: [main, develop]
88

@@ -74,11 +74,11 @@ jobs:
7474
bundle audit --update || true
7575
continue-on-error: true
7676

77-
publish:
77+
build:
7878
runs-on: ubuntu-latest
7979
needs: [test, security]
80-
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release/'))
81-
80+
if: github.event_name == 'push'
81+
8282
steps:
8383
- uses: actions/checkout@v4
8484

@@ -91,25 +91,45 @@ jobs:
9191
- name: Modify version for develop branch
9292
if: github.ref == 'refs/heads/develop'
9393
run: |
94-
sed -i "s/VERSION = '\([^']*\)'/VERSION = '\1.dev'/" lib/sudo/constants.rb
94+
SHORT_SHA=$(git rev-parse --short HEAD)
95+
sed -i "s/VERSION = '\([^']*\)'/VERSION = '\1.dev.${SHORT_SHA}'/" lib/sudo/constants.rb
96+
echo "VERSION_SUFFIX=.dev.${SHORT_SHA}" >> $GITHUB_ENV
9597
9698
- name: Modify version for release branch
9799
if: startsWith(github.ref, 'refs/heads/release/')
98100
run: |
99-
sed -i "s/VERSION = '\([^']*\)'/VERSION = '\1.rc'/" lib/sudo/constants.rb
101+
SHORT_SHA=$(git rev-parse --short HEAD)
102+
sed -i "s/VERSION = '\([^']*\)'/VERSION = '\1.rc.${SHORT_SHA}'/" lib/sudo/constants.rb
103+
echo "VERSION_SUFFIX=.rc.${SHORT_SHA}" >> $GITHUB_ENV
104+
105+
- name: Set version suffix for main
106+
if: github.ref == 'refs/heads/main'
107+
run: echo "VERSION_SUFFIX=" >> $GITHUB_ENV
100108

101109
- name: Build gem
102110
run: gem build sudo.gemspec
103111

104-
- name: Publish to GitHub Packages
112+
- name: Get gem info
113+
id: gem_info
114+
run: |
115+
GEM_FILE=$(ls *.gem)
116+
GEM_VERSION=$(echo $GEM_FILE | sed 's/sudo-\(.*\)\.gem/\1/')
117+
echo "gem_file=$GEM_FILE" >> $GITHUB_OUTPUT
118+
echo "gem_version=$GEM_VERSION" >> $GITHUB_OUTPUT
119+
120+
- name: Store gem artifact
121+
uses: actions/upload-artifact@v4
122+
with:
123+
name: gem-${{ steps.gem_info.outputs.gem_version }}
124+
path: "*.gem"
125+
retention-days: 30
126+
127+
- name: Create build summary
105128
run: |
106-
mkdir -p ~/.gem
107-
cat << EOF > ~/.gem/credentials
108-
---
109-
:github: Bearer ${{ secrets.GITHUB_TOKEN }}
110-
EOF
111-
chmod 600 ~/.gem/credentials
112-
# Temporarily remove allowed_push_host restriction for GitHub Packages
113-
sed -i "s/spec.metadata\['allowed_push_host'\].*$//" sudo.gemspec
114-
gem build sudo.gemspec
115-
gem push --key github --host https://rubygems.pkg.github.com/TwilightCoders *.gem
129+
echo "## Gem Built Successfully πŸ’Ž" >> $GITHUB_STEP_SUMMARY
130+
echo "- **Version**: ${{ steps.gem_info.outputs.gem_version }}" >> $GITHUB_STEP_SUMMARY
131+
echo "- **File**: ${{ steps.gem_info.outputs.gem_file }}" >> $GITHUB_STEP_SUMMARY
132+
echo "- **Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
133+
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
134+
echo "" >> $GITHUB_STEP_SUMMARY
135+
echo "πŸš€ **Ready to publish!** Use the 'Manual Release' workflow to publish this gem." >> $GITHUB_STEP_SUMMARY
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
name: Manual Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
target:
7+
description: 'Release target'
8+
required: true
9+
default: 'github'
10+
type: choice
11+
options:
12+
- github
13+
- rubygems
14+
- both
15+
run_id:
16+
description: 'CI Run ID to use (optional - leave empty to build from current commit)'
17+
required: false
18+
type: string
19+
version_override:
20+
description: 'Version override (optional - leave empty to use VERSION constant)'
21+
required: false
22+
type: string
23+
confirm:
24+
description: 'Type "confirm" to proceed with release'
25+
required: true
26+
type: string
27+
28+
permissions:
29+
actions: write
30+
contents: read
31+
id-token: write
32+
packages: write
33+
34+
jobs:
35+
validate:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Validate confirmation
39+
if: inputs.confirm != 'confirm'
40+
run: |
41+
echo "::error::You must type 'confirm' to proceed with release"
42+
exit 1
43+
44+
release:
45+
runs-on: ubuntu-latest
46+
needs: validate
47+
48+
steps:
49+
- uses: actions/checkout@v4
50+
51+
- name: Set up Ruby
52+
uses: ruby/setup-ruby@v1
53+
with:
54+
ruby-version: "3.3"
55+
bundler-cache: true
56+
57+
- name: Download gem artifact
58+
if: inputs.run_id != ''
59+
uses: actions/download-artifact@v4
60+
with:
61+
run-id: ${{ inputs.run_id }}
62+
pattern: gem-*
63+
merge-multiple: true
64+
65+
- name: Build gem from current commit
66+
if: inputs.run_id == ''
67+
run: |
68+
# Override version if specified
69+
if [ "${{ inputs.version_override }}" != "" ]; then
70+
sed -i "s/VERSION = '\([^']*\)'/VERSION = '${{ inputs.version_override }}'/" lib/sudo/constants.rb
71+
echo "Version overridden to: ${{ inputs.version_override }}"
72+
fi
73+
74+
# Run tests first
75+
bundle exec rspec
76+
77+
# Build gem
78+
gem build sudo.gemspec
79+
80+
- name: Show gem info and get publish details
81+
id: gem_details
82+
run: |
83+
echo "Available gems:"
84+
ls -la *.gem
85+
echo ""
86+
87+
# Get the gem file (assuming single gem)
88+
GEM_FILE=$(ls *.gem | head -1)
89+
GEM_VERSION=$(echo $GEM_FILE | sed 's/sudo-\(.*\)\.gem/\1/')
90+
91+
echo "gem_file=$GEM_FILE" >> $GITHUB_OUTPUT
92+
echo "gem_version=$GEM_VERSION" >> $GITHUB_OUTPUT
93+
94+
echo "## πŸ’Ž PUBLISHING CONFIRMATION"
95+
echo "**Gem Name:** sudo"
96+
echo "**Version:** $GEM_VERSION"
97+
echo "**File:** $GEM_FILE"
98+
echo "**Target:** ${{ inputs.target }}"
99+
echo "**Size:** $(ls -lh $GEM_FILE | awk '{print $5}')"
100+
echo ""
101+
echo "Gem contents preview:"
102+
gem contents "$GEM_FILE" | head -10
103+
echo "... (and $(gem contents "$GEM_FILE" | wc -l) total files)"
104+
105+
- name: Confirm publication details
106+
run: |
107+
echo "## πŸš€ READY TO PUBLISH" >> $GITHUB_STEP_SUMMARY
108+
echo "- **Gem**: sudo" >> $GITHUB_STEP_SUMMARY
109+
echo "- **Version**: ${{ steps.gem_details.outputs.gem_version }}" >> $GITHUB_STEP_SUMMARY
110+
echo "- **File**: ${{ steps.gem_details.outputs.gem_file }}" >> $GITHUB_STEP_SUMMARY
111+
echo "- **Target**: ${{ inputs.target }}" >> $GITHUB_STEP_SUMMARY
112+
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
113+
echo "" >> $GITHUB_STEP_SUMMARY
114+
echo "Publishing in 5 seconds..." >> $GITHUB_STEP_SUMMARY
115+
sleep 5
116+
117+
- name: Publish to GitHub Packages
118+
if: inputs.target == 'github' || inputs.target == 'both'
119+
run: |
120+
mkdir -p ~/.gem
121+
cat << EOF > ~/.gem/credentials
122+
---
123+
:github: Bearer ${{ secrets.GITHUB_TOKEN }}
124+
EOF
125+
chmod 600 ~/.gem/credentials
126+
# Temporarily remove allowed_push_host restriction for GitHub Packages
127+
sed -i "s/spec.metadata\['allowed_push_host'\].*$//" sudo.gemspec
128+
gem build sudo.gemspec
129+
gem push --key github --host https://rubygems.pkg.github.com/TwilightCoders *.gem
130+
131+
- name: Publish to RubyGems.org
132+
if: inputs.target == 'rubygems' || inputs.target == 'both'
133+
env:
134+
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
135+
run: |
136+
mkdir -p ~/.gem
137+
cat << EOF > ~/.gem/credentials
138+
---
139+
:rubygems_api_key: ${{ secrets.RUBYGEMS_API_KEY }}
140+
EOF
141+
chmod 600 ~/.gem/credentials
142+
gem push *.gem
143+
144+
- name: Create release summary
145+
run: |
146+
echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
147+
echo "- **Target**: ${{ inputs.target }}" >> $GITHUB_STEP_SUMMARY
148+
echo "- **Version**: $(ls *.gem | sed 's/sudo-\(.*\)\.gem/\1/')" >> $GITHUB_STEP_SUMMARY
149+
echo "- **Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
150+
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY

β€ŽREADME.mdβ€Ž

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
[![CI](https://github.com/TwilightCoders/rubysu/actions/workflows/ci.yml/badge.svg)](https://github.com/TwilightCoders/rubysu/actions/workflows/ci.yml)
33
[![Maintainability](https://qlty.sh/badges/e63e40be-4d72-4519-ad77-d4f94803a7b9/maintainability.svg)](https://qlty.sh/TwilightCoders/rubysu)
44
[![Test Coverage](https://qlty.sh/badges/e63e40be-4d72-4519-ad77-d4f94803a7b9/test_coverage.svg)](https://qlty.sh/TwilightCoders/rubysu)
5+
![GitHub License](https://img.shields.io/github/license/twilightcoders/rubysu)
56

67
# Ruby Sudo
78

@@ -139,27 +140,22 @@ end
139140

140141
Guido De Rosa ([@gderosa](http://github.com/gderosa/)).
141142

142-
See LICENSE.
143+
See ([LICENSE](https://github.com/TwilightCoders/rubysu/blob/main/LICENSE)).
143144

144145
### Contributors
145146

146-
Dale Stevens ([@voltechs](https://github.com/voltechs))
147+
- Dale Stevens ([@voltechs](https://github.com/voltechs))
148+
- Robert M. Koch ([@threadmetal](https://github.com/threadmetal))
149+
- Wolfgang Teuber ([@wteuber](https://github.com/wteuber))
147150

148-
Robert M. Koch ([@threadmetal](https://github.com/threadmetal))
151+
### Acknowledgements
149152

150-
Wolfgang Teuber ([@wteuber](https://github.com/wteuber))
151-
152-
### Other acknowledgements
153-
154-
155-
Thanks to Tony Arcieri and Brian Candler for suggestions on
156-
[ruby-talk](http://www.ruby-forum.com/topic/262655).
157-
158-
Initially developed by G. D. while working at [@vemarsas](https://github.com/vemarsas).
153+
- Thanks to Tony Arcieri and Brian Candler for suggestions on [ruby-talk](http://www.ruby-forum.com/topic/262655).
154+
- Initially developed by Guido De Rosa while working at [@vemarsas](https://github.com/vemarsas).
159155

160156
## Contributing
161157

162-
1. Fork it ( https://github.com/TwilightCoders/rubysu/fork )
158+
1. Fork it ( <https://github.com/TwilightCoders/rubysu/fork> )
163159
2. Create your feature branch (`git checkout -b my-new-feature`)
164160
3. Commit your changes (`git commit -am 'Add some feature'`)
165161
4. Push to the branch (`git push origin my-new-feature`)

β€Žspec/lib/sudo/proxy_spec.rbβ€Ž

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
require 'spec_helper'
22

3+
describe Sudo::MethodProxy do
4+
let(:object) { double('test_object') }
5+
let(:proxy) { double('proxy') }
6+
subject { described_class.new(object, proxy) }
7+
8+
describe '#initialize' do
9+
it 'stores object and proxy references' do
10+
method_proxy = described_class.new(object, proxy)
11+
expect(method_proxy.instance_variable_get(:@object)).to eq(object)
12+
expect(method_proxy.instance_variable_get(:@proxy)).to eq(proxy)
13+
end
14+
end
15+
16+
describe '#method_missing' do
17+
it 'delegates method calls to proxy' do
18+
expect(proxy).to receive(:proxy).with(object, :test_method, 'arg1', 'arg2')
19+
subject.test_method('arg1', 'arg2')
20+
end
21+
22+
it 'supports blocks' do
23+
block = proc { 'test' }
24+
expect(proxy).to receive(:proxy).with(object, :test_method, &block)
25+
subject.test_method(&block)
26+
end
27+
end
28+
29+
describe '#respond_to_missing?' do
30+
it 'returns true if object responds to method' do
31+
allow(object).to receive(:respond_to?).with(:test_method, false).and_return(true)
32+
expect(subject.respond_to?(:test_method)).to be true
33+
end
34+
35+
it 'returns false if object does not respond to method' do
36+
allow(object).to receive(:respond_to?).with(:unknown_method, false).and_return(false)
37+
expect(subject.respond_to?(:unknown_method)).to be false
38+
end
39+
end
40+
end
41+
342
describe Sudo::Proxy do
443
it 'proxies the call' do
544
expect(subject.proxy(Kernel)).to eq(Kernel)
@@ -11,6 +50,12 @@
1150
expect(subject.loaded_specs).to_not be_empty
1251
expect(subject.loaded_specs).to all(be_a(String))
1352
end
53+
54+
it 'handles errors gracefully' do
55+
allow(Gem).to receive(:loaded_specs).and_raise(StandardError.new('test error'))
56+
allow(subject).to receive(:warn) # Suppress warning output in tests
57+
expect(subject.loaded_specs).to eq([])
58+
end
1459
end
1560

1661
context '#load_path' do

0 commit comments

Comments
Β (0)