1 Commits

Author SHA1 Message Date
c2000c3f4d WIP
All checks were successful
run tests / check (push) Successful in 44s
run tests / release (push) Successful in 16s
2025-12-10 06:06:19 +01:00
17 changed files with 178 additions and 91 deletions

View File

@@ -1 +1 @@
version_descriptor: version.txt version_descriptor: setup.py

View File

@@ -23,17 +23,28 @@ jobs:
- run: set -u; echo "$RELEASE_PROJECT_CURRENT_VERSION" - run: set -u; echo "$RELEASE_PROJECT_CURRENT_VERSION"
test-declare-with-release-yaml: test-is-not-yet-released:
runs-on: action-runner runs-on: action-runner
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# skip login locally
- if: github.repository == 'actions/release'
uses: https://gitea.puzzleyou.net/actions/configure-runner-environment@master
- uses: ./declare - uses: ./declare
with: with:
configure_runner_environment: false configure_runner_environment: false
filename: ./src/release/tests/assets/release.yaml version_descriptor: test-assets/version.txt
- run: set -u; echo "$RELEASE_PROJECT_CURRENT_VERSION" - uses: ./check-is-not-released
with:
configure_runner_environment: false
test-declare-with-release-yaml:
runs-on: action-runner
steps:
- uses: actions/checkout@v4
# skip login locally # skip login locally
- if: github.repository == 'actions/release' - if: github.repository == 'actions/release'
@@ -42,6 +53,9 @@ jobs:
- uses: ./release - uses: ./release
with: with:
dry_run: true dry_run: true
configure_runner_environment: false
- run: set -u; echo "$RELEASE_PROJECT_CURRENT_VERSION"
test-declare-directly: test-declare-directly:
runs-on: action-runner runs-on: action-runner
@@ -95,12 +109,21 @@ jobs:
repository: europe-docker.hetzner.cloud/puzzleyou/helm repository: europe-docker.hetzner.cloud/puzzleyou/helm
condition: always condition: always
- name: ensure that environment variables are set - name: ensure that default artefact is set
run: env | grep "RELEASE_IMAGE_LOCAL_NAME_DEFAULT_IMAGE"
- name: ensure that additional artefact is set
run: env | grep "RELEASE_IMAGE_LOCAL_NAME_SOME_DOCKER_IMAGE" run: env | grep "RELEASE_IMAGE_LOCAL_NAME_SOME_DOCKER_IMAGE"
- name: dump release environment variables - name: dump release environment variables
run: env | grep "RELEASE_" run: env | grep "RELEASE_"
- name: check version
run: echo "$RELEASE_PROJECT_CURRENT_VERSION" | grep "1.33.7"
- name: check state file is set
run: set -u; echo "$RELEASE_ACTION_STATEFILE"
- name: dump project description - name: dump project description
uses: ./dump uses: ./dump

View File

@@ -14,5 +14,4 @@ jobs:
runs-on: action-runner runs-on: action-runner
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./declare # TODO single action - uses: ./
- uses: ./release

1
action.yaml Symbolic link
View File

@@ -0,0 +1 @@
release/action.yaml

View File

@@ -0,0 +1,48 @@
name: "check not released"
description: "ensure that there is no release with the current version"
inputs:
configure_runner_environment:
required: false
default: true
runs:
using: composite
steps:
- if: inputs.configure_runner_environment == 'true'
uses: https://gitea.puzzleyou.net/actions/configure-runner-environment@master
- name: declare project if neccessary
run: |
if [[ ! -z "${RELEASE_PROJECT_CURRENT_VERSION}" ]]; then
echo "already set up."
exit 0
fi
nix run ${{ github.action_path }} -- \
declare \
--release-yaml-filename ".gitea/release.yaml" \
--gitea-instance "https://gitea.puzzleyou.net" \
--release-repository-name "${{ github.repository }}" \
--release-ref-name "${{ github.ref_name }}" \
--release-run-number "${{ github.run_number }}" \
--release-commit-sha "${{ github.sha }}" \
--write-env-vars-to-filename "$GITHUB_ENV"
- name: check release state
run: |
set +e
nix run ${{ github.action_path }} -- \
dump \
--state "${RELEASE_ACTION_STATEFILE}" \
| grep "is already released: True"
IS_RELEASED_EXIT_CODE=$?
if [[ "$IS_RELEASED_EXIT_CODE" -eq 0 ]]; then
VERSION="$RELEASE_PROJECT_CURRENT_VERSION"
echo "Project is already released with version ${VERSION}."
exit -1
else
echo "Project is not yet released."
fi

View File

