3 Commits

Author SHA1 Message Date
3c17fbf81a Merge pull request 'README geschrieben' (!1) from doc into master
Some checks failed
run tests / check (push) Failing after 33s
run tests / release (push) Has been skipped
Reviewed-on: #1
2026-01-07 09:19:35 +00:00
b239e51a79 README geschrieben
Some checks failed
run tests / unittest (pull_request) Successful in 14s
run tests / test-declare-default (pull_request) Successful in 15s
check if project is already released / unittest (pull_request) Failing after 25s
run tests / test-is-not-yet-released (pull_request) Successful in 23s
run tests / test-sync-versions (pull_request) Successful in 26s
run tests / test-declare-with-release-yaml (pull_request) Successful in 24s
run tests / test-skip-release-if-already-released (pull_request) Successful in 26s
run tests / test-declare-directly (pull_request) Successful in 23s
2026-01-07 10:18:05 +01:00
bf30b4defe Implementierung v0
All checks were successful
run tests / check (push) Successful in 27s
run tests / release (push) Successful in 15s
2025-12-12 11:00:11 +01:00
17 changed files with 308 additions and 77 deletions

View File

@@ -1 +1 @@
version_descriptor: setup.py version_descriptor: pyproject.toml

View File

@@ -1,7 +1,6 @@
name: check if project is already released name: check if project is already released
on: on:
- pull_request - pull_request
- push # TODO test
jobs: jobs:
unittest: unittest:

View File

@@ -135,7 +135,7 @@ jobs:
- uses: ./add-artefact - uses: ./add-artefact
with: with:
type: wheel type: wheel
pattern: "test-assets/wheels/*.whl" filename: "test-assets/wheels/*.whl"
- uses: ./add-artefact - uses: ./add-artefact
with: with:

195
README.md
View File

