diff --git a/.github/workflows/ci.yml b/.github/workflows/build.yml similarity index 84% rename from .github/workflows/ci.yml rename to .github/workflows/build.yml index e74a43b..51d6644 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: CI +name: Build on: push: @@ -6,7 +6,7 @@ on: pull_request: jobs: - build: + package: runs-on: ubuntu-latest outputs: @@ -83,7 +83,7 @@ jobs: test: runs-on: ubuntu-latest - needs: build + needs: package strategy: matrix: @@ -119,6 +119,35 @@ jobs: - name: Run tests from repo run: python -m pytest tests/ -v + docker: + runs-on: ubuntu-latest + needs: package + + steps: + - uses: actions/checkout@v6.0.3 + with: + fetch-depth: 0 + + - name: Download built package + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Build Docker image + run: | + docker build -t ghcr.io/synacker/git-version-utils:${{ needs.package.outputs.version }} \ + --build-arg WHEEL=dist/*.whl . + docker tag ghcr.io/synacker/git-version-utils:${{ needs.package.outputs.version }} \ + ghcr.io/synacker/git-version-utils:latest + + - name: Show version info + run: | + docker run --rm \ + -v "$(pwd):/workspace" -w /workspace \ + ghcr.io/synacker/git-version-utils:latest \ + git-version --safe-directory '*' --property env + lint: runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c52c972..213988c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,12 +1,16 @@ -name: Publish to PyPI +name: Publish to PyPI and Docker on: workflow_dispatch: +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + jobs: publish: runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' + if: github.ref_name == 'main' || startsWith(github.ref_name, 'release/') steps: - uses: actions/checkout@v6.0.3 @@ -32,4 +36,42 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.14.0 with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} + + publish-docker: + needs: publish + runs-on: ubuntu-latest + if: github.ref_name == 'main' || startsWith(github.ref_name, 'release/') + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v6.0.3 + with: + fetch-depth: 0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..86fb6dc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# syntax=docker/dockerfile:1 +# +# git-version-utils Docker image +# +# Provides a lightweight container with git-version-utils pre-installed +# for use in CI pipelines (GitHub Actions, GitLab CI, etc.). +# +# Build: +# # From local source: +# docker build -t ghcr.io/synacker/git-version-utils:latest . +# +# # From pre-built wheel (used in CI after package build): +# docker build -t ghcr.io/synacker/git-version-utils:latest \ +# --build-arg WHEEL=dist/git_version_utils-*.whl . +# +# Usage: +# docker run --rm -v $(pwd):/workspace -w /workspace \ +# ghcr.io/synacker/git-version-utils:latest \ +# git-version --safe-directory '*' --property env + +# ============================================================ +# Stage 1: Builder -- install the package +# ============================================================ +FROM python:3.13-alpine AS builder + +RUN apk add --no-cache git + +ARG WHEEL +COPY . /build/ + +# Install from wheel if provided, otherwise from local source +RUN if [ -n "$WHEEL" ]; then \ + wheel_path=$(ls /build/$WHEEL 2>/dev/null | head -1) && \ + pip install --no-cache-dir "$wheel_path"; \ + else \ + pip install --no-cache-dir /build; \ + fi + +# Record the installed version for labelling the runtime image +RUN python -c "from git_version import __version__; print(__version__)" > /version.txt + +# ============================================================ +# Stage 2: Runtime -- minimal image with git + the package +# ============================================================ +FROM python:3.13-alpine AS runtime + +# git is required -- git-version-utils calls the git CLI via subprocess +RUN apk add --no-cache git + +# Copy only the git_version package (not the entire site-packages with pip/setuptools) +COPY --from=builder /usr/local/lib/python3.13/site-packages/git_version /usr/local/lib/python3.13/site-packages/git_version +COPY --from=builder /usr/local/lib/python3.13/site-packages/git_version_utils-*.dist-info /usr/local/lib/python3.13/site-packages/ + +# Copy the git-version entrypoint script +COPY --from=builder /usr/local/bin/git-version /usr/local/bin/git-version + +# Copy version label +COPY --from=builder /version.txt /version.txt \ No newline at end of file diff --git a/README.md b/README.md index 1ae7c45..8369ecb 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,159 @@ execute_process( ```dockerfile RUN pip install git-version-utils RUN source <(git-version --property env) && echo "Building $BUILD_VERSION" +``` + +## Docker CI Container + +A pre-built Docker image with `git-version-utils` is available at +`ghcr.io/synacker/git-version-utils`. + +The image is based on `python:3.13-slim` and includes `git` + `git-version-utils`. +It is designed to be used as the **job container** in CI pipelines. + +### Usage + +```bash +# Run git-version inside the container +docker run --rm \ + -v $(pwd):/workspace -w /workspace \ + ghcr.io/synacker/git-version-utils:latest \ + git-version --safe-directory '*' --property env +``` + +### GitHub Actions — Job Outputs + +Use `container:` to run the job inside the image, then parse `git-version --property env` +into `$GITHUB_OUTPUT`. Downstream jobs consume the values via `needs.set-version.outputs.*`. + +```yaml +name: CI with job outputs + +on: + push: + branches: [main, "release/*"] + +jobs: + set-version: + runs-on: ubuntu-latest + container: + image: ghcr.io/synacker/git-version-utils:latest + options: --workdir /__w/${{ github.event.repository.name }}/${{ github.event.repository.name }} + outputs: + BUILD_VERSION: ${{ steps.git_version.outputs.BUILD_VERSION }} + BUILD_VERSION_MAJOR: ${{ steps.git_version.outputs.BUILD_VERSION_MAJOR }} + BUILD_VERSION_MINOR: ${{ steps.git_version.outputs.BUILD_VERSION_MINOR }} + BUILD_VERSION_PATCH: ${{ steps.git_version.outputs.BUILD_VERSION_PATCH }} + BUILD_VERSION_BUILD: ${{ steps.git_version.outputs.BUILD_VERSION_BUILD }} + BUILD_VERSION_TAG: ${{ steps.git_version.outputs.BUILD_VERSION_TAG }} + BUILD_VERSION_FULL: ${{ steps.git_version.outputs.BUILD_VERSION_FULL }} + BUILD_VERSION_EXTENDED: ${{ steps.git_version.outputs.BUILD_VERSION_EXTENDED }} + BUILD_VERSION_SHORT: ${{ steps.git_version.outputs.BUILD_VERSION_SHORT }} + BUILD_VERSION_COMMIT: ${{ steps.git_version.outputs.BUILD_VERSION_COMMIT }} + BUILD_VERSION_BRANCH: ${{ steps.git_version.outputs.BUILD_VERSION_BRANCH }} + BUILD_VERSION_DEFAULT_BRANCH: ${{ steps.git_version.outputs.BUILD_VERSION_DEFAULT_BRANCH }} + BUILD_VERSION_RELEASE_BRANCHES: ${{ steps.git_version.outputs.BUILD_VERSION_RELEASE_BRANCHES }} + + steps: + - uses: actions/checkout@v6.0.3 + with: + fetch-depth: 0 + + - name: Extract version info + id: git_version + run: | + while IFS='=' read -r key value; do + echo "$key=$value" >> "$GITHUB_OUTPUT" + done < <(git-version --property env) + + build: + runs-on: ubuntu-latest + needs: set-version + steps: + - uses: actions/checkout@v6.0.3 + + - name: Use version info + run: | + echo "Building version: ${{ needs.set-version.outputs.BUILD_VERSION }}" + echo "Tag: ${{ needs.set-version.outputs.BUILD_VERSION_TAG }}" +``` + +### GitHub Actions — Env File Artifact + +A simpler approach: write the version info to a file and share it as an artifact. + +```yaml +name: CI with env file + +on: + push: + branches: [main, "release/*"] + +jobs: + set-version: + runs-on: ubuntu-latest + container: + image: ghcr.io/synacker/git-version-utils:latest + options: --workdir /__w/${{ github.event.repository.name }}/${{ github.event.repository.name }} + steps: + - uses: actions/checkout@v6.0.3 + with: + fetch-depth: 0 + + - name: Generate version.env + run: git-version --property env > version.env + + - name: Upload version env file + uses: actions/upload-artifact@v4 + with: + name: version-env + path: version.env + + build: + runs-on: ubuntu-latest + needs: set-version + steps: + - uses: actions/checkout@v6.0.3 + + - name: Download version env file + uses: actions/download-artifact@v4 + with: + name: version-env + + - name: Load version and use it + run: | + source version.env + echo "Building version: $BUILD_VERSION" + echo "Tag: $BUILD_VERSION_TAG" +``` + +### GitLab CI + +Use the image directly and share version info via +[dotenv artifacts](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdotenv). + +```yaml +stages: + - set-version + - build + +set-version: + stage: set-version + image: ghcr.io/synacker/git-version-utils:latest + variables: + GIT_DEPTH: 0 + script: + - git-version --property env > version.env + artifacts: + reports: + dotenv: version.env + +build: + stage: build + image: python:3.13-slim + needs: + - job: set-version + artifacts: true + script: + - echo "Building version: $BUILD_VERSION" + - echo "Tag: $BUILD_VERSION_TAG" diff --git a/src/git_version/cli.py b/src/git_version/cli.py index 2059437..b1483cc 100644 --- a/src/git_version/cli.py +++ b/src/git_version/cli.py @@ -22,6 +22,12 @@ def main() -> None: default="v[0-9]*", help="Glob pattern to match version tags (default: v[0-9]*)", ) + parser.add_argument( + "--safe-directory", + default=None, + help="Pass ``-c safe.directory=`` to git commands. " + "Use ``'*'`` to allow all directories (useful in Docker containers).", + ) parser.add_argument( "--property", "-P", choices=[ @@ -34,7 +40,11 @@ def main() -> None: ) args = parser.parse_args() - gv = GitVersion(repo_path=args.repo, tag_pattern=args.tag_pattern) + gv = GitVersion( + repo_path=args.repo, + tag_pattern=args.tag_pattern, + safe_directory=args.safe_directory, + ) if args.property == "env": for key, value in gv.env(prefix=args.prefix).items(): diff --git a/src/git_version/core.py b/src/git_version/core.py index adac78c..8884338 100644 --- a/src/git_version/core.py +++ b/src/git_version/core.py @@ -13,6 +13,8 @@ class GitVersion: tag_pattern: Glob pattern to match version tags (default: "v[0-9]*"). release_branches: List of branch patterns considered as release branches. Defaults to [default_branch, "release/*"]. + safe_directory: Pass ``-c safe.directory=`` to every git command. + Use ``"*"`` to allow all directories (useful in Docker containers). """ def __init__( @@ -20,16 +22,22 @@ def __init__( repo_path: str | None = None, tag_pattern: str = "v[0-9]*", release_branches: list[str] | None = None, + safe_directory: str | None = None, ): self.repo_path = os.path.abspath(repo_path or os.getcwd()) self.tag_pattern = tag_pattern self._release_branches = release_branches + self._safe_directory = safe_directory def _git(self, *args: str) -> str: """Execute a git command safely and return stripped stdout.""" try: + cmd = ["git", "-C", self.repo_path] + if self._safe_directory is not None: + cmd.extend(["-c", f"safe.directory={self._safe_directory}"]) + cmd.extend(args) result = subprocess.run( - ["git", "-C", self.repo_path, *args], + cmd, capture_output=True, text=True, check=True,