@@ -102,57 +102,18 @@ runs:
- if: inputs.configure_runner_environment == 'true' - if: inputs.configure_runner_environment == 'true'
uses: https://gitea.puzzleyou.net/actions/configure-runner-environment@master uses: https://gitea.puzzleyou.net/actions/configure-runner-environment@master
- name: init action state
run: |
RELEASE_ACTION_STATEFILE=$(mktemp)
echo "[release] statefile: $RELEASE_ACTION_STATEFILE"
echo "RELEASE_ACTION_STATEFILE="$RELEASE_ACTION_STATEFILE"" \
>> "$GITHUB_ENV"
- name: declare release project - name: declare release project
if: inputs.version_descriptor == ''
run: | run: |
if [[ "${{ github.ref_name }}" == "master" || "${{ github.ref_name }}" == "main" ]]; then
IS_PRE_RELEASE="0"
else
IS_PRE_RELEASE="1"
fi
nix run ${{ github.action_path }} -- \ nix run ${{ github.action_path }} -- \
declare \ declare \
--state "${RELEASE_ACTION_STATEFILE}" \
--release-yaml-filename "${{ inputs.filename }}" \ --release-yaml-filename "${{ inputs.filename }}" \
--gitea-instance "${{ inputs.gitea_instance }}" \
--release-repository-name "${{ inputs.repository }}" \
--release-ref-name "${{ github.ref_name }}" \
--release-run-number "${{ github.run_number }}" \
--release-commit-sha "${{ github.sha }}" \
--is-pre-release "${IS_PRE_RELEASE}"
nix run ${{ github.action_path }} -- \
dump \
--state "${RELEASE_ACTION_STATEFILE}" \
--write-env-vars-to-filename "$GITHUB_ENV"
- name: declare release project
if: inputs.version_descriptor != ''
run: |
if [[ "${{ github.ref_name }}" == "master" || "${{ github.ref_name }}" == "main" ]]; then
IS_PRE_RELEASE="0"
else
IS_PRE_RELEASE="1"
fi
nix run ${{ github.action_path }} -- \
declare \
--state "${RELEASE_ACTION_STATEFILE}" \
--version-descriptor "${{ inputs.version_descriptor }}" \ --version-descriptor "${{ inputs.version_descriptor }}" \
--gitea-instance "${{ inputs.gitea_instance }}" \ --gitea-instance "${{ inputs.gitea_instance }}" \
--release-repository-name "${{ inputs.repository }}" \ --release-repository-name "${{ inputs.repository }}" \
--release-ref-name "${{ github.ref_name }}" \ --release-ref-name "${{ github.ref_name }}" \
--release-run-number "${{ github.run_number }}" \ --release-run-number "${{ github.run_number }}" \
--release-commit-sha "${{ github.sha }}" \ --release-commit-sha "${{ github.sha }}" \
--is-pre-release "${IS_PRE_RELEASE}" \ --write-env-vars-to-filename "$GITHUB_ENV"
if [[ ! -z "${{ inputs.artefact_type }}" ]]; then if [[ ! -z "${{ inputs.artefact_type }}" ]]; then
nix run ${{ github.action_path }} -- \ nix run ${{ github.action_path }} -- \
@@ -179,8 +140,3 @@ runs:
--deployment-namespace "${{ inputs.deployment_namespace }}" \ --deployment-namespace "${{ inputs.deployment_namespace }}" \
--deployment-repository "${{ inputs.deployment_repository }}" --deployment-repository "${{ inputs.deployment_repository }}"
fi fi
nix run ${{ github.action_path }} -- \
dump \
--state "${RELEASE_ACTION_STATEFILE}" \
--write-env-vars-to-filename "$GITHUB_ENV"

View File

@@ -18,6 +18,7 @@
toml toml
requests requests
pyyaml pyyaml
packaging
]); ]);
pythonPackage = pkgs.python3Packages.buildPythonPackage { pythonPackage = pkgs.python3Packages.buildPythonPackage {
@@ -44,6 +45,7 @@
requests requests
pythonPackage pythonPackage
pyyaml pyyaml
packaging
]; ];
} }
(builtins.readFile ./src/main.py) (builtins.readFile ./src/main.py)

View File

@@ -24,4 +24,11 @@ test-workflows:
--workflows ./.gitea/workflows/check.yaml \ --workflows ./.gitea/workflows/check.yaml \
--job test-declare-default --job test-declare-default
act_runner exec \
--image "-self-hosted" \
--event pull_request \
--workflows ./.gitea/workflows/check.yaml \
--job test-is-not-yet-released
# TODO
# --image "europe-docker.pkg.dev/puzzle-and-play/docker/action-runner-job:latest" \ # --image "europe-docker.pkg.dev/puzzle-and-play/docker/action-runner-job:latest" \