@@ -1,3 +1,196 @@
# release action # release action
Gitea/Github-Action, die hilft das Veröffentlichen von Projekten zu
automatisieren.
TODO document usage ## Features
- Laden von Credentials und Einrichten der Runner-Umgebung
- Veröffentlichen von Artefakten
- Docker Images
- Python Pakete (wheel, sdist)
- NPM Pakete
- Tarballs
- Ausrollen von Helm-Releases
- Anlegen von Gitea-Releases
- Anlegen von Git-Tags
## Verwendung
Die Action erwartet eine Beschreibung des zu veröffentlichenden Projekts in
`.gitea/release.yaml`. Darin werden folgende Eigenschaften beschrieben:
- Welche Datei legt die aktuelle Version fest?
- Welche Artefakte sollen veröffentlicht werden?
- Welche Releases sollen wann aktualisiert werden?
### Beispiel für release.yaml
```yaml
# Die Version wird in der Datei Cargo.toml im Hauptverzeichnis des Projekts festgelegt
version_descriptor: Cargo.toml
artefacts:
# Es soll ein Docker/OCI-Image mit dem Namen masa-images veröffentlicht werden
- type: oci_image
name: masa-images
# Es soll ein Python-Paket als Wheel veröffentlicht werden
- type: wheel
# Unter diesem Pfad ist das Wheel abgelegt (vor dem Upload in die Registry)
filename: ./scratch/wheels/*.whl
# Diese Datei bestimmt die Version des Pakets.
# Wird synchronisiert mit der Version des Projekts.
version_descriptor: pyproject.toml
deployments:
# Bei einem Prerelease (push in testing) soll das Helm-Release masa-images-testing
# aktualisiert werden.
- type: helm
release_name: masa-images-testing
```
Alle möglichen Einstellungen können
[hier](/src/release/tests/assets/release.yaml) eingesehen werden.
### Aufruf im Workflow
Die Action kann die generischen Aufgaben bei einem Release übernehmen. Was sie
nicht kann ist die Artefakte bauen. Wie das zu tun ist muss das Projekt selbst
beschreiben.
Dafür gibt es zwei Möglichkeiten: Die Kurzform in einem Schritt und die
Langform in mehreren Schritten.
#### Langform
Wenn ein Projekt veröffentlicht wird, dann werden folgende Schritt ausgeführt:
- `actions/release/declare`: Hier wird die `release.yaml` gelesen und die
Credentials werden geladen.
- `actions/release/sync-versions`: Hier werden die Versionen der Artefakte mit
der Version des Projekts synchronisiert.
- Hier muss das Projekt die Artefakte bauen.
- `actions/release/release`: Hier werden die Artefakte veröffentlicht, das
Helm-Release aktualisiert und das Gitea-Release angelegt.
Beispiel:
```yaml
name: release
on:
push:
branches:
- master
- testing
jobs:
check:
uses: ./.gitea/workflows/checks.yaml
release:
needs: ["check"]
runs-on: action-runner
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@v4
- uses: https://gitea.puzzleyou.net/actions/release/declare@v0
- uses: https://gitea.puzzleyou.net/actions/release/sync-versions@v0
- name: build image
run: |
nix develop --command just build-image ${RELEASE_IMAGE_TAG}
sccache --show-stats
- name: build wheel
run: |
mkdir -p ./scratch/wheels
nix develop --command \
maturin build \
--release \
--out ./scratch/wheels/ \
--strip \
-m Cargo.toml \
--compatibility linux \
--interpreter python3.13
sccache --show-stats
- uses: https://gitea.puzzleyou.net/actions/release@v0
```
#### Kurzform
Wenn es nur ein Artefakt gibt und das auch einfach zu bauen ist, dann kann die
Kurzform `actions/release` verwendet werden. Das ist der Normalfall. Im
Parameter `build_run` muss das Projekt (in Form eines Bash-Scripts) beschreiben
wie die Artefakte gebaut werden.
Beispiel:
```yaml
name: release
on:
push:
branches:
- master
- testing
jobs:
release:
runs-on: action-runner
steps:
- uses: actions/checkout@v4
- uses: https://gitea.puzzleyou.net/actions/release@v0
with:
build_run: docker build -t "${RELEASE_IMAGE_LOCAL_NAME_ALI}" .
```
## Umgebungsvariablen
Die Action stellt einige Umgebungsvariablen bereit, die im Workflow nützlich
sein können:
- `RELEASE_IMAGE_TAG`: Tag, der Docker/OCI-Images zugewiesen werden muss, damit
diese veröffentlicht und in Helm-Releases verwendet werden können. Entspricht
momentan dem Commit-Hash.
- `RELEASE_IMAGE_LOCAL_NAME_XXX`: Voller Name eines Docker/OCI-Images mit Tag.
- `RELEASE_IS_PRERELEASE`: Wird gerade ein Release (master/main) oder ein
Prerelease (testing) verarbeitet?
- `RELEASE_PROJECT_IS_RELEASED`: Wurde das Projekt schon unter dieser Version
veröffentlicht?
- `RELEASE_PROJECT_CURRENT_VERSION`: Aktuelle Version des Projekts, z.B.
'2.10.4'
- `RELEASE_PROJECT_PLANNED_VERSION`: "Geplante" Version des Projekts.
Entspricht der aktuellen Version mit optionalem Suffix bei Prerelease, z.B.
'2.10.4-dev42'
## Release vs. Prerelease
Die Release-Action kann für Releases (merge in master/main) oder Prereleases
(merge in testing) verwendet werden.
Es unterscheiden sich dann image-tags, git-tags und Versionsnummern der
Artefakte.
### Release
- Image-Tags: `v1`, `v1.2`, `v1.2.3`, `latest`, `master/main.latest`
- Git-Tags: `v1`, `v1.2`, `v1.2.3`, `latest`
- Versionsummern: Wie Projekt
### Prerelease
- Image-Tags: `v1.2.3-dev4`, `development`, `testing.latest`
- Git-Tags: `v1.2.3-dev4`, `development`
- Versionsnummern: Wie Projekt, mit Suffix `-dev$ACTION_LAUF_NUMMER`
## Prüfung ob eine Version schon veröffentlicht wurde
Beim Release selbst (merge in main/master) wird geprüft ob eine Version bereits
veröffentlicht wurde. Falls ja, ist das kein Fehler. **ABER** es wird in dem
Fall weder ein Docker/OCI-Image hochgeladen, noch ein Helm-Release aktualisiert
oder irgendwelche anderen Artefakte veröffentlicht. Das kann erwünscht sein,
wenn eine Änderung nicht ausgerollt werden muss (z.b. Dokumentation angepasst,
fixtures verändert, etc.)
Um bei einem Pull-Request eine Warnung zu sehen, wenn die Version nicht erhöht
wurde kann folgender Workflow verwendet werden. Es ist keine Anpassung nötig.
```yaml
name: check if project is already released
on:
- pull_request
jobs:
check:
runs-on: action-runner
steps:
- uses: actions/checkout@v4
- uses: https://gitea.puzzleyou.net/actions/release/check-is-not-released@v0
```
Bei einem Prerelease (merge in testing) muss die Version nicht angepasst
werden. Das geschieht automatisch.

