intial commit
This commit is contained in:
commit
07cf61be11
7 files changed
+446
No files matched your search
@@ -0,0 +1,18 @@
|
||||
name: Test action
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
|
||||
- name: Run integration test
|
||||
run: ./tests/integration.sh
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Nurinda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Bump ERP App Version
|
||||
|
||||
A forge-neutral composite action for preparing a versioned Frappe/ERPNext app before merging its release branch. It uses [`bump-my-version`](https://callowayproject.github.io/bump-my-version/) and ordinary Git commands, without `gh` or GitHub API calls.
|
||||
|
||||
The action compares `[tool.bumpversion].current_version` on the checked-out target branch with the configured base branch:
|
||||
|
||||
- Equal: bump, commit, and optionally push the new version.
|
||||
- Target newer than base: do nothing, making PR synchronization events idempotent.
|
||||
- Target older than base: fail and require branch synchronization.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Linux runner with Bash, Git, and Python 3.11 or newer.
|
||||
- A clean checkout of the branch that should receive the version commit.
|
||||
- Persisted Git credentials with permission to push that branch.
|
||||
- Stable `MAJOR.MINOR.PATCH` versions.
|
||||
- A `bump-my-version` configuration that includes every version file.
|
||||
|
||||
Example `pyproject.toml` for a Frappe app:
|
||||
|
||||
```toml
|
||||
[tool.bumpversion]
|
||||
current_version = "0.0.1"
|
||||
commit = false
|
||||
tag = false
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "erp_operations/__init__.py"
|
||||
search = '__version__ = "{current_version}"'
|
||||
replace = '__version__ = "{new_version}"'
|
||||
```
|
||||
|
||||
## Gitea Actions usage
|
||||
|
||||
```yaml
|
||||
name: Prepare application version
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
version:
|
||||
if: ${{ gitea.head_ref == 'develop' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out develop
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
with:
|
||||
ref: develop
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Prepare version
|
||||
id: version
|
||||
uses: Nurinda/bump-erp-app-version@v1
|
||||
with:
|
||||
config-file: pyproject.toml
|
||||
base-branch: main
|
||||
target-branch: develop
|
||||
bump: patch
|
||||
git-user-name: ERP Operations Release Bot
|
||||
git-user-email: it@nurinda.co.id
|
||||
|
||||
- name: Report version
|
||||
run: echo "${{ steps.version.outputs.previous-version }} -> ${{ steps.version.outputs.current-version }}"
|
||||
```
|
||||
|
||||
Gitea may suppress workflows caused by pushes authenticated with its built-in `GITEA_TOKEN`. If CI checks must rerun on the generated commit, pass a narrowly scoped bot repository token to `actions/checkout` instead.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Default | Description |
|
||||
|---|---|---|
|
||||
| `config-file` | `pyproject.toml` | `bump-my-version` TOML configuration |
|
||||
| `base-branch` | `main` | Branch providing the baseline version |
|
||||
| `target-branch` | `develop` | Checked-out branch receiving the commit |
|
||||
| `bump` | `patch` | `major`, `minor`, or `patch` |
|
||||
| `remote` | `origin` | Git remote used for fetch and push |
|
||||
| `commit-message` | `chore(release): prepare v{version}` | Version commit message |
|
||||
| `git-user-name` | `ERP App Release Bot` | Git commit identity |
|
||||
| `git-user-email` | `actions@localhost` | Git commit identity |
|
||||
| `bump-my-version-version` | `1.5.1` | Exact package version installed in an isolated environment |
|
||||
| `push` | `true` | Whether to push the created commit |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
|---|---|
|
||||
| `bumped` | `true` when a commit was created |
|
||||
| `previous-version` | Version before the action ran |
|
||||
| `current-version` | Version after the action ran |
|
||||
| `commit` | Created commit SHA, empty for a no-op |
|
||||
|
||||
## Safety behavior
|
||||
|
||||
The action does not force-push, create tags, publish releases, or call forge APIs. Concurrent release jobs are resolved safely by the remote rejecting a non-fast-forward push. Configure workflow-level concurrency if release PRs can overlap.
|
||||
|
||||
## Development
|
||||
|
||||
Run the integration test:
|
||||
|
||||
```bash
|
||||
./tests/integration.sh
|
||||
```
|
||||
|
||||
The test creates a temporary Git remote and verifies bumping, committing, pushing, and idempotent reruns.
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
name: Bump ERP app version
|
||||
description: Bump a Frappe/ERPNext app version on a release branch using bump-my-version
|
||||
inputs:
|
||||
config-file:
|
||||
description: Path to the bump-my-version TOML configuration
|
||||
required: false
|
||||
default: pyproject.toml
|
||||
base-branch:
|
||||
description: Branch whose version is used as the release baseline
|
||||
required: false
|
||||
default: main
|
||||
target-branch:
|
||||
description: Checked-out branch that receives the version commit
|
||||
required: false
|
||||
default: develop
|
||||
bump:
|
||||
description: Version component passed to bump-my-version
|
||||
required: false
|
||||
default: patch
|
||||
remote:
|
||||
description: Git remote used to fetch and push
|
||||
required: false
|
||||
default: origin
|
||||
commit-message:
|
||||
description: Commit message; {version} is replaced with the new version
|
||||
required: false
|
||||
default: "chore(release): prepare v{version}"
|
||||
git-user-name:
|
||||
description: Author and committer name for the version commit
|
||||
required: false
|
||||
default: ERP App Release Bot
|
||||
git-user-email:
|
||||
description: Author and committer email for the version commit
|
||||
required: false
|
||||
default: actions@localhost
|
||||
bump-my-version-version:
|
||||
description: Pinned bump-my-version package version to install
|
||||
required: false
|
||||
default: "1.5.1"
|
||||
push:
|
||||
description: Push the version commit to target-branch
|
||||
required: false
|
||||
default: "true"
|
||||
outputs:
|
||||
bumped:
|
||||
description: Whether this run created a version commit
|
||||
value: ${{ steps.bump.outputs.bumped }}
|
||||
previous-version:
|
||||
description: Version before this action ran
|
||||
value: ${{ steps.bump.outputs.previous-version }}
|
||||
current-version:
|
||||
description: Version after this action ran
|
||||
value: ${{ steps.bump.outputs.current-version }}
|
||||
commit:
|
||||
description: Created commit SHA, or an empty string when no bump was needed
|
||||
value: ${{ steps.bump.outputs.commit }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Compare, bump, commit, and push
|
||||
id: bump
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_CONFIG_FILE: ${{ inputs.config-file }}
|
||||
INPUT_BASE_BRANCH: ${{ inputs.base-branch }}
|
||||
INPUT_TARGET_BRANCH: ${{ inputs.target-branch }}
|
||||
INPUT_BUMP: ${{ inputs.bump }}
|
||||
INPUT_REMOTE: ${{ inputs.remote }}
|
||||
INPUT_COMMIT_MESSAGE: ${{ inputs.commit-message }}
|
||||
INPUT_GIT_USER_NAME: ${{ inputs.git-user-name }}
|
||||
INPUT_GIT_USER_EMAIL: ${{ inputs.git-user-email }}
|
||||
INPUT_BUMP_MY_VERSION_VERSION: ${{ inputs.bump-my-version-version }}
|
||||
INPUT_PUSH: ${{ inputs.push }}
|
||||
run: ${{ github.action_path }}/scripts/bump.sh
|
||||
branding:
|
||||
icon: chevrons-up
|
||||
color: blue
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${INPUT_CONFIG_FILE:?config-file is required}"
|
||||
: "${INPUT_BASE_BRANCH:?base-branch is required}"
|
||||
: "${INPUT_TARGET_BRANCH:?target-branch is required}"
|
||||
: "${INPUT_BUMP:?bump is required}"
|
||||
: "${INPUT_REMOTE:?remote is required}"
|
||||
: "${INPUT_COMMIT_MESSAGE:?commit-message is required}"
|
||||
: "${INPUT_GIT_USER_NAME:?git-user-name is required}"
|
||||
: "${INPUT_GIT_USER_EMAIL:?git-user-email is required}"
|
||||
: "${INPUT_BUMP_MY_VERSION_VERSION:?bump-my-version-version is required}"
|
||||
: "${INPUT_PUSH:?push is required}"
|
||||
: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}"
|
||||
|
||||
case "${INPUT_PUSH}" in
|
||||
true | false) ;;
|
||||
*) echo "push must be 'true' or 'false'" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
case "${INPUT_BUMP}" in
|
||||
major | minor | patch) ;;
|
||||
*) echo "bump must be major, minor, or patch" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "${INPUT_CONFIG_FILE}" ]]; then
|
||||
echo "Configuration file not found: ${INPUT_CONFIG_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
echo "Working tree must be clean before bumping the version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_branch=$(git branch --show-current)
|
||||
if [[ "${current_branch}" != "${INPUT_TARGET_BRANCH}" ]]; then
|
||||
echo "Expected checked-out branch '${INPUT_TARGET_BRANCH}', found '${current_branch:-detached HEAD}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch "${INPUT_REMOTE}" \
|
||||
"${INPUT_BASE_BRANCH}:refs/remotes/${INPUT_REMOTE}/${INPUT_BASE_BRANCH}"
|
||||
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
base_ref="refs/remotes/${INPUT_REMOTE}/${INPUT_BASE_BRANCH}"
|
||||
current_version=$(python3 "${script_dir}/read_version.py" "${INPUT_CONFIG_FILE}")
|
||||
base_version=$(python3 "${script_dir}/read_version.py" "${INPUT_CONFIG_FILE}" --ref "${base_ref}")
|
||||
|
||||
comparison=$(python3 - "${current_version}" "${base_version}" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
pattern = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
||||
versions = []
|
||||
for value in sys.argv[1:]:
|
||||
match = pattern.fullmatch(value)
|
||||
if not match:
|
||||
raise SystemExit(f"Version must use stable MAJOR.MINOR.PATCH format: {value}")
|
||||
versions.append(tuple(map(int, match.groups())))
|
||||
|
||||
print((versions[0] > versions[1]) - (versions[0] < versions[1]))
|
||||
PY
|
||||
)
|
||||
|
||||
if [[ "${comparison}" == "1" ]]; then
|
||||
echo "Version ${current_version} is already prepared (base: ${base_version})"
|
||||
{
|
||||
echo "bumped=false"
|
||||
echo "previous-version=${current_version}"
|
||||
echo "current-version=${current_version}"
|
||||
echo "commit="
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${comparison}" == "-1" ]]; then
|
||||
echo "Target branch version ${current_version} is older than base branch version ${base_version}" >&2
|
||||
echo "Synchronize ${INPUT_TARGET_BRANCH} with ${INPUT_BASE_BRANCH} before releasing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
venv_dir=$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/bump-erp-app-version.XXXXXX")
|
||||
trap 'rm -rf "${venv_dir}"' EXIT
|
||||
python3 -m venv "${venv_dir}"
|
||||
"${venv_dir}/bin/pip" install --disable-pip-version-check \
|
||||
"bump-my-version==${INPUT_BUMP_MY_VERSION_VERSION}"
|
||||
|
||||
git config user.name "${INPUT_GIT_USER_NAME}"
|
||||
git config user.email "${INPUT_GIT_USER_EMAIL}"
|
||||
|
||||
message=${INPUT_COMMIT_MESSAGE//\{version\}/\{new_version\}}
|
||||
"${venv_dir}/bin/bump-my-version" bump \
|
||||
--config-file "${INPUT_CONFIG_FILE}" \
|
||||
--commit \
|
||||
--no-tag \
|
||||
--message "${message}" \
|
||||
"${INPUT_BUMP}"
|
||||
|
||||
new_version=$(python3 "${script_dir}/read_version.py" "${INPUT_CONFIG_FILE}")
|
||||
commit=$(git rev-parse HEAD)
|
||||
|
||||
if [[ "${INPUT_PUSH}" == "true" ]]; then
|
||||
git push "${INPUT_REMOTE}" "HEAD:${INPUT_TARGET_BRANCH}"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "bumped=true"
|
||||
echo "previous-version=${current_version}"
|
||||
echo "current-version=${new_version}"
|
||||
echo "commit=${commit}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Prepared ${current_version} -> ${new_version} (${commit})"
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read bump-my-version's current_version from a local file or Git ref."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import tomllib
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("config_file")
|
||||
parser.add_argument("--ref")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
try:
|
||||
if args.ref:
|
||||
content = subprocess.check_output(
|
||||
["git", "show", f"{args.ref}:{args.config_file}"],
|
||||
)
|
||||
else:
|
||||
with open(args.config_file, "rb") as config_file:
|
||||
content = config_file.read()
|
||||
except (OSError, subprocess.CalledProcessError) as error:
|
||||
raise SystemExit(f"Could not load {args.config_file}: {error}") from error
|
||||
|
||||
try:
|
||||
version = tomllib.loads(content.decode())["tool"]["bumpversion"]["current_version"]
|
||||
except (KeyError, TypeError, UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
|
||||
raise SystemExit(
|
||||
f"Could not read tool.bumpversion.current_version from {args.config_file}: {error}"
|
||||
) from error
|
||||
|
||||
if not isinstance(version, str):
|
||||
raise SystemExit("tool.bumpversion.current_version must be a string")
|
||||
print(version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
test_root=$(mktemp -d)
|
||||
trap 'rm -rf "${test_root}"' EXIT
|
||||
|
||||
git init --bare -q "${test_root}/remote.git"
|
||||
git init -q -b main "${test_root}/app"
|
||||
cd "${test_root}/app"
|
||||
git config user.name Test
|
||||
git config user.email test@example.com
|
||||
mkdir erp_operations
|
||||
cat > pyproject.toml <<'TOML'
|
||||
[tool.bumpversion]
|
||||
current_version = "0.0.1"
|
||||
commit = false
|
||||
tag = false
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "erp_operations/__init__.py"
|
||||
search = '__version__ = "{current_version}"'
|
||||
replace = '__version__ = "{new_version}"'
|
||||
TOML
|
||||
printf '%s\n' '__version__ = "0.0.1"' > erp_operations/__init__.py
|
||||
git add .
|
||||
git commit -qm initial
|
||||
git remote add origin "${test_root}/remote.git"
|
||||
git push -qu origin main
|
||||
git switch -qc develop
|
||||
git push -qu origin develop
|
||||
|
||||
export INPUT_CONFIG_FILE=pyproject.toml
|
||||
export INPUT_BASE_BRANCH=main
|
||||
export INPUT_TARGET_BRANCH=develop
|
||||
export INPUT_BUMP=patch
|
||||
export INPUT_REMOTE=origin
|
||||
export INPUT_COMMIT_MESSAGE='chore(release): prepare v{version}'
|
||||
export INPUT_GIT_USER_NAME='ERP App Release Bot'
|
||||
export INPUT_GIT_USER_EMAIL='actions@example.com'
|
||||
export INPUT_BUMP_MY_VERSION_VERSION=1.5.1
|
||||
export INPUT_PUSH=true
|
||||
export RUNNER_TEMP="${test_root}"
|
||||
export GITHUB_OUTPUT="${test_root}/output"
|
||||
|
||||
"${repo_root}/scripts/bump.sh"
|
||||
grep -q '^current_version = "0.0.2"$' pyproject.toml
|
||||
grep -q '^__version__ = "0.0.2"$' erp_operations/__init__.py
|
||||
grep -q '^bumped=true$' "${GITHUB_OUTPUT}"
|
||||
grep -q '^previous-version=0.0.1$' "${GITHUB_OUTPUT}"
|
||||
grep -q '^current-version=0.0.2$' "${GITHUB_OUTPUT}"
|
||||
[[ "$(git log -1 --format=%s)" == "chore(release): prepare v0.0.2" ]]
|
||||
[[ "$(git rev-parse HEAD)" == "$(git --git-dir="${test_root}/remote.git" rev-parse develop)" ]]
|
||||
|
||||
: > "${GITHUB_OUTPUT}"
|
||||
"${repo_root}/scripts/bump.sh"
|
||||
grep -q '^bumped=false$' "${GITHUB_OUTPUT}"
|
||||
[[ "$(git rev-list --count main..develop)" == "1" ]]
|
||||
|
||||
echo "Integration test passed"
|
||||
Reference in new issue
Block a user