aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--.github/workflows/ci.yml44+0 −44
-rw-r--r--.github/workflows/legal.yml34+0 −34
-rw-r--r--.gitignore5+0 −5
-rw-r--r--.mailmap4+0 −4
-rw-r--r--CLA.md170+0 −170
-rw-r--r--LICENSE214+12 −202
l---------LICENSES/0BSD.txt1+1 −0
l---------LICENSES/Apache-2.0.txt1+0 −1
-rw-r--r--LICENSES/CC-BY-3.0.txt319+0 −319
-rw-r--r--README.md195+18 −177
-rw-r--r--REUSE.toml19+2 −17
-rw-r--r--config/cla-unsupported-commits7+0 −7
-rw-r--r--config/signoff-approved-emails2+1 −1
-rw-r--r--mise.lock81+0 −81
-rw-r--r--mise.toml30+0 −30
-rwxr-xr-xtasks/chart.py87+0 −87
-rwxr-xr-xtasks/container.py105+0 −105
-rw-r--r--tasks/libs/__init__.py2+0 −2
-rw-r--r--tasks/libs/apk.py144+0 −144
-rw-r--r--tasks/libs/apt.py79+0 −79
-rw-r--r--tasks/libs/common.py178+0 −178
-rw-r--r--tasks/libs/repository.py111+0 −111
-rw-r--r--tasks/libs/rpm.py54+0 −54
-rw-r--r--tasks/libs/storage.py156+0 −156
-rwxr-xr-xtasks/licenses13+13 −0
-rwxr-xr-xtasks/licenses-json.py249+0 −249
-rwxr-xr-xtasks/licenses.py95+0 −95
-rwxr-xr-xtasks/package.py224+0 −224
-rwxr-xr-xtasks/publish.py66+0 −66
-rwxr-xr-xtasks/signoff66+66 −0
-rwxr-xr-xtasks/signoff.py208+0 −208
-rw-r--r--tests/fakes/boto3.py127+0 −127
-rwxr-xr-xtests/licenses-json.py120+0 −120
-rwxr-xr-xtests/oci-tasks.py147+0 −147
-rwxr-xr-xtests/package-integration.py93+0 −93
-rwxr-xr-xtests/package.py270+0 −270
-rwxr-xr-xtests/publish-integration.py346+0 −346
37 files changed, 113 insertions, 3953 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-name: CI
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-permissions:
- contents: read
-
-jobs:
- release-tasks:
- name: Package and publish tasks
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
- with:
- version: 2026.7.5
- experimental: true
- install: false
-
- - name: Install dependencies
- run: mise bootstrap --locked --yes --update
-
- - name: Check Python
- run: |
- ruff check tasks tests
- ruff format --check tasks tests
-
- - name: Tests
- run: |
- tests/licenses-json.py
- tests/oci-tasks.py
-
- tests/package.py
- tests/package-integration.py
-
- tests/publish-integration.py
diff --git a/.github/workflows/legal.yml b/.github/workflows/legal.yml
deleted file mode 100644
--- a/.github/workflows/legal.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-name: Legal
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-permissions:
- contents: read
-
-jobs:
- legal:
- name: Legal checks
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- with:
- fetch-depth: 0
-
- - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
- with:
- version: 2026.7.5
- experimental: true
- install: false
-
- - name: Check contribution sign-off
- run: mise run signoff
-
- - name: Check licensing policy
- run: mise run licenses
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
--- a/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-__pycache__/
-*.py[cod]
diff --git a/.mailmap b/.mailmap
deleted file mode 100644
--- a/.mailmap
+++ /dev/null
@@ -1,4 +0,0 @@
-# Add new entries in alphabetical order
-
-Nikolay Govorov <me@govorov.online>
-Nikolay Govorov <mr@dimidiumlabs.io>
diff --git a/CLA.md b/CLA.md
deleted file mode 100644
--- a/CLA.md
+++ /dev/null
@@ -1,170 +0,0 @@
-# Dimidium Labs Platform Individual Contributor License Agreement
-
-Version 1.0
-
-> This agreement is based on the Harmony Individual Contributor License
-> Agreement Version 1.0 licensed under a
-> [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/).
-
-Thank you for your interest in contributing to Dimidium Labs Platform (the
-"Project"). In this Agreement, "We" and "Us" mean Nikolay Govorov.
-
-This contributor agreement ("Agreement") documents the rights granted by
-contributors to Us. To make this document effective, You must personally add
-the following trailers to every commit You Submit:
-
-```text
-CLA-Version: 1.0
-Signed-off-by: Your Name <your.email@example.com>
-```
-
-The name and email address in `Signed-off-by` must identify You and match the
-commit author. By Submitting a commit containing these trailers, You
-electronically sign and accept this Agreement. No other person or automated
-system may add the `Signed-off-by` trailer on Your behalf. This is a legally
-binding document, so please read it carefully before agreeing to it.
-
-## 1. Definitions
-
-"You" means the individual who Submits a Contribution to Us.
-
-"Contribution" means any work of authorship that is Submitted by You to Us in
-which You own or assert ownership of the Copyright. If You do not own the
-Copyright in the entire work of authorship, please follow the instructions in
-Section 3(d).
-
-"Copyright" means all rights protecting works of authorship owned or controlled
-by You, including copyright, moral and neighboring rights, as appropriate, for
-the full term of their existence including any extensions by You.
-
-"Material" means the work of authorship which is made available by Us to third
-parties as part of the Project. After You Submit the Contribution, it may be
-included in the Material.
-
-"Submit" means any form of electronic, verbal, or written communication sent to
-Us or our representatives, including but not limited to electronic mailing
-lists, source code control systems, and issue tracking systems that are managed
-by, or on behalf of, Us for the purpose of discussing and improving the
-Material, but excluding communication that is conspicuously marked or otherwise
-designated in writing by You as "Not a Contribution."
-
-"Submission Date" means the date on which You Submit a Contribution to Us.
-
-"Effective Date" means the date You execute this Agreement or the date You first
-Submit a Contribution to Us, whichever is earlier.
-
-"Media" means any portion of a Contribution which is not software.
-
-## 2. Grant of Rights
-
-### 2.1 Copyright License
-
-(a) You retain ownership of the Copyright in Your Contribution and have the same
-rights to use or license the Contribution which You would have had without
-entering into the Agreement.
-
-(b) To the maximum extent permitted by the relevant law, You grant to Us a
-perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable
-license under the Copyright covering the Contribution, with the right to
-sublicense such rights through multiple tiers of sublicensees, to reproduce,
-modify, display, perform and distribute the Contribution as part of the
-Material; provided that this license is conditioned upon compliance with Section
-2.3.
-
-### 2.2 Patent License
-
-For patent claims including, without limitation, method, process, and apparatus
-claims which You own, control or have the right to grant,
-now or in the future, You grant to Us a perpetual, worldwide, non-exclusive,
-transferable, royalty-free, irrevocable patent license, with the right to
-sublicense these rights to multiple tiers of sublicensees, to make, have made,
-use, sell, offer for sale, import and otherwise transfer the Contribution and
-the Contribution in combination with the Material (and portions of such
-combination). This license is granted only to the extent that the exercise of
-the licensed rights infringes such patent claims; and provided that this license
-is conditioned upon compliance with Section 2.3.
-
-### 2.3 Outbound License
-
-Based on the grant of rights in Sections 2.1 and 2.2, if We include Your
-Contribution in a Material, We may license the Contribution under any license,
-including copyleft, permissive, commercial, or proprietary licenses. As a
-condition on the exercise of this right, We agree to also license the
-Contribution under the terms of the license or licenses which We are using for
-the Material on the Submission Date.
-
-### 2.4 Moral Rights
-
-If moral rights apply to the Contribution, to the maximum extent permitted by
-law, You waive and agree not to assert such moral rights against Us or our
-successors in interest, or any of our licensees, either direct or indirect.
-
-### 2.5 Our Rights
-
-You acknowledge that We are not obligated to use Your Contribution as part of
-the Material and may decide to include any Contribution We consider appropriate.
-
-### 2.6 Reservation of Rights
-
-Any rights not expressly licensed under this section are expressly
-reserved by You.
-
-## 3. Agreement
-
-You confirm that:
-
-(a) You have the legal authority to enter into this Agreement.
-
-(b) You own the Copyright and patent claims covering the
-Contribution which are required to grant the rights under Section 2.
-
-(c) The grant of rights under Section 2 does not violate any grant
-of rights which You have made to third parties, including Your employer. If You
-are an employee, You have had Your employer approve this Agreement or sign the
-Entity version of this document. If You are less than eighteen years old, please
-have Your parents or guardian sign the Agreement.
-
-(d) If You do not own the Copyright in the entire work of authorship Submitted,
-You have clearly identified the third-party work, its source, and its license in
-the Submission.
-
-## 4. Disclaimer
-
-EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS
-IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT
-LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US.
-TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY
-IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW.
-
-## 5. Consequential Damage Waiver
-
-TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE
-LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA,
-INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT
-OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR
-OTHERWISE) UPON WHICH THE CLAIM IS BASED.
-
-## 6. Miscellaneous
-
-6.1 This Agreement sets out the entire agreement between You and Us for Your
-Contributions to Us and overrides all other agreements or understandings.
-
-6.2 If You or We assign the rights or obligations received through this
-Agreement to a third party, as a condition of the assignment, that third party
-must agree in writing to abide by all the rights and obligations in the
-Agreement.
-
-6.3 The failure of either party to require performance by the other party of any
-provision of this Agreement in one situation shall not affect the right of a
-party to require such performance at any time in the future. A waiver of
-performance under a provision in one situation shall not be considered a waiver
-of the performance of the provision in the future or a waiver of the provision
-in its entirety.
-
-6.4 If any provision of this Agreement is found void and unenforceable, such
-provision will be replaced to the extent possible with a provision that comes
-closest to the meaning of the original provision and which is enforceable. The
-terms and conditions set forth in this Agreement shall apply notwithstanding any
-failure of essential purpose of this Agreement or any limited remedy to the
-maximum extent possible under law.
diff --git a/LICENSE b/LICENSE
index d645695..7d70db4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,202 +1,12 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Copyright (C) 2026 Nikolay Govorov
+
+Permission to use, copy, modify, and/or distribute this software for any purpose
+with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+THIS SOFTWARE.
diff --git a/LICENSES/0BSD.txt b/LICENSES/0BSD.txt
new file mode 120000
--- /dev/null
+++ b/LICENSES/0BSD.txt
@@ -0,0 +1 @@
+../LICENSE
\ No newline at end of file
diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt
deleted file mode 120000
--- a/LICENSES/Apache-2.0.txt
+++ /dev/null
@@ -1 +0,0 @@
-../LICENSE
\ No newline at end of file
diff --git a/LICENSES/CC-BY-3.0.txt b/LICENSES/CC-BY-3.0.txt
deleted file mode 100644
--- a/LICENSES/CC-BY-3.0.txt
+++ /dev/null
@@ -1,319 +0,0 @@
-Creative Commons Legal Code
-
-Attribution 3.0 Unported
-
- CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
- LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
- ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
- INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
- REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
- DAMAGES RESULTING FROM ITS USE.
-
-License
-
-THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
-COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
-COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
-AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
-
-BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
-TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
-BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
-CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
-CONDITIONS.
-
-1. Definitions
-
- a. "Adaptation" means a work based upon the Work, or upon the Work and
- other pre-existing works, such as a translation, adaptation,
- derivative work, arrangement of music or other alterations of a
- literary or artistic work, or phonogram or performance and includes
- cinematographic adaptations or any other form in which the Work may be
- recast, transformed, or adapted including in any form recognizably
- derived from the original, except that a work that constitutes a
- Collection will not be considered an Adaptation for the purpose of
- this License. For the avoidance of doubt, where the Work is a musical
- work, performance or phonogram, the synchronization of the Work in
- timed-relation with a moving image ("synching") will be considered an
- Adaptation for the purpose of this License.
- b. "Collection" means a collection of literary or artistic works, such as
- encyclopedias and anthologies, or performances, phonograms or
- broadcasts, or other works or subject matter other than works listed
- in Section 1(f) below, which, by reason of the selection and
- arrangement of their contents, constitute intellectual creations, in
- which the Work is included in its entirety in unmodified form along
- with one or more other contributions, each constituting separate and
- independent works in themselves, which together are assembled into a
- collective whole. A work that constitutes a Collection will not be
- considered an Adaptation (as defined above) for the purposes of this
- License.
- c. "Distribute" means to make available to the public the original and
- copies of the Work or Adaptation, as appropriate, through sale or
- other transfer of ownership.
- d. "Licensor" means the individual, individuals, entity or entities that
- offer(s) the Work under the terms of this License.
- e. "Original Author" means, in the case of a literary or artistic work,
- the individual, individuals, entity or entities who created the Work
- or if no individual or entity can be identified, the publisher; and in
- addition (i) in the case of a performance the actors, singers,
- musicians, dancers, and other persons who act, sing, deliver, declaim,
- play in, interpret or otherwise perform literary or artistic works or
- expressions of folklore; (ii) in the case of a phonogram the producer
- being the person or legal entity who first fixes the sounds of a
- performance or other sounds; and, (iii) in the case of broadcasts, the
- organization that transmits the broadcast.
- f. "Work" means the literary and/or artistic work offered under the terms
- of this License including without limitation any production in the
- literary, scientific and artistic domain, whatever may be the mode or
- form of its expression including digital form, such as a book,
- pamphlet and other writing; a lecture, address, sermon or other work
- of the same nature; a dramatic or dramatico-musical work; a
- choreographic work or entertainment in dumb show; a musical
- composition with or without words; a cinematographic work to which are
- assimilated works expressed by a process analogous to cinematography;
- a work of drawing, painting, architecture, sculpture, engraving or
- lithography; a photographic work to which are assimilated works
- expressed by a process analogous to photography; a work of applied
- art; an illustration, map, plan, sketch or three-dimensional work
- relative to geography, topography, architecture or science; a
- performance; a broadcast; a phonogram; a compilation of data to the
- extent it is protected as a copyrightable work; or a work performed by
- a variety or circus performer to the extent it is not otherwise
- considered a literary or artistic work.
- g. "You" means an individual or entity exercising rights under this
- License who has not previously violated the terms of this License with
- respect to the Work, or who has received express permission from the
- Licensor to exercise rights under this License despite a previous
- violation.
- h. "Publicly Perform" means to perform public recitations of the Work and
- to communicate to the public those public recitations, by any means or
- process, including by wire or wireless means or public digital
- performances; to make available to the public Works in such a way that
- members of the public may access these Works from a place and at a
- place individually chosen by them; to perform the Work to the public
- by any means or process and the communication to the public of the
- performances of the Work, including by public digital performance; to
- broadcast and rebroadcast the Work by any means including signs,
- sounds or images.
- i. "Reproduce" means to make copies of the Work by any means including
- without limitation by sound or visual recordings and the right of
- fixation and reproducing fixations of the Work, including storage of a
- protected performance or phonogram in digital form or other electronic
- medium.
-
-2. Fair Dealing Rights. Nothing in this License is intended to reduce,
-limit, or restrict any uses free from copyright or rights arising from
-limitations or exceptions that are provided for in connection with the
-copyright protection under copyright law or other applicable laws.
-
-3. License Grant. Subject to the terms and conditions of this License,
-Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
-perpetual (for the duration of the applicable copyright) license to
-exercise the rights in the Work as stated below:
-
- a. to Reproduce the Work, to incorporate the Work into one or more
- Collections, and to Reproduce the Work as incorporated in the
- Collections;
- b. to create and Reproduce Adaptations provided that any such Adaptation,
- including any translation in any medium, takes reasonable steps to
- clearly label, demarcate or otherwise identify that changes were made
- to the original Work. For example, a translation could be marked "The
- original work was translated from English to Spanish," or a
- modification could indicate "The original work has been modified.";
- c. to Distribute and Publicly Perform the Work including as incorporated
- in Collections; and,
- d. to Distribute and Publicly Perform Adaptations.
- e. For the avoidance of doubt:
-
- i. Non-waivable Compulsory License Schemes. In those jurisdictions in
- which the right to collect royalties through any statutory or
- compulsory licensing scheme cannot be waived, the Licensor
- reserves the exclusive right to collect such royalties for any
- exercise by You of the rights granted under this License;
- ii. Waivable Compulsory License Schemes. In those jurisdictions in
- which the right to collect royalties through any statutory or
- compulsory licensing scheme can be waived, the Licensor waives the
- exclusive right to collect such royalties for any exercise by You
- of the rights granted under this License; and,
- iii. Voluntary License Schemes. The Licensor waives the right to
- collect royalties, whether individually or, in the event that the
- Licensor is a member of a collecting society that administers
- voluntary licensing schemes, via that society, from any exercise
- by You of the rights granted under this License.
-
-The above rights may be exercised in all media and formats whether now
-known or hereafter devised. The above rights include the right to make
-such modifications as are technically necessary to exercise the rights in
-other media and formats. Subject to Section 8(f), all rights not expressly
-granted by Licensor are hereby reserved.
-
-4. Restrictions. The license granted in Section 3 above is expressly made
-subject to and limited by the following restrictions:
-
- a. You may Distribute or Publicly Perform the Work only under the terms
- of this License. You must include a copy of, or the Uniform Resource
- Identifier (URI) for, this License with every copy of the Work You
- Distribute or Publicly Perform. You may not offer or impose any terms
- on the Work that restrict the terms of this License or the ability of
- the recipient of the Work to exercise the rights granted to that
- recipient under the terms of the License. You may not sublicense the
- Work. You must keep intact all notices that refer to this License and
- to the disclaimer of warranties with every copy of the Work You
- Distribute or Publicly Perform. When You Distribute or Publicly
- Perform the Work, You may not impose any effective technological
- measures on the Work that restrict the ability of a recipient of the
- Work from You to exercise the rights granted to that recipient under
- the terms of the License. This Section 4(a) applies to the Work as
- incorporated in a Collection, but this does not require the Collection
- apart from the Work itself to be made subject to the terms of this
- License. If You create a Collection, upon notice from any Licensor You
- must, to the extent practicable, remove from the Collection any credit
- as required by Section 4(b), as requested. If You create an
- Adaptation, upon notice from any Licensor You must, to the extent
- practicable, remove from the Adaptation any credit as required by
- Section 4(b), as requested.
- b. If You Distribute, or Publicly Perform the Work or any Adaptations or
- Collections, You must, unless a request has been made pursuant to
- Section 4(a), keep intact all copyright notices for the Work and
- provide, reasonable to the medium or means You are utilizing: (i) the
- name of the Original Author (or pseudonym, if applicable) if supplied,
- and/or if the Original Author and/or Licensor designate another party
- or parties (e.g., a sponsor institute, publishing entity, journal) for
- attribution ("Attribution Parties") in Licensor's copyright notice,
- terms of service or by other reasonable means, the name of such party
- or parties; (ii) the title of the Work if supplied; (iii) to the
- extent reasonably practicable, the URI, if any, that Licensor
- specifies to be associated with the Work, unless such URI does not
- refer to the copyright notice or licensing information for the Work;
- and (iv) , consistent with Section 3(b), in the case of an Adaptation,
- a credit identifying the use of the Work in the Adaptation (e.g.,
- "French translation of the Work by Original Author," or "Screenplay
- based on original Work by Original Author"). The credit required by
- this Section 4 (b) may be implemented in any reasonable manner;
- provided, however, that in the case of a Adaptation or Collection, at
- a minimum such credit will appear, if a credit for all contributing
- authors of the Adaptation or Collection appears, then as part of these
- credits and in a manner at least as prominent as the credits for the
- other contributing authors. For the avoidance of doubt, You may only
- use the credit required by this Section for the purpose of attribution
- in the manner set out above and, by exercising Your rights under this
- License, You may not implicitly or explicitly assert or imply any
- connection with, sponsorship or endorsement by the Original Author,
- Licensor and/or Attribution Parties, as appropriate, of You or Your
- use of the Work, without the separate, express prior written
- permission of the Original Author, Licensor and/or Attribution
- Parties.
- c. Except as otherwise agreed in writing by the Licensor or as may be
- otherwise permitted by applicable law, if You Reproduce, Distribute or
- Publicly Perform the Work either by itself or as part of any
- Adaptations or Collections, You must not distort, mutilate, modify or
- take other derogatory action in relation to the Work which would be
- prejudicial to the Original Author's honor or reputation. Licensor
- agrees that in those jurisdictions (e.g. Japan), in which any exercise
- of the right granted in Section 3(b) of this License (the right to
- make Adaptations) would be deemed to be a distortion, mutilation,
- modification or other derogatory action prejudicial to the Original
- Author's honor and reputation, the Licensor will waive or not assert,
- as appropriate, this Section, to the fullest extent permitted by the
- applicable national law, to enable You to reasonably exercise Your
- right under Section 3(b) of this License (right to make Adaptations)
- but not otherwise.
-
-5. Representations, Warranties and Disclaimer
-
-UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
-OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
-KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
-INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
-FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
-LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
-WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
-OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
-
-6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
-LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
-ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
-ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
-BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-7. Termination
-
- a. This License and the rights granted hereunder will terminate
- automatically upon any breach by You of the terms of this License.
- Individuals or entities who have received Adaptations or Collections
- from You under this License, however, will not have their licenses
- terminated provided such individuals or entities remain in full
- compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
- survive any termination of this License.
- b. Subject to the above terms and conditions, the license granted here is
- perpetual (for the duration of the applicable copyright in the Work).
- Notwithstanding the above, Licensor reserves the right to release the
- Work under different license terms or to stop distributing the Work at
- any time; provided, however that any such election will not serve to
- withdraw this License (or any other license that has been, or is
- required to be, granted under the terms of this License), and this
- License will continue in full force and effect unless terminated as
- stated above.
-
-8. Miscellaneous
-
- a. Each time You Distribute or Publicly Perform the Work or a Collection,
- the Licensor offers to the recipient a license to the Work on the same
- terms and conditions as the license granted to You under this License.
- b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
- offers to the recipient a license to the original Work on the same
- terms and conditions as the license granted to You under this License.
- c. If any provision of this License is invalid or unenforceable under
- applicable law, it shall not affect the validity or enforceability of
- the remainder of the terms of this License, and without further action
- by the parties to this agreement, such provision shall be reformed to
- the minimum extent necessary to make such provision valid and
- enforceable.
- d. No term or provision of this License shall be deemed waived and no
- breach consented to unless such waiver or consent shall be in writing
- and signed by the party to be charged with such waiver or consent.
- e. This License constitutes the entire agreement between the parties with
- respect to the Work licensed here. There are no understandings,
- agreements or representations with respect to the Work not specified
- here. Licensor shall not be bound by any additional provisions that
- may appear in any communication from You. This License may not be
- modified without the mutual written agreement of the Licensor and You.
- f. The rights granted under, and the subject matter referenced, in this
- License were drafted utilizing the terminology of the Berne Convention
- for the Protection of Literary and Artistic Works (as amended on
- September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
- Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
- and the Universal Copyright Convention (as revised on July 24, 1971).
- These rights and subject matter take effect in the relevant
- jurisdiction in which the License terms are sought to be enforced
- according to the corresponding provisions of the implementation of
- those treaty provisions in the applicable national law. If the
- standard suite of rights granted under applicable copyright law
- includes additional rights not granted under this License, such
- additional rights are deemed to be included in the License; this
- License is not intended to restrict the license of any rights under
- applicable law.
-
-
-Creative Commons Notice
-
- Creative Commons is not a party to this License, and makes no warranty
- whatsoever in connection with the Work. Creative Commons will not be
- liable to You or any party on any legal theory for any damages
- whatsoever, including without limitation any general, special,
- incidental or consequential damages arising in connection to this
- license. Notwithstanding the foregoing two (2) sentences, if Creative
- Commons has expressly identified itself as the Licensor hereunder, it
- shall have all rights and obligations of Licensor.
-
- Except for the limited purpose of indicating to the public that the
- Work is licensed under the CCPL, Creative Commons does not authorize
- the use by either party of the trademark "Creative Commons" or any
- related trademark or logo of Creative Commons without the prior
- written consent of Creative Commons. Any permitted use will be in
- compliance with Creative Commons' then-current trademark usage
- guidelines, as may be published on its website or otherwise made
- available upon request from time to time. For the avoidance of doubt,
- this trademark restriction does not form part of this License.
-
- Creative Commons may be contacted at https://creativecommons.org/.
diff --git a/README.md b/README.md
index 941b3fd..6e464e1 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,9 @@
-# Dimidium Labs platform
+# Dimidium Labs infrastructure
-This repository contains shared building blocks for Dimidium Labs projects:
-reusable development and release tasks, common Go and npm libraries, and shared
-documentation.
+This repository contains executable development and release tasks shared by
+Dimidium Labs projects. GitHub Actions is only a runner for these tasks.
-The current executable tasks live in `tasks/`. GitHub Actions is only a runner
-for these tasks. Projects include `tasks/` with
+Projects include `tasks/` with
[mise remote Git includes](https://mise.jdx.dev/tasks/task-configuration.html#remote-git-includes).
By default, tasks are fetched directly from this public repository over HTTPS:
@@ -16,183 +14,26 @@ mise run licenses
Consuming projects pin this repository by commit SHA.
-## Packaging
+## Sign-off policy
-Projects build and stage their own binaries and keep their nFPM configuration.
-The shared [`package`](tasks/package.py) task creates only the formats explicitly
-requested by a project: nFPM packages (`deb`, `rpm`, or `apk`) and portable
-archives (`tar.gz` or `zip`). APK configurations may use
-`${PACKAGE_KEY_VERSION}` in `apk.signature.key_name`; the task renders the
-four-digit generation before invoking nFPM. DEB and RPM payloads are built by
-nFPM and then signed through `debsigs` and `rpmsign`, allowing CI to use only an
-OpenPGP signing subkey while the certification key remains offline. The shared
-[`publish`](tasks/publish.py) task adds
-explicitly selected package formats to signed repositories in the organization
-package bucket.
+`tasks/signoff` verifies that:
-```console
-mise run package -- \
- --version VERSION --arch ARCH --output DIR \
- [--config nfpm.yaml] [--apk-public-key NAME.rsa.pub] \
- deb rpm apk
-
-mise run package -- \
- --archive-root DIR --archive-name NAME --output DIR \
- tar.gz zip
-```
-
-## OCI artifacts
-
-The shared [`container`](tasks/container.py) task builds one or more tagged OCI
-images with Docker Buildx. Registry authentication is deliberately left to the
-calling workflow, so the same build can be pushed to GHCR, Cloudflare, or
-another OCI registry. The [`chart`](tasks/chart.py) task strictly lints a Helm
-chart, packages an immutable version, and can push it to one or more OCI
-repositories.
-
-```console
-mise run container -- \
- --context . --file deploy/Dockerfile \
- --platform linux/amd64,linux/arm64 \
- --target site --build-arg APP=site \
- --tag ghcr.io/example/site:1.2.3 \
- --cache-scope site --push
-
-mise run chart -- \
- --chart charts/service --version 1.2.3 --app-version 1.2.3 \
- --output dist/charts --push oci://ghcr.io/example/charts
-```
-
-Container tags, chart versions, credentials, and release policy remain owned by
-the consuming project. `--provenance false --sbom false` is available for
-registries that do not accept OCI attestation indexes. Without `--push` or
-`--load`, Buildx only validates and caches the build result.
-
-## Package repositories
-
-Projects publish beneath a service-owned prefix at
-`https://pkg.dimidiumlabs.io/<service>/`. Channels are explicit, previously
-published package payloads are retained, and an S3 lock serializes metadata
-updates for each service/channel.
-
-```console
-mise run publish -- \
- --service SERVICE --channel CHANNEL --input DIR \
- deb rpm apk
-```
-
-The selected formats map to these layouts:
-
-- APT: `<service>/apt/{dists,pool}/<channel>/`
-- RPM: `<service>/rpm/<channel>/`
-- APK: `<service>/apk/<channel>/<architecture>/`
-
-APT and RPM metadata refer to the aggregate organization OpenPGP bundle at
-`/packages.gpg`. Immutable generation keys live at
-`/keys/packages.<version>.gpg` and
-`/keys/packages.<version>.rsa.pub`. APK packages and indexes embed
-the versioned RSA key name. Public keys are provisioned independently; each
-publication checks its signing keys against the selected generation and never
-creates or replaces key objects.
-
-Bucket configuration comes from `S3_BUCKET`, `S3_ENDPOINT`, `S3_PUBLIC_URL`,
-`S3_ACCESS_KEY_ID`, and `S3_SECRET_ACCESS_KEY`. `PACKAGE_KEY_VERSION` selects
-the four-digit key generation. OpenPGP signing uses `GPG_PRIVATE_KEY`,
-`GPG_PASSPHRASE`, and `GPG_KEY_ID`; APK index signing uses `APK_PRIVATE_KEY`.
-
-## Tool provisioning
-
-Each project declares its toolchain and standalone CLI dependencies in
-`mise.toml`. A fresh checkout is provisioned with one command:
-
-```console
-mise bootstrap
-```
-
-Shared tasks declare task-specific tools in their `#MISE tools` metadata, so
-`mise run` installs the same pinned versions on demand. Python tasks declare
-their dependencies inline with PEP 723 and run through pipx's standard pip
-backend, without a project virtual environment or install step. System libraries
-that cannot be installed as portable tools belong in `[bootstrap.packages]`.
-
-## Guardrails
-
-### Licensing policy
-
-`tasks/licenses.py` runs a pinned REUSE version and verifies the repository's
-licensing metadata and canonical SPDX copyright headers. In Rust projects it
-also runs a pinned `cargo deny check`.
-
-The separate `licenses-json` task uses cargo-about to generate a deterministic,
-embeddable JSON bundle for a Rust binary. It accepts every license declared by
-the dependency graph because policy enforcement remains the responsibility of
-`cargo deny`. Repeat `--target` to produce one bundle for all supported targets:
-
-```console
-mise run licenses-json -- \
- --manifest-path crates/server/Cargo.toml \
- --output crates/server/licenses.json \
- --target x86_64-unknown-linux-gnu \
- --target aarch64-unknown-linux-gnu
-```
-
-Use `--check` with the same arguments in CI to verify that a committed bundle is
-up to date, or `--offline` when all dependency sources are already cached.
-
-### Sign-off policy
-
-`tasks/signoff.py` verifies that:
-
-- authors and co-authors with an email from
- `config/signoff-approved-emails` are trusted without a trailer;
-- `CLA.md` declares exactly one version;
-- every non-approved author and co-author has a `Signed-off-by` trailer exactly
- matching their commit identity;
-- every commit with a non-approved author or co-author has exactly one
- `CLA-Version` trailer matching the version declared by `CLA.md` in that
- commit;
-- commits listed in `config/cla-unsupported-commits` retain their
- `Signed-off-by` requirement but are explicitly not treated as covered by a
- versioned CLA;
+- commits authored with an email from `config/signoff-approved-emails` are
+ trusted without a trailer;
+- every other non-merge commit contains a `Signed-off-by` trailer exactly
+ matching its author;
- every non-approved author and committer email in the complete non-merge
history is registered in `.mailmap`.
-Approved emails and unsupported commits are maintained centrally so a pull
-request in a consuming repository cannot grant itself an exemption.
-
-## Contributing
-
-We welcome your contributions, including code, bug reports, ideas, and success
-stories.
-
-If you are making a contribution for the first time or from a new email, please
-add yourself to the `.mailmap`.
-
-### Signoff
-
-To include your code, we ask that you read and agree to the [CLA](./CLA.md). To
-sign, add a `CLA-Version: 1.0` and a `Signed-off-by` trailer to every commit
-(`git commit -s --trailer "CLA-Version: 1.0"`). Each commit in a pull request
-must carry a valid `Signed-off-by` line matching the commit author. Please use
-your real name. We cannot include code from anonymous contributors.
-
-AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the
-Contributor License Agreement.
-
-### AI policy
-
-You may use AI agents when writing code and documentation. AI is not allowed for
-media including images, videos, fonts at all. You must fully read, understand,
-and cleanup any code generated by the agent. We ask that you disclose the
-agent's use and indicate the tool, model, and extent of contribution.
+Approved emails are maintained centrally so a pull request in a consuming
+repository cannot grant itself an exemption.
-Contributions should include an Assisted-by tag in the following format:
-`Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]`, for example:
-`Assisted-by: Claude:claude-4.6-opus coccinelle sparse`
+## Licensing policy
-Remember, AI agents should make software better, not worse.
+`tasks/licenses` runs a pinned REUSE version and verifies the repository's
+licensing metadata. In Rust projects it also runs a pinned `cargo deny check`.
-## Licensing
+## License
-Unless noted otherwise, software and configuration are licensed under Apache-2.0.
-Documentation is licensed under CC-BY-4.0.
+Executable tasks and their configuration are licensed under 0BSD.
+Documentation is licensed under CC BY 4.0.
diff --git a/REUSE.toml b/REUSE.toml
index f29e237..c87ec2f 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
+# SPDX-License-Identifier: 0BSD
version = 1
@@ -7,20 +7,5 @@ version = 1
path = [
"README.md",
]
-SPDX-FileCopyrightText = "2026 Nikolay Govorov"
+SPDX-FileCopyrightText = "2026 Nikolay Govorov <me@govorov.online>"
SPDX-License-Identifier = "CC-BY-4.0"
-
-[[annotations]]
-path = [
- "CLA.md",
-]
-SPDX-FileCopyrightText = "2026 Nikolay Govorov"
-SPDX-License-Identifier = "CC-BY-3.0"
-
-[[annotations]]
-path = [
- ".mailmap",
- "mise.lock",
-]
-SPDX-FileCopyrightText = "2026 Nikolay Govorov"
-SPDX-License-Identifier = "Apache-2.0"
diff --git a/config/cla-unsupported-commits b/config/cla-unsupported-commits
deleted file mode 100644
--- a/config/cla-unsupported-commits
+++ /dev/null
@@ -1,7 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-#
-# External contributions retained in history but not covered by a versioned CLA.
-
-ca483864b5f1f8048905c9016aff20ef36395840
-eb9caf82d99d61a022f67695f208067992a1a9ec
diff --git a/config/signoff-approved-emails b/config/signoff-approved-emails
index dd67622..234b671 100644
--- a/config/signoff-approved-emails
+++ b/config/signoff-approved-emails
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
+# SPDX-License-Identifier: 0BSD
#
# One exact author or committer email per line.
diff --git a/mise.lock b/mise.lock
deleted file mode 100644
--- a/mise.lock
+++ /dev/null
@@ -1,81 +0,0 @@
-# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
-
-[[tools.pipx]]
-version = "1.16.7"
-backend = "aqua:pypa/pipx"
-
-[tools.pipx."platforms.linux-arm64"]
-checksum = "sha256:302633d0061e0ab4269257501cbe1338cde5f00f22f121543706624aada0145e"
-url = "https://github.com/pypa/pipx/releases/download/1.16.7/pipx.pyz"
-url_api = "https://api.github.com/repos/pypa/pipx/releases/assets/513588802"
-
-[tools.pipx."platforms.linux-arm64-musl"]
-checksum = "sha256:302633d0061e0ab4269257501cbe1338cde5f00f22f121543706624aada0145e"
-url = "https://github.com/pypa/pipx/releases/download/1.16.7/pipx.pyz"
-url_api = "https://api.github.com/repos/pypa/pipx/releases/assets/513588802"
-
-[tools.pipx."platforms.linux-x64"]
-checksum = "sha256:302633d0061e0ab4269257501cbe1338cde5f00f22f121543706624aada0145e"
-url = "https://github.com/pypa/pipx/releases/download/1.16.7/pipx.pyz"
-url_api = "https://api.github.com/repos/pypa/pipx/releases/assets/513588802"
-
-[tools.pipx."platforms.linux-x64-musl"]
-checksum = "sha256:302633d0061e0ab4269257501cbe1338cde5f00f22f121543706624aada0145e"
-url = "https://github.com/pypa/pipx/releases/download/1.16.7/pipx.pyz"
-url_api = "https://api.github.com/repos/pypa/pipx/releases/assets/513588802"
-
-[tools.pipx."platforms.macos-arm64"]
-checksum = "sha256:302633d0061e0ab4269257501cbe1338cde5f00f22f121543706624aada0145e"
-url = "https://github.com/pypa/pipx/releases/download/1.16.7/pipx.pyz"
-url_api = "https://api.github.com/repos/pypa/pipx/releases/assets/513588802"
-
-[tools.pipx."platforms.macos-x64"]
-checksum = "sha256:302633d0061e0ab4269257501cbe1338cde5f00f22f121543706624aada0145e"
-url = "https://github.com/pypa/pipx/releases/download/1.16.7/pipx.pyz"
-url_api = "https://api.github.com/repos/pypa/pipx/releases/assets/513588802"
-
-[[tools.ruff]]
-version = "0.16.3"
-backend = "aqua:astral-sh/ruff"
-
-[tools.ruff."platforms.linux-arm64"]
-checksum = "sha256:8319ba22f655e6efc086103486d7165bf0de73f71ff1c5f25ba580153ad05feb"
-url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-aarch64-unknown-linux-musl.tar.gz"
-url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140238"
-provenance = "github-attestations"
-
-[tools.ruff."platforms.linux-arm64-musl"]
-checksum = "sha256:8319ba22f655e6efc086103486d7165bf0de73f71ff1c5f25ba580153ad05feb"
-url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-aarch64-unknown-linux-musl.tar.gz"
-url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140238"
-provenance = "github-attestations"
-
-[tools.ruff."platforms.linux-x64"]
-checksum = "sha256:d67c9b5949981698c48915abf65e0b3406ba9184ad73521cdf20a926bc889c73"
-url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-unknown-linux-musl.tar.gz"
-url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140356"
-provenance = "github-attestations"
-
-[tools.ruff."platforms.linux-x64-musl"]
-checksum = "sha256:d67c9b5949981698c48915abf65e0b3406ba9184ad73521cdf20a926bc889c73"
-url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-unknown-linux-musl.tar.gz"
-url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140356"
-provenance = "github-attestations"
-
-[tools.ruff."platforms.macos-arm64"]
-checksum = "sha256:136a4db6512d9b16dda56ac8604696ed65c3b1a914a142de029e7f8d5006f1d9"
-url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-aarch64-apple-darwin.tar.gz"
-url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140218"
-provenance = "github-attestations"
-
-[tools.ruff."platforms.macos-x64"]
-checksum = "sha256:05c2a6705e7c0c056d6d93ff538978583f0c47b4c28d334ab9d58d2e8daf4c24"
-url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-apple-darwin.tar.gz"
-url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140338"
-provenance = "github-attestations"
-
-[tools.ruff."platforms.windows-x64"]
-checksum = "sha256:f10c709755b393fd9821506b21070bcca969b9966504edd1e490efd08e3662ba"
-url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-pc-windows-msvc.zip"
-url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140346"
-provenance = "github-attestations"
diff --git a/mise.toml b/mise.toml
deleted file mode 100644
--- a/mise.toml
+++ /dev/null
@@ -1,30 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-min_version = "2026.7.5"
-
-[settings]
-experimental = true
-
-[tools]
-pipx = "1.16.7"
-ruff = "0.16.3"
-
-[bootstrap.packages]
-# APT
-"apt:apt-utils" = "latest"
-"apt:createrepo-c" = "latest"
-"apt:debsigs" = "latest"
-"apt:gnupg" = "latest"
-"apt:openssl" = "latest"
-"apt:rpm" = "latest"
-
-# DNF
-"dnf:createrepo_c" = "latest"
-"dnf:gnupg2" = "latest"
-"dnf:openssl" = "latest"
-"dnf:rpm" = "latest"
-
-[task_config]
-dir = "{{cwd}}"
-includes = ["tasks"]
diff --git a/tasks/chart.py b/tasks/chart.py
deleted file mode 100755
--- a/tasks/chart.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# fmt: off
-#MISE description="Lint, package, and optionally publish a Helm chart"
-#MISE tools={"pipx"="1.16.7","python"="3.14.7","helm"="4.1.1"}
-# fmt: on
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import re
-import sys
-from collections.abc import Sequence
-from pathlib import Path
-
-sys.dont_write_bytecode = True
-
-from libs.common import TaskError, require_command, run, task_main
-
-TASK = "chart"
-CHART_NAME = re.compile(r"^name:\s*([A-Za-z0-9_.-][A-Za-z0-9_.-]*)\s*$")
-
-
-def chart_name(chart: Path) -> str:
- for line in (chart / "Chart.yaml").read_text().splitlines():
- if match := CHART_NAME.fullmatch(line):
- return match.group(1)
- raise TaskError(f"{TASK}: cannot read chart name from {chart}/Chart.yaml")
-
-
-async def main(args: Sequence[str]) -> None:
- command = argparse.ArgumentParser(prog="mise run chart --")
- command.add_argument("--chart", required=True, type=Path)
- command.add_argument("--version")
- command.add_argument("--app-version")
- command.add_argument("--output", default=Path("dist/charts"), type=Path)
- command.add_argument("--push", action="append", default=[], metavar="OCI_URL")
- command.add_argument("--lint-only", action="store_true")
- arguments = command.parse_args(args)
-
- require_command("helm", TASK)
- if not arguments.chart.is_dir():
- raise TaskError(f"{TASK}: directory not found: {arguments.chart}")
- if not (arguments.chart / "Chart.yaml").is_file():
- raise TaskError(f"{TASK}: Chart.yaml not found in {arguments.chart}")
- for registry in arguments.push:
- if not registry.startswith("oci://"):
- command.error(f"registry must use oci://: {registry}")
-
- name = chart_name(arguments.chart)
- await run("helm", "lint", arguments.chart, "--strict")
-
- if arguments.lint_only:
- if arguments.version or arguments.app_version or arguments.push:
- command.error("--lint-only cannot package or push a chart")
- return
- if not arguments.version:
- command.error("--version is required unless --lint-only is used")
-
- arguments.output.mkdir(parents=True, exist_ok=True)
- package_arguments: list[str | Path] = [
- "helm",
- "package",
- arguments.chart,
- "--destination",
- arguments.output,
- "--version",
- arguments.version,
- ]
- if arguments.app_version:
- package_arguments.extend(("--app-version", arguments.app_version))
- await run(package_arguments)
-
- package = arguments.output / f"{name}-{arguments.version}.tgz"
- if not package.is_file():
- raise TaskError(f"{TASK}: Helm did not create expected package: {package}")
- for registry in arguments.push:
- await run("helm", "push", package, registry.rstrip("/"))
-
-
-if __name__ == "__main__":
- task_main(TASK, main, sys.argv[1:])
diff --git a/tasks/container.py b/tasks/container.py
deleted file mode 100755
--- a/tasks/container.py
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# fmt: off
-#MISE description="Build and optionally publish an OCI container image"
-#MISE tools={"pipx"="1.16.7","python"="3.14.7"}
-# fmt: on
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import sys
-from collections.abc import Sequence
-from pathlib import Path
-
-sys.dont_write_bytecode = True
-
-from libs.common import TaskError, require_command, run, task_main
-
-TASK = "container"
-
-
-async def main(args: Sequence[str]) -> None:
- command = argparse.ArgumentParser(prog="mise run container --")
- command.add_argument("--context", default=Path("."), type=Path)
- command.add_argument("--file", default=Path("Dockerfile"), type=Path)
- command.add_argument("--platform", default="linux/amd64")
- command.add_argument("--target")
- command.add_argument("--build-arg", action="append", default=[])
- command.add_argument("--label", action="append", default=[])
- command.add_argument("--tag", action="append", required=True)
- command.add_argument("--cache-scope")
- command.add_argument("--provenance", choices=("true", "false"), default="true")
- command.add_argument("--sbom", choices=("true", "false"), default="true")
- command.add_argument("--push", action="store_true")
- command.add_argument("--load", action="store_true")
- arguments = command.parse_args(args)
-
- require_command("docker", TASK)
- if not arguments.context.is_dir():
- raise TaskError(f"{TASK}: context directory not found: {arguments.context}")
- dockerfile = (
- arguments.file
- if arguments.file.is_absolute()
- else arguments.context / arguments.file
- )
- if not dockerfile.is_file():
- raise TaskError(f"{TASK}: Dockerfile not found: {dockerfile}")
- for value in arguments.build_arg:
- if "\n" in value:
- command.error("build arguments cannot contain newlines")
- for value in arguments.label:
- if "\n" in value:
- command.error("labels cannot contain newlines")
- for tag in arguments.tag:
- if not tag or any(character.isspace() for character in tag):
- command.error(f"invalid image tag: {tag}")
- if arguments.push and arguments.load:
- command.error("--push and --load are mutually exclusive")
- if arguments.load and "," in arguments.platform:
- command.error("--load supports exactly one platform")
-
- build: list[str | Path] = [
- "docker",
- "buildx",
- "build",
- "--file",
- dockerfile,
- "--platform",
- arguments.platform,
- f"--provenance={arguments.provenance}",
- f"--sbom={arguments.sbom}",
- ]
- if arguments.target:
- build.extend(("--target", arguments.target))
- for value in arguments.build_arg:
- build.extend(("--build-arg", value))
- for value in arguments.label:
- build.extend(("--label", value))
- for tag in arguments.tag:
- build.extend(("--tag", tag))
- if arguments.cache_scope:
- build.extend(
- (
- "--cache-from",
- f"type=gha,scope={arguments.cache_scope}",
- "--cache-to",
- f"type=gha,mode=max,scope={arguments.cache_scope}",
- )
- )
- if arguments.push:
- build.append("--push")
- elif arguments.load:
- build.append("--load")
- build.append(arguments.context)
-
- await run(*build)
-
-
-if __name__ == "__main__":
- task_main(TASK, main, sys.argv[1:])
diff --git a/tasks/libs/__init__.py b/tasks/libs/__init__.py
deleted file mode 100644
--- a/tasks/libs/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
diff --git a/tasks/libs/apk.py b/tasks/libs/apk.py
deleted file mode 100644
--- a/tasks/libs/apk.py
+++ /dev/null
@@ -1,144 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-from __future__ import annotations
-
-import asyncio
-import gzip
-import hashlib
-import io
-import os
-import platform
-import shutil
-import tarfile
-import urllib.request
-from pathlib import Path
-
-from shellous import sh
-
-from .common import TaskError, capture, run
-
-TASK = "publish"
-APK_TOOLS_VERSION = "2.14.10-r0"
-APK_TOOLS_SHA256 = {
- "x86_64": "c86e3822764e5fe19f41ce2e13553e48cac1ea4e74f858338e8d44bf0b616b61",
- "aarch64": "3e22f80dd0272dc487e4ca84b2c6b660ca392cbad970764efe9ef9555b806ac8",
-}
-
-
-async def architecture(package: Path) -> str:
- metadata = await capture("tar", "-xOzf", package, ".PKGINFO").stderr(sh.DEVNULL)
- for line in metadata.splitlines():
- if line.startswith("arch = "):
- return line.removeprefix("arch = ")
- raise TaskError(f"{TASK}: cannot read APK architecture: {package}")
-
-
-async def apk_tool(work: Path) -> Path:
- configured = os.environ.get("APK_TOOL")
- if configured:
- tool = Path(configured)
- if os.access(tool, os.X_OK):
- return tool
- raise TaskError(f"{TASK}: APK_TOOL is not executable: {tool}")
- for name in ("apk.static", "apk"):
- if command := shutil.which(name):
- return Path(command)
-
- machine = platform.machine()
- apk_arch = {
- "x86_64": "x86_64",
- "amd64": "x86_64",
- "aarch64": "aarch64",
- "arm64": "aarch64",
- }.get(machine)
- if apk_arch is None:
- raise TaskError(f"{TASK}: apk-tools is unavailable for {machine}")
- archive = work / "apk-tools-static.apk"
- url = (
- "https://dl-cdn.alpinelinux.org/alpine/v3.22/main/"
- f"{apk_arch}/apk-tools-static-{APK_TOOLS_VERSION}.apk"
- )
- await asyncio.to_thread(urllib.request.urlretrieve, url, archive)
- if hashlib.sha256(archive.read_bytes()).hexdigest() != APK_TOOLS_SHA256[apk_arch]:
- raise TaskError(f"{TASK}: apk-tools checksum mismatch")
- directory = work / "apk-tools"
- directory.mkdir()
- await run("tar", "-xzf", archive, "-C", directory, "sbin/apk.static").stderr(
- sh.DEVNULL
- )
- return directory / "sbin" / "apk.static"
-
-
-async def sign_index(context, index: Path) -> None:
- name = f".SIGN.RSA256.{context.apk_signing.public_key_name}"
- signature = index.parent / name
- await run.set(env=context.apk_signing.environment, inherit_env=False)(
- "openssl",
- "dgst",
- "-sha256",
- "-sign",
- context.apk_signing.private_key_file,
- "-out",
- signature,
- index,
- )
- data = signature.read_bytes()
- stream = io.BytesIO()
- with tarfile.open(fileobj=stream, mode="w", format=tarfile.USTAR_FORMAT) as archive:
- information = tarfile.TarInfo(name)
- information.size = len(data)
- information.mode = 0o644
- information.mtime = int(os.environ.get("SOURCE_DATE_EPOCH", "0"))
- archive.addfile(information, io.BytesIO(data))
- size = 512 + ((len(data) + 511) // 512) * 512
- index.write_bytes(
- gzip.compress(stream.getvalue()[:size], mtime=0) + index.read_bytes()
- )
- signature.unlink()
-
-
-async def publish(context) -> None:
- tool = await apk_tool(context.work)
- packages = context.packages["apk"]
- package_architectures = dict(
- zip(
- packages,
- await asyncio.gather(*(architecture(package) for package in packages)),
- strict=True,
- )
- )
- architectures = sorted(set(package_architectures.values()))
- keys = context.work / "apk-keys"
- keys.mkdir()
- for public_key in context.apk_public_keys:
- shutil.copy2(public_key, keys / public_key.name)
-
- for apk_arch in architectures:
- root = context.work / "apk" / apk_arch
- root.mkdir(parents=True)
- remote = context.storage.service_key("apk", context.channel, apk_arch)
- context.storage.download_prefix(remote, root, "*.apk")
- for package in packages:
- if package_architectures[package] == apk_arch:
- context.add_package(package, root)
- for package in root.glob("*.apk"):
- result = await run.result(tool, "verify", "--keys-dir", keys, package)
- if result.exit_code:
- raise TaskError(
- f"{TASK}: APK signature verification failed: {package.name}"
- )
- index = root / "APKINDEX.tar.gz"
- await run(
- tool,
- "--allow-untrusted",
- "index",
- "--description",
- f"Dimidium Labs {context.service} {context.channel}",
- "--output",
- index,
- sorted(root.glob("*.apk")),
- )
- await sign_index(context, index)
- context.storage.upload_payloads(root, remote, "*.apk")
- context.storage.upload(index, f"{remote}/APKINDEX.tar.gz")
diff --git a/tasks/libs/apt.py b/tasks/libs/apt.py
deleted file mode 100644
--- a/tasks/libs/apt.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-from __future__ import annotations
-
-from .common import TaskError, capture, require_command, run
-
-TASK = "publish"
-
-
-async def publish(context) -> None:
- require_command("apt-ftparchive", TASK)
- require_command("dpkg-deb", TASK)
- root = context.work / "apt"
- pool = root / "pool" / context.channel
- metadata = root / "dists" / context.channel
- pool.mkdir(parents=True)
- pool_prefix = context.storage.service_key("apt", "pool", context.channel)
- metadata_prefix = context.storage.service_key("apt", "dists", context.channel)
- context.storage.download_prefix(pool_prefix, pool, "*.deb")
- for package in context.packages["deb"]:
- context.add_package(package, pool)
-
- architectures = sorted(
- {
- (await capture("dpkg-deb", "-f", package, "Architecture")).strip()
- for package in pool.glob("*.deb")
- }
- )
- if not architectures or "" in architectures:
- raise TaskError(f"{TASK}: no DEB architectures found")
- for architecture in architectures:
- (metadata / "main" / f"binary-{architecture}").mkdir(
- parents=True, exist_ok=True
- )
-
- cache = context.work / "apt-cache"
- cache.mkdir()
- config = context.work / "apt-ftparchive.conf"
- architecture_list = " ".join(architectures)
- config.write_text(
- f'''Dir {{ ArchiveDir "{root}"; CacheDir "{cache}"; }};
-Default {{ Packages::Compress ". gzip"; Packages::Extensions ".deb"; }};
-TreeDefault {{
- Packages "$(DIST)/$(SECTION)/binary-$(ARCH)/Packages";
- BinCacheDB "packages-$(ARCH).db";
-}};
-Tree "dists/{context.channel}" {{
- Sections "main";
- Architectures "{architecture_list}";
- Directory "pool/{context.channel}";
-}};
-'''
- )
- await run("apt-ftparchive", "generate", config)
- release = metadata / "Release"
- await run(
- "apt-ftparchive",
- "-o",
- "APT::FTPArchive::Release::Origin=Dimidium Labs",
- "-o",
- f"APT::FTPArchive::Release::Label={context.service} {context.channel}",
- "-o",
- f"APT::FTPArchive::Release::Suite={context.channel}",
- "-o",
- f"APT::FTPArchive::Release::Codename={context.channel}",
- "-o",
- "APT::FTPArchive::Release::Components=main",
- "-o",
- f"APT::FTPArchive::Release::Architectures={architecture_list}",
- "release",
- f"{metadata}/",
- ).stdout(release)
- await context.gpg.sign(
- metadata / "Release.gpg", "--armor", "--detach-sign", release
- )
- await context.gpg.sign(metadata / "InRelease", "--clearsign", release)
- context.storage.upload_payloads(pool, pool_prefix, "*.deb")
- context.storage.replace_prefix(metadata, metadata_prefix)
diff --git a/tasks/libs/common.py b/tasks/libs/common.py
deleted file mode 100644
--- a/tasks/libs/common.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-from __future__ import annotations
-
-import asyncio
-import os
-import shutil
-import sys
-from collections.abc import Awaitable, Callable, Sequence
-from pathlib import Path
-
-from shellous import ResultError, sh
-
-run = sh.stdout(sh.INHERIT).stderr(sh.INHERIT)
-capture = sh.stderr(sh.INHERIT)
-
-
-class TaskError(RuntimeError):
- pass
-
-
-def require_command(name: str, task: str) -> str:
- command = shutil.which(name)
- if command is None:
- raise TaskError(f"{task}: {name} is required")
- return command
-
-
-def required_env(name: str, task: str, purpose: str = "") -> str:
- value = os.environ.get(name)
- if value:
- return value
- suffix = f" {purpose}" if purpose else ""
- raise TaskError(f"{task}: {name} is required{suffix}")
-
-
-def task_main(
- task: str,
- main: Callable[[Sequence[str]], Awaitable[None]],
- args: Sequence[str],
-) -> None:
- try:
- asyncio.run(main(args))
- except TaskError as error:
- print(error, file=sys.stderr)
- raise SystemExit(1) from None
- except ResultError as error:
- exit_code = error.result.exit_code
- print(f"{task}: command failed with exit code {exit_code}", file=sys.stderr)
- raise SystemExit(exit_code) from None
-
-
-class GPGSigning:
- def __init__(self, task: str, work: Path):
- self.task = task
- require_command("gpg", task)
- private_key = required_env("GPG_PRIVATE_KEY", task)
- self.passphrase = required_env("GPG_PASSPHRASE", task)
- self.key_id = required_env("GPG_KEY_ID", task)
- self.short_key_id = self.key_id[-16:]
- self.home = work / "gnupg"
- self.home.mkdir(mode=0o700)
- self.private_key_file = work / "signing.asc"
- self.private_key_file.write_text(private_key)
- self.private_key_file.chmod(0o600)
- self.environment = dict(os.environ)
- self.environment["GNUPGHOME"] = str(self.home)
- self.environment.pop("GPG_PRIVATE_KEY", None)
- self.environment.pop("GPG_PASSPHRASE", None)
- self.environment.pop("APK_PRIVATE_KEY", None)
-
- @classmethod
- async def create(cls, task: str, work: Path) -> GPGSigning:
- signing = cls(task, work)
- command = run.set(env=signing.environment, inherit_env=False)
- await (
- f"{signing.passphrase}\n"
- | command(
- "gpg",
- "--batch",
- "--yes",
- "--pinentry-mode",
- "loopback",
- "--passphrase-fd",
- "0",
- "--import",
- signing.private_key_file,
- )
- )
- return signing
-
- def package_environment(self) -> dict[str, str]:
- environment = dict(self.environment)
- environment["GPG_KEY_ID"] = self.short_key_id
- return environment
-
- async def prime_agent(self) -> None:
- signature = self.private_key_file.with_suffix(".sig")
- await self.sign(signature, "--detach-sign", self.private_key_file)
- signature.unlink()
-
- async def export_public_key(self, output: Path) -> None:
- command = run.set(env=self.environment, inherit_env=False)
- await command(
- "gpg",
- "--batch",
- "--yes",
- "--armor",
- "--export",
- self.key_id,
- ).stdout(output)
-
- async def verify_public_bundle(self, bundle: Path) -> None:
- command = capture.set(env=self.environment, inherit_env=False)
- output = await command(
- "gpg",
- "--batch",
- "--with-colons",
- "--show-keys",
- bundle,
- )
- fingerprints = {
- line.split(":")[9]
- for line in output.splitlines()
- if line.startswith("fpr:")
- }
- if self.key_id not in fingerprints:
- raise TaskError(
- f"{self.task}: packages.gpg does not contain signing key {self.key_id}"
- )
-
- async def sign(self, output: Path, *arguments: str | Path) -> None:
- command = run.set(env=self.environment, inherit_env=False)
- await (
- f"{self.passphrase}\n"
- | command(
- "gpg",
- f"--default-key={self.key_id}",
- "--batch",
- "--yes",
- "--pinentry-mode",
- "loopback",
- "--passphrase-fd",
- "0",
- "-o",
- output,
- arguments,
- )
- )
-
-
-class APKSigning:
- def __init__(self, task: str, work: Path, key_name: str = "packages"):
- require_command("openssl", task)
- private_key = required_env("APK_PRIVATE_KEY", task, "for APK signing")
- self.key_name = key_name
- self.public_key_name = f"{key_name}.rsa.pub"
- self.private_key_file = work / f"{key_name}.rsa"
- self.private_key_file.write_text(private_key)
- self.private_key_file.chmod(0o600)
- self.environment = dict(os.environ)
- self.environment.pop("APK_PRIVATE_KEY", None)
- self.environment.pop("GPG_PRIVATE_KEY", None)
- self.environment.pop("GPG_PASSPHRASE", None)
- self.environment["APK_SIGNING_KEY"] = str(self.private_key_file)
-
- async def export_public_key(self, output: Path) -> None:
- command = run.set(env=self.environment, inherit_env=False)
- await command(
- "openssl",
- "rsa",
- "-in",
- self.private_key_file,
- "-pubout",
- "-out",
- output,
- )
diff --git a/tasks/libs/repository.py b/tasks/libs/repository.py
deleted file mode 100644
--- a/tasks/libs/repository.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-from __future__ import annotations
-
-import re
-import shutil
-from pathlib import Path
-
-from .common import APKSigning, GPGSigning, TaskError, required_env
-from .storage import S3Storage
-
-TASK = "publish"
-KEY_VERSION = re.compile(r"^[0-9]{4}$")
-RSA_PUBLIC_KEY = re.compile(r"^keys/packages\.[0-9]{4}\.rsa\.pub$")
-
-
-class Repository:
- def __init__(self, service, channel, input_directory, formats, work):
- self.service = service
- self.channel = channel
- self.formats = formats
- self.work = work
- self.public_url = required_env("S3_PUBLIC_URL", TASK).rstrip("/")
- self.key_version = required_env("PACKAGE_KEY_VERSION", TASK)
- if not KEY_VERSION.fullmatch(self.key_version):
- raise TaskError(f"{TASK}: invalid PACKAGE_KEY_VERSION: {self.key_version}")
- self.storage = S3Storage(TASK, service)
- self.gpg = None
- self.gpg_public_key = None
- self.apk_signing = None
- self.apk_public_keys = []
- self.packages = {
- package_format: sorted(input_directory.glob(f"*.{package_format}"))
- for package_format in formats
- }
- for package_format, packages in self.packages.items():
- if not packages:
- raise TaskError(
- f"{TASK}: no .{package_format} packages found in {input_directory}"
- )
-
- def check_public_key(self, source: Path, key: str) -> None:
- existing = self.work / f"existing-{source.name}"
- if not self.storage.download(key, existing):
- raise TaskError(f"{TASK}: organization key {key} is not provisioned")
- if source.read_bytes() != existing.read_bytes():
- raise TaskError(f"{TASK}: signing key does not match {key}")
-
- @staticmethod
- def add_package(source: Path, directory: Path) -> None:
- destination = directory / source.name
- if destination.exists() and source.read_bytes() != destination.read_bytes():
- raise TaskError(
- f"{TASK}: immutable package filename has different content: {source.name}"
- )
- if not destination.exists():
- shutil.copy2(source, destination)
-
- async def setup_openpgp(self) -> None:
- self.gpg = await GPGSigning.create(TASK, self.work)
- current = self.work / "current-packages.gpg"
- await self.gpg.export_public_key(current)
- self.check_public_key(current, f"keys/packages.{self.key_version}.gpg")
-
- bundle = self.work / "packages.gpg"
- if not self.storage.download("packages.gpg", bundle):
- raise TaskError(f"{TASK}: organization key packages.gpg is not provisioned")
- await self.gpg.verify_public_bundle(bundle)
- self.gpg_public_key = bundle
-
- async def setup_rsa(self) -> None:
- key_name = f"packages.{self.key_version}"
- self.apk_signing = APKSigning(TASK, self.work, key_name)
- current = self.work / self.apk_signing.public_key_name
- await self.apk_signing.export_public_key(current)
- self.check_public_key(
- current,
- f"keys/{self.apk_signing.public_key_name}",
- )
-
- key_directory = self.work / "rsa-public-keys"
- key_directory.mkdir()
- for key in sorted(self.storage.objects("keys/")):
- if RSA_PUBLIC_KEY.fullmatch(key):
- destination = key_directory / Path(key).name
- if destination.exists():
- raise TaskError(f"{TASK}: duplicate RSA public key name: {key}")
- self.storage.download(key, destination)
- self.apk_public_keys.append(destination)
- if current.name not in {key.name for key in self.apk_public_keys}:
- raise TaskError(
- f"{TASK}: current RSA public key is absent from key archive"
- )
-
- async def setup_signing(self) -> None:
- if {"deb", "rpm"} & set(self.formats):
- await self.setup_openpgp()
- if "apk" in self.formats:
- await self.setup_rsa()
-
- async def publish(self) -> None:
- from .apk import publish as publish_apk
- from .apt import publish as publish_apt
- from .rpm import publish as publish_rpm
-
- publishers = {"deb": publish_apt, "rpm": publish_rpm, "apk": publish_apk}
- with self.storage.lock(self.channel):
- await self.setup_signing()
- for package_format in self.formats:
- await publishers[package_format](self)
diff --git a/tasks/libs/rpm.py b/tasks/libs/rpm.py
deleted file mode 100644
--- a/tasks/libs/rpm.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-from __future__ import annotations
-
-import shutil
-
-from .common import TaskError, capture, require_command, run
-
-TASK = "publish"
-
-
-async def publish(context) -> None:
- require_command("createrepo_c", TASK)
- require_command("rpmkeys", TASK)
- root = context.work / "rpm"
- root.mkdir()
- remote = context.storage.service_key("rpm", context.channel)
- context.storage.download_prefix(remote, root, "*.rpm")
- for package in context.packages["rpm"]:
- context.add_package(package, root)
-
- rpm_database = context.work / "rpmdb"
- rpm_database.mkdir()
- await run("rpmkeys", "--dbpath", rpm_database, "--import", context.gpg_public_key)
- for package in root.glob("*.rpm"):
- result = await capture(
- "rpmkeys", "--dbpath", rpm_database, "--checksig", package
- )
- if "signatures OK" not in result:
- raise TaskError(
- f"{TASK}: RPM is not signed by a trusted key: {package.name}"
- )
-
- shutil.rmtree(root / "repodata", ignore_errors=True)
- await run("createrepo_c", root)
- repomd = root / "repodata" / "repomd.xml"
- await context.gpg.sign(
- repomd.with_suffix(".xml.asc"), "--armor", "--detach-sign", repomd
- )
- definition = root / f"{context.service}-{context.channel}.repo"
- definition.write_text(
- f"""[{context.service}-{context.channel}]
-name={context.service} {context.channel}
-gpgkey={context.public_url}/packages.gpg
-baseurl={context.public_url}/{context.service}/rpm/{context.channel}/
-enabled=1
-gpgcheck=1
-repo_gpgcheck=1
-"""
- )
- context.storage.upload_payloads(root, remote, "*.rpm")
- context.storage.upload(definition, f"{remote}/{definition.name}")
- context.storage.replace_prefix(root / "repodata", f"{remote}/repodata")
diff --git a/tasks/libs/storage.py b/tasks/libs/storage.py
deleted file mode 100644
--- a/tasks/libs/storage.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-from __future__ import annotations
-
-import fnmatch
-import hashlib
-import json
-import time
-import uuid
-from contextlib import contextmanager
-from pathlib import Path
-
-import boto3
-from botocore.exceptions import ClientError
-
-from .common import TaskError, required_env
-
-
-class S3Storage:
- def __init__(self, task: str, service: str):
- self.task = task
- self.bucket = required_env("S3_BUCKET", task)
- self.service_root = service
- self.client = boto3.client(
- "s3",
- endpoint_url=required_env("S3_ENDPOINT", task),
- aws_access_key_id=required_env("S3_ACCESS_KEY_ID", task),
- aws_secret_access_key=required_env("S3_SECRET_ACCESS_KEY", task),
- region_name="auto",
- )
-
- def service_key(self, *parts: str) -> str:
- return "/".join((self.service_root, *parts))
-
- def download(self, key: str, destination: Path) -> bool:
- destination.parent.mkdir(parents=True, exist_ok=True)
- try:
- self.client.download_file(self.bucket, key, str(destination))
- except ClientError as error:
- if error.response.get("Error", {}).get("Code") in {
- "404",
- "NoSuchKey",
- "NotFound",
- }:
- return False
- raise
- return True
-
- def objects(self, prefix: str) -> set[str]:
- pages = self.client.get_paginator("list_objects_v2").paginate(
- Bucket=self.bucket, Prefix=prefix
- )
- return {item["Key"] for page in pages for item in page.get("Contents", [])}
-
- def download_prefix(self, prefix: str, destination: Path, pattern: str) -> None:
- destination.mkdir(parents=True, exist_ok=True)
- prefix = prefix.rstrip("/") + "/"
- for key in self.objects(prefix):
- relative = key.removeprefix(prefix).lstrip("/")
- if relative and "/" not in relative and fnmatch.fnmatch(relative, pattern):
- self.client.download_file(self.bucket, key, str(destination / relative))
-
- @staticmethod
- def digest(path: Path) -> str:
- with path.open("rb") as stream:
- return hashlib.file_digest(stream, "sha256").hexdigest()
-
- @staticmethod
- def conflict(error: ClientError) -> bool:
- return error.response.get("Error", {}).get("Code") in {
- "409",
- "412",
- "ConditionalRequestConflict",
- "PreconditionFailed",
- }
-
- def upload_immutable(self, source: Path, key: str) -> None:
- digest = self.digest(source)
- try:
- with source.open("rb") as stream:
- self.client.put_object(
- Bucket=self.bucket,
- Key=key,
- Body=stream,
- Metadata={"sha256": digest},
- IfNoneMatch="*",
- )
- return
- except ClientError as error:
- if not self.conflict(error):
- raise
-
- existing = self.client.head_object(Bucket=self.bucket, Key=key)
- existing_digest = existing.get("Metadata", {}).get("sha256")
- if not existing_digest:
- body = self.client.get_object(Bucket=self.bucket, Key=key)["Body"]
- existing_digest = hashlib.sha256(body.read()).hexdigest()
- if existing_digest != digest:
- raise TaskError(
- f"{self.task}: immutable object has different content: {key}"
- )
-
- def upload_payloads(self, source: Path, prefix: str, pattern: str) -> None:
- for path in sorted(source.glob(pattern)):
- self.upload_immutable(path, f"{prefix.rstrip('/')}/{path.name}")
-
- def upload(self, source: Path, key: str) -> None:
- self.client.upload_file(str(source), self.bucket, key)
-
- @contextmanager
- def lock(self, name: str, lifetime: int = 3600):
- key = self.service_key("_locks", name)
- body = json.dumps(
- {"expires": int(time.time()) + lifetime, "id": uuid.uuid4().hex}
- )
- try:
- result = self.client.put_object(
- Bucket=self.bucket, Key=key, Body=body.encode(), IfNoneMatch="*"
- )
- except ClientError as error:
- if not self.conflict(error):
- raise
- current = self.client.get_object(Bucket=self.bucket, Key=key)
- state = json.loads(current["Body"].read())
- if state["expires"] > time.time():
- raise TaskError(f"{self.task}: publication already in progress: {name}")
- result = self.client.put_object(
- Bucket=self.bucket,
- Key=key,
- Body=body.encode(),
- IfMatch=current["ETag"],
- )
- try:
- yield
- finally:
- self.client.delete_object(
- Bucket=self.bucket, Key=key, IfMatch=result["ETag"]
- )
-
- def replace_prefix(self, source: Path, prefix: str) -> None:
- prefix = prefix.rstrip("/") + "/"
- wanted: set[str] = set()
- for path in sorted(item for item in source.rglob("*") if item.is_file()):
- key = prefix + path.relative_to(source).as_posix()
- wanted.add(key)
- self.client.upload_file(str(path), self.bucket, key)
- stale = sorted(set(self.objects(prefix)) - wanted)
- for offset in range(0, len(stale), 1000):
- self.client.delete_objects(
- Bucket=self.bucket,
- Delete={
- "Objects": [{"Key": key} for key in stale[offset : offset + 1000]],
- "Quiet": True,
- },
- )
diff --git a/tasks/licenses b/tasks/licenses
new file mode 100755
--- /dev/null
+++ b/tasks/licenses
@@ -0,0 +1,13 @@
+#!/bin/sh
+# SPDX-FileCopyrightText: 2026 Nikolay Govorov
+# SPDX-License-Identifier: 0BSD
+#MISE description="Verify repository licensing metadata"
+#MISE tools={"pipx:reuse"="6.2.0","aqua:EmbarkStudios/cargo-deny"="0.19.0"}
+
+set -eu
+
+reuse lint
+
+if [ -f Cargo.toml ]; then
+ cargo-deny check
+fi
diff --git a/tasks/licenses-json.py b/tasks/licenses-json.py
deleted file mode 100755
--- a/tasks/licenses-json.py
+++ /dev/null
@@ -1,249 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# fmt: off
-#MISE description="Generate embeddable Rust dependency license JSON"
-#MISE tools={"pipx"="1.16.7","python"="3.14.7","cargo:cargo-about"="0.8.4"}
-# fmt: on
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import json
-import re
-import sys
-import tempfile
-from collections.abc import Sequence
-from pathlib import Path
-from typing import Any
-
-sys.dont_write_bytecode = True
-
-from libs.common import TaskError, capture, require_command, run, task_main
-
-TASK = "licenses-json"
-# 0.8.4 intentionally matches the license-file deduplication behavior of the
-# legacy in-tree generator. Keep the task tool pin and normalizer in sync.
-SPDX_TOKEN = re.compile(r"[A-Za-z0-9][A-Za-z0-9.+:-]*|[()]")
-SPDX_OPERATORS = {"AND", "OR", "WITH"}
-
-
-def license_requirements(expression: str) -> list[str]:
- """Return SPDX requirements in expression order, matching the legacy generator."""
- normalized = expression.replace("/", " OR ")
- tokens = SPDX_TOKEN.findall(normalized)
- requirements: list[str] = []
-
- index = 0
- while index < len(tokens):
- token = tokens[index]
- if token in {"(", ")", "AND", "OR"}:
- index += 1
- continue
- if token == "WITH":
- raise TaskError(f"{TASK}: invalid SPDX expression: {expression}")
-
- requirement = token
- if index + 1 < len(tokens) and tokens[index + 1] == "WITH":
- if index + 2 >= len(tokens) or tokens[index + 2] in SPDX_OPERATORS | {
- "(",
- ")",
- }:
- raise TaskError(f"{TASK}: invalid SPDX expression: {expression}")
- requirement = f"{token} WITH {tokens[index + 2]}"
- index += 2
-
- if requirement not in requirements:
- requirements.append(requirement)
- index += 1
-
- return requirements
-
-
-def cargo_about_config(metadata: dict[str, Any]) -> str:
- accepted_by_crate: dict[str, list[str]] = {}
- for package in metadata.get("packages", []):
- expression = package.get("license")
- if not expression:
- continue
-
- accepted = accepted_by_crate.setdefault(package["name"], [])
- for requirement in license_requirements(expression):
- if requirement not in accepted:
- accepted.append(requirement)
-
- lines = [
- "accepted = []",
- "private = { ignore = true }",
- "ignore-build-dependencies = true",
- "ignore-dev-dependencies = true",
- "ignore-transitive-dependencies = false",
- ]
- for name, accepted in sorted(accepted_by_crate.items()):
- lines.extend(
- ("", f"[{json.dumps(name)}]", f"accepted = {json.dumps(accepted)}")
- )
-
- return "\n".join(lines) + "\n"
-
-
-def normalized_output(report: dict[str, Any]) -> str:
- licenses: list[dict[str, Any]] = []
- for source in report.get("licenses", []):
- used_by = [
- {
- "crate": {
- "name": usage["crate"]["name"],
- "version": usage["crate"]["version"],
- "repository": usage["crate"].get("repository"),
- }
- }
- for usage in source["used_by"]
- ]
- used_by.sort(key=lambda usage: len(usage["crate"]["name"]))
- licenses.append(
- {
- "name": source["name"],
- "id": source["id"],
- "first_of_kind": False,
- "text": source["text"],
- "used_by": used_by,
- }
- )
-
- licenses.sort(key=lambda license_: license_["id"])
-
- overview_by_id: dict[str, dict[str, Any]] = {}
- for license_ in licenses:
- first = license_["id"] not in overview_by_id
- license_["first_of_kind"] = first
- overview = overview_by_id.setdefault(
- license_["id"],
- {
- "count": 0,
- "name": license_["name"],
- "id": license_["id"],
- },
- )
- overview["count"] += len(license_["used_by"])
-
- overview = sorted(overview_by_id.values(), key=lambda item: item["name"])
- output = {"overview": overview, "licenses": licenses}
- return json.dumps(output, ensure_ascii=False, indent=2) + "\n"
-
-
-async def cargo_metadata(manifest_path: Path, offline: bool) -> dict[str, Any]:
- arguments: list[str | Path] = [
- "cargo",
- "metadata",
- "--format-version",
- "1",
- "--locked",
- "--manifest-path",
- manifest_path,
- ]
- if offline:
- arguments.append("--offline")
-
- output = await capture(arguments)
- try:
- return json.loads(output)
- except json.JSONDecodeError as error:
- raise TaskError(
- f"{TASK}: cargo metadata returned invalid JSON: {error}"
- ) from error
-
-
-async def generate(
- manifest_path: Path,
- targets: Sequence[str],
- offline: bool,
-) -> str:
- metadata = await cargo_metadata(manifest_path, offline)
-
- with tempfile.TemporaryDirectory(prefix=f"{TASK}-") as temporary:
- work = Path(temporary)
- config = work / "about.toml"
- report = work / "report.json"
- config.write_text(cargo_about_config(metadata), encoding="utf-8")
-
- arguments: list[str | Path] = [
- "cargo-about",
- "generate",
- "--config",
- config,
- "--manifest-path",
- manifest_path,
- "--format",
- "json",
- "--locked",
- "--output-file",
- report,
- ]
- if offline:
- arguments.append("--offline")
- for target in targets:
- arguments.extend(("--target", target))
-
- quiet_arguments = ["cargo-about", "-L", "off", *arguments[1:]]
- result = await capture.result(quiet_arguments)
- if result.exit_code != 0:
- # Repeat with diagnostics enabled only on failure. cargo-about 0.8.4
- # otherwise reports harmless scanner errors for deprecated SPDX IDs.
- await run(arguments)
- try:
- raw_report = json.loads(report.read_text(encoding="utf-8"))
- except (OSError, json.JSONDecodeError) as error:
- raise TaskError(
- f"{TASK}: cargo-about returned invalid JSON: {error}"
- ) from error
-
- return normalized_output(raw_report)
-
-
-async def main(args: Sequence[str]) -> None:
- command = argparse.ArgumentParser(prog="mise run licenses-json --")
- command.add_argument(
- "--manifest-path",
- default=Path("Cargo.toml"),
- type=Path,
- )
- command.add_argument("--output", required=True, type=Path)
- command.add_argument("--target", action="append", required=True)
- command.add_argument("--offline", action="store_true")
- command.add_argument("--check", action="store_true")
- arguments = command.parse_args(args)
-
- require_command("cargo", TASK)
- require_command("cargo-about", TASK)
- if not arguments.manifest_path.is_file():
- raise TaskError(f"{TASK}: manifest not found: {arguments.manifest_path}")
-
- output = await generate(
- arguments.manifest_path.resolve(),
- arguments.target,
- arguments.offline,
- )
- if arguments.check:
- if not arguments.output.is_file():
- raise TaskError(f"{TASK}: output not found: {arguments.output}")
- if arguments.output.read_text(encoding="utf-8") != output:
- raise TaskError(
- f"{TASK}: {arguments.output} is out of date; regenerate it without --check"
- )
- return
-
- arguments.output.parent.mkdir(parents=True, exist_ok=True)
- if (
- not arguments.output.is_file()
- or arguments.output.read_text(encoding="utf-8") != output
- ):
- arguments.output.write_text(output, encoding="utf-8")
-
-
-if __name__ == "__main__":
- task_main(TASK, main, sys.argv[1:])
diff --git a/tasks/licenses.py b/tasks/licenses.py
deleted file mode 100755
--- a/tasks/licenses.py
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# fmt: off
-#MISE description="Verify repository licensing metadata"
-#MISE tools={"pipx"="1.16.7","python"="3.14.7","pipx:reuse"="6.2.0","aqua:EmbarkStudios/cargo-deny"="0.19.0"}
-# fmt: on
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import re
-import sys
-from collections.abc import Sequence
-from pathlib import Path
-
-sys.dont_write_bytecode = True
-
-from libs.common import TaskError, capture, run, task_main
-
-TASK = "licenses"
-HEADER = (
- r"^((<!--|#|//|/\*|\*)[[:space:]]*)?"
- r"(Copyright[[:space:]]+(\([cC]\)|©)|SPDX-FileCopyrightText:)"
-)
-LEGACY_COPYRIGHT = re.compile(r"^\s*((<!--|#|//|/\*|\*)\s*)?Copyright\s+(\([cC]\)|©)")
-CANONICAL_COPYRIGHT = re.compile(
- r"SPDX-FileCopyrightText: 2026 Nikolay Govorov(?:\s*(?:\*/|-->))?$"
-)
-
-
-async def check_copyright_headers() -> None:
- result = await capture.result(
- "git",
- "grep",
- "-n",
- "-I",
- "-E",
- HEADER,
- "--",
- ".",
- ":(exclude)*.md",
- ":(exclude)LICENSE",
- ":(exclude)LICENSES/**",
- ":(exclude)COPYING*",
- )
- if result.exit_code not in {0, 1}:
- raise TaskError(f"{TASK}: git grep failed with exit code {result.exit_code}")
-
- invalid: list[str] = []
- for line in result.output.splitlines():
- match = re.search(r":([0-9]+):", line)
- if match is None or int(match.group(1)) > 10:
- continue
- text = line[match.end() :]
- reason = ""
- if LEGACY_COPYRIGHT.match(text):
- reason = "legacy copyright header"
- elif (position := text.find("SPDX-FileCopyrightText:")) >= 0:
- suffix = text[position + len("SPDX-FileCopyrightText:") :]
- if (
- not suffix
- or not suffix.startswith(" ")
- or (len(suffix) > 1 and suffix[1].isspace())
- ):
- reason = "expected exactly one space after colon"
- if (
- not reason
- and "SPDX-FileCopyrightText:" in text
- and "Nikolay Govorov" in text
- and not CANONICAL_COPYRIGHT.search(text)
- ):
- reason = "expected 2026 Nikolay Govorov"
- if reason:
- invalid.append(f"{reason}: {line}")
-
- if invalid:
- print("Invalid copyright headers:", *invalid, sep="\n", file=sys.stderr)
- raise TaskError(f"{TASK}: invalid copyright headers")
-
-
-async def main(args: Sequence[str]) -> None:
- argparse.ArgumentParser(prog="mise run licenses --").parse_args(args)
- await check_copyright_headers()
- await run("reuse", "lint")
- if Path("Cargo.toml").is_file():
- await run("cargo-deny", "check")
-
-
-if __name__ == "__main__":
- task_main(TASK, main, sys.argv[1:])
diff --git a/tasks/package.py b/tasks/package.py
deleted file mode 100755
--- a/tasks/package.py
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# fmt: off
-#MISE description="Build release archives and signed Linux packages"
-#MISE tools={"pipx"="1.16.7","python"="3.14.7","nfpm"="2.47.0"}
-# fmt: on
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import os
-import re
-import shutil
-import sys
-import tempfile
-from collections.abc import Sequence
-from pathlib import Path
-
-sys.dont_write_bytecode = True
-
-from libs.common import (
- APKSigning,
- GPGSigning,
- TaskError,
- require_command,
- run,
- task_main,
-)
-
-TASK = "package"
-SYSTEM_FORMATS = {"deb", "rpm", "apk"}
-ARCHIVE_FORMATS = {"tar.gz", "zip"}
-SAFE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
-SAFE_KEY_VERSION = re.compile(r"^[0-9]{4}$")
-
-
-def validate(arguments: argparse.Namespace, command: argparse.ArgumentParser) -> None:
- formats = set(arguments.formats)
- if formats & SYSTEM_FORMATS:
- if not arguments.version or not arguments.arch:
- command.error("--version and --arch are required for deb, rpm, and apk")
- if not arguments.config.is_file():
- raise TaskError(f"{TASK}: {arguments.config} not found")
- if formats & ARCHIVE_FORMATS:
- if arguments.archive_root is None or not arguments.archive_name:
- command.error(
- "--archive-root and --archive-name are required for tar.gz and zip"
- )
- if not arguments.archive_root.is_dir():
- raise TaskError(f"{TASK}: {arguments.archive_root} not found")
- if not SAFE_NAME.fullmatch(arguments.archive_name):
- raise TaskError(f"{TASK}: invalid archive name: {arguments.archive_name}")
- if arguments.apk_public_key and not SAFE_NAME.fullmatch(arguments.apk_public_key):
- raise TaskError(
- f"{TASK}: invalid APK public key name: {arguments.apk_public_key}"
- )
-
-
-async def create_archive(
- archive_format: str,
- output: Path,
- root: Path,
- name: str,
-) -> None:
- if archive_format == "tar.gz":
- require_command("tar", TASK)
- await run("tar", "-czf", output / f"{name}.tar.gz", "-C", root, ".")
- return
- require_command("zip", TASK)
- destination = output / f"{name}.zip"
- destination.unlink(missing_ok=True)
- await run.set(cwd=root)("zip", "-qry", destination, ".")
-
-
-async def sign_package(
- package_format: str, package: Path, signing: GPGSigning | None
-) -> None:
- if signing is None or package_format not in {"deb", "rpm"}:
- return
- command = run.set(env=signing.environment, inherit_env=False)
- if package_format == "deb":
- require_command("debsigs", TASK)
- await command(
- "debsigs",
- "--sign=origin",
- f"--default-key={signing.key_id}",
- package,
- )
- else:
- require_command("rpmsign", TASK)
- await command(
- "rpmsign",
- "--define",
- f"_gpg_name {signing.key_id}",
- "--addsign",
- package,
- )
-
-
-async def main(args: Sequence[str]) -> None:
- command = argparse.ArgumentParser(
- prog="mise run package --",
- usage=(
- "%(prog)s --output DIR [--config FILE] "
- "[--version VERSION --arch ARCH] [--apk-public-key FILE] "
- "[--archive-root DIR --archive-name NAME] "
- "deb|rpm|apk|tar.gz|zip..."
- ),
- )
- command.add_argument("--config", default="nfpm.yaml", type=Path)
- command.add_argument("--version")
- command.add_argument("--arch")
- command.add_argument("--output", required=True, type=Path)
- command.add_argument("--archive-root", type=Path)
- command.add_argument("--archive-name")
- command.add_argument("--apk-public-key")
- command.add_argument(
- "formats", nargs="+", choices=sorted(SYSTEM_FORMATS | ARCHIVE_FORMATS)
- )
- arguments = command.parse_args(args)
- validate(arguments, command)
-
- arguments.output.mkdir(parents=True, exist_ok=True)
- output = arguments.output.resolve()
- formats = set(arguments.formats)
-
- with tempfile.TemporaryDirectory(prefix="package-") as directory:
- work = Path(directory)
- environment = dict(os.environ)
- for name in (
- "GPG_PRIVATE_KEY",
- "APK_PRIVATE_KEY",
- "SIGNING_PRIVATE_KEY",
- "NFPM_PASSPHRASE",
- "NFPM_DEB_PASSPHRASE",
- "NFPM_RPM_PASSPHRASE",
- ):
- environment.pop(name, None)
- config = arguments.config
- if "apk" in formats and "${PACKAGE_KEY_VERSION}" in config.read_text():
- key_version = os.environ.get("PACKAGE_KEY_VERSION", "")
- if not SAFE_KEY_VERSION.fullmatch(key_version):
- raise TaskError(
- f"{TASK}: invalid PACKAGE_KEY_VERSION: {key_version or '<empty>'}"
- )
- config = work / "nfpm.yaml"
- config.write_text(
- arguments.config.read_text().replace(
- "${PACKAGE_KEY_VERSION}", key_version
- )
- )
-
- gpg_signing: GPGSigning | None = None
- if formats & {"deb", "rpm"} and os.environ.get("GPG_PRIVATE_KEY"):
- gpg_signing = await GPGSigning.create(TASK, work)
- await gpg_signing.prime_agent()
- environment = gpg_signing.package_environment()
-
- apk_signing: APKSigning | None = None
- if "apk" in formats and os.environ.get("APK_PRIVATE_KEY"):
- apk_signing = APKSigning(TASK, work)
- environment["APK_SIGNING_KEY"] = str(apk_signing.private_key_file)
- elif os.environ.get("APK_SIGNING_KEY"):
- environment["APK_SIGNING_KEY"] = os.environ["APK_SIGNING_KEY"]
-
- if "apk" in formats and arguments.apk_public_key:
- public_key = output / arguments.apk_public_key
- if apk_signing is not None:
- await apk_signing.export_public_key(public_key)
- elif environment.get("APK_SIGNING_KEY"):
- require_command("openssl", TASK)
- await run.set(env=environment, inherit_env=False)(
- "openssl",
- "rsa",
- "-in",
- environment["APK_SIGNING_KEY"],
- "-pubout",
- "-out",
- public_key,
- )
-
- for package_format in arguments.formats:
- if package_format in SYSTEM_FORMATS:
- require_command("nfpm", TASK)
- package_environment = dict(environment)
- package_environment.update(
- ARCH=arguments.arch,
- VERSION=arguments.version,
- )
- package_output = work / f"output-{package_format}"
- package_output.mkdir()
- await run.set(env=package_environment, inherit_env=False)(
- "nfpm",
- "package",
- "--config",
- config,
- "--packager",
- package_format,
- "--target",
- f"{package_output}/",
- )
- packages = list(package_output.iterdir())
- if len(packages) != 1 or not packages[0].is_file():
- raise TaskError(
- f"{TASK}: nFPM produced an unexpected number of packages"
- )
- await sign_package(package_format, packages[0], gpg_signing)
- shutil.move(packages[0], output / packages[0].name)
- else:
- await create_archive(
- package_format,
- output,
- arguments.archive_root,
- arguments.archive_name,
- )
-
-
-if __name__ == "__main__":
- task_main(TASK, main, sys.argv[1:])
diff --git a/tasks/publish.py b/tasks/publish.py
deleted file mode 100755
--- a/tasks/publish.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# fmt: off
-#MISE description="Publish signed package repositories to shared S3 storage"
-#MISE tools={"pipx"="1.16.7","python"="3.14.7"}
-# fmt: on
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["boto3==1.43.75", "shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import os
-import re
-import sys
-import tempfile
-from collections.abc import Sequence
-from pathlib import Path
-
-sys.dont_write_bytecode = True
-
-from libs.common import TaskError, task_main
-
-if test_path := os.environ.get("PUBLISH_TEST_PYTHONPATH"):
- sys.path.insert(0, test_path)
-
-from libs.repository import Repository
-
-TASK = "publish"
-FORMATS = {"deb", "rpm", "apk"}
-SAFE_SLUG = re.compile(r"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")
-
-
-async def main(args: Sequence[str]) -> None:
- command = argparse.ArgumentParser(
- prog="mise run publish --",
- usage="%(prog)s --service NAME --channel CHANNEL --input DIR deb|rpm|apk...",
- )
- command.add_argument("--service", required=True)
- command.add_argument("--channel", required=True)
- command.add_argument("--input", required=True, type=Path)
- command.add_argument("formats", nargs="+", choices=sorted(FORMATS))
- arguments = command.parse_args(args)
- for label, value in (
- ("service name", arguments.service),
- ("channel", arguments.channel),
- ):
- if not SAFE_SLUG.fullmatch(value) or "--" in value:
- raise TaskError(f"{TASK}: invalid {label}: {value}")
- if not arguments.input.is_dir():
- raise TaskError(f"{TASK}: {arguments.input} not found")
- with tempfile.TemporaryDirectory(prefix="publish-") as directory:
- await Repository(
- arguments.service,
- arguments.channel,
- arguments.input.resolve(),
- arguments.formats,
- Path(directory),
- ).publish()
-
-
-if __name__ == "__main__":
- task_main(TASK, main, sys.argv[1:])
diff --git a/tasks/signoff b/tasks/signoff
new file mode 100755
--- /dev/null
+++ b/tasks/signoff
@@ -0,0 +1,66 @@
+#!/bin/sh -eu
+# SPDX-FileCopyrightText: 2026 Nikolay Govorov
+# SPDX-License-Identifier: 0BSD
+#MISE description="Verify contributor identities and Signed-off-by trailers"
+
+root=$(git rev-parse --show-toplevel)
+cd "$root"
+
+task_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+approved_emails_file="$task_dir/../config/signoff-approved-emails"
+
+is_approved_email() {
+ grep -v '^[[:space:]]*#' "$approved_emails_file" |
+ grep -Fqx -- "$1"
+}
+
+if [ ! -f "$approved_emails_file" ]; then
+ echo "Approved email configuration is missing: $approved_emails_file" >&2
+ exit 1
+fi
+
+bad=0
+for sha in $(git log --no-merges --format=%H); do
+ author=$(git show -s --format='%an' "$sha")
+ email=$(git show -s --format='%ae' "$sha")
+ if is_approved_email "$email"; then
+ continue
+ fi
+
+ expected="${author} <${email}>"
+
+ if ! git show -s --format='%(trailers:key=Signed-off-by,valueonly)' "$sha" |
+ grep -Fqx -- "$expected"; then
+ short_sha=$(printf '%.8s' "$sha")
+ echo "Commit $short_sha by $expected is missing a valid Signed-off-by"
+ bad=1
+ fi
+done
+
+if [ "$bad" -ne 0 ]; then
+ echo "Every commit from a non-approved author email must be signed off (git commit -s)"
+ echo "See CLA.md"
+ exit 1
+fi
+
+if [ ! -f .mailmap ]; then
+ echo "Contributor registry .mailmap is missing" >&2
+ exit 1
+fi
+
+missing=0
+for email in $(git log --no-merges --format='%ae%n%ce' | sort -u); do
+ if is_approved_email "$email"; then
+ continue
+ fi
+
+ if ! grep -v '^[[:space:]]*#' .mailmap | grep -qF -- "<${email}>"; then
+ echo "Email <${email}> is not in .mailmap"
+ missing=1
+ fi
+done
+
+if [ "$missing" -ne 0 ]; then
+ echo "All authors and committers must be listed in .mailmap"
+ exit 1
+fi
diff --git a/tasks/signoff.py b/tasks/signoff.py
deleted file mode 100755
--- a/tasks/signoff.py
+++ /dev/null
@@ -1,208 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# fmt: off
-#MISE description="Verify contributor identities and CLA acceptance trailers"
-#MISE tools={"pipx"="1.16.7","python"="3.14.7"}
-# fmt: on
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import os
-import re
-import sys
-from collections.abc import Sequence
-from pathlib import Path
-
-sys.dont_write_bytecode = True
-
-from libs.common import TaskError, capture, task_main
-from shellous import sh
-
-TASK = "signoff"
-
-
-def configured_values(path: Path) -> set[str]:
- return {
- line for line in path.read_text().splitlines() if not re.match(r"^\s*#", line)
- }
-
-
-def cla_version(document: str) -> tuple[int, str]:
- versions = [
- line.removeprefix("Version ")
- for line in document.splitlines()
- if line.startswith("Version ")
- ]
- return len(versions), versions[0] if len(versions) == 1 else ""
-
-
-def check_identity(
- identity: str,
- role: str,
- short_sha: str,
- approved_emails: set[str],
- signoffs: set[str],
-) -> tuple[bool, bool]:
- match = re.fullmatch(r".*<([^<>]*)>", identity)
- if match is None or not match.group(1):
- print(f"Commit {short_sha} has an invalid {role} identity: {identity}")
- return True, False
- if match.group(1) in approved_emails:
- return False, False
- if identity not in signoffs:
- print(
- f"Commit {short_sha} {role} {identity} is missing a matching Signed-off-by"
- )
- return True, True
- return False, True
-
-
-async def git(*arguments: str) -> str:
- return await capture("git", arguments)
-
-
-async def main(args: Sequence[str]) -> None:
- argparse.ArgumentParser(prog="mise run signoff --").parse_args(args)
- root = Path((await git("rev-parse", "--show-toplevel")).strip())
- os.chdir(root)
-
- task_directory = Path(__file__).resolve().parent
- approved_emails_file = task_directory.parent / "config/signoff-approved-emails"
- unsupported_commits_file = task_directory.parent / "config/cla-unsupported-commits"
-
- cla_file = Path("CLA.md")
- if not cla_file.is_file():
- raise TaskError("CLA.md is missing")
- version_count, head_version = cla_version(cla_file.read_text())
- if version_count != 1:
- raise TaskError("CLA.md must declare exactly one version")
- if not head_version:
- raise TaskError("CLA.md declares an empty version")
- if not approved_emails_file.is_file():
- raise TaskError(
- f"Approved email configuration is missing: {approved_emails_file}"
- )
- if not unsupported_commits_file.is_file():
- raise TaskError(
- f"Unsupported commit configuration is missing: {unsupported_commits_file}"
- )
-
- approved_emails = configured_values(approved_emails_file)
- unsupported_commits = configured_values(unsupported_commits_file)
- bad = False
-
- commits = (await git("log", "--no-merges", "--format=%H")).splitlines()
- for sha in commits:
- short_sha = sha[:8]
- signoffs = set(
- (
- await git(
- "show",
- "-s",
- "--format=%(trailers:key=Signed-off-by,valueonly)",
- sha,
- )
- ).splitlines()
- )
- requires_cla = False
-
- author = (await git("show", "-s", "--format=%an <%ae>", sha)).rstrip("\n")
- invalid, required = check_identity(
- author, "author", short_sha, approved_emails, signoffs
- )
- bad |= invalid
- requires_cla |= required
-
- coauthor_output = (
- await git(
- "show",
- "-s",
- "--format=%(trailers:key=Co-authored-by,valueonly)",
- sha,
- )
- ).rstrip("\n")
- for coauthor in coauthor_output.splitlines() if coauthor_output else ():
- invalid, required = check_identity(
- coauthor, "co-author", short_sha, approved_emails, signoffs
- )
- bad |= invalid
- requires_cla |= required
-
- if requires_cla and sha not in unsupported_commits:
- result = await capture.result("git", "show", f"{sha}:CLA.md").stderr(
- sh.DEVNULL
- )
- document = result.output if result.exit_code == 0 else ""
- expected_count, expected_version = cla_version(document)
- if expected_count != 1:
- print(
- f"Commit {short_sha} does not contain a CLA.md with exactly one version"
- )
- bad = True
- continue
- if not expected_version:
- print(f"Commit {short_sha} contains an empty CLA version")
- bad = True
- continue
-
- commit_version = (
- await git(
- "show",
- "-s",
- "--format=%(trailers:key=CLA-Version,valueonly)",
- sha,
- )
- ).rstrip("\n")
- if commit_version != expected_version:
- if not commit_version:
- print(
- f"Commit {short_sha} is missing CLA-Version: {expected_version}"
- )
- else:
- print(
- f"Commit {short_sha} has invalid CLA-Version: {commit_version}"
- )
- print(f"Expected CLA-Version: {expected_version}")
- bad = True
-
- if bad:
- print(
- "Every non-approved author and co-author must accept the CLA in their commit"
- )
- print("Required trailers:")
- print(" CLA-Version: <version from CLA.md>")
- print(" Signed-off-by: Name <email>")
- print("See CLA.md")
- raise SystemExit(1)
-
- mailmap = Path(".mailmap")
- if not mailmap.is_file():
- raise TaskError("Contributor registry .mailmap is missing")
- mailmap_lines = [
- line
- for line in mailmap.read_text().splitlines()
- if not re.match(r"^\s*#", line)
- ]
- emails = sorted(
- set((await git("log", "--no-merges", "--format=%ae%n%ce")).splitlines())
- )
- missing = False
- for email in emails:
- if email in approved_emails:
- continue
- if not any(f"<{email}>" in line for line in mailmap_lines):
- print(f"Email <{email}> is not in .mailmap")
- missing = True
- if missing:
- print("All authors and committers must be listed in .mailmap")
- raise SystemExit(1)
-
-
-if __name__ == "__main__":
- task_main(TASK, main, sys.argv[1:])
diff --git a/tests/fakes/boto3.py b/tests/fakes/boto3.py
deleted file mode 100644
--- a/tests/fakes/boto3.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-
-from __future__ import annotations
-
-import hashlib
-import io
-import os
-import shutil
-from pathlib import Path
-
-from botocore.exceptions import ClientError
-
-
-def error(code: str, operation: str) -> ClientError:
- return ClientError({"Error": {"Code": code, "Message": code}}, operation)
-
-
-class Paginator:
- def __init__(self, client):
- self.client = client
-
- def paginate(self, *, Bucket: str, Prefix: str):
- root = self.client.root / Bucket
- contents = []
- if root.is_dir():
- for path in root.rglob("*"):
- if path.is_file():
- key = path.relative_to(root).as_posix()
- if key.startswith(Prefix):
- contents.append({"Key": key, "Size": path.stat().st_size})
- yield {"Contents": contents}
-
-
-class Client:
- def __init__(self):
- self.root = Path(os.environ["PUBLISH_REMOTE"])
- self.metadata: dict[tuple[str, str], dict[str, str]] = {}
- self.log = os.environ.get("PUBLISH_TEST_LOG")
-
- def record(self, operation: str, key: str) -> None:
- if self.log:
- with Path(self.log).open("a") as stream:
- stream.write(f"s3 {operation} {key}\n")
-
- def path(self, bucket: str, key: str) -> Path:
- path = self.root / bucket / key
- path.resolve().relative_to(self.root.resolve())
- return path
-
- def get_paginator(self, name: str):
- assert name == "list_objects_v2"
- return Paginator(self)
-
- def download_file(self, bucket: str, key: str, destination: str) -> None:
- source = self.path(bucket, key)
- if not source.is_file():
- raise error("NoSuchKey", "DownloadFile")
- Path(destination).parent.mkdir(parents=True, exist_ok=True)
- shutil.copy2(source, destination)
- self.record("download", key)
-
- def upload_file(self, source: str, bucket: str, key: str) -> None:
- destination = self.path(bucket, key)
- destination.parent.mkdir(parents=True, exist_ok=True)
- shutil.copy2(source, destination)
- self.record("upload", key)
-
- def put_object(
- self,
- *,
- Bucket,
- Key,
- Body,
- Metadata=None,
- IfNoneMatch=None,
- IfMatch=None,
- ):
- destination = self.path(Bucket, Key)
- if IfNoneMatch == "*" and destination.exists():
- raise error("PreconditionFailed", "PutObject")
- if IfMatch and self.etag(destination) != IfMatch:
- raise error("PreconditionFailed", "PutObject")
- destination.parent.mkdir(parents=True, exist_ok=True)
- data = Body.read() if hasattr(Body, "read") else Body
- destination.write_bytes(data)
- self.metadata[(Bucket, Key)] = Metadata or {}
- self.record("immutable", Key)
- return {"ETag": self.etag(destination)}
-
- def head_object(self, *, Bucket, Key):
- path = self.path(Bucket, Key)
- if not path.exists():
- raise error("NoSuchKey", "HeadObject")
- return {"Metadata": self.metadata.get((Bucket, Key), {})}
-
- @staticmethod
- def etag(path: Path) -> str | None:
- if not path.exists():
- return None
- return f'"{hashlib.md5(path.read_bytes(), usedforsecurity=False).hexdigest()}"'
-
- def get_object(self, *, Bucket, Key):
- path = self.path(Bucket, Key)
- return {"Body": io.BytesIO(path.read_bytes()), "ETag": self.etag(path)}
-
- def delete_object(self, *, Bucket, Key, IfMatch):
- path = self.path(Bucket, Key)
- if self.etag(path) != IfMatch:
- raise error("PreconditionFailed", "DeleteObject")
- path.unlink()
- self.record("delete", Key)
- return {}
-
- def delete_objects(self, *, Bucket, Delete):
- for item in Delete["Objects"]:
- self.path(Bucket, item["Key"]).unlink(missing_ok=True)
- self.record("delete", item["Key"])
- return {}
-
-
-def client(name: str, **kwargs):
- assert name == "s3"
- assert kwargs["endpoint_url"]
- assert kwargs["aws_access_key_id"]
- assert kwargs["aws_secret_access_key"]
- return Client()
diff --git a/tests/licenses-json.py b/tests/licenses-json.py
deleted file mode 100755
--- a/tests/licenses-json.py
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import importlib.util
-import json
-import sys
-from pathlib import Path
-from types import ModuleType
-
-
-def load_task() -> ModuleType:
- path = Path(__file__).parents[1] / "tasks/licenses-json.py"
- sys.path.insert(0, str(path.parent))
- spec = importlib.util.spec_from_file_location("licenses_json", path)
- assert spec is not None and spec.loader is not None
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module
-
-
-def main() -> None:
- task = load_task()
-
- assert task.license_requirements("MIT/Apache-2.0") == ["MIT", "Apache-2.0"]
- assert task.license_requirements(
- "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT"
- ) == ["Apache-2.0 WITH LLVM-exception", "Apache-2.0", "MIT"]
-
- config = task.cargo_about_config(
- {
- "packages": [
- {"name": "dual", "license": "MIT OR Apache-2.0"},
- {"name": "dual", "license": "MIT/Apache-2.0"},
- {"name": "private", "license": None},
- ]
- }
- )
- assert "accepted = []\nprivate = { ignore = true }" in config
- assert '["dual"]\naccepted = ["MIT", "Apache-2.0"]' in config
- assert '["private"]' not in config
-
- output = json.loads(
- task.normalized_output(
- {
- "overview": [],
- "licenses": [
- {
- "name": "MIT License",
- "id": "MIT",
- "first_of_kind": True,
- "source_path": "/tmp/LICENSE",
- "text": "MIT text",
- "used_by": [
- {
- "crate": {
- "name": "long-name",
- "version": "2.0.0",
- "repository": None,
- "manifest_path": "/tmp/Cargo.toml",
- },
- "path": None,
- },
- {
- "crate": {
- "name": "short",
- "version": "1.0.0",
- "repository": "https://example.invalid/short",
- },
- "path": None,
- },
- ],
- },
- {
- "name": "Apache License 2.0",
- "id": "Apache-2.0",
- "first_of_kind": False,
- "text": "Apache text",
- "used_by": [
- {
- "crate": {
- "name": "dependency",
- "version": "3.0.0",
- "repository": None,
- }
- }
- ],
- },
- ],
- "crates": [{"package": {"manifest_path": "/tmp/Cargo.toml"}}],
- }
- )
- )
-
- assert output["overview"] == [
- {"count": 1, "name": "Apache License 2.0", "id": "Apache-2.0"},
- {"count": 2, "name": "MIT License", "id": "MIT"},
- ]
- assert [license_["id"] for license_ in output["licenses"]] == [
- "Apache-2.0",
- "MIT",
- ]
- assert all(license_["first_of_kind"] for license_ in output["licenses"])
- assert [usage["crate"]["name"] for usage in output["licenses"][1]["used_by"]] == [
- "short",
- "long-name",
- ]
- assert "source_path" not in output["licenses"][1]
- assert "manifest_path" not in output["licenses"][1]["used_by"][0]["crate"]
- assert "crates" not in output
-
-
-if __name__ == "__main__":
- main()
diff --git a/tests/oci-tasks.py b/tests/oci-tasks.py
deleted file mode 100755
--- a/tests/oci-tasks.py
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import asyncio
-import os
-import sys
-import tempfile
-from collections.abc import Sequence
-from pathlib import Path
-
-from shellous import sh
-
-run = sh.stdout(sh.INHERIT).stderr(sh.INHERIT)
-
-
-def executable(path: Path, source: str) -> None:
- path.write_text("#!/usr/bin/env python3\n" + source)
- path.chmod(0o755)
-
-
-async def main(args: Sequence[str]) -> None:
- if args:
- raise SystemExit(f"unexpected arguments: {' '.join(args)}")
- root = Path(__file__).resolve().parent.parent
- with tempfile.TemporaryDirectory() as directory:
- work = Path(directory)
- binary = work / "bin"
- project = work / "project"
- chart = project / "chart"
- (chart / "templates").mkdir(parents=True)
- (project / "Dockerfile").touch()
- (chart / "Chart.yaml").write_text(
- "apiVersion: v2\nname: fixture\nversion: 0.0.0\n"
- )
- (chart / "values.yaml").write_text("image: fixture\n")
- (chart / "templates/configmap.yaml").write_text(
- "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: fixture\n"
- )
- binary.mkdir()
- executable(
- binary / "docker",
- """import os
-import sys
-from pathlib import Path
-Path(os.environ["OCI_TEST_DOCKER_LOG"]).write_text("\\n".join(sys.argv[1:]) + "\\n")
-""",
- )
- executable(
- binary / "helm",
- """import os
-import sys
-from pathlib import Path
-arguments = sys.argv[1:]
-log = Path(os.environ["OCI_TEST_HELM_LOG"])
-with log.open("a") as stream:
- stream.write("\\n".join(arguments) + "\\n")
-if arguments and arguments[0] == "package":
- destination = Path(arguments[arguments.index("--destination") + 1])
- version = arguments[arguments.index("--version") + 1]
- (destination / f"fixture-{version}.tgz").touch()
-""",
- )
-
- docker_log = work / "docker.log"
- helm_log = work / "helm.log"
- environment = dict(os.environ)
- environment.update(
- PATH=f"{binary}:{environment['PATH']}",
- OCI_TEST_DOCKER_LOG=str(docker_log),
- OCI_TEST_HELM_LOG=str(helm_log),
- )
- command = run.set(env=environment, inherit_env=False)
-
- await command(
- root / "tasks/container.py",
- "--context",
- project,
- "--platform",
- "linux/amd64",
- "--target",
- "site",
- "--build-arg",
- "APP=site",
- "--build-arg",
- "TITLE=hello world",
- "--label",
- "org.example.title=Example site",
- "--tag",
- "ghcr.io/example/site:sha-abc",
- "--tag",
- "ghcr.io/example/site:latest",
- "--cache-scope",
- "site",
- "--provenance",
- "false",
- "--sbom",
- "false",
- "--push",
- )
- docker_arguments = docker_log.read_text().splitlines()
- for expected in (
- "buildx",
- "--target",
- "site",
- "TITLE=hello world",
- "org.example.title=Example site",
- "ghcr.io/example/site:sha-abc",
- "ghcr.io/example/site:latest",
- "type=gha,mode=max,scope=site",
- "--push",
- ):
- assert expected in docker_arguments
-
- await command(
- root / "tasks/chart.py",
- "--chart",
- chart,
- "--version",
- "1.2.3",
- "--app-version",
- "sha-abc",
- "--output",
- work / "output",
- "--push",
- "oci://ghcr.io/example/charts",
- )
- helm_arguments = helm_log.read_text().splitlines()
- for expected in (
- "lint",
- "package",
- "push",
- "oci://ghcr.io/example/charts",
- ):
- assert expected in helm_arguments
-
- print("oci tasks: ok")
-
-
-if __name__ == "__main__":
- asyncio.run(main(sys.argv[1:]))
diff --git a/tests/package-integration.py b/tests/package-integration.py
deleted file mode 100755
--- a/tests/package-integration.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import asyncio
-import sys
-import tempfile
-from collections.abc import Sequence
-from pathlib import Path
-
-from shellous import sh
-
-run = sh.stdout(sh.INHERIT).stderr(sh.INHERIT)
-capture = sh.stderr(sh.INHERIT)
-
-
-async def main(args: Sequence[str]) -> None:
- if args:
- raise SystemExit(f"unexpected arguments: {' '.join(args)}")
- root = Path(__file__).resolve().parent.parent
- with tempfile.TemporaryDirectory() as directory:
- work = Path(directory)
- stage = work / "stage"
- stage.mkdir()
- (stage / "package-contract").write_text("package contract\n")
- config = work / "nfpm.yaml"
- config.write_text(
- """name: package-contract
-arch: ${ARCH}
-version: ${VERSION}
-platform: linux
-maintainer: Dimidium Labs <me@govorov.online>
-description: Shared package task integration fixture
-license: 0BSD
-"""
- )
- output = work / "out"
- await run(
- "mise",
- "--cd",
- root,
- "run",
- "package",
- "--",
- "--config",
- config,
- "--version",
- "1.2.3~nightly.42",
- "--arch",
- "amd64",
- "--output",
- output,
- "--archive-root",
- stage,
- "--archive-name",
- "package-contract-linux-amd64",
- "deb",
- "rpm",
- "apk",
- "tar.gz",
- "zip",
- )
-
- assert len(list(output.glob("*.deb"))) == 1
- assert len(list(output.glob("*.rpm"))) == 1
- assert len(list(output.glob("*.apk"))) == 1
- tarball = output / "package-contract-linux-amd64.tar.gz"
- zipfile = output / "package-contract-linux-amd64.zip"
- assert tarball.is_file()
- assert zipfile.is_file()
-
- package = next(output.glob("*.deb"))
- assert (await capture("dpkg-deb", "-f", package, "Package")).strip() == (
- "package-contract"
- )
- assert (await capture("dpkg-deb", "-f", package, "Version")).strip() == (
- "1.2.3~nightly.42"
- )
- assert (await capture("dpkg-deb", "-f", package, "Architecture")).strip() == (
- "amd64"
- )
- assert "./package-contract" in await capture("tar", "-tzf", tarball)
- assert "package-contract" in await capture("unzip", "-l", zipfile)
-
-
-if __name__ == "__main__":
- asyncio.run(main(sys.argv[1:]))
diff --git a/tests/package.py b/tests/package.py
deleted file mode 100755
--- a/tests/package.py
+++ /dev/null
@@ -1,270 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import asyncio
-import os
-import sys
-import tempfile
-from collections.abc import Sequence
-from pathlib import Path
-
-from shellous import sh
-
-run = sh.stdout(sh.INHERIT).stderr(sh.INHERIT)
-capture = sh.stderr(sh.INHERIT)
-
-
-def executable(path: Path, source: str) -> None:
- path.write_text("#!/usr/bin/env python3\n" + source)
- path.chmod(0o755)
-
-
-async def main(args: Sequence[str]) -> None:
- if args:
- raise SystemExit(f"unexpected arguments: {' '.join(args)}")
- root = Path(__file__).resolve().parent.parent
- with tempfile.TemporaryDirectory() as directory:
- work = Path(directory)
- binary = work / "bin"
- stage = work / "stage"
- output = work / "out"
- binary.mkdir()
- (stage / "sub").mkdir(parents=True)
- output.mkdir()
- (stage / "tool").write_text("payload\n")
- (stage / "sub/file").write_text("nested\n")
- outside = work / "outside"
- outside.write_text("outside\n")
- (stage / "outside-link").symlink_to(outside)
-
- executable(
- binary / "nfpm",
- """import os
-import sys
-from pathlib import Path
-secrets = (
- "GPG_PRIVATE_KEY", "APK_PRIVATE_KEY", "SIGNING_PRIVATE_KEY",
- "NFPM_PASSPHRASE",
-)
-if any(os.environ.get(name) for name in secrets):
- print("raw private key leaked to nFPM", file=sys.stderr)
- raise SystemExit(1)
-arguments = sys.argv[1:]
-config = Path(arguments[arguments.index("--config") + 1])
-packager = arguments[arguments.index("--packager") + 1]
-target = Path(arguments[arguments.index("--target") + 1])
-with Path(os.environ["PACKAGE_TEST_LOG"]).open("a") as stream:
- stream.write(f"ARCH={os.environ['ARCH']}\\n")
- stream.write(f"VERSION={os.environ['VERSION']}\\n")
- stream.write(f"GPG_KEY_ID={os.environ.get('GPG_KEY_ID', '')}\\n")
- stream.write(f"{packager}\\n")
- for line in config.read_text().splitlines():
- if line.startswith("key_name:"):
- stream.write(f"{line}\\n")
-(target / f"test.{packager}").touch()
-""",
- )
- executable(
- binary / "gpg",
- """import os
-import sys
-from pathlib import Path
-with Path(os.environ["PACKAGE_TEST_LOG"]).open("a") as stream:
- stream.write("gpg\\n")
-arguments = sys.argv[1:]
-if "-o" in arguments:
- Path(arguments[arguments.index("-o") + 1]).write_text("signature")
-""",
- )
- for name in ("debsigs", "rpmsign"):
- executable(
- binary / name,
- """import os
-import sys
-from pathlib import Path
-with Path(os.environ["PACKAGE_TEST_LOG"]).open("a") as stream:
- stream.write(f"{Path(sys.argv[0]).name}\\n")
-""",
- )
- executable(
- binary / "openssl",
- """import sys
-from pathlib import Path
-arguments = sys.argv[1:]
-Path(arguments[arguments.index("-out") + 1]).write_text("public key\\n")
-""",
- )
-
- config = work / "nfpm.yaml"
- config.write_text("name: test\n")
- log = work / "package.log"
- environment = dict(os.environ)
- environment.update(
- PATH=f"{binary}:{environment['PATH']}",
- PACKAGE_TEST_LOG=str(log),
- )
- command = run.set(env=environment, inherit_env=False)
- package = root / "tasks/package.py"
-
- await command(
- package,
- "--output",
- output,
- "--archive-root",
- stage,
- "--archive-name",
- "test-linux-amd64",
- "tar.gz",
- "zip",
- )
- tarball = output / "test-linux-amd64.tar.gz"
- zipfile = output / "test-linux-amd64.zip"
- assert tarball.is_file()
- assert zipfile.is_file()
- assert "./tool" in await capture("tar", "-tzf", tarball)
- assert "sub/file" in await capture("unzip", "-l", zipfile)
- assert (await capture("unzip", "-p", zipfile, "outside-link")) == str(outside)
-
- await command(
- package,
- "--config",
- config,
- "--version",
- "1.2.3~nightly.42",
- "--arch",
- "arm64",
- "--output",
- output,
- "deb",
- "rpm",
- )
- assert (output / "test.deb").is_file()
- assert (output / "test.rpm").is_file()
- assert "ARCH=arm64" in log.read_text().splitlines()
- assert "VERSION=1.2.3~nightly.42" in log.read_text().splitlines()
-
- signing_environment = dict(environment)
- signing_environment.update(
- GPG_PRIVATE_KEY="private",
- GPG_PASSPHRASE="passphrase",
- GPG_KEY_ID="0123456789ABCDEF0123456789ABCDEF01234567",
- APK_PRIVATE_KEY="apk-private",
- )
- await run.set(env=signing_environment, inherit_env=False)(
- package,
- "--config",
- config,
- "--version",
- "1.2.3",
- "--arch",
- "amd64",
- "--output",
- output,
- "--apk-public-key",
- "test.rsa.pub",
- "deb",
- "rpm",
- "apk",
- )
- assert (output / "test.apk").is_file()
- assert (output / "test.rsa.pub").is_file()
- log_lines = log.read_text().splitlines()
- for expected in (
- "gpg",
- "debsigs",
- "rpmsign",
- "GPG_KEY_ID=89ABCDEF01234567",
- ):
- assert expected in log_lines
-
- apk_environment = dict(environment, APK_PRIVATE_KEY="apk-private")
- await run.set(env=apk_environment, inherit_env=False)(
- package,
- "--config",
- config,
- "--version",
- "1.2.3",
- "--arch",
- "amd64",
- "--output",
- output,
- "apk",
- )
-
- versioned_config = work / "versioned.yaml"
- versioned_config.write_text("key_name: packages.${PACKAGE_KEY_VERSION}\n")
- result = await run.result.set(env=apk_environment, inherit_env=False)(
- package,
- "--config",
- versioned_config,
- "--version",
- "1.2.3",
- "--arch",
- "amd64",
- "--output",
- output,
- "apk",
- ).stderr(sh.DEVNULL)
- assert result.exit_code != 0
-
- versioned_environment = dict(apk_environment, PACKAGE_KEY_VERSION="0001")
- await run.set(env=versioned_environment, inherit_env=False)(
- package,
- "--config",
- versioned_config,
- "--version",
- "1.2.3",
- "--arch",
- "amd64",
- "--output",
- output,
- "apk",
- )
- assert "key_name: packages.0001" in log.read_text().splitlines()
-
- invalid_commands = (
- (package, "--output", output, "deb"),
- (
- package,
- "--output",
- output,
- "--archive-root",
- stage,
- "--archive-name",
- "../escape",
- "zip",
- ),
- )
- for arguments in invalid_commands:
- result = await command.result(*arguments).stderr(sh.DEVNULL)
- assert result.exit_code != 0
-
- external_key_environment = dict(
- environment, APK_SIGNING_KEY=str(work / "apk.rsa")
- )
- result = await run.result.set(env=external_key_environment, inherit_env=False)(
- package,
- "--config",
- config,
- "--version",
- "1.2.3",
- "--arch",
- "amd64",
- "--output",
- output,
- "--apk-public-key",
- "../escape",
- "apk",
- ).stderr(sh.DEVNULL)
- assert result.exit_code != 0
-
-
-if __name__ == "__main__":
- asyncio.run(main(sys.argv[1:]))
diff --git a/tests/publish-integration.py b/tests/publish-integration.py
deleted file mode 100755
--- a/tests/publish-integration.py
+++ /dev/null
@@ -1,346 +0,0 @@
-#!/usr/bin/env -S pipx run --backend pip
-# SPDX-FileCopyrightText: 2026 Nikolay Govorov
-# SPDX-License-Identifier: Apache-2.0
-# /// script
-# requires-python = ">=3.11"
-# dependencies = ["shellous==0.42.0"]
-# ///
-
-from __future__ import annotations
-
-import asyncio
-import hashlib
-import os
-import re
-import shutil
-import sys
-import tempfile
-import urllib.request
-from collections.abc import Sequence
-from pathlib import Path
-
-from shellous import Result, sh
-
-run = sh.stdout(sh.INHERIT).stderr(sh.INHERIT)
-capture = sh.stderr(sh.INHERIT)
-APK_TOOLS_URL = (
- "https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/"
- "apk-tools-static-2.14.10-r0.apk"
-)
-APK_TOOLS_SHA256 = "c86e3822764e5fe19f41ce2e13553e48cac1ea4e74f858338e8d44bf0b616b61"
-
-
-async def main(args: Sequence[str]) -> None:
- if args:
- raise SystemExit(f"unexpected arguments: {' '.join(args)}")
- root = Path(__file__).resolve().parent.parent
- with tempfile.TemporaryDirectory() as directory:
- work = Path(directory)
- remote = work / "remote/integration"
- binary = work / "bin"
- package_input = work / "input"
- fixture = work / "fixture"
- for path in (remote, binary, package_input, fixture):
- path.mkdir(parents=True)
-
- payload = fixture / "publish-contract"
- payload.write_text("publish integration fixture\n")
- config = fixture / "nfpm.yaml"
- config.write_text(
- f"""name: publish-contract
-arch: ${{ARCH}}
-version: ${{VERSION}}
-platform: linux
-maintainer: Dimidium Labs <me@govorov.online>
-description: Shared publish task integration fixture
-license: 0BSD
-contents:
- - src: {payload}
- dst: /usr/local/bin/publish-contract
-deb:
- signature:
- method: debsign
- key_id: ${{GPG_KEY_ID}}
- key_file: ${{SIGNING_PRIVATE_KEY}}
-rpm:
- signature:
- key_id: ${{GPG_KEY_ID}}
- key_file: ${{SIGNING_PRIVATE_KEY}}
-apk:
- signature:
- key_file: ${{APK_SIGNING_KEY}}
- key_name: packages.${{PACKAGE_KEY_VERSION}}
-"""
- )
-
- source_gnupg = work / "source-gnupg"
- source_gnupg.mkdir(mode=0o700)
- source_environment = dict(os.environ, GNUPGHOME=str(source_gnupg))
- gpg = run.set(env=source_environment, inherit_env=False)
- gpg_capture = capture.set(env=source_environment, inherit_env=False)
- await gpg(
- "gpg",
- "--batch",
- "--pinentry-mode",
- "loopback",
- "--passphrase",
- "integration-pass",
- "--quick-generate-key",
- "Publish Integration <publish@example.invalid>",
- "rsa2048",
- "sign",
- "1d",
- ).stdout(sh.DEVNULL)
- key_listing = await gpg_capture(
- "gpg", "--batch", "--with-colons", "--list-secret-keys"
- )
- fingerprint = next(
- fields[9]
- for line in key_listing.splitlines()
- if (fields := line.split(":"))[0] == "fpr"
- )
- private_gpg = work / "private.gpg"
- await gpg(
- "gpg",
- "--batch",
- "--pinentry-mode",
- "loopback",
- "--passphrase",
- "integration-pass",
- "--armor",
- "--export-secret-keys",
- fingerprint,
- ).stdout(private_gpg)
- private_rsa = work / "private.rsa"
- await (
- run("openssl", "genrsa", "-out", private_rsa, "2048")
- .stdout(sh.DEVNULL)
- .stderr(sh.DEVNULL)
- )
-
- key_version = "0001"
- keys = remote / "keys"
- keys.mkdir()
- public_gpg = remote / "packages.gpg"
- await gpg("gpg", "--batch", "--armor", "--export", fingerprint).stdout(
- public_gpg
- )
- shutil.copy2(public_gpg, keys / f"packages.{key_version}.gpg")
- await (
- run(
- "openssl",
- "rsa",
- "-in",
- private_rsa,
- "-pubout",
- "-out",
- keys / f"packages.{key_version}.rsa.pub",
- )
- .stdout(sh.DEVNULL)
- .stderr(sh.DEVNULL)
- )
- gpg_private_key = private_gpg.read_text()
- apk_private_key = private_rsa.read_text()
-
- async def package_version(version: str) -> None:
- environment = dict(os.environ)
- environment.update(
- GPG_PRIVATE_KEY=gpg_private_key,
- GPG_PASSPHRASE="integration-pass",
- GPG_KEY_ID=fingerprint,
- APK_PRIVATE_KEY=apk_private_key,
- PACKAGE_KEY_VERSION=key_version,
- )
- await run.set(env=environment, inherit_env=False)(
- "mise",
- "--cd",
- root,
- "run",
- "package",
- "--",
- "--config",
- config,
- "--version",
- version,
- "--arch",
- "amd64",
- "--output",
- package_input,
- "deb",
- "rpm",
- "apk",
- )
-
- publish_environment = dict(os.environ)
- publish_environment.update(
- PUBLISH_REMOTE=str(work / "remote"),
- PUBLISH_TEST_LOG=str(work / "s3.log"),
- PUBLISH_TEST_PYTHONPATH=str(root / "tests/fakes"),
- PATH=f"{binary}:{os.environ['PATH']}",
- S3_BUCKET="integration",
- S3_ENDPOINT="https://example.invalid",
- S3_PUBLIC_URL="https://pkg.dimidiumlabs.io",
- S3_ACCESS_KEY_ID="integration",
- S3_SECRET_ACCESS_KEY="integration",
- GPG_PRIVATE_KEY=gpg_private_key,
- GPG_PASSPHRASE="integration-pass",
- GPG_KEY_ID=fingerprint,
- APK_PRIVATE_KEY=apk_private_key,
- PACKAGE_KEY_VERSION=key_version,
- )
- publish_command = run.result.set(env=publish_environment, inherit_env=False)
-
- async def publish(*, quiet: bool = False) -> Result:
- command = publish_command(
- root / "tasks/publish.py",
- "--service",
- "publish-contract",
- "--channel",
- "nightly",
- "--input",
- package_input,
- "deb",
- "rpm",
- "apk",
- )
- if quiet:
- command = command.stderr(sh.DEVNULL)
- return await command
-
- await package_version("1.2.3~nightly.42")
- assert (await publish()).exit_code == 0
-
- apt_root = remote / "publish-contract/apt"
- rpm_root = remote / "publish-contract/rpm/nightly"
- apk_root = remote / "publish-contract/apk/nightly/x86_64"
- assert public_gpg.is_file()
- assert (keys / f"packages.{key_version}.gpg").is_file()
- assert (keys / f"packages.{key_version}.rsa.pub").is_file()
- assert len(list((apt_root / "pool/nightly").glob("*.deb"))) == 1
- assert len(list(rpm_root.glob("*.rpm"))) == 1
- assert len(list(apk_root.glob("*.apk"))) == 1
- packages_file = apt_root / "dists/nightly/main/binary-amd64/Packages"
- assert "Package: publish-contract" in packages_file.read_text().splitlines()
- repository_file = rpm_root / "publish-contract-nightly.repo"
- assert (
- "baseurl=https://pkg.dimidiumlabs.io/publish-contract/rpm/nightly/"
- in repository_file.read_text().splitlines()
- )
-
- keyring = work / "packages.gpg"
- await run("gpg", "--batch", "--dearmor", "-o", keyring, public_gpg)
- await run(
- "gpgv",
- "--keyring",
- keyring,
- apt_root / "dists/nightly/InRelease",
- ).stdout(sh.DEVNULL)
- await run(
- "gpgv",
- "--keyring",
- keyring,
- rpm_root / "repodata/repomd.xml.asc",
- rpm_root / "repodata/repomd.xml",
- ).stdout(sh.DEVNULL)
-
- apk_tool = next(
- (
- path
- for base in (
- Path.home() / ".cache/mise",
- Path.home() / ".local/share/mise",
- )
- if base.is_dir()
- for path in base.rglob("apk.static")
- if path.is_file()
- ),
- None,
- )
- if apk_tool is None:
- archive = work / "apk-tools-static.apk"
- await asyncio.to_thread(urllib.request.urlretrieve, APK_TOOLS_URL, archive)
- assert hashlib.sha256(archive.read_bytes()).hexdigest() == APK_TOOLS_SHA256
- apk_directory = work / "apk-tools"
- apk_directory.mkdir()
- await run(
- "tar",
- "-xzf",
- archive,
- "-C",
- apk_directory,
- "sbin/apk.static",
- ).stderr(sh.DEVNULL)
- apk_tool = apk_directory / "sbin/apk.static"
- apk_keys = work / "apk-keys"
- apk_keys.mkdir()
- shutil.copy2(keys / f"packages.{key_version}.rsa.pub", apk_keys)
- await run(
- apk_tool,
- "verify",
- "--keys-dir",
- apk_keys,
- apk_root / "APKINDEX.tar.gz",
- ).stdout(sh.DEVNULL)
-
- (apt_root / "dists/nightly/stale").write_text("stale")
- (rpm_root / "repodata/stale").write_text("stale")
- shutil.rmtree(package_input)
- package_input.mkdir()
- await package_version("1.2.3~nightly.43")
- assert (await publish()).exit_code == 0
- assert len(list((apt_root / "pool/nightly").glob("*.deb"))) == 2
- assert len(list(rpm_root.glob("*.rpm"))) == 2
- assert len(list(apk_root.glob("*.apk"))) == 2
- assert not (apt_root / "dists/nightly/stale").exists()
- assert not (rpm_root / "repodata/stale").exists()
- assert (
- packages_file.read_text().splitlines().count("Package: publish-contract")
- == 2
- )
- await run(
- apk_tool,
- "verify",
- "--keys-dir",
- apk_keys,
- apk_root / "APKINDEX.tar.gz",
- ).stdout(sh.DEVNULL)
-
- deb = next(package_input.glob("*.deb"))
- with deb.open("a") as stream:
- stream.write("\nchanged\n")
- assert (await publish(quiet=True)).exit_code != 0
- shutil.copy2(apt_root / "pool/nightly" / deb.name, deb)
- lock = remote / "publish-contract/_locks/nightly"
- lock.parent.mkdir(parents=True, exist_ok=True)
- lock.write_text('{"expires":9999999999}\n')
- assert (await publish(quiet=True)).exit_code != 0
- lock.unlink()
- (keys / f"packages.{key_version}.gpg").write_text("different key\n")
- assert (await publish(quiet=True)).exit_code != 0
-
- allowed_delete = re.compile(
- r"^publish-contract/(apt/dists|rpm/nightly/repodata|_locks/nightly)"
- )
- for line in (work / "s3.log").read_text().splitlines():
- fields = line.split(maxsplit=2)
- if len(fields) == 3 and fields[:2] == ["s3", "delete"]:
- assert allowed_delete.match(fields[2])
-
- invalid_environment = dict(
- os.environ, PUBLISH_TEST_PYTHONPATH=str(root / "tests/fakes")
- )
- result = await run.result.set(env=invalid_environment, inherit_env=False)(
- root / "tasks/publish.py",
- "--service",
- "../escape",
- "--channel",
- "nightly",
- "--input",
- package_input,
- "deb",
- ).stderr(sh.DEVNULL)
- assert result.exit_code != 0
-
-
-if __name__ == "__main__":
- asyncio.run(main(sys.argv[1:]))