View File

@@ -18,7 +18,7 @@ inputs:
filename: filename:
required: false required: false
description: "required for tarball, sdist" description: "required for tarball, sdist, wheel"
default: "" default: ""
package_name: package_name:
@@ -26,11 +26,6 @@ inputs:
description: "required for tarball" description: "required for tarball"
default: "" default: ""
pattern:
required: false
description: "required for wheel"
default: ""
directory: directory:
required: false required: false
description: "required for npm" description: "required for npm"
@@ -54,7 +49,6 @@ runs:
--artefact-name "${{ inputs.name }}" \ --artefact-name "${{ inputs.name }}" \
--artefact-filename "${{ inputs.filename }}" \ --artefact-filename "${{ inputs.filename }}" \
--artefact-package-name "${{ inputs.package_name }}" \ --artefact-package-name "${{ inputs.package_name }}" \
--artefact-pattern "${{ inputs.pattern }}" \
--artefact-directory "${{ inputs.directory }}" \ --artefact-directory "${{ inputs.directory }}" \
--version-descriptor "${{ inputs.version_descriptor }}" \ --version-descriptor "${{ inputs.version_descriptor }}" \
--write-env-vars-to-filename "$GITHUB_ENV" --write-env-vars-to-filename "$GITHUB_ENV"

View File

@@ -41,7 +41,7 @@ inputs:
artefact_filename: artefact_filename:
required: false required: false
description: "required for tarball, sdist" description: "required for tarball, sdist, wheel"
default: "" default: ""
artefact_package_name: artefact_package_name:
@@ -49,11 +49,6 @@ inputs:
description: "required for tarball" description: "required for tarball"
default: "" default: ""
artefact_pattern:
required: false
description: "required for wheel"
default: ""
artefact_directory: artefact_directory:
required: false required: false
description: "required for npm" description: "required for npm"
@@ -129,7 +124,6 @@ runs:
--artefact-name "${{ inputs.artefact_name }}" \ --artefact-name "${{ inputs.artefact_name }}" \
--artefact-filename "${{ inputs.artefact_filename }}" \ --artefact-filename "${{ inputs.artefact_filename }}" \
--artefact-package-name "${{ inputs.artefact_package_name }}" \ --artefact-package-name "${{ inputs.artefact_package_name }}" \
--artefact-pattern "${{ inputs.artefact_pattern }}" \
--artefact-directory "${{ inputs.artefact_directory }}" \ --artefact-directory "${{ inputs.artefact_directory }}" \
--version-descriptor "${{ inputs.artefact_version_descriptor }}" --version-descriptor "${{ inputs.artefact_version_descriptor }}"
fi fi

View File

@@ -4,6 +4,23 @@ description: "dump current project description"
runs: runs:
using: composite using: composite
steps: steps:
- 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: dump project description - name: dump project description
run: | run: |
nix run ${{ github.action_path }} -- \ nix run ${{ github.action_path }} -- \

23
flake.lock generated
View File

@@ -34,10 +34,31 @@
"type": "github" "type": "github"
} }
}, },
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1764134915,
"narHash": "sha256-xaKvtPx6YAnA3HQVp5LwyYG1MaN4LLehpQI8xEdBvBY=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "2c8df1383b32e5443c921f61224b198a2282a657",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"pyproject-nix": "pyproject-nix"
} }
}, },
"systems": { "systems": {

View File

@@ -4,52 +4,41 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, flake-utils, ... }: outputs = { self, nixpkgs, flake-utils, pyproject-nix }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
python = pkgs.python313.withPackages (ps: with ps; [ pythonProject = pyproject-nix.lib.project.loadPyproject {
isort projectRoot = ./.;
flake8
semver
toml
requests
pyyaml
packaging
]);
pythonPackage = pkgs.python3Packages.buildPythonPackage {
name = "release-action";
src = ./.;
}; };
pythonInterpreter = pkgs.python313;
in in
{ {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
buildInputs = [ buildInputs = [
pkgs.envsubst
pkgs.just pkgs.just
pkgs.gitea-actions-runner pkgs.gitea-actions-runner
python (pythonInterpreter.withPackages
(pythonProject.renderers.withPackages {
python = pythonInterpreter;
extraPackages = ps: with ps; [ flake8 isort ];
}))
]; ];
}; };
packages.default = pkgs.writers.writePython3Bin packages.default = pythonInterpreter.pkgs.buildPythonPackage (
"release-action" pythonProject.renderers.buildPythonPackage {
{ python = pythonInterpreter;
libraries = with pkgs.python3Packages; [ });
semver # TODO move to setup.py?
toml
requests
pythonPackage
pyyaml
packaging
];
}
(builtins.readFile ./src/main.py)
;
} }
); );
} }

