From 788cf6f5f6336b979a3164e8af26c1003879888d Mon Sep 17 00:00:00 2001 From: Craig West Date: Mon, 14 Jul 2025 15:13:03 +0100 Subject: [PATCH 1/4] docs: set new version im pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3fc92cd..89e962a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gha-python" -version = "3.2.0" +version = "3.3.0" description = "Generic template for Python GitHub Actions with pre-commit and semantic versioning with conventional commit" readme = "README.md" requires-python = ">=3.10" From 5939424ad6b3f92a2fc22ce18f7fe6ce53f188ef Mon Sep 17 00:00:00 2001 From: Craig West Date: Tue, 15 Jul 2025 17:43:13 +0100 Subject: [PATCH 2/4] docs: todo --- TODO.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5f827ff --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +The goal on them will be to: +- Assess what's in place currently (e.g., on the AI Backend codebase we have some checks, although I don't know how comprehensive they are) +- Propose what should be added in each of them to ensure we reach production-ready code and software development "good practices" +- Establish and implement CI/CD pipelines and pre-commit hooks (e.g., ensure development in branches, pushing to main not allowed, linting and code formatting, bumpversion/versioning) +- Finally, define github actions to deploy on Azure + +There are several persons actively developing in them, I can introduce you to them. With your help, my goal is to define a set of "rules" we will always follow, irrespective of the type of project or programming language, to ensure hig-quality standards + +Documentation on the project is available at: https://www.notion.so/attercop/Wholepal-Supplier-Build-9a8297fada8f49ce9ad2413ae062d289 + +Regarding the list I outlined before, I would add, enforcer developers to always branch and developed features in branches. To have a main branch, which is protected, which only can receive merges from another branch, which let's call develop . Thus the workflow will be, main and develop start sync, developer branches from develop , the feature us developed and testes and merged into develop. Once that that feature, and others are done, they can be merged into main. Develop will have a CI/CD to deploy into Azure development environment and main to production one From 84eb7e0bfdd91d1fe2e9c3f7cf4f49788d833ee8 Mon Sep 17 00:00:00 2001 From: Craig West Date: Fri, 18 Jul 2025 07:20:51 +0100 Subject: [PATCH 3/4] chore: creating ci template --- .github/{workflows => templates}/ci.yaml | 0 .github/workflows/ci_template.yaml | 543 +++++++++++++++++++++++ 2 files changed, 543 insertions(+) rename .github/{workflows => templates}/ci.yaml (100%) create mode 100644 .github/workflows/ci_template.yaml diff --git a/.github/workflows/ci.yaml b/.github/templates/ci.yaml similarity index 100% rename from .github/workflows/ci.yaml rename to .github/templates/ci.yaml diff --git a/.github/workflows/ci_template.yaml b/.github/workflows/ci_template.yaml new file mode 100644 index 0000000..4663bf8 --- /dev/null +++ b/.github/workflows/ci_template.yaml @@ -0,0 +1,543 @@ +name: CI/CD Pipeline Template + +run-name: Template CI/CD Pipeline + +# This workflow is a template for CI/CD pipelines for Python projects but can have Python swapped for Node.js or other languages. + +# It includes steps for testing, security scanning, code analysis, and release management. + +'on': + # push: + # branches: + # - main + workflow_dispatch: # Allow manual triggering + + +# Prevent duplicate runs for PR pushes +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Run Tests and Coverage + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - '3.12' + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: 'Set up Python ${{ matrix.python-version }}' + uses: actions/setup-python@v4 + with: + python-version: '${{ matrix.python-version }}' + - name: Install uv + uses: astral-sh/setup-uv@v2 + with: + version: latest + - name: Create virtual environment + run: uv venv + - name: Install dependencies + run: | + uv pip install -e . + uv pip install pytest pytest-cov pytest-xdist + - name: Run tests with pytest + run: | + source .venv/bin/activate + pytest --cov=attercop --cov-report=xml --cov-report=term-missing -v + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + security: + name: Security Scans With Bandit + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install uv + uses: astral-sh/setup-uv@v2 + with: + version: latest + - name: Create virtual environment + run: uv venv + - name: Install dependencies + run: | + uv pip install -e . + uv pip install bandit safety pip-audit + - name: Run Bandit security scan + run: | + source .venv/bin/activate + bandit -r attercop/ -f json -o bandit-report.json || true + bandit -r attercop/ -f txt + continue-on-error: true + - name: Run Safety check + run: | + source .venv/bin/activate + safety check --json --output safety-report.json || true + safety check + continue-on-error: true + - name: Run pip-audit + run: | + source .venv/bin/activate + pip-audit --format=json --output=pip-audit-report.json || true + pip-audit + continue-on-error: true + - name: Upload security reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: security-reports + path: | + bandit-report.json + safety-report.json + pip-audit-report.json + codeql: + name: CodeQL Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + queries: 'security-extended,security-and-quality' + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + scan: + name: Check GitLeaks for Secrets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + semantic_release: + name: Get Next Version + needs: [test, security, codeql, scan] + + runs-on: ubuntu-latest + + outputs: + next-version: ${{ steps.version.outputs.next-version }} + current-version: ${{ steps.version.outputs.current-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch full history for semantic versioning + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + pip install GitPython + + - name: Calculate next version + id: version + run: | + python << 'EOF' + import os + import re + import subprocess + from git import Repo + + def get_current_version(): + """Get the current version from git tags""" + try: + result = subprocess.run(['git', 'describe', '--tags', '--abbrev=0'], + capture_output=True, text=True, check=True) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "v0.0.0" + + def parse_version(version_str): + """Parse version string into components""" + # Remove 'v' prefix if present + clean_version = version_str.lstrip('v') + parts = clean_version.split('.') + return { + 'major': int(parts[0]) if len(parts) > 0 else 0, + 'minor': int(parts[1]) if len(parts) > 1 else 0, + 'patch': int(parts[2]) if len(parts) > 2 else 0 + } + + def analyze_commits_since_tag(repo, last_tag): + """Analyze commits since last tag to determine version bump""" + try: + if last_tag == "v0.0.0": + # No previous tags, get all commits + commits = list(repo.iter_commits()) + else: + # Get commits since last tag + commits = list(repo.iter_commits(f'{last_tag}..HEAD')) + except: + commits = list(repo.iter_commits()) + + has_breaking = False + has_feature = False + has_fix = False + + for commit in commits: + message = commit.message.lower().strip() + + # Check for breaking changes + if ('breaking change' in message or + message.startswith('feat!:') or + message.startswith('fix!:') or + '!' in message.split(':')[0] if ':' in message else False): + has_breaking = True + + # Check for features + elif message.startswith('feat:') or message.startswith('feature:'): + has_feature = True + + # Check for fixes + elif message.startswith('fix:') or message.startswith('bugfix:'): + has_fix = True + + return has_breaking, has_feature, has_fix + + def calculate_next_version(current_version, has_breaking, has_feature, has_fix): + """Calculate next version based on changes""" + version = parse_version(current_version) + + if has_breaking: + version['major'] += 1 + version['minor'] = 0 + version['patch'] = 0 + elif has_feature: + version['minor'] += 1 + version['patch'] = 0 + elif has_fix: + version['patch'] += 1 + else: + # No semantic changes found + return current_version + + # Maintain 'v' prefix if current version has it + prefix = 'v' if current_version.startswith('v') else '' + return f"{prefix}{version['major']}.{version['minor']}.{version['patch']}" + + # Main logic + repo = Repo('.') + current_version = get_current_version() + + print(f"Current version: {current_version}") + + has_breaking, has_feature, has_fix = analyze_commits_since_tag(repo, current_version) + + print(f"Analysis results:") + print(f" Breaking changes: {has_breaking}") + print(f" New features: {has_feature}") + print(f" Bug fixes: {has_fix}") + + next_version = calculate_next_version(current_version, has_breaking, has_feature, has_fix) + + print(f"Next version: {next_version}") + + # Set GitHub outputs + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f"current-version={current_version}\n") + f.write(f"next-version={next_version}\n") + + # Set environment variables for next steps + with open(os.environ['GITHUB_ENV'], 'a') as f: + f.write(f"CURRENT_VERSION={current_version}\n") + f.write(f"NEXT_VERSION={next_version}\n") + EOF + + - name: Print version info + run: | + echo "Current version: ${{ steps.version.outputs.current-version }}" + echo "Next version: ${{ steps.version.outputs.next-version }}" + if [ "${{ steps.version.outputs.current-version }}" != "${{ steps.version.outputs.next-version }}" ]; then + echo "✅ Version will be bumped!" + else + echo "No version bump needed" + fi + + create_release: + name: Create GitHub Release + needs: [semantic_release] # Assuming this gets the version from previous job + runs-on: ubuntu-latest + + # Only run if there's a new version to release + if: needs.semantic_release.outputs.next-version != needs.semantic_release.outputs.current-version + + permissions: + contents: write # Required for creating releases and pushing tags + pull-requests: write # Required for some release operations + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + pip install GitPython + + + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Fetch full history to get complete commit information + fetch-depth: 0 + + - name: Get latest commit info for CHANGELOG.md + id: changelog-commit + run: | + # Get the latest commit hash that modified CHANGELOG.md + COMMIT_HASH=$(git log -1 --format="%H" -- CHANGELOG.md) + + # Get commit details + COMMIT_SHORT=$(git log -1 --format="%h" -- CHANGELOG.md) + COMMIT_AUTHOR=$(git log -1 --format="%an" -- CHANGELOG.md) + COMMIT_EMAIL=$(git log -1 --format="%ae" -- CHANGELOG.md) + COMMIT_DATE=$(git log -1 --format="%ad" --date=iso -- CHANGELOG.md) + COMMIT_MESSAGE=$(git log -1 --format="%s" -- CHANGELOG.md) + + # Output to GitHub Actions environment + echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT + echo "commit_short=$COMMIT_SHORT" >> $GITHUB_OUTPUT + echo "commit_author=$COMMIT_AUTHOR" >> $GITHUB_OUTPUT + echo "commit_email=$COMMIT_EMAIL" >> $GITHUB_OUTPUT + echo "commit_date=$COMMIT_DATE" >> $GITHUB_OUTPUT + echo "commit_message=$COMMIT_MESSAGE" >> $GITHUB_OUTPUT + + # Print commit information + echo "=== Latest Commit Information for CHANGELOG.md ===" + echo "Full Hash: $COMMIT_HASH" + echo "Short Hash: $COMMIT_SHORT" + echo "Author: $COMMIT_AUTHOR <$COMMIT_EMAIL>" + echo "Date: $COMMIT_DATE" + echo "Message: $COMMIT_MESSAGE" + echo "==============================================" + + - name: Check if CHANGELOG.md exists + id: check-changelog + run: | + if [ -f "CHANGELOG.md" ]; then + echo "changelog_exists=true" >> $GITHUB_OUTPUT + echo "✅ CHANGELOG.md exists" + else + echo "changelog_exists=false" >> $GITHUB_OUTPUT + echo "❌ CHANGELOG.md not found" + fi + + - name: Get latest commit contents for CHANGELOG.md + id: changelog-content + if: steps.check-changelog.outputs.changelog_exists == 'true' + run: | + # Get the content of CHANGELOG.md from the latest commit that modified it + COMMIT_HASH="${{ steps.changelog-commit.outputs.commit_hash }}" + + # Get the file content from that specific commit + CHANGELOG_CONTENT=$(git show $COMMIT_HASH:CHANGELOG.md) + + # Extract the latest version section (everything from first ## to next ## or end of file) + LATEST_SECTION=$(echo "$CHANGELOG_CONTENT" | awk '/^## / {if(found) exit; found=1} found') + + # If no ## sections found, take the first 50 lines + if [ -z "$LATEST_SECTION" ]; then + LATEST_SECTION=$(echo "$CHANGELOG_CONTENT" | head -50) + fi + + # Save to file for multi-line content + echo "$LATEST_SECTION" > latest_changelog_section.md + + # Also save full content + echo "$CHANGELOG_CONTENT" > full_changelog.md + + echo "=== Latest Changelog Section ===" + echo "$LATEST_SECTION" + echo "===============================" + + # Set output for use in other steps + echo "latest_section_file=latest_changelog_section.md" >> $GITHUB_OUTPUT + echo "full_changelog_file=full_changelog.md" >> $GITHUB_OUTPUT + + - name: Get commit URL and display details + if: steps.check-changelog.outputs.changelog_exists == 'true' + run: | + REPO_URL="https://github.com/${{ github.repository }}" + COMMIT_URL="$REPO_URL/commit/${{ steps.changelog-commit.outputs.commit_hash }}" + echo "🔗 View commit: $COMMIT_URL" + echo "commit_url=$COMMIT_URL" >> $GITHUB_ENV + + echo "=== Using Commit Information ===" + echo "- Hash: ${{ steps.changelog-commit.outputs.commit_hash }}" + echo "- Author: ${{ steps.changelog-commit.outputs.commit_author }}" + echo "- Date: ${{ steps.changelog-commit.outputs.commit_date }}" + echo "- Message: ${{ steps.changelog-commit.outputs.commit_message }}" + + + - name: Generate release notes + id: release_notes + env: + NEXT_VERSION: ${{ needs.semantic_release.outputs.next-version }} + CURRENT_VERSION: ${{ needs.semantic_release.outputs.current-version }} + run: | + python << 'EOF' + import os + import subprocess + from git import Repo + from datetime import datetime + + def get_commits_since_tag(repo, last_tag, next_version): + """Get commits since last tag and categorize them""" + try: + if last_tag == "v0.0.0": + commits = list(repo.iter_commits()) + else: + commits = list(repo.iter_commits(f'{last_tag}..HEAD')) + except: + commits = list(repo.iter_commits()) + + features = [] + fixes = [] + breaking = [] + other = [] + + for commit in commits: + message = commit.message.strip() + first_line = message.split('\n')[0] + + if ('breaking change' in message.lower() or + first_line.lower().startswith('feat!:') or + first_line.lower().startswith('fix!:') or + '!' in first_line.split(':')[0] if ':' in first_line else False): + breaking.append(f"- {first_line}") + elif first_line.lower().startswith('feat:'): + features.append(f"- {first_line[5:].strip()}") + elif first_line.lower().startswith('fix:'): + fixes.append(f"- {first_line[4:].strip()}") + else: + other.append(f"- {first_line}") + + return features, fixes, breaking, other + + def generate_release_notes(next_version, current_version, features, fixes, breaking, other): + """Generate formatted release notes""" + # notes = f"# Release ${{ needs.semantic_release.outputs.next-version }}\n\n" + notes = "${{ steps.changelog-commit.outputs.commit_hash }}\n\n" + notes += f"${{ steps.changelog-commit.outputs.commit_author }}\n" + notes += f"${{ steps.changelog-commit.outputs.commit_date }}\n\n" + notes += f"${{ needs.semantic_release.outputs.next-version }}\n\n" + notes += f"${{ steps.changelog-commit.outputs.commit_message }}\n\n" + notes += f"**Release Date**: {datetime.now().strftime('%Y-%m-%d')}\n\n" # Add release date + + if breaking: + notes += "## 💥 Breaking Changes\n" + notes += "\n".join(breaking) + "\n\n" + + if features: + notes += "## ✨ New Features\n" + notes += "\n".join(features) + "\n\n" + + if fixes: + notes += "## 🐛 Bug Fixes\n" + notes += "\n".join(fixes) + "\n\n" + + if other: + notes += "## 📝 Other Changes\n" + notes += "\n".join(other) + "\n\n" + notes += "## CHANGELOG.md Section\n" + # Read the latest section from CHANGELOG.md + if os.path.exists('latest_changelog_section.md'): + with open('latest_changelog_section.md', 'r') as f: + changelog_section = f.read() + notes += changelog_section + "\n\n" + else: + notes += "No CHANGELOG.md section found.\n\n" + + notes += f"**Full Changelog**: https://github.com/${{ github.repository }}/compare/{current_version}...{next_version}\n" + + return notes + + # Generate release notes + repo = Repo('.') + next_version = os.environ['NEXT_VERSION'] + current_version = os.environ['CURRENT_VERSION'] + + features, fixes, breaking, other = get_commits_since_tag(repo, current_version, next_version) + release_notes = generate_release_notes(next_version, current_version, features, fixes, breaking, other) + + print("Generated release notes:") + print(release_notes) + + # Write to file for GitHub Actions + with open('release_notes.md', 'w') as f: + f.write(release_notes) + + # Set output for next step + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + # Escape newlines for GitHub Actions + escaped_notes = release_notes.replace('\n', '\\n').replace('\r', '\\r') + f.write(f"release_notes={escaped_notes}\n") + EOF + + - name: Create Git Tag + env: + NEXT_VERSION: ${{ needs.semantic_release.outputs.next-version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git tag -a "$NEXT_VERSION" -m "Release $NEXT_VERSION" + git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git + git push origin "$NEXT_VERSION" + + - name: Create GitHub Release + id: create_release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.semantic_release.outputs.next-version }} + name: Release ${{ needs.semantic_release.outputs.next-version }} + body_path: release_notes.md + draft: false + prerelease: false + + - name: Upload Release Assets (Optional) + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.semantic_release.outputs.next-version }} + files: ./dist/* # Adjust path to your build artifacts + # Only run if you have artifacts to upload + if: false # Change to true if you want to upload artifacts From a085124fc8f42f6c8a19e64c2a1c15ae0328a167 Mon Sep 17 00:00:00 2001 From: Craig West Date: Fri, 18 Jul 2025 07:22:05 +0100 Subject: [PATCH 4/4] fix: creating ci template --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 329b24a..1c202db 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - uses a custom Python script to check for secrets - uses `detect-secrets` to check for secrets + ## Setup `git clone https://github.com/Python-Test-Engineer/gha-python.git` @@ -170,4 +171,4 @@ Closes #123 - Use the imperative mood ("add" not "added" or "adds") - Keep the description under 50 characters when possible - Use the body to explain what and why vs. how -- Reference issues and pull requests in the footer \ No newline at end of file +- Reference issues and pull requests in the footer pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy