第16章:外部協力者との連携
16.1 Fork & Pull Requestモデル
Fork & PRワークフローの基本
外部協力者向けのワークフロー
# Contributing Guide for External Collaborators
## Getting Started
1. **Fork the repository**
- Navigate to https://github.com/itdojp/github-workflow-book-public
- Click the "Fork" button
- Clone your fork:
```bash
git clone https://github.com/YOUR_USERNAME/github-workflow-book-public.git
cd github-workflow-book-public
git remote add upstream https://github.com/itdojp/github-workflow-book-public.git
- Create a feature branch
git checkout -b feature/your-feature-name
- Make your changes
- Follow our coding standards
- Add tests for new functionality
- Update documentation
- Keep your fork updated
git fetch upstream git checkout main git merge upstream/main git push origin main
- Submit a Pull Request
- Push your branch to your fork
- Create a PR from your fork to the main repository
- Fill out the PR template completely ```
フォーク管理の自動化
フォーク同期スクリプト
# scripts/fork_manager.py
import subprocess
import requests
from github import Github
class ForkManager:
def __init__(self, token, upstream_repo):
self.github = Github(token)
self.upstream_repo = upstream_repo
def get_active_forks(self, days=30):
"""アクティブなフォークを取得"""
repo = self.github.get_repo(self.upstream_repo)
forks = []
for fork in repo.get_forks():
# 最終更新日をチェック
if (datetime.now() - fork.updated_at).days <= days:
forks.append({
'owner': fork.owner.login,
'name': fork.name,
'url': fork.html_url,
'behind_by': self._get_behind_count(fork)
})
return forks
def _get_behind_count(self, fork):
"""フォークが何コミット遅れているかを計算"""
upstream = self.github.get_repo(self.upstream_repo)
try:
comparison = upstream.compare(
f"{fork.owner.login}:{fork.default_branch}",
upstream.default_branch
)
return comparison.behind_by
except:
return None
def notify_outdated_forks(self, threshold_commits=10):
"""古いフォークの所有者に通知"""
active_forks = self.get_active_forks()
for fork in active_forks:
if fork['behind_by'] and fork['behind_by'] > threshold_commits:
self._send_sync_reminder(fork)
def _send_sync_reminder(self, fork):
"""同期リマインダーを送信"""
issue_title = "Your fork is out of sync"
issue_body = f"""
Hi @{fork['owner']},
Your fork is {fork['behind_by']} commits behind the upstream repository.
To sync your fork:
```bash
git checkout main
git fetch upstream
git merge upstream/main
git push origin main
Or use GitHub’s web interface:
- Go to your fork: {fork[‘url’]}
- Click “Sync fork”
- Click “Update branch”
Let us know if you need any help! “””
# フォークにIssueを作成(可能な場合)
fork_repo = self.github.get_repo(f"{fork['owner']}/{fork['name']}")
try:
fork_repo.create_issue(title=issue_title, body=issue_body)
except:
# Issueが無効な場合はスキップ
pass ```
Pull Requestテンプレート
包括的なPRテンプレート
<!-- .github/pull_request_template.md -->
## Description
<!-- Provide a brief description of the changes -->
## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring
## Changes Made
<!-- List the specific changes made -->
-
-
-
## Testing
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
## Performance Impact
<!-- Describe any performance implications -->
- Training time:
- Inference time:
- Memory usage:
## Dependencies
<!-- List any new dependencies added -->
-
## Screenshots (if applicable)
<!-- Add screenshots to help explain your changes -->
## Additional Notes
<!-- Any additional information that reviewers should know -->
## Checklist for Reviewers
- [ ] Code quality and style
- [ ] Test coverage
- [ ] Documentation updates
- [ ] Performance implications
- [ ] Security considerations
16.2 コントリビューターライセンス契約(CLA)
CLAの実装
CLA自動化ボット
# .github/workflows/cla.yml
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, closed, synchronize]
jobs:
CLAAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
uses: cla-assistant/github-action@v2.1.3-beta
env:
GITHUB_TOKEN: $
PERSONAL_ACCESS_TOKEN: $
with:
path-to-signatures: 'signatures/version1/cla.json'
path-to-document: 'https://github.com/itdojp/github-workflow-book-public/blob/main/CLA.md'
branch: 'cla-signatures'
allowlist: bot*
# カスタムメッセージ
custom-notsigned-prcomment: |
Thank you for your contribution!
Before we can merge your PR, we need you to sign our Contributor License Agreement (CLA).
Please read and sign the CLA by commenting:
```
I have read the CLA Document and I hereby sign the CLA
```
You can read the full CLA here: [CLA Document](https://github.com/itdojp/github-workflow-book-public/blob/main/CLA.md)
custom-allsigned-prcomment: |
All contributors have signed the CLA ✅
Thank you for your contribution!
CLAドキュメント
CLA.md
# Contributor License Agreement
## Purpose
This agreement clarifies the intellectual property license granted with contributions from any person or entity.
## Agreement
By making a contribution to this project, I agree to the following:
### 1. Definitions
- "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement.
- "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work.
### 2. Grant of Copyright License
You hereby grant to the project maintainers and to recipients of software distributed by the project maintainers a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
### 3. Grant of Patent License
You hereby grant to the project maintainers and to recipients of software distributed by the project maintainers a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work.
### 4. Representations
You represent that:
- You are legally entitled to grant the above license
- Your Contribution is Your original creation
- Your Contribution does not violate any third party rights
### 5. No Support Obligations
You are not expected to provide support for Your Contributions, except to the extent You desire to provide support.
## How to Sign
Comment on the PR with:
I have read the CLA Document and I hereby sign the CLA
CLA管理システム
CLA署名者の管理
# scripts/cla_manager.py
import json
from datetime import datetime
from pathlib import Path
class CLAManager:
def __init__(self, signatures_file='signatures/cla.json'):
self.signatures_file = Path(signatures_file)
self.signatures = self._load_signatures()
def _load_signatures(self):
"""署名情報を読み込み"""
if self.signatures_file.exists():
with open(self.signatures_file) as f:
return json.load(f)
return {'signatures': []}
def add_signature(self, github_username, pr_number, email=None):
"""新しい署名を追加"""
signature = {
'github_username': github_username,
'signed_at': datetime.now().isoformat(),
'pr_number': pr_number,
'email': email,
'cla_version': '1.0'
}
# 既存の署名をチェック
if not self.has_signed(github_username):
self.signatures['signatures'].append(signature)
self._save_signatures()
return signature
def has_signed(self, github_username):
"""ユーザーが署名済みかチェック"""
for sig in self.signatures['signatures']:
if sig['github_username'] == github_username:
return True
return False
def get_unsigned_contributors(self, pr_contributors):
"""未署名のコントリビューターを取得"""
unsigned = []
for contributor in pr_contributors:
if not self.has_signed(contributor):
unsigned.append(contributor)
return unsigned
def generate_report(self):
"""CLA署名レポートを生成"""
total_signatures = len(self.signatures['signatures'])
# 月別の署名数
monthly_stats = {}
for sig in self.signatures['signatures']:
month = sig['signed_at'][:7] # YYYY-MM
monthly_stats[month] = monthly_stats.get(month, 0) + 1
report = f"""# CLA Signature Report
## Summary
- Total Signatures: {total_signatures}
- CLA Version: 1.0
## Monthly Statistics
"""
for month, count in sorted(monthly_stats.items()):
report += f"- {month}: {count} signatures\n"
return report
def _save_signatures(self):
"""署名情報を保存"""
self.signatures_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.signatures_file, 'w') as f:
json.dump(self.signatures, f, indent=2)
16.3 外部貢献者の権限管理
段階的な権限付与
コントリビューター成長パス
# contributor_management.py
from enum import Enum
import yaml
class ContributorLevel(Enum):
NEW = "new_contributor"
ACTIVE = "active_contributor"
TRUSTED = "trusted_contributor"
MAINTAINER = "maintainer"
class ContributorManager:
def __init__(self, config_file='contributor_levels.yaml'):
self.config = self._load_config(config_file)
self.contributors = {}
def _load_config(self, config_file):
"""権限レベル設定を読み込み"""
with open(config_file) as f:
return yaml.safe_load(f)
def track_contribution(self, username, contribution_type):
"""貢献を追跡"""
if username not in self.contributors:
self.contributors[username] = {
'level': ContributorLevel.NEW,
'contributions': {
'prs_merged': 0,
'issues_created': 0,
'reviews_done': 0,
'docs_improved': 0
},
'first_contribution': datetime.now().isoformat()
}
# 貢献をカウント
if contribution_type in self.contributors[username]['contributions']:
self.contributors[username]['contributions'][contribution_type] += 1
# レベルアップをチェック
self._check_level_up(username)
def _check_level_up(self, username):
"""レベルアップ条件をチェック"""
contributor = self.contributors[username]
contributions = contributor['contributions']
# レベルアップ条件
if contributor['level'] == ContributorLevel.NEW:
if contributions['prs_merged'] >= 3:
self._promote_contributor(username, ContributorLevel.ACTIVE)
elif contributor['level'] == ContributorLevel.ACTIVE:
if (contributions['prs_merged'] >= 10 and
contributions['reviews_done'] >= 5):
self._promote_contributor(username, ContributorLevel.TRUSTED)
def _promote_contributor(self, username, new_level):
"""コントリビューターを昇格"""
old_level = self.contributors[username]['level']
self.contributors[username]['level'] = new_level
# 権限を更新
self._update_permissions(username, new_level)
# 通知を送信
self._notify_promotion(username, old_level, new_level)
def _update_permissions(self, username, level):
"""GitHubの権限を更新"""
permissions = self.config['levels'][level.value]['permissions']
# GitHub APIを使用して権限を設定
if 'triage' in permissions:
# Triage権限を付与
self._grant_triage_permission(username)
if 'write' in permissions:
# Write権限を付与(特定のリポジトリ)
self._grant_write_permission(username, permissions['write'])
外部コントリビューター向けのガイドライン
CONTRIBUTING.md
# Contributing to ML Project
We love your input! We want to make contributing to this project as easy and transparent as possible.
## Development Process
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
## Code Contributions
### 1. Fork the Repository
- Fork the repo and create your branch from `develop`
- If you've added code that should be tested, add tests
- If you've changed APIs, update the documentation
### 2. Code Style
- Use [Black](https://github.com/psf/black) for Python code formatting
- Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/)
- Use type hints where applicable
### 3. Testing
- Write unit tests for new functionality
- Ensure all tests pass: `pytest tests/`
- Maintain test coverage above 80%
### 4. Commit Messages
Follow the conventional commits specification:
- `feat:` New feature
- `fix:` Bug fix
- `docs:` Documentation changes
- `style:` Code style changes
- `refactor:` Code refactoring
- `test:` Test additions or corrections
- `chore:` Maintenance tasks
### 5. Pull Requests
- Fill in the required template
- Include screenshots for UI changes
- Link related issues
## First Time Contributors
Looking for a good first issue? Check out issues labeled [`good first issue`](https://github.com/itdojp/github-workflow-book-public/labels/good%20first%20issue).
### Setting Up Development Environment
```bash
# Clone your fork
git clone https://github.com/YOUR_USERNAME/ml-project.git
cd ml-project
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements-dev.txt
# Install pre-commit hooks
pre-commit install
Code Review Process
- Automated checks must pass
- At least one maintainer approval required
- No merge conflicts
- CLA must be signed
Community
- Join our Discord server
- Attend our monthly contributor meetings
- Check out our roadmap
Recognition
We recognize our contributors! Check out our Contributors page.
Contributor Levels
- 🌱 New Contributor: First PR merged
- 🌿 Active Contributor: 3+ PRs merged
- 🌳 Trusted Contributor: 10+ PRs, active reviewer
- 🌲 Maintainer: Core team member
License
By contributing, you agree that your contributions will be licensed under the same license as the project.
## 16.4 コントリビューションガイドラインの作成
### 技術的ガイドライン
#### コーディング標準
```python
# .github/coding_standards.py
"""
ML Project Coding Standards
This module demonstrates our coding standards for Python files.
"""
from typing import List, Optional, Tuple, Union
import numpy as np
import torch
import torch.nn as nn
class ModelBase(nn.Module):
"""Base class for all models in the project.
All models should inherit from this class and implement
the required methods.
Args:
input_dim: Input dimension
output_dim: Output dimension
hidden_dims: List of hidden layer dimensions
activation: Activation function (default: ReLU)
Example:
>>> model = ModelBase(784, 10, [256, 128])
>>> output = model(torch.randn(32, 784))
"""
def __init__(
self,
input_dim: int,
output_dim: int,
hidden_dims: Optional[List[int]] = None,
activation: nn.Module = nn.ReLU()
):
super().__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.hidden_dims = hidden_dims or [256, 128]
self.activation = activation
# Build layers
self.layers = self._build_layers()
def _build_layers(self) -> nn.ModuleList:
"""Build the neural network layers.
Returns:
ModuleList containing all layers
"""
layers = nn.ModuleList()
# Input layer
prev_dim = self.input_dim
# Hidden layers
for hidden_dim in self.hidden_dims:
layers.append(nn.Linear(prev_dim, hidden_dim))
layers.append(self.activation)
layers.append(nn.BatchNorm1d(hidden_dim))
prev_dim = hidden_dim
# Output layer
layers.append(nn.Linear(prev_dim, self.output_dim))
return layers
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Forward pass through the network.
Args:
x: Input tensor of shape (batch_size, input_dim)
Returns:
Output tensor of shape (batch_size, output_dim)
"""
for layer in self.layers:
x = layer(x)
return x
def get_num_parameters(self) -> int:
"""Get the total number of trainable parameters.
Returns:
Number of parameters
"""
return sum(p.numel() for p in self.parameters() if p.requires_grad)
def validate_input(
data: Union[np.ndarray, torch.Tensor],
expected_shape: Optional[Tuple[int, ...]] = None,
expected_dtype: Optional[type] = None
) -> torch.Tensor:
"""Validate and convert input data.
Args:
data: Input data
expected_shape: Expected shape (None for any)
expected_dtype: Expected data type
Returns:
Validated tensor
Raises:
ValueError: If validation fails
"""
# Convert to tensor if needed
if isinstance(data, np.ndarray):
data = torch.from_numpy(data)
# Validate shape
if expected_shape is not None:
if data.shape != expected_shape:
raise ValueError(
f"Expected shape {expected_shape}, got {data.shape}"
)
# Validate dtype
if expected_dtype is not None:
if data.dtype != expected_dtype:
data = data.to(expected_dtype)
return data
コードレビューチェックリスト
レビュー自動化
# .github/review_checklist.py
import ast
import subprocess
from pathlib import Path
class CodeReviewChecker:
"""Automated code review checks."""
def __init__(self, pr_files):
self.pr_files = pr_files
self.issues = []
def run_all_checks(self):
"""Run all automated checks."""
checks = [
self.check_code_style,
self.check_type_hints,
self.check_docstrings,
self.check_test_coverage,
self.check_security,
self.check_performance
]
for check in checks:
check()
return self.issues
def check_code_style(self):
"""Check code style with Black and flake8."""
# Black check
result = subprocess.run(
['black', '--check'] + self.pr_files,
capture_output=True
)
if result.returncode != 0:
self.issues.append({
'type': 'style',
'severity': 'medium',
'message': 'Code is not formatted with Black'
})
# Flake8 check
result = subprocess.run(
['flake8'] + self.pr_files,
capture_output=True,
text=True
)
if result.stdout:
self.issues.append({
'type': 'style',
'severity': 'low',
'message': f'Flake8 issues:\n{result.stdout}'
})
def check_type_hints(self):
"""Check for missing type hints."""
for file_path in self.pr_files:
if not file_path.endswith('.py'):
continue
with open(file_path) as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
# Check return type
if node.returns is None and node.name != '__init__':
self.issues.append({
'type': 'type_hints',
'severity': 'low',
'message': f'Missing return type: {file_path}:{node.name}'
})
# Check argument types
for arg in node.args.args:
if arg.annotation is None and arg.arg != 'self':
self.issues.append({
'type': 'type_hints',
'severity': 'low',
'message': f'Missing type hint for {arg.arg} in {node.name}'
})
def check_docstrings(self):
"""Check for missing or incomplete docstrings."""
for file_path in self.pr_files:
if not file_path.endswith('.py'):
continue
with open(file_path) as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
docstring = ast.get_docstring(node)
if not docstring:
self.issues.append({
'type': 'documentation',
'severity': 'medium',
'message': f'Missing docstring: {file_path}:{node.name}'
})
elif isinstance(node, ast.FunctionDef):
# Check for Args and Returns sections
if 'Args:' not in docstring and len(node.args.args) > 1:
self.issues.append({
'type': 'documentation',
'severity': 'low',
'message': f'Missing Args section in docstring: {node.name}'
})
16.5 オープンソースプロジェクトの運営
プロジェクトガバナンス
GOVERNANCE.md
# Project Governance
## Overview
This document describes the governance model for the ML Project.
## Roles and Responsibilities
### Users
- Use the software
- Report bugs and request features
- Contribute to discussions
### Contributors
- Submit patches and pull requests
- Review pull requests
- Participate in project discussions
- Help new contributors
### Committers
- All privileges of Contributors
- Merge pull requests
- Vote on project decisions
- Nominate new Committers
### Technical Steering Committee (TSC)
- Set project direction and roadmap
- Make architectural decisions
- Resolve conflicts
- Approve new Committers
## Decision Making
### Consensus
Most decisions are made by consensus through discussion in issues and pull requests.
### Voting
When consensus cannot be reached, decisions are made by voting:
- Simple majority for most decisions
- 2/3 majority for:
- Adding/removing TSC members
- Major architectural changes
- License changes
### Proposal Process
1. Create an RFC (Request for Comments) issue
2. Allow at least 1 week for discussion
3. TSC reviews and approves/rejects
4. Implementation begins if approved
## Becoming a Committer
Contributors can be nominated to become Committers based on:
- Quality and quantity of contributions
- Engagement in the community
- Demonstrated understanding of the project
## Code of Conduct
All participants must follow our [Code of Conduct](CODE_OF_CONDUCT.md).
## Changes to Governance
This governance model can be changed through the proposal process with TSC approval.
コミュニティ管理
コミュニティ健全性メトリクス
# scripts/community_health.py
import requests
from datetime import datetime, timedelta
from collections import defaultdict
class CommunityHealthMonitor:
def __init__(self, github_token, repo_name):
self.headers = {'Authorization': f'token {github_token}'}
self.repo_name = repo_name
self.base_url = f'https://api.github.com/repos/{repo_name}'
def calculate_health_metrics(self, days=30):
"""コミュニティ健全性メトリクスを計算"""
since = datetime.now() - timedelta(days=days)
metrics = {
'response_time': self._calculate_response_time(since),
'issue_resolution': self._calculate_issue_resolution(since),
'pr_velocity': self._calculate_pr_velocity(since),
'contributor_growth': self._calculate_contributor_growth(since),
'engagement': self._calculate_engagement(since),
'diversity': self._calculate_diversity(since)
}
# 総合スコアを計算
metrics['overall_health'] = self._calculate_overall_health(metrics)
return metrics
def _calculate_response_time(self, since):
"""Issue/PRへの初回応答時間"""
response_times = []
# Issues
issues = self._get_issues(since)
for issue in issues:
if issue['comments'] > 0:
first_comment = self._get_first_comment(issue['number'], 'issues')
if first_comment:
created = datetime.fromisoformat(issue['created_at'].rstrip('Z'))
commented = datetime.fromisoformat(first_comment['created_at'].rstrip('Z'))
response_times.append((commented - created).total_seconds() / 3600)
# Pull Requests
prs = self._get_pull_requests(since)
for pr in prs:
if pr['comments'] > 0 or pr['review_comments'] > 0:
first_comment = self._get_first_comment(pr['number'], 'pulls')
if first_comment:
created = datetime.fromisoformat(pr['created_at'].rstrip('Z'))
commented = datetime.fromisoformat(first_comment['created_at'].rstrip('Z'))
response_times.append((commented - created).total_seconds() / 3600)
if response_times:
return {
'median_hours': sorted(response_times)[len(response_times) // 2],
'average_hours': sum(response_times) / len(response_times)
}
return {'median_hours': None, 'average_hours': None}
def _calculate_contributor_growth(self, since):
"""新規コントリビューターの成長率"""
# コミット作者を取得
commits = self._get_commits(since)
contributors_by_month = defaultdict(set)
for commit in commits:
if commit['author']:
month = commit['commit']['author']['date'][:7]
contributors_by_month[month].add(commit['author']['login'])
# 月別の新規コントリビューター数を計算
all_contributors = set()
new_by_month = {}
for month in sorted(contributors_by_month.keys()):
new_contributors = contributors_by_month[month] - all_contributors
new_by_month[month] = len(new_contributors)
all_contributors.update(contributors_by_month[month])
return {
'total_new': sum(new_by_month.values()),
'by_month': new_by_month,
'growth_rate': self._calculate_growth_rate(new_by_month)
}
def generate_health_report(self, metrics):
"""健全性レポートを生成"""
report = f"""# Community Health Report
Generated: {datetime.now().strftime('%Y-%m-%d')}
## Overall Health Score: {metrics['overall_health']:.1f}/100
## Key Metrics
### 📊 Response Time
- Median: {metrics['response_time']['median_hours']:.1f} hours
- Average: {metrics['response_time']['average_hours']:.1f} hours
### 🔄 Issue Resolution
- Close rate: {metrics['issue_resolution']['close_rate']:.1%}
- Median time to close: {metrics['issue_resolution']['median_days']:.1f} days
### 🚀 PR Velocity
- Merge rate: {metrics['pr_velocity']['merge_rate']:.1%}
- Median time to merge: {metrics['pr_velocity']['median_days']:.1f} days
### 👥 Contributor Growth
- New contributors: {metrics['contributor_growth']['total_new']}
- Growth rate: {metrics['contributor_growth']['growth_rate']:.1%}
### 💬 Engagement
- Comments per issue: {metrics['engagement']['comments_per_issue']:.1f}
- Active discussions: {metrics['engagement']['active_discussions']}
### 🌍 Diversity
- Unique contributors: {metrics['diversity']['unique_contributors']}
- Geographic distribution: {metrics['diversity']['countries']} countries
## Recommendations
{self._generate_recommendations(metrics)}
"""
return report
def _generate_recommendations(self, metrics):
"""改善推奨事項を生成"""
recommendations = []
if metrics['response_time']['median_hours'] > 48:
recommendations.append(
"- Consider setting up automated responses for new issues/PRs"
)
if metrics['issue_resolution']['close_rate'] < 0.7:
recommendations.append(
"- Review and close stale issues to improve close rate"
)
if metrics['contributor_growth']['growth_rate'] < 0.05:
recommendations.append(
"- Create more 'good first issue' labels to attract new contributors"
)
return '\n'.join(recommendations) if recommendations else "No specific recommendations at this time."
リリース管理
自動リリースワークフロー
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Generate changelog
id: changelog
run: |
# 前回のタグからの変更を取得
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^)
# Conventional Commitsを解析
npm install -g conventional-changelog-cli
conventional-changelog -p angular -i CHANGELOG.md -s -r 0
# GitHubリリース用の本文を生成
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md | sed -n "/## \[${GITHUB_REF_NAME#v}\]/,/## \[/p" | sed '$d' >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: $
with:
tag_name: $
release_name: Release $
body: $
draft: false
prerelease: $
- name: Build and publish package
run: |
python -m pip install --upgrade pip
pip install build twine
# パッケージをビルド
python -m build
# PyPIに公開
python -m twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: $
- name: Update documentation
run: |
# ドキュメントをビルド
pip install -r docs/requirements.txt
cd docs && make html
# GitHub Pagesにデプロイ
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git checkout gh-pages
cp -r docs/_build/html/* .
git add .
git commit -m "Update documentation for $"
git push
まとめ
本章では、外部協力者との効果的な連携方法を学習しました:
- Fork & Pull Requestモデルによる貢献の受け入れ
- CLAによる知的財産権の明確化
- 段階的な権限管理とコントリビューター育成
- 明確なコントリビューションガイドライン
- 健全なオープンソースコミュニティの運営
次章では、コンプライアンスとガバナンスについて学習します。
確認事項
- Fork & PRワークフローを理解している
- CLAシステムを実装できる
- コントリビューターレベルを設計できる
- コントリビューションガイドラインを作成できる
- コミュニティの健全性を測定できる