16
pyproject.toml Normal file
View File

@@ -0,0 +1,16 @@
[project]
name = "gitea-release-action"
version = "0.0.1"
description = "reusable action for release workflows"
authors = [ ]
requires-python = ">=3.13"
dependencies = [
"semver",
"toml",
"requests",
"pyyaml",
"packaging"
]
[project.scripts]
gitea-release-action = "main:main_cli"

View File

@@ -1,6 +0,0 @@
from setuptools import setup
setup(
name='release-action',
version='0.0.2',
)

View File

@@ -82,7 +82,7 @@ def dump_project_description(project_description: ProjectDescription):
print(' release version name: %s' % release_info.version_str) print(' release version name: %s' % release_info.version_str)
elif isinstance(generated, Wheel): elif isinstance(generated, Wheel):
print(' - wheel: %s' % generated.pattern) print(' - wheel: %s' % generated.filename)
print(' repository: %s' % generated.repository) print(' repository: %s' % generated.repository)
print(' release version name: %s' % release_info.version_str) print(' release version name: %s' % release_info.version_str)
@@ -152,7 +152,6 @@ def make_artefact(type: str,
name: str, name: str,
filename: str, filename: str,
package_name: str, package_name: str,
pattern: str,
directory: str, directory: str,
version_descriptor) -> ArtefactDescription: version_descriptor) -> ArtefactDescription:
@@ -172,8 +171,8 @@ def make_artefact(type: str,
**maybe_repository) **maybe_repository)
elif type == 'wheel': elif type == 'wheel':
assert pattern is not None assert filename is not None
generated = Wheel(pattern=pattern, **maybe_repository) generated = Wheel(filename=filename, **maybe_repository)
elif type == 'sdist': elif type == 'sdist':
assert filename is not None assert filename is not None
@@ -248,7 +247,7 @@ def sync_versions(project_description: ProjectDescription):
sync(artefact.version_descriptor) sync(artefact.version_descriptor)
if __name__ == '__main__': def main_cli():
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument('action', choices=[ parser.add_argument('action', choices=[
'declare', 'declare',
@@ -297,7 +296,6 @@ if __name__ == '__main__':
parser.add_argument('--artefact-name', type=nullable_string) parser.add_argument('--artefact-name', type=nullable_string)
parser.add_argument('--artefact-package-name', type=nullable_string) parser.add_argument('--artefact-package-name', type=nullable_string)
parser.add_argument('--artefact-filename', type=nullable_string) parser.add_argument('--artefact-filename', type=nullable_string)
parser.add_argument('--artefact-pattern', type=nullable_string)
parser.add_argument('--artefact-directory', type=nullable_string) parser.add_argument('--artefact-directory', type=nullable_string)
parser.add_argument('--deployment-type', parser.add_argument('--deployment-type',
@@ -359,7 +357,6 @@ if __name__ == '__main__':
args.artefact_name, args.artefact_name,
args.artefact_filename, args.artefact_filename,
args.artefact_package_name, args.artefact_package_name,
args.artefact_pattern,
args.artefact_directory, args.artefact_directory,
args.version_descriptor) args.version_descriptor)

View File

@@ -97,14 +97,14 @@ class Tarball:
@dataclass(frozen=True) @dataclass(frozen=True)
class WheelReleaseInfo: class WheelReleaseInfo:
pattern: str filename: str
repository: str repository: str
version_str: str version_str: str
@dataclass(frozen=True) @dataclass(frozen=True)
class Wheel: class Wheel:
pattern: str filename: str
repository: str = DEFAULT_PYPI_REPOSITORY_NAME repository: str = DEFAULT_PYPI_REPOSITORY_NAME
def make_environment_variables(self, context, version): def make_environment_variables(self, context, version):
@@ -112,7 +112,7 @@ class Wheel:
def make_release_info(self, context: ReleaseContext, version: Version): def make_release_info(self, context: ReleaseContext, version: Version):
return WheelReleaseInfo( return WheelReleaseInfo(
pattern=self.pattern, filename=self.filename,
repository=self.repository, repository=self.repository,
version_str=python_version_str(version)) version_str=python_version_str(version))
@@ -327,8 +327,8 @@ def parse_project_description(obj):
**optional(tar, 'repository')) **optional(tar, 'repository'))
def parse_wheel(whl): def parse_wheel(whl):
assert 'pattern' in whl assert 'filename' in whl
return Wheel(pattern=whl['pattern'], return Wheel(filename=whl['filename'],
**optional(whl, 'repository')) **optional(whl, 'repository'))
def parse_sdist(sdist): def parse_sdist(sdist):

View File

@@ -84,7 +84,7 @@ def publish_wheel(info: WheelReleaseInfo, cli: Cli):
'upload', 'upload',
'--verbose', '--verbose',
'--repository', info.repository, '--repository', info.repository,
info.pattern) info.filename)
print() print()

