Skip to content

Automatic Version Management in CI/CD ​

pfy ci update can generate a pipeline that bumps the patch version in project.yaml on every push to main, commits the change, and pushes a Docker image tagged with the new version. What that pipeline does, which projects get it, and how to work with it is below.

Node.js projects only

The automatic bump and image push are generated only when ci.language is node. For Go projects the generated pipeline runs secret scanning, lint, tests, and a deployment-drift check; releases are cut by GoReleaser when you push a v* tag, and project.yaml is never changed by CI. Go teams bump the version themselves with pfy version bump.

How it works ​

On every push to main, the Node.js pipeline:

  1. Runs tests and security scans against the current version.
  2. Runs pfy version bump --part patch, which rewrites version: in project.yaml.
  3. Commits project.yaml with the message chore: bump version to X.Y.Z [skip ci], so the commit does not start another run.
  4. Builds the image and pushes it with two tags: the new version and latest.

Feature branches and pull requests only run the checks. Nothing is bumped or pushed.

Image tags ​

Only two tags are ever pushed:

  • my-app:1.2.3: the exact version
  • my-app:latest: the newest main build

No commit-SHA or branch-name tags are produced.

The generated GitHub Actions job ​

This is the version-and-push job from .github/workflows/ci.yml as pfy ci update --language node writes it (registry and image name come from project.yaml):

yaml
version-and-push:
  runs-on: ubuntu-latest
  needs: build-and-scan
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  permissions:
    contents: write
    packages: write
  steps:
    - uses: actions/checkout@v6
      with:
        fetch-depth: 0
        token: ${{ secrets.GITHUB_TOKEN }}

    - name: Bump version
      run: |
        # Increment patch version
        pfy version bump --part patch
        echo "NEW_VERSION=$(pfy version show)" >> $GITHUB_ENV

    - name: Commit version bump
      run: |
        git config user.name "github-actions[bot]"
        git config user.email "github-actions[bot]@users.noreply.github.com"
        git add project.yaml
        git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
        git push

    - name: Set up QEMU
      uses: docker/setup-qemu-action@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v4

    - name: Log in to Container Registry
      uses: docker/login-action@v4
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Build and push multi-arch image
      uses: docker/build-push-action@v6
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: |
          ghcr.io/my-app:${{ env.NEW_VERSION }}
          ghcr.io/my-app:latest

The generated Node.js job does not install pfy

The "Install pfy CLI" step in the template is inside a Go-only block, so the Node.js workflow above calls pfy without installing it and the Bump version step fails on a fresh runner. Until the template is fixed, add an install step before Bump version:

yaml
    - name: Install pfy CLI
      run: |
        PFY_VERSION=$(curl -fsSL https://api.github.com/repos/ProductifyFW/cli/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
        curl -fsSLO "https://github.com/ProductifyFW/cli/releases/download/v${PFY_VERSION}/pfy_${PFY_VERSION}_linux_amd64.tar.gz"
        tar -xzf "pfy_${PFY_VERSION}_linux_amd64.tar.gz" pfy
        sudo mv pfy /usr/local/bin/pfy

The GitLab template has the same gap (its push job only has a comment where the install should be).

The generated GitLab CI job ​

The push job from .gitlab-ci.yml (Node.js):

yaml
push:
  stage: deploy
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
    - docker load -i image.tar
    - apk add --no-cache git nodejs npm
    # Install pfy CLI here if Node.js version available
  script:
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        # Bump version
        git config user.name "GitLab CI"
        git config user.email "ci@gitlab.com"
        pfy version bump --part patch
        NEW_VERSION=$(pfy version show)
        git add project.yaml
        git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
        git push https://oauth2:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git HEAD:${CI_COMMIT_BRANCH}

        # Tag and push images
        docker tag $IMAGE_NAME:0.1.0 $IMAGE_NAME:$NEW_VERSION
        docker tag $IMAGE_NAME:0.1.0 $IMAGE_NAME:latest
        docker push $IMAGE_NAME:$NEW_VERSION
        docker push $IMAGE_NAME:latest
      fi
  only:
    - main
    - tags

0.1.0 here is the version that was in project.yaml when you ran pfy ci update. The build stage builds the image with that tag; the push stage re-tags it with the bumped version. If you change the version by hand, run pfy ci update again so the two stay in step.

Permissions the pipeline needs ​

GitHub Actions: the job declares them itself:

yaml
permissions:
  contents: write # push the version commit
  packages: write # push the image

If your repository restricts the default GITHUB_TOKEN, allow "Read and write permissions" for workflows, or the git push step fails.

GitLab CI: CI_JOB_TOKEN must be allowed to push to the repository, and CI_REGISTRY_USER/CI_REGISTRY_PASSWORD must be able to push to the registry.

Version strategy ​

Every push to main bumps patch: 1.0.0 → 1.0.1 → 1.0.2 …

For a feature release or a breaking change, bump minor or major yourself before merging:

bash
pfy version bump --part minor  # 1.0.3 → 1.1.0
pfy version bump --part major  # 1.1.0 → 2.0.0
git add project.yaml
git commit -m "Release v1.1.0"
git push origin main

CI then bumps once more (1.1.0 → 1.1.1) and pushes that image. If you want the exact version you set to be the one that ships, tag it and let the release.yml workflow build from the tag instead.

Example ​

bash
pfy init --name my-service --language node   # version: 0.1.0
pfy ci update
git add .
git commit -m "Initial setup"
git push origin main

CI: tests → bump (0.1.0 → 0.1.1) → build → push my-service:0.1.1 and my-service:latest.

The bump commits show up in history:

bash
$ git log --oneline --grep="bump version"
abc123d chore: bump version to 0.2.1 [skip ci]
def456e chore: bump version to 0.2.0 [skip ci]

Troubleshooting ​

If the pipeline runs in a loop, the bump commit is missing [skip ci]. Check the commit message in the generated job.

If git push is denied: on GitHub, make sure the workflow has contents: write and the checkout step passes the GITHUB_TOKEN secret as its token input (as in the generated job above). On GitLab, the push URL must use oauth2:${CI_JOB_TOKEN} and the job token must be allowed to write.

pfy: command not found means the generated Node.js job did not install the CLI; see the warning above.

If the image tag is stale: the build stage tags with the version that was in project.yaml when the pipeline file was generated. Re-run pfy ci update after a manual bump.

Using the tags in deployments ​

Nomad deployments rendered with pfy nomad deploy take the tag from --tag or from project.yaml with --use-project-version (see Nomad Deployment). For other consumers, refer to the image by version:

yaml
image: ghcr.io/my-service:1.2.3

Avoid latest in production; it moves on every merge.