View File

@@ -7,9 +7,33 @@ inputs:
description: "do not change external state" description: "do not change external state"
default: false default: false
configure_runner_environment:
required: false
default: true
runs: runs:
using: composite using: composite
steps: steps:
- if: inputs.configure_runner_environment == 'true'
uses: https://gitea.puzzleyou.net/actions/configure-runner-environment@master
- name: declare project if neccessary
run: |
if [[ ! -z "${RELEASE_PROJECT_CURRENT_VERSION}" ]]; then
echo "already set up."
exit 0
fi
nix run ${{ github.action_path }} -- \
declare \
--release-yaml-filename ".gitea/release.yaml" \
--gitea-instance "https://gitea.puzzleyou.net" \
--release-repository-name "${{ github.repository }}" \
--release-ref-name "${{ github.ref_name }}" \
--release-run-number "${{ github.run_number }}" \
--release-commit-sha "${{ github.sha }}" \
--write-env-vars-to-filename "$GITHUB_ENV"
- name: publish artefacts - name: publish artefacts
run: | run: |
nix run ${{ github.action_path }} -- \ nix run ${{ github.action_path }} -- \

View File

@@ -2,6 +2,8 @@ import pickle
import re import re
from argparse import ArgumentParser from argparse import ArgumentParser
from dataclasses import replace from dataclasses import replace
from os import path
from tempfile import gettempdir
from typing import Optional from typing import Optional
import yaml import yaml
@@ -235,7 +237,6 @@ if __name__ == '__main__':
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument('action', choices=[ parser.add_argument('action', choices=[
# TODO missing: adjust version (development) # TODO missing: adjust version (development)
# TODO missing: check if release already exists
'declare', 'declare',
'check', 'check',
'add-artefact', 'add-artefact',
@@ -262,7 +263,7 @@ if __name__ == '__main__':
else: else:
raise Exception('flag can be "0" or "1". got: %s' % val) raise Exception('flag can be "0" or "1". got: %s' % val)
parser.add_argument('--state', required=True) parser.add_argument('--state', type=nullable_string)
parser.add_argument('--version-descriptor', type=nullable_string) parser.add_argument('--version-descriptor', type=nullable_string)
parser.add_argument('--release-yaml-filename', type=nullable_string) parser.add_argument('--release-yaml-filename', type=nullable_string)
parser.add_argument('--dry-run', type=true_or_false) parser.add_argument('--dry-run', type=true_or_false)
@@ -296,6 +297,8 @@ if __name__ == '__main__':
parser.add_argument('--write-env-vars-to-filename') parser.add_argument('--write-env-vars-to-filename')
args = parser.parse_args() args = parser.parse_args()
state_file = (args.state
or str(path.join(gettempdir(), 'release_project_state')))
def clean_repository_name(name: str) -> str: def clean_repository_name(name: str) -> str:
if name.startswith('//'): if name.startswith('//'):
@@ -304,13 +307,22 @@ if __name__ == '__main__':
return name return name
if args.action == 'declare': if args.action == 'declare':
if args.release_yaml_filename is None: if args.version_descriptor is None:
project_description = make_project_description(
args.version_descriptor)
else:
with open(args.release_yaml_filename, 'r') as f: with open(args.release_yaml_filename, 'r') as f:
project_description = parse_project_description( project_description = parse_project_description(
yaml.safe_load(f)) yaml.safe_load(f))
else:
project_description = make_project_description(
args.version_descriptor)
if args.is_pre_release is None:
assert args.release_ref_name is not None
is_pre_release = not any(
map(lambda rn: rn in args.release_ref_name,
['master', 'main']))
else:
is_pre_release = args.is_pre_release
project_description = replace( project_description = replace(
project_description, project_description,
@@ -320,12 +332,12 @@ if __name__ == '__main__':
args.release_ref_name, args.release_ref_name,
args.release_run_number, args.release_run_number,
args.release_commit_sha, args.release_commit_sha,
args.is_pre_release)) is_pre_release))
save_project_description(args.state, project_description) save_project_description(state_file, project_description)
elif args.action == 'add-artefact': elif args.action == 'add-artefact':
project_description = load_project_description(args.state) project_description = load_project_description(state_file)
artefact = make_artefact(args.artefact_type, artefact = make_artefact(args.artefact_type,
args.artefact_repository, args.artefact_repository,
@@ -340,10 +352,10 @@ if __name__ == '__main__':
project_description, project_description,
artefacts=project_description.artefacts + [artefact]) artefacts=project_description.artefacts + [artefact])
save_project_description(args.state, project_description) save_project_description(state_file, project_description)
elif args.action == 'add-deployment': elif args.action == 'add-deployment':
project_description = load_project_description(args.state) project_description = load_project_description(state_file)
deployment = make_deployment(args.deployment_type, deployment = make_deployment(args.deployment_type,
args.deployment_release_name, args.deployment_release_name,
@@ -356,38 +368,42 @@ if __name__ == '__main__':
project_description, project_description,
deployments=project_description.deployments + [deployment]) deployments=project_description.deployments + [deployment])
save_project_description(args.state, project_description) save_project_description(state_file, project_description)
elif args.action == 'publish-artefacts': elif args.action == 'publish-artefacts':
project_description = load_project_description(args.state) project_description = load_project_description(state_file)
publish_artefacts(project_description, args.dry_run) publish_artefacts(project_description, args.dry_run)
elif args.action == 'update-deployments': elif args.action == 'update-deployments':
project_description = load_project_description(args.state) project_description = load_project_description(state_file)
update_deployments(project_description, args.dry_run) update_deployments(project_description, args.dry_run)
elif args.action == 'create-release': elif args.action == 'create-release':
project_description = load_project_description(args.state) project_description = load_project_description(state_file)
create_release(project_description, args.dry_run) create_release(project_description, args.dry_run)
elif args.action == 'dump': elif args.action == 'dump':
project_description = load_project_description(args.state) project_description = load_project_description(state_file)
dump_project_description(project_description) dump_project_description(project_description)
elif args.action == 'check': elif args.action == 'check':
project_description = load_project_description(args.state) project_description = load_project_description(state_file)
else: else:
raise NotImplementedError() raise NotImplementedError()
assert project_description is not None assert project_description is not None
assert state_file is not None
env_var_filename = args.write_env_vars_to_filename env_var_filename = args.write_env_vars_to_filename
if env_var_filename is not None: if env_var_filename is not None:
env_vars = project_description.environment_variables env_vars = {
'RELEASE_ACTION_STATEFILE': state_file,
**project_description.environment_variables
}
assert not any(map(lambda v: '"' in v, env_vars.values())) assert not any(map(lambda v: '"' in v, env_vars.values()))
with open(env_var_filename, 'a') as f: with open(env_var_filename, 'a') as f:
f.write( f.write(
'\n'.join(map(lambda it: '%s="%s"' % it, env_vars.items()))) '\n'.join(map(lambda it: '%s=%s' % it, env_vars.items())))

12
src/release/common.py Normal file
View File

@@ -0,0 +1,12 @@
from packaging.version import parse as parse_version
from semver import Version
def python_version_str(version: Version) -> str:
return ('%d.%d.%d' % (version.major, version.minor, version.patch)) \
+ ('' if version.prerelease is None else '.%s' % version.prerelease)
def python_parse_version(txt: str) -> Version:
version = parse_version(txt)
return Version(*version.release, version.pre)

View File

@@ -6,6 +6,7 @@ from itertools import chain
from typing import Optional, Union from typing import Optional, Union
from release import toolkit, versioning from release import toolkit, versioning
from release.common import python_version_str
from release.context import ReleaseContext from release.context import ReleaseContext
from release.versioning import Version from release.versioning import Version
@@ -20,11 +21,6 @@ def _normalize_env_var_fragment(txt: str) -> str:
return txt.upper().replace('-', '_').replace('.', '_') return txt.upper().replace('-', '_').replace('.', '_')
def _python_version_str(version: Version) -> str:
return ('%d.%d.%d' % (version.major, version.minor, version.patch)) \
+ ('' if version.prerelease is None else '.%s' % version.prerelease)
@dataclass(frozen=True) @dataclass(frozen=True)
class OciImageReleaseInfo: class OciImageReleaseInfo:
image_name: str image_name: str
@@ -118,7 +114,7 @@ class Wheel:
return WheelReleaseInfo( return WheelReleaseInfo(
pattern=self.pattern, pattern=self.pattern,
repository=self.repository, repository=self.repository,
version_str=_python_version_str(version)) version_str=python_version_str(version))
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -140,7 +136,7 @@ class Sdist:
return SdistReleaseInfo( return SdistReleaseInfo(
filename=self.filename, filename=self.filename,
repository=self.repository, repository=self.repository,
version_str=_python_version_str(version)) version_str=python_version_str(version))
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -63,7 +63,7 @@ def publish_tarball(info: TarballReleaseInfo, cli: Cli):
cli('curl', cli('curl',
'--netrc', '--netrc',
'--fail-with-body', '--fail-with-body',
'--upload-file %s' % info.filename, '--upload-file', info.filename,
'-i', '-i',
'-X', 'PUT', '-X', 'PUT',
'%s/api/packages/puzzleYOU/generic/%s/%s/%s' % ( # TODO owner '%s/api/packages/puzzleYOU/generic/%s/%s/%s' % ( # TODO owner

View File

@@ -293,11 +293,12 @@ class TestProjectDescription(TestCase):
img.make_release_info(release_context, release_version)) img.make_release_info(release_context, release_version))
def test_project_release_info(self): def test_project_release_info(self):
project_0 = ProjectDescription(version_descriptor='version.txt') project_0 = ProjectDescription(
version_descriptor='test-assets/version.txt')
self.assertIsNone(project_0.release_info) self.assertIsNone(project_0.release_info)
project_pre = ProjectDescription( project_pre = ProjectDescription(
version_descriptor='version.txt', version_descriptor='test-assets/version.txt',
context=ReleaseContext( context=ReleaseContext(
repository_name='resi', repository_name='resi',
ref_name='testing', ref_name='testing',
@@ -308,16 +309,16 @@ class TestProjectDescription(TestCase):
self.assertEqual( self.assertEqual(
ProjectReleaseInfo( ProjectReleaseInfo(
gitea_release_title='Version 0.0.1-dev42', gitea_release_title='Version 1.33.7-dev42',
gitea_release_description='', gitea_release_description='',
gitea_is_prerelease=True, gitea_is_prerelease=True,
gitea_git_commitish='PROBABLY_BROKEN', gitea_git_commitish='PROBABLY_BROKEN',
git_tags=['v0.0.1-dev42', 'development'], git_tags=['v1.33.7-dev42', 'development'],
), ),
project_pre.release_info) project_pre.release_info)
project = ProjectDescription( project = ProjectDescription(
version_descriptor='version.txt', version_descriptor='test-assets/version.txt',
context=ReleaseContext( context=ReleaseContext(
repository_name='resi', repository_name='resi',
ref_name='master', ref_name='master',
@@ -328,11 +329,11 @@ class TestProjectDescription(TestCase):
self.assertEqual( self.assertEqual(
ProjectReleaseInfo( ProjectReleaseInfo(
gitea_release_title='Version 0.0.1', gitea_release_title='Version 1.33.7',
gitea_release_description='', gitea_release_description='',
gitea_is_prerelease=False, gitea_is_prerelease=False,
gitea_git_commitish='PROBABLY_BROKEN', gitea_git_commitish='PROBABLY_BROKEN',
git_tags=['v0.0.1', 'v0.0', 'v0', 'latest'], git_tags=['v1.33.7', 'v1.33', 'v1', 'latest'],
), ),
project.release_info) project.release_info)

View File

@@ -18,12 +18,12 @@ def _test_can_read_version(test, filename, expected):
def _test_can_write_version(test, filename): def _test_can_write_version(test, filename):
v = use_any(_asset_path(filename)) v = use_any(_asset_path(filename))
v.version = Version(1, 33, 7) v.version = Version(1, 33, 7, 42)
with NamedTemporaryFile() as tf: with NamedTemporaryFile() as tf:
v.store(tf.name) v.store(tf.name)
v2 = use_any(tf.name) v2 = use_any(tf.name)
test.assertEqual(v2.version, Version(1, 33, 7)) test.assertEqual(v2.version, Version(1, 33, 7, 42))
class TestSetupPy(TestCase): class TestSetupPy(TestCase):

View File

@@ -6,6 +6,8 @@ from logging import getLogger
import toml import toml
from semver import Version from semver import Version
from release.common import python_parse_version, python_version_str
logger = getLogger(__name__) logger = getLogger(__name__)
RE_SETUP_PY = re.compile(r'version\s?=\s?[\'"](.*)[\'"]') RE_SETUP_PY = re.compile(r'version\s?=\s?[\'"](.*)[\'"]')
@@ -24,12 +26,13 @@ class SetupPy:
self.filename = filename self.filename = filename
self.content = content self.content = content
self.version = Version.parse(version_string.group(1)) self.version = python_parse_version(version_string.group(1))
def store(self, destination=None): def store(self, destination=None):
destination = destination or self.filename destination = destination or self.filename
edited = RE_SETUP_PY.sub( edited = RE_SETUP_PY.sub(
'version=\'%s\'' % self.version, self.content) 'version=\'%s\'' % python_version_str(self.version),
self.content)
with open(destination, 'w') as f: with open(destination, 'w') as f:
f.write(edited) f.write(edited)

View File

@@ -1 +0,0 @@
0.0.1