View File

@@ -19,10 +19,10 @@ artefacts:
repository: balls repository: balls
- type: wheel - type: wheel
pattern: './scratch/wheels/*.whl' filename: './scratch/wheels/*.whl'
- type: wheel - type: wheel
pattern: './scratch/wheels/*.whl' filename: './scratch/wheels/*.whl'
repository: other repository: other
- type: sdist - type: sdist

View File

@@ -30,7 +30,7 @@ class TestProjectDescription(TestCase):
version_descriptor='src/python/Cargo.toml', version_descriptor='src/python/Cargo.toml',
artefacts=[ artefacts=[
ArtefactDescription( ArtefactDescription(
generated=Wheel(pattern='./scratch/wheels/*.whl')) generated=Wheel(filename='./scratch/wheels/*.whl'))
]) ])
# productdesignerd # productdesignerd
@@ -55,7 +55,7 @@ class TestProjectDescription(TestCase):
ArtefactDescription( ArtefactDescription(
generated=OciImage(name='masa-images')), generated=OciImage(name='masa-images')),
ArtefactDescription( ArtefactDescription(
generated=Wheel(pattern='./scratch/wheels/*.whl')) generated=Wheel(filename='./scratch/wheels/*.whl'))
], ],
deployments=[ deployments=[
DeploymentDescription( DeploymentDescription(
@@ -134,7 +134,7 @@ class TestProjectDescription(TestCase):
artefacts=[ artefacts=[
ArtefactDescription( ArtefactDescription(
version_descriptor='src/python/pyproject.toml', version_descriptor='src/python/pyproject.toml',
generated=Wheel(pattern='./scratch/wheels/*.whl')), generated=Wheel(filename='./scratch/wheels/*.whl')),
ArtefactDescription(generated=OciImage(name='prngl')), ArtefactDescription(generated=OciImage(name='prngl')),
], ],
deployments=[ deployments=[
@@ -211,9 +211,9 @@ class TestProjectDescription(TestCase):
tarball.make_release_info(None, Version(1, 2, 3, 'dev4'))) tarball.make_release_info(None, Version(1, 2, 3, 'dev4')))
def test_wheel_release_info(self): def test_wheel_release_info(self):
wheel = Wheel(pattern='dist/wheels/*') wheel = Wheel(filename='dist/wheels/*')
self.assertEqual( self.assertEqual(
WheelReleaseInfo(pattern='dist/wheels/*', WheelReleaseInfo(filename='dist/wheels/*',
repository='gitea', repository='gitea',
version_str='1.2.3.dev4'), version_str='1.2.3.dev4'),
wheel.make_release_info(None, Version(1, 2, 3, 'dev4'))) wheel.make_release_info(None, Version(1, 2, 3, 'dev4')))
@@ -366,9 +366,9 @@ class TestProjectDescription(TestCase):
repository='balls')), repository='balls')),
ArtefactDescription(generated=Wheel( ArtefactDescription(generated=Wheel(
pattern='./scratch/wheels/*.whl')), filename='./scratch/wheels/*.whl')),
ArtefactDescription(generated=Wheel( ArtefactDescription(generated=Wheel(
pattern='./scratch/wheels/*.whl', repository='other')), filename='./scratch/wheels/*.whl', repository='other')),
ArtefactDescription(generated=Sdist( ArtefactDescription(generated=Sdist(
filename='./dist/papyru-0.0.1.tar.gz')), filename='./dist/papyru-0.0.1.tar.gz')),

View File

@@ -6,6 +6,23 @@ inputs: {}
runs: runs:
using: composite using: composite
steps: steps:
- 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: sync versions - name: sync versions
run: | run: |
nix run ${{ github.action_path }} -- \ nix run ${{ github.action_path }} -- \