diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index c34066d318..12f52289b6 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index f1b51debf1..ae2238ad2d 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: crowdin/github-action@v1 with: upload_sources: true diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index edceef0092..b21341a277 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -34,7 +34,7 @@ jobs: swagger: ${{ steps.changes.outputs.swagger }} yaml: ${{ steps.changes.outputs.yaml }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dorny/paths-filter@v3 id: changes with: diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 6f8991ed4e..f73772e934 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -16,8 +16,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -31,7 +31,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - run: uv python install 3.12 - uses: pnpm/action-setup@v4 @@ -47,7 +47,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - run: uv python install 3.12 - run: make deps-py @@ -58,7 +58,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -71,8 +71,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -83,8 +83,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -100,8 +100,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -115,8 +115,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -128,7 +128,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -144,8 +144,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -176,7 +176,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -189,8 +189,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index a7ad7ed5c3..21ec76b48e 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -38,8 +38,8 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -66,8 +66,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -124,8 +124,8 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -177,8 +177,8 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -217,8 +217,8 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index 89b32260ca..4f806e93bd 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -18,8 +18,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index 812819b599..d05483e56c 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -15,6 +15,6 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: sync-labels: true diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 3d652e4ad8..16ce0fd643 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -12,11 +12,11 @@ jobs: nightly-binary: runs-on: namespace-profile-gitea-release-binary steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -61,11 +61,11 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -103,11 +103,11 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index f4776a9ed8..c239ff392b 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -13,11 +13,11 @@ jobs: binary: runs-on: namespace-profile-gitea-release-binary steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -71,7 +71,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -112,7 +112,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index ad0820f31f..289b0e9d9c 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -17,11 +17,11 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -75,7 +75,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -118,7 +118,7 @@ jobs: docker-rootless: runs-on: namespace-profile-gitea-release-docker steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 9c19080e24..b105757683 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -1096,8 +1096,13 @@ }, { "name": "github.com/sorairolake/lzip-go", - "path": "github.com/sorairolake/lzip-go/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n---\n\nMIT License\n\nCopyright (c) 2024 Shun Sakai\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + "path": "github.com/sorairolake/lzip-go/LICENSE-APACHE", + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, + { + "name": "github.com/spf13/afero", + "path": "github.com/spf13/afero/LICENSE.txt", + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n" }, { "name": "github.com/ssor/bom", @@ -1225,8 +1230,8 @@ "licenseText": "Copyright (c) 2016-2024 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, { - "name": "go4.org", - "path": "go4.org/LICENSE", + "name": "go4.org/readerutil", + "path": "go4.org/readerutil/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" }, { diff --git a/eslint.config.ts b/eslint.config.ts index 678a49647c..02aacefca2 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -52,21 +52,16 @@ export default defineConfig([ }, plugins: { '@eslint-community/eslint-comments': comments, - // @ts-expect-error '@stylistic': stylistic, '@typescript-eslint': typescriptPlugin.plugin, 'array-func': arrayFunc, // @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/203 'import-x': importPlugin, 'no-use-extend-native': noUseExtendNative, - // @ts-expect-error regexp, - // @ts-expect-error sonarjs, - // @ts-expect-error unicorn, github, - // @ts-expect-error wc, }, settings: { @@ -595,6 +590,7 @@ export default defineConfig([ 'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars 'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define 'no-use-extend-native/no-use-extend-native': [2], + 'no-useless-assignment': [2], 'no-useless-backreference': [2], 'no-useless-call': [2], 'no-useless-catch': [2], @@ -900,7 +896,6 @@ export default defineConfig([ 'yoda': [2, 'never'], }, }, - // @ts-expect-error { ...playwright.configs['flat/recommended'], files: ['tests/e2e/**'], @@ -916,7 +911,6 @@ export default defineConfig([ }, }, extends: [ - // @ts-expect-error vue.configs['flat/recommended'], // @ts-expect-error vueScopedCss.configs['flat/recommended'], diff --git a/flake.lock b/flake.lock index 16a487ba13..5cb95c1aed 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755186698, - "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", + "lastModified": 1760038930, + "narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", + "rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index 64a7dcc708..cf4774801e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.25.1 +go 1.25.3 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: @@ -35,7 +35,7 @@ require ( github.com/bohde/codel v0.2.0 github.com/buildkite/terminal-to-html/v3 v3.16.8 github.com/caddyserver/certmagic v0.24.0 - github.com/charmbracelet/git-lfs-transfer v0.2.0 + github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 github.com/chi-middleware/proxy v1.1.1 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 github.com/djherbis/buffer v1.2.0 @@ -56,7 +56,7 @@ require ( github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.2 github.com/go-git/go-billy/v5 v5.6.2 - github.com/go-git/go-git/v5 v5.16.2 + github.com/go-git/go-git/v5 v5.16.3 github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.9.3 @@ -84,7 +84,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-sqlite3 v1.14.32 github.com/meilisearch/meilisearch-go v0.33.2 - github.com/mholt/archives v0.1.3 + github.com/mholt/archives v0.0.0-20251009205813-e30ac6010726 github.com/microcosm-cc/bluemonday v1.0.27 github.com/microsoft/go-mssqldb v1.9.3 github.com/minio/minio-go/v7 v7.0.95 @@ -116,13 +116,13 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 gitlab.com/gitlab-org/api/client-go v0.142.4 - golang.org/x/crypto v0.41.0 + golang.org/x/crypto v0.42.0 golang.org/x/image v0.30.0 - golang.org/x/net v0.43.0 + golang.org/x/net v0.44.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.17.0 - golang.org/x/sys v0.35.0 - golang.org/x/text v0.29.0 + golang.org/x/sys v0.37.0 + golang.org/x/text v0.30.0 google.golang.org/grpc v1.75.0 google.golang.org/protobuf v1.36.8 gopkg.in/ini.v1 v1.67.0 @@ -142,7 +142,7 @@ require ( github.com/DataDog/zstd v1.5.7 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect - github.com/STARRY-S/zip v0.2.1 // indirect + github.com/STARRY-S/zip v0.2.3 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect @@ -172,7 +172,7 @@ require ( github.com/blevesearch/zapx/v16 v16.2.4 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect - github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.1.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect @@ -233,14 +233,14 @@ require ( github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minlz v1.0.0 // indirect + github.com/minio/minlz v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nwaples/rardecode/v2 v2.1.0 // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.1.0 // indirect @@ -259,7 +259,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect - github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/sorairolake/lzip-go v0.3.8 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/tinylib/msgp v1.4.0 // indirect github.com/unknwon/com v1.0.1 // indirect @@ -278,9 +279,9 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect - golang.org/x/mod v0.27.0 // indirect + golang.org/x/mod v0.28.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -297,9 +298,6 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 -// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why -replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0 - replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 exclude github.com/gofrs/uuid v3.2.0+incompatible diff --git a/go.sum b/go.sum index 3e9d75c3b8..9acef3b977 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c= gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= -gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= -gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4= gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= @@ -93,8 +91,8 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06 github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I= github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0= github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= -github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= -github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= +github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R1QWahSeMJkjSkq6CYAZu1aIbYSpfJ4o= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= @@ -193,8 +191,8 @@ github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= -github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= -github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= +github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E= @@ -219,6 +217,8 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 h1:2d64+4Jek9vjYwhY93AjbleiVH+AeWvPwPmDi1mfKFQ= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21/go.mod h1:fNlYtCHWTRC8MofQERZkVUNUWaOvZeTBqHn/amSbKZI= github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ= github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= @@ -339,8 +339,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= -github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= +github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= @@ -572,8 +572,8 @@ github.com/meilisearch/meilisearch-go v0.33.2 h1:YgsQSLYhAkRN2ias6I1KNRTjdYCN5w2 github.com/meilisearch/meilisearch-go v0.33.2/go.mod h1:6eOPcQ+OAuwXvnONlfSgfgvr7TIAWM/6OdhcVHg8cF0= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= -github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= -github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU= +github.com/mholt/archives v0.0.0-20251009205813-e30ac6010726 h1:narluFTg20M5KBwKxedpFiSMkdjQRRNUlpY4uAsKMwk= +github.com/mholt/archives v0.0.0-20251009205813-e30ac6010726/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs= @@ -588,8 +588,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= -github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= -github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= +github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -610,8 +610,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0= github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= -github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= -github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -714,9 +714,11 @@ github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= -github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= +github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -729,6 +731,7 @@ github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -837,8 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -875,8 +878,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -905,8 +908,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -972,8 +975,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -984,8 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -999,8 +1002,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -1036,8 +1039,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/models/actions/run.go b/models/actions/run.go index f5ccba06c2..4da6958e2d 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -16,13 +16,13 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" - "github.com/nektos/act/pkg/jobparser" "xorm.io/builder" ) @@ -30,7 +30,7 @@ import ( type ActionRun struct { ID int64 Title string - RepoID int64 `xorm:"index unique(repo_index)"` + RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` WorkflowID string `xorm:"index"` // the name of workflow file @@ -49,6 +49,9 @@ type ActionRun struct { TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow Status Status `xorm:"index"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed + RawConcurrency string // raw concurrency + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 Started timeutil.TimeStamp Stopped timeutil.TimeStamp @@ -102,6 +105,15 @@ func (run *ActionRun) PrettyRef() string { return refName.ShortName() } +// RefTooltip return a tooltop of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName. +func (run *ActionRun) RefTooltip() string { + payload, err := run.GetPullRequestEventPayload() + if err == nil && payload != nil && payload.PullRequest != nil { + return payload.PullRequest.Title + } + return git.RefName(run.Ref).ShortName() +} + // LoadAttributes load Repo TriggerUser if not loaded func (run *ActionRun) LoadAttributes(ctx context.Context) error { if run == nil { @@ -181,7 +193,7 @@ func (run *ActionRun) IsSchedule() bool { return run.ScheduleID > 0 } -func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { +func UpdateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID). NoAutoTime(). SetExpr("num_action_runs", @@ -238,116 +250,62 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin return cancelledJobs, err } - // Iterate over each job and attempt to cancel it. - for _, job := range jobs { - // Skip jobs that are already in a terminal state (completed, cancelled, etc.). - status := job.Status - if status.IsDone() { - continue - } - - // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. - if job.TaskID == 0 { - job.Status = StatusCancelled - job.Stopped = timeutil.TimeStampNow() - - // Update the job's status and stopped time in the database. - n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") - if err != nil { - return cancelledJobs, err - } - - // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. - if n == 0 { - return cancelledJobs, errors.New("job has changed, try again") - } - - cancelledJobs = append(cancelledJobs, job) - // Continue with the next job. - continue - } - - // If the job has an associated task, try to stop the task, effectively cancelling the job. - if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { - return cancelledJobs, err - } - cancelledJobs = append(cancelledJobs, job) + cjs, err := CancelJobs(ctx, jobs) + if err != nil { + return cancelledJobs, err } + cancelledJobs = append(cancelledJobs, cjs...) } // Return nil to indicate successful cancellation of all running and waiting jobs. return cancelledJobs, nil } -// InsertRun inserts a run -// The title will be cut off at 255 characters if it's longer than 255 characters. -func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { - return db.WithTx(ctx, func(ctx context.Context) error { - index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) - if err != nil { - return err - } - run.Index = index - run.Title = util.EllipsisDisplayString(run.Title, 255) - - if err := db.Insert(ctx, run); err != nil { - return err +func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, error) { + cancelledJobs := make([]*ActionRunJob, 0, len(jobs)) + // Iterate over each job and attempt to cancel it. + for _, job := range jobs { + // Skip jobs that are already in a terminal state (completed, cancelled, etc.). + status := job.Status + if status.IsDone() { + continue } - if run.Repo == nil { - repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. + if job.TaskID == 0 { + job.Status = StatusCancelled + job.Stopped = timeutil.TimeStampNow() + + // Update the job's status and stopped time in the database. + n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") if err != nil { - return err + return cancelledJobs, err } - run.Repo = repo + + // If the update affected 0 rows, it means the job has changed in the meantime + if n == 0 { + log.Error("Failed to cancel job %d because it has changed", job.ID) + continue + } + + cancelledJobs = append(cancelledJobs, job) + // Continue with the next job. + continue } - if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { - return err + // If the job has an associated task, try to stop the task, effectively cancelling the job. + if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { + return cancelledJobs, err } + updatedJob, err := GetRunJobByID(ctx, job.ID) + if err != nil { + return cancelledJobs, fmt.Errorf("get job: %w", err) + } + cancelledJobs = append(cancelledJobs, updatedJob) + } - runJobs := make([]*ActionRunJob, 0, len(jobs)) - var hasWaiting bool - for _, v := range jobs { - id, job := v.Job() - needs := job.Needs() - if err := v.SetJob(id, job.EraseNeeds()); err != nil { - return err - } - payload, _ := v.Marshal() - status := StatusWaiting - if len(needs) > 0 || run.NeedApproval { - status = StatusBlocked - } else { - hasWaiting = true - } - job.Name = util.EllipsisDisplayString(job.Name, 255) - runJobs = append(runJobs, &ActionRunJob{ - RunID: run.ID, - RepoID: run.RepoID, - OwnerID: run.OwnerID, - CommitSHA: run.CommitSHA, - IsForkPullRequest: run.IsForkPullRequest, - Name: job.Name, - WorkflowPayload: payload, - JobID: id, - Needs: needs, - RunsOn: job.RunsOn(), - Status: status, - }) - } - if err := db.Insert(ctx, runJobs); err != nil { - return err - } - - // if there is a job in the waiting status, increase tasks version. - if hasWaiting { - if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { - return err - } - } - return nil - }) + // Return nil to indicate successful cancellation of all running and waiting jobs. + return cancelledJobs, nil } func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) { @@ -432,7 +390,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if err = run.LoadRepo(ctx); err != nil { return err } - if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { + if err := UpdateRepoRunsNumbers(ctx, run.Repo); err != nil { return err } } @@ -441,3 +399,59 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { } type ActionRunIndex db.ResourceIndex + +func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) { + runs, err := db.Find[ActionRun](ctx, &FindRunOptions{ + RepoID: repoID, + ConcurrencyGroup: concurrencyGroup, + Status: status, + }) + if err != nil { + return nil, nil, fmt.Errorf("find runs: %w", err) + } + + jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{ + RepoID: repoID, + ConcurrencyGroup: concurrencyGroup, + Statuses: status, + }) + if err != nil { + return nil, nil, fmt.Errorf("find jobs: %w", err) + } + + return runs, jobs, nil +} + +func CancelPreviousJobsByRunConcurrency(ctx context.Context, actionRun *ActionRun) ([]*ActionRunJob, error) { + if actionRun.ConcurrencyGroup == "" { + return nil, nil + } + + var jobsToCancel []*ActionRunJob + + statusFindOption := []Status{StatusWaiting, StatusBlocked} + if actionRun.ConcurrencyCancel { + statusFindOption = append(statusFindOption, StatusRunning) + } + runs, jobs, err := GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, statusFindOption) + if err != nil { + return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + jobsToCancel = append(jobsToCancel, jobs...) + + // cancel runs in the same concurrency group + for _, run := range runs { + if run.ID == actionRun.ID { + continue + } + jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ + RunID: run.ID, + }) + if err != nil { + return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + } + jobsToCancel = append(jobsToCancel, jobs...) + } + + return CancelJobs(ctx, jobsToCancel) +} diff --git a/models/actions/run_job.go b/models/actions/run_job.go index e7fa21270c..f72a7040e3 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "github.com/nektos/act/pkg/jobparser" "xorm.io/builder" ) @@ -22,23 +23,38 @@ type ActionRunJob struct { ID int64 RunID int64 `xorm:"index"` Run *ActionRun `xorm:"-"` - RepoID int64 `xorm:"index"` + RepoID int64 `xorm:"index(repo_concurrency)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` CommitSHA string `xorm:"index"` IsForkPullRequest bool Name string `xorm:"VARCHAR(255)"` Attempt int64 - WorkflowPayload []byte - JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id - Needs []string `xorm:"JSON TEXT"` - RunsOn []string `xorm:"JSON TEXT"` - TaskID int64 // the latest task of the job - Status Status `xorm:"index"` - Started timeutil.TimeStamp - Stopped timeutil.TimeStamp - Created timeutil.TimeStamp `xorm:"created"` - Updated timeutil.TimeStamp `xorm:"updated index"` + + // WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse + // it should contain exactly one job with global workflow fields for this model + WorkflowPayload []byte + + JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id + Needs []string `xorm:"JSON TEXT"` + RunsOn []string `xorm:"JSON TEXT"` + TaskID int64 // the latest task of the job + Status Status `xorm:"index"` + + RawConcurrency string // raw concurrency from job YAML's "concurrency" section + + // IsConcurrencyEvaluated is only valid/needed when this job's RawConcurrency is not empty. + // If RawConcurrency can't be evaluated (e.g. depend on other job's outputs or have errors), this field will be false. + // If RawConcurrency has been successfully evaluated, this field will be true, ConcurrencyGroup and ConcurrencyCancel are also set. + IsConcurrencyEvaluated bool + + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` // evaluated concurrency.group + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // evaluated concurrency.cancel-in-progress + + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` } func init() { @@ -84,6 +100,24 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { return job.Run.LoadAttributes(ctx) } +// ParseJob parses the job structure from the ActionRunJob.WorkflowPayload +func (job *ActionRunJob) ParseJob() (*jobparser.Job, error) { + // job.WorkflowPayload is a SingleWorkflow created from an ActionRun's workflow, which exactly contains this job's YAML definition. + // Ideally it shouldn't be called "Workflow", it is just a job with global workflow fields + trigger + parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + if err != nil { + return nil, fmt.Errorf("job %d single workflow: unable to parse: %w", job.ID, err) + } else if len(parsedWorkflows) != 1 { + return nil, fmt.Errorf("job %d single workflow: not single workflow", job.ID) + } + _, workflowJob := parsedWorkflows[0].Job() + if workflowJob == nil { + // it shouldn't happen, and since the callers don't check nil, so return an error instead of nil + return nil, util.ErrorWrap(util.ErrNotExist, "job %d single workflow: payload doesn't contain a job", job.ID) + } + return workflowJob, nil +} + func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { var job ActionRunJob has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job) @@ -125,7 +159,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return affected, nil } - if affected != 0 && slices.Contains(cols, "status") && job.Status.IsWaiting() { + if slices.Contains(cols, "status") && job.Status.IsWaiting() { // if the status of job changes to waiting again, increase tasks version. if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { return 0, err @@ -197,3 +231,39 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { return StatusUnknown // it shouldn't happen } } + +func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) (jobsToCancel []*ActionRunJob, _ error) { + if job.RawConcurrency == "" { + return nil, nil + } + if !job.IsConcurrencyEvaluated { + return nil, nil + } + if job.ConcurrencyGroup == "" { + return nil, nil + } + + statusFindOption := []Status{StatusWaiting, StatusBlocked} + if job.ConcurrencyCancel { + statusFindOption = append(statusFindOption, StatusRunning) + } + runs, jobs, err := GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption) + if err != nil { + return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + jobs = slices.DeleteFunc(jobs, func(j *ActionRunJob) bool { return j.ID == job.ID }) + jobsToCancel = append(jobsToCancel, jobs...) + + // cancel runs in the same concurrency group + for _, run := range runs { + jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ + RunID: run.ID, + }) + if err != nil { + return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + } + jobsToCancel = append(jobsToCancel, jobs...) + } + + return CancelJobs(ctx, jobsToCancel) +} diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 5f7bb62878..10f76d3641 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -69,12 +69,13 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err type FindRunJobOptions struct { db.ListOptions - RunID int64 - RepoID int64 - OwnerID int64 - CommitSHA string - Statuses []Status - UpdatedBefore timeutil.TimeStamp + RunID int64 + RepoID int64 + OwnerID int64 + CommitSHA string + Statuses []Status + UpdatedBefore timeutil.TimeStamp + ConcurrencyGroup string } func (opts FindRunJobOptions) ToConds() builder.Cond { @@ -94,6 +95,12 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.UpdatedBefore > 0 { cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore}) } + if opts.ConcurrencyGroup != "" { + if opts.RepoID == 0 { + panic("Invalid FindRunJobOptions: repo_id is required") + } + cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup}) + } return cond } diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 12c55e538e..2628c4712f 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -64,15 +64,16 @@ func (runs RunList) LoadRepos(ctx context.Context) error { type FindRunOptions struct { db.ListOptions - RepoID int64 - OwnerID int64 - WorkflowID string - Ref string // the commit/tag/… that caused this workflow - TriggerUserID int64 - TriggerEvent webhook_module.HookEventType - Approved bool // not util.OptionalBool, it works only when it's true - Status []Status - CommitSHA string + RepoID int64 + OwnerID int64 + WorkflowID string + Ref string // the commit/tag/… that caused this workflow + TriggerUserID int64 + TriggerEvent webhook_module.HookEventType + Approved bool // not util.OptionalBool, it works only when it's true + Status []Status + ConcurrencyGroup string + CommitSHA string } func (opts FindRunOptions) ToConds() builder.Cond { @@ -101,6 +102,12 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.CommitSHA != "" { cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) } + if len(opts.ConcurrencyGroup) > 0 { + if opts.RepoID == 0 { + panic("Invalid FindRunOptions: repo_id is required") + } + cond = cond.And(builder.Eq{"`action_run`.concurrency_group": opts.ConcurrencyGroup}) + } return cond } diff --git a/models/actions/task.go b/models/actions/task.go index c1306a8418..7417af8b45 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -21,7 +21,6 @@ import ( runnerv1 "code.gitea.io/actions-proto-go/runner/v1" lru "github.com/hashicorp/golang-lru/v2" - "github.com/nektos/act/pkg/jobparser" "google.golang.org/protobuf/types/known/timestamppb" "xorm.io/builder" ) @@ -278,13 +277,10 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask return nil, false, err } - parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + workflowJob, err := job.ParseJob() if err != nil { - return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) - } else if len(parsedWorkflows) != 1 { - return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID) + return nil, false, fmt.Errorf("load job %d: %w", job.ID, err) } - _, workflowJob := parsedWorkflows[0].Job() if _, err := e.Insert(task); err != nil { return nil, false, err diff --git a/models/issues/review_list.go b/models/issues/review_list.go index bbb8c489fa..86b1a2e76e 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -173,7 +173,7 @@ func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, mig reviewersMap := make(map[int64][]*Review) // key is reviewer id originalReviewersMap := make(map[int64][]*Review) // key is original author id reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id - countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest} + countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment} for _, review := range reviews { if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed { if review.OriginalAuthorID != 0 { diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 7b8537cc7d..6795ea8e66 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -122,6 +122,7 @@ func TestGetReviewersByIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) @@ -129,6 +130,12 @@ func TestGetReviewersByIssueID(t *testing.T) { expectedReviews := []*issues_model.Review{} expectedReviews = append(expectedReviews, + &issues_model.Review{ + ID: 5, + Reviewer: user1, + Type: issues_model.ReviewTypeComment, + UpdatedUnix: 946684810, + }, &issues_model.Review{ ID: 7, Reviewer: org3, @@ -167,8 +174,9 @@ func TestGetReviewersByIssueID(t *testing.T) { for _, review := range allReviews { assert.NoError(t, review.LoadReviewer(t.Context())) } - if assert.Len(t, allReviews, 5) { + if assert.Len(t, allReviews, 6) { for i, review := range allReviews { + assert.Equal(t, expectedReviews[i].ID, review.ID) assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) assert.Equal(t, expectedReviews[i].Type, review.Type) assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1b1558f39d..8fb10e84cf 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -394,6 +394,7 @@ func prepareMigrationTasks() []*migration { // Gitea 1.24.0 ends at database version 321 newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs), newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength), + newMigration(323, "Add support for actions concurrency", v1_25.AddActionsConcurrency), } return preparedMigrations } diff --git a/models/migrations/v1_25/v323.go b/models/migrations/v1_25/v323.go new file mode 100644 index 0000000000..5f38ea8545 --- /dev/null +++ b/models/migrations/v1_25/v323.go @@ -0,0 +1,43 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "xorm.io/xorm" +) + +func AddActionsConcurrency(x *xorm.Engine) error { + type ActionRun struct { + RepoID int64 `xorm:"index(repo_concurrency)"` + RawConcurrency string + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` + } + + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRun)); err != nil { + return err + } + + if err := x.Sync(new(ActionRun)); err != nil { + return err + } + + type ActionRunJob struct { + RepoID int64 `xorm:"index(repo_concurrency)"` + RawConcurrency string + IsConcurrencyEvaluated bool + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` + } + + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRunJob)); err != nil { + return err + } + + return nil +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 401775047b..819356dfad 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -229,10 +229,6 @@ func RelativePath(ownerName, repoName string) string { return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" } -func RelativeWikiPath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" -} - // RelativePath should be an unix style path like username/reponame.git func (repo *Repository) RelativePath() string { return RelativePath(repo.OwnerName, repo.Name) @@ -245,12 +241,6 @@ func (sr StorageRepo) RelativePath() string { return string(sr) } -// WikiStorageRepo returns the storage repo for the wiki -// The wiki repository should have the same object format as the code repository -func (repo *Repository) WikiStorageRepo() StorageRepo { - return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) -} - // SanitizedOriginalURL returns a sanitized OriginalURL func (repo *Repository) SanitizedOriginalURL() string { if repo.OriginalURL == "" { diff --git a/models/repo/wiki.go b/models/repo/wiki.go index 9f41445bf8..47c8fa43ab 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -7,7 +7,6 @@ package repo import ( "context" "fmt" - "path/filepath" "strings" user_model "code.gitea.io/gitea/models/user" @@ -76,12 +75,12 @@ func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User return repo.cloneLink(ctx, doer, repo.Name+".wiki") } -// WikiPath returns wiki data path by given user and repository name. -func WikiPath(userName, repoName string) string { - return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".wiki.git") +func RelativeWikiPath(ownerName, repoName string) string { + return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" } -// WikiPath returns wiki data path for given repository. -func (repo *Repository) WikiPath() string { - return WikiPath(repo.OwnerName, repo.Name) +// WikiStorageRepo returns the storage repo for the wiki +// The wiki repository should have the same object format as the code repository +func (repo *Repository) WikiStorageRepo() StorageRepo { + return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) } diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go index 41e53d93d9..636c78009b 100644 --- a/models/repo/wiki_test.go +++ b/models/repo/wiki_test.go @@ -4,12 +4,10 @@ package repo_test import ( - "path/filepath" "testing" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) @@ -23,15 +21,10 @@ func TestRepository_WikiCloneLink(t *testing.T) { assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) } -func TestWikiPath(t *testing.T) { +func TestRepository_RelativeWikiPath(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, repo_model.WikiPath("user2", "repo1")) -} -func TestRepository_WikiPath(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, repo.WikiPath()) + assert.Equal(t, "user2/repo1.wiki.git", repo_model.RelativeWikiPath(repo.OwnerName, repo.Name)) + assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().RelativePath()) } diff --git a/modules/git/key.go b/modules/git/key.go index 8c14742f34..39e79ddbe0 100644 --- a/modules/git/key.go +++ b/modules/git/key.go @@ -3,7 +3,13 @@ package git -import "code.gitea.io/gitea/modules/setting" +import ( + "context" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" +) // Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat const ( @@ -24,3 +30,48 @@ func (s *SigningKey) String() string { setting.PanicInDevOrTesting("don't call SigningKey.String() - it exposes the KeyID which might be a local file path") return "SigningKey:" + s.Format } + +// GetSigningKey returns the KeyID and git Signature for the repo +func GetSigningKey(ctx context.Context, repoPath string) (*SigningKey, *Signature) { + if setting.Repository.Signing.SigningKey == "none" { + return nil, nil + } + + if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { + // Can ignore the error here as it means that commit.gpgsign is not set + value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repoPath).RunStdString(ctx) + sign, valid := ParseBool(strings.TrimSpace(value)) + if !sign || !valid { + return nil, nil + } + + format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repoPath).RunStdString(ctx) + signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repoPath).RunStdString(ctx) + signingName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repoPath).RunStdString(ctx) + signingEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repoPath).RunStdString(ctx) + + if strings.TrimSpace(signingKey) == "" { + return nil, nil + } + + return &SigningKey{ + KeyID: strings.TrimSpace(signingKey), + Format: strings.TrimSpace(format), + }, &Signature{ + Name: strings.TrimSpace(signingName), + Email: strings.TrimSpace(signingEmail), + } + } + + if setting.Repository.Signing.SigningKey == "" { + return nil, nil + } + + return &SigningKey{ + KeyID: setting.Repository.Signing.SigningKey, + Format: setting.Repository.Signing.SigningFormat, + }, &Signature{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } +} diff --git a/modules/git/url/url_test.go b/modules/git/url/url_test.go index 6655c20be3..76aa74a128 100644 --- a/modules/git/url/url_test.go +++ b/modules/git/url/url_test.go @@ -34,12 +34,12 @@ func TestParseGitURLs(t *testing.T) { }, }, { - kase: "git@[fe80:14fc:cec5:c174:d88%2510]:go-gitea/gitea.git", + kase: "git@[fe80::14fc:cec5:c174:d88%2510]:go-gitea/gitea.git", expected: &GitURL{ URL: &url.URL{ Scheme: "ssh", User: url.User("git"), - Host: "[fe80:14fc:cec5:c174:d88%10]", + Host: "[fe80::14fc:cec5:c174:d88%10]", Path: "go-gitea/gitea.git", }, extraMark: 1, @@ -137,11 +137,11 @@ func TestParseGitURLs(t *testing.T) { }, }, { - kase: "https://[fe80:14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git", + kase: "https://[fe80::14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git", expected: &GitURL{ URL: &url.URL{ Scheme: "https", - Host: "[fe80:14fc:cec5:c174:d88%10]:20", + Host: "[fe80::14fc:cec5:c174:d88%10]:20", Path: "/go-gitea/gitea.git", }, extraMark: 0, diff --git a/modules/gitrepo/clone.go b/modules/gitrepo/clone.go new file mode 100644 index 0000000000..8c437f657c --- /dev/null +++ b/modules/gitrepo/clone.go @@ -0,0 +1,20 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +// CloneExternalRepo clones an external repository to the managed repository. +func CloneExternalRepo(ctx context.Context, fromRemoteURL string, toRepo Repository, opts git.CloneRepoOptions) error { + return git.Clone(ctx, fromRemoteURL, repoPath(toRepo), opts) +} + +// CloneRepoToLocal clones a managed repository to a local path. +func CloneRepoToLocal(ctx context.Context, fromRepo Repository, toLocalPath string, opts git.CloneRepoOptions) error { + return git.Clone(ctx, repoPath(fromRepo), toLocalPath, opts) +} diff --git a/modules/gitrepo/commitgraph.go b/modules/gitrepo/commitgraph.go new file mode 100644 index 0000000000..7310e167f6 --- /dev/null +++ b/modules/gitrepo/commitgraph.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func WriteCommitGraph(ctx context.Context, repo Repository) error { + return git.WriteCommitGraph(ctx, repoPath(repo)) +} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 59d2323599..4dd03c18fe 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -7,9 +7,12 @@ import ( "context" "fmt" "io" + "io/fs" + "os" "path/filepath" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -86,3 +89,12 @@ func RenameRepository(ctx context.Context, repo, newRepo Repository) error { func InitRepository(ctx context.Context, repo Repository, objectFormatName string) error { return git.InitRepository(ctx, repoPath(repo), true, objectFormatName) } + +func UpdateServerInfo(ctx context.Context, repo Repository) error { + _, _, err := RunCmdBytes(ctx, repo, gitcmd.NewCommand("update-server-info")) + return err +} + +func GetRepoFS(repo Repository) fs.FS { + return os.DirFS(repoPath(repo)) +} diff --git a/modules/gitrepo/push.go b/modules/gitrepo/push.go new file mode 100644 index 0000000000..18808cac24 --- /dev/null +++ b/modules/gitrepo/push.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func Push(ctx context.Context, repo Repository, opts git.PushOptions) error { + return git.Push(ctx, repoPath(repo), opts) +} diff --git a/modules/gitrepo/signing.go b/modules/gitrepo/signing.go new file mode 100644 index 0000000000..c50978d15a --- /dev/null +++ b/modules/gitrepo/signing.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func GetSigningKey(ctx context.Context, repo Repository) (*git.SigningKey, *git.Signature) { + return git.GetSigningKey(ctx, repoPath(repo)) +} diff --git a/modules/hcaptcha/hcaptcha_test.go b/modules/hcaptcha/hcaptcha_test.go index 55e01ec535..5906faf17c 100644 --- a/modules/hcaptcha/hcaptcha_test.go +++ b/modules/hcaptcha/hcaptcha_test.go @@ -4,7 +4,10 @@ package hcaptcha import ( + "errors" + "io" "net/http" + "net/url" "os" "strings" "testing" @@ -21,6 +24,33 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +type mockTransport struct{} + +func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.String() != verifyURL { + return nil, errors.New("unsupported url") + } + + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + + bodyValues, err := url.ParseQuery(string(body)) + if err != nil { + return nil, err + } + + var responseText string + if bodyValues.Get("response") == dummyToken { + responseText = `{"success":true,"credit":false,"hostname":"dummy-key-pass","challenge_ts":"2025-10-08T16:02:56.136Z"}` + } else { + responseText = `{"success":false,"error-codes":["invalid-input-response"]}` + } + + return &http.Response{Request: req, Body: io.NopCloser(strings.NewReader(responseText))}, nil +} + func TestCaptcha(t *testing.T) { tt := []struct { Name string @@ -54,7 +84,8 @@ func TestCaptcha(t *testing.T) { for _, tc := range tt { t.Run(tc.Name, func(t *testing.T) { client, err := New(tc.Secret, WithHTTP(&http.Client{ - Timeout: time.Second * 5, + Timeout: time.Second * 5, + Transport: mockTransport{}, })) if err != nil { // The only error that can be returned from creating a client diff --git a/modules/httplib/request.go b/modules/httplib/request.go index 49ea6f4b73..8542a57d36 100644 --- a/modules/httplib/request.go +++ b/modules/httplib/request.go @@ -7,54 +7,53 @@ package httplib import ( "bytes" "context" - "crypto/tls" - "errors" "fmt" "io" "net" "net/http" "net/url" "strings" + "sync" "time" ) -var defaultSetting = Settings{"GiteaServer", 60 * time.Second, 60 * time.Second, nil, nil} - -// newRequest returns *Request with specific method -func newRequest(url, method string) *Request { - var resp http.Response - req := http.Request{ - Method: method, - Header: make(http.Header), - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, +var defaultTransport = sync.OnceValue(func() http.RoundTripper { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: DialContextWithTimeout(10 * time.Second), // it is good enough in modern days + } +}) + +func DialContextWithTimeout(timeout time.Duration) func(ctx context.Context, network, address string) (net.Conn, error) { + return func(ctx context.Context, network, address string) (net.Conn, error) { + return (&net.Dialer{Timeout: timeout}).DialContext(ctx, network, address) } - return &Request{url, &req, map[string]string{}, defaultSetting, &resp, nil} } -// NewRequest returns *Request with specific method func NewRequest(url, method string) *Request { - return newRequest(url, method) + return &Request{ + url: url, + req: &http.Request{ + Method: method, + Header: make(http.Header), + Proto: "HTTP/1.1", // FIXME: from legacy httplib, it shouldn't be hardcoded + ProtoMajor: 1, + ProtoMinor: 1, + }, + params: map[string]string{}, + + // ATTENTION: from legacy httplib, callers must pay more attention to it, it will cause annoying bugs when the response takes a long time + readWriteTimeout: 60 * time.Second, + } } -// Settings is the default settings for http client -type Settings struct { - UserAgent string - ConnectTimeout time.Duration - ReadWriteTimeout time.Duration - TLSClientConfig *tls.Config - Transport http.RoundTripper -} - -// Request provides more useful methods for requesting one url than http.Request. type Request struct { - url string - req *http.Request - params map[string]string - setting Settings - resp *http.Response - body []byte + url string + req *http.Request + params map[string]string + + readWriteTimeout time.Duration + transport http.RoundTripper } // SetContext sets the request's Context @@ -63,36 +62,24 @@ func (r *Request) SetContext(ctx context.Context) *Request { return r } -// SetTimeout sets connect time out and read-write time out for BeegoRequest. -func (r *Request) SetTimeout(connectTimeout, readWriteTimeout time.Duration) *Request { - r.setting.ConnectTimeout = connectTimeout - r.setting.ReadWriteTimeout = readWriteTimeout +// SetTransport sets the request transport, if not set, will use httplib's default transport with environment proxy support +// ATTENTION: the http.Transport has a connection pool, so it should be reused as much as possible, do not create a lot of transports +func (r *Request) SetTransport(transport http.RoundTripper) *Request { + r.transport = transport return r } func (r *Request) SetReadWriteTimeout(readWriteTimeout time.Duration) *Request { - r.setting.ReadWriteTimeout = readWriteTimeout + r.readWriteTimeout = readWriteTimeout return r } -// SetTLSClientConfig sets tls connection configurations if visiting https url. -func (r *Request) SetTLSClientConfig(config *tls.Config) *Request { - r.setting.TLSClientConfig = config - return r -} - -// Header add header item string in request. +// Header set header item string in request. func (r *Request) Header(key, value string) *Request { r.req.Header.Set(key, value) return r } -// SetTransport sets transport to -func (r *Request) SetTransport(transport http.RoundTripper) *Request { - r.setting.Transport = transport - return r -} - // Param adds query param in to request. // params build query string as ?key1=value1&key2=value2... func (r *Request) Param(key, value string) *Request { @@ -125,11 +112,9 @@ func (r *Request) Body(data any) *Request { return r } -func (r *Request) getResponse() (*http.Response, error) { - if r.resp.StatusCode != 0 { - return r.resp, nil - } - +// Response executes request client and returns the response. +// Caller MUST close the response body if no error occurs. +func (r *Request) Response() (*http.Response, error) { var paramBody string if len(r.params) > 0 { var buf bytes.Buffer @@ -160,59 +145,19 @@ func (r *Request) getResponse() (*http.Response, error) { return nil, err } - trans := r.setting.Transport - if trans == nil { - // create default transport - trans = &http.Transport{ - TLSClientConfig: r.setting.TLSClientConfig, - Proxy: http.ProxyFromEnvironment, - DialContext: TimeoutDialer(r.setting.ConnectTimeout), - } - } else if t, ok := trans.(*http.Transport); ok { - if t.TLSClientConfig == nil { - t.TLSClientConfig = r.setting.TLSClientConfig - } - if t.DialContext == nil { - t.DialContext = TimeoutDialer(r.setting.ConnectTimeout) - } - } - client := &http.Client{ - Transport: trans, - Timeout: r.setting.ReadWriteTimeout, + Transport: r.transport, + Timeout: r.readWriteTimeout, + } + if client.Transport == nil { + client.Transport = defaultTransport() } - if len(r.setting.UserAgent) > 0 && len(r.req.Header.Get("User-Agent")) == 0 { - r.req.Header.Set("User-Agent", r.setting.UserAgent) + if r.req.Header.Get("User-Agent") == "" { + r.req.Header.Set("User-Agent", "GiteaHttpLib") } - resp, err := client.Do(r.req) - if err != nil { - return nil, err - } - r.resp = resp - return resp, nil -} - -// Response executes request client gets response manually. -// Caller MUST close the response body if no error occurs -func (r *Request) Response() (*http.Response, error) { - if r == nil { - return nil, errors.New("invalid request") - } - return r.getResponse() -} - -// TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field. -func TimeoutDialer(cTimeout time.Duration) func(ctx context.Context, net, addr string) (c net.Conn, err error) { - return func(ctx context.Context, netw, addr string) (net.Conn, error) { - d := net.Dialer{Timeout: cTimeout} - conn, err := d.DialContext(ctx, netw, addr) - if err != nil { - return nil, err - } - return conn, nil - } + return client.Do(r.req) } func (r *Request) GoString() string { diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go index dd4108ea56..f4e6157091 100644 --- a/modules/lfstransfer/backend/backend.go +++ b/modules/lfstransfer/backend/backend.go @@ -157,7 +157,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans } // Download implements transfer.Backend. The returned reader must be closed by the caller. -func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) { +func (g *GiteaBackend) Download(oid string, args transfer.Args) (_ io.ReadCloser, _ int64, retErr error) { idMapStr, exists := args[argID] if !exists { return nil, 0, ErrMissingID @@ -188,7 +188,15 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, if err != nil { return nil, 0, fmt.Errorf("failed to get response: %w", err) } - // no need to close the body here by "defer resp.Body.Close()", see below + // We must return the ReaderCloser but not "ReadAll", to avoid OOM. + // "transfer.Backend" will check io.Closer interface and close the Body reader. + // So only close the Body when error occurs + defer func() { + if retErr != nil { + _ = resp.Body.Close() + } + }() + if resp.StatusCode != http.StatusOK { return nil, 0, statusCodeToErr(resp.StatusCode) } @@ -197,7 +205,6 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, if err != nil { return nil, 0, fmt.Errorf("failed to parse content length: %w", err) } - // transfer.Backend will check io.Closer interface and close this Body reader return resp.Body, respSize, nil } diff --git a/modules/private/internal.go b/modules/private/internal.go index e599c6eb8e..1fd72a3732 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "strings" + "sync" "time" "code.gitea.io/gitea/modules/httplib" @@ -33,6 +34,35 @@ func getClientIP() string { return strings.Fields(sshConnEnv)[0] } +func dialContextInternalAPI(ctx context.Context, network, address string) (conn net.Conn, err error) { + d := net.Dialer{Timeout: 10 * time.Second} + if setting.Protocol == setting.HTTPUnix { + conn, err = d.DialContext(ctx, "unix", setting.HTTPAddr) + } else { + conn, err = d.DialContext(ctx, network, address) + } + if err != nil { + return nil, err + } + if setting.LocalUseProxyProtocol { + if err = proxyprotocol.WriteLocalHeader(conn); err != nil { + _ = conn.Close() + return nil, err + } + } + return conn, nil +} + +var internalAPITransport = sync.OnceValue(func() http.RoundTripper { + return &http.Transport{ + DialContext: dialContextInternalAPI, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: setting.Domain, + }, + } +}) + func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request { if setting.InternalToken == "" { log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. @@ -43,49 +73,11 @@ Ensure you are running in the correct environment or set the correct configurati log.Fatal("Invalid internal request URL: %q", url) } - req := httplib.NewRequest(url, method). + return httplib.NewRequest(url, method). SetContext(ctx). + SetTransport(internalAPITransport()). Header("X-Real-IP", getClientIP()). - Header("X-Gitea-Internal-Auth", "Bearer "+setting.InternalToken). - SetTLSClientConfig(&tls.Config{ - InsecureSkipVerify: true, - ServerName: setting.Domain, - }) - - if setting.Protocol == setting.HTTPUnix { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr) - if err != nil { - return conn, err - } - if setting.LocalUseProxyProtocol { - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - } - return conn, err - }, - }) - } else if setting.LocalUseProxyProtocol { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, network, address) - if err != nil { - return conn, err - } - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - return conn, err - }, - }) - } - return req + Header("X-Gitea-Internal-Auth", "Bearer "+setting.InternalToken) } func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request { @@ -98,6 +90,6 @@ func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) log.Fatal("Too many arguments for newInternalRequestAPI") } - req.SetTimeout(10*time.Second, 60*time.Second) + req.SetReadWriteTimeout(60 * time.Second) return req } diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go index 9c3a008142..9d65962fcd 100644 --- a/modules/private/restore_repo.go +++ b/modules/private/restore_repo.go @@ -6,7 +6,6 @@ package private import ( "context" "fmt" - "time" "code.gitea.io/gitea/modules/setting" ) @@ -31,6 +30,6 @@ func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units Units: units, Validation: validation, }) - req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout + req.SetReadWriteTimeout(0) // since the request will spend much time, don't timeout return requestJSONClientMsg(req, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName)) } diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 9144916e45..475954f03a 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -109,6 +109,7 @@ copy_path=パスをコピー copy_success=コピーされました! copy_error=コピーに失敗しました copy_type_unsupported=このファイルタイプはコピーできません +copy_filename=ファイル名をコピー write=書き込み preview=プレビュー @@ -2433,6 +2434,9 @@ settings.event_workflow_job_desc=Gitea Actions のワークフロージョブが settings.event_package=パッケージ settings.event_package_desc=リポジトリにパッケージが作成または削除されたとき。 settings.branch_filter=ブランチ フィルター +settings.branch_filter_desc_1=プッシュ、ブランチ作成、ブランチ削除イベントに対するブランチ(およびref名)の許可リストで、globパターンで指定します。 空または*の場合、すべてのブランチとタグのイベントが報告されます。 +settings.branch_filter_desc_2=完全なref名にマッチさせるには、 refs/heads/ または refs/tags/ を前に付けてください。 +settings.branch_filter_desc_doc=書き方についてはドキュメント %[2]s を参照してください。 settings.authorization_header=Authorizationヘッダー settings.authorization_header_desc=入力した場合、リクエストにAuthorizationヘッダーとして付加します。 例: %s settings.active=有効 diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 0960cd800f..b6292c139b 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -2075,6 +2075,8 @@ settings=Configurações settings.desc=Configurações é onde você pode gerenciar as opções para o repositório. settings.options=Repositório settings.public_access=Acesso Público +settings.public_access_desc=Configurar permissões de acesso do visitante público para substituir os padrões deste repositório. +settings.public_access.docs.not_set=Não definido: nenhuma permissão extra de acesso público. A permissão do visitante segue a visibilidade e as permissões de membro do repositório. settings.collaboration=Colaboradores settings.collaboration.admin=Administrador settings.collaboration.write=Escrita @@ -2760,6 +2762,11 @@ view_as_role=Ver como: %s view_as_public_hint=Você está vendo o README como um usuário público. view_as_member_hint=Você está vendo o README como um membro desta organização. +worktime.date_range_start=Data de início +worktime.date_range_end=Data de término +worktime.by_repositories=Por repositórios +worktime.by_milestones=Por marcos +worktime.by_members=Por membros [admin] maintenance=Manutenção @@ -3371,6 +3378,7 @@ versions=Versões versions.view_all=Ver todas dependency.id=ID dependency.version=Versão +search_in_external_registry=Pesquisar em %s alpine.registry=Configure este registro adicionando o URL no arquivo /etc/apk/repositories: alpine.registry.key=Baixe a chave RSA pública do registro para a pasta /etc/apk/keys/ para verificar a assinatura do índice: alpine.registry.info=Escolha o $branch e $repository da lista abaixo. @@ -3398,6 +3406,7 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando: container.details.type=Tipo de Imagem container.details.platform=Plataforma container.pull=Puxe a imagem pela linha de comando: +container.images=Imagens container.digest=Digest container.multi_arch=S.O. / Arquitetura container.layers=Camadas da Imagem @@ -3506,6 +3515,8 @@ creation.name_placeholder=apenas caracteres alfanuméricos ou underline (_), nã creation.value_placeholder=Insira qualquer conteúdo. Espaços em branco no início e no fim serão omitidos. +add_secret=Adicionar segredo +edit_secret=Editar segredo deletion=Excluir segredo deletion.description=A exclusão de um segredo é permanente e não pode ser desfeita. Continuar? deletion.success=O segredo foi excluído. @@ -3605,9 +3616,11 @@ variables.update.success=A variável foi editada. [projects] +deleted.display_name=Excluir Projeto type-1.display_name=Projeto Individual type-2.display_name=Projeto do Repositório type-3.display_name=Projeto da Organização +enter_fullscreen=Tela cheia exit_fullscreen=Sair da Tela Cheia [git.filemode] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 9d353faa64..29eb6be949 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -109,6 +109,7 @@ copy_path=复制路径 copy_success=复制成功! copy_error=复制失败 copy_type_unsupported=无法复制此类型的文件内容 +copy_filename=复制文件名 write=撰写 preview=预览 @@ -2434,6 +2435,9 @@ settings.event_workflow_job_desc=Gitea 工作流队列中、等待中、正在 settings.event_package=软件包 settings.event_package_desc=软件包在仓库中已创建或删除。 settings.branch_filter=分支过滤 +settings.branch_filter_desc_1=推送、分支创建和分支删除事件的分支(和引用名称)白名单,以全局模式指定。如果为空或为 *,则报告所有分支和标签的事件。 +settings.branch_filter_desc_2=使用 refs/heads/refs/tags/ 前缀来匹配完整的引用名称。 +settings.branch_filter_desc_doc=请参阅 %[2]s 文档了解语法。 settings.authorization_header=授权标头 settings.authorization_header_desc=当存在时将被作为授权标头包含在内。例如: %s。 settings.active=激活 diff --git a/package.json b/package.json index e8e6f63eeb..2c5d5f8a0a 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,13 @@ "@github/relative-time-element": "4.4.8", "@github/text-expander-element": "2.9.2", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.18.0", + "@primer/octicons": "19.19.0", "@resvg/resvg-wasm": "2.6.2", "@silverwind/vue3-calendar-heatmap": "2.0.6", "@techknowlogick/license-checker-webpack-plugin": "0.3.0", "add-asset-webpack-plugin": "3.1.1", "ansi_up": "6.0.6", - "asciinema-player": "3.10.0", + "asciinema-player": "3.12.0", "chart.js": "4.5.0", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", @@ -31,22 +31,22 @@ "dayjs": "1.11.18", "dropzone": "6.0.0-beta.2", "easymde": "2.20.0", - "esbuild-loader": "4.3.0", + "esbuild-loader": "4.4.0", "htmx.org": "2.0.7", "idiomorph": "0.7.4", "jquery": "3.7.1", - "katex": "0.16.22", + "katex": "0.16.23", "mermaid": "11.12.0", "mini-css-extract-plugin": "2.9.4", - "monaco-editor": "0.53.0", - "monaco-editor-webpack-plugin": "7.1.0", + "monaco-editor": "0.54.0", + "monaco-editor-webpack-plugin": "7.1.1", "online-3d-viewer": "0.16.0", "pdfobject": "2.3.1", "perfect-debounce": "2.0.0", "postcss": "8.5.6", "postcss-loader": "8.2.0", "sortablejs": "1.15.6", - "swagger-ui-dist": "5.29.1", + "swagger-ui-dist": "5.29.4", "tailwindcss": "3.4.17", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", @@ -60,13 +60,13 @@ "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", - "webpack": "5.102.0", + "webpack": "5.102.1", "webpack-cli": "6.0.1", "wrap-ansi": "9.0.2" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", - "@playwright/test": "1.55.1", + "@playwright/test": "1.56.0", "@stylistic/eslint-plugin": "5.4.0", "@stylistic/stylelint-plugin": "4.0.0", "@types/codemirror": "5.60.16", @@ -79,10 +79,10 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.45.0", + "@typescript-eslint/parser": "8.46.0", "@vitejs/plugin-vue": "6.0.1", - "@vitest/eslint-plugin": "1.3.13", - "eslint": "9.36.0", + "@vitest/eslint-plugin": "1.3.16", + "eslint": "9.37.0", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-array-func": "5.1.0", "eslint-plugin-github": "6.0.0", @@ -96,23 +96,23 @@ "eslint-plugin-vue-scoped-css": "2.12.0", "eslint-plugin-wc": "3.0.2", "globals": "16.4.0", - "happy-dom": "19.0.2", + "happy-dom": "20.0.2", "markdownlint-cli": "0.45.0", "material-icon-theme": "5.27.0", "nolyfill": "1.0.44", "postcss-html": "1.8.0", "spectral-cli-bundle": "1.0.3", - "stylelint": "16.24.0", + "stylelint": "16.25.0", "stylelint-config-recommended": "17.0.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.11", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "4.0.0", - "typescript-eslint": "8.45.0", - "updates": "16.7.4", + "typescript-eslint": "8.46.0", + "updates": "16.8.0", "vite-string-plugin": "1.4.6", "vitest": "3.2.4", - "vue-tsc": "3.1.0" + "vue-tsc": "3.1.1" }, "browserslist": [ "defaults" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 376d70c506..c0950022e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,8 +54,8 @@ importers: specifier: 0.1.0-alpha-3 version: 0.1.0-alpha-3 '@primer/octicons': - specifier: 19.18.0 - version: 19.18.0 + specifier: 19.19.0 + version: 19.19.0 '@resvg/resvg-wasm': specifier: 2.6.2 version: 2.6.2 @@ -64,16 +64,16 @@ importers: version: 2.0.6(tippy.js@6.3.7)(vue@3.5.22(typescript@5.9.3)) '@techknowlogick/license-checker-webpack-plugin': specifier: 0.3.0 - version: 0.3.0(webpack@5.102.0) + version: 0.3.0(webpack@5.102.1) add-asset-webpack-plugin: specifier: 3.1.1 - version: 3.1.1(webpack@5.102.0) + version: 3.1.1(webpack@5.102.1) ansi_up: specifier: 6.0.6 version: 6.0.6 asciinema-player: - specifier: 3.10.0 - version: 3.10.0 + specifier: 3.12.0 + version: 3.12.0 chart.js: specifier: 4.5.0 version: 4.5.0 @@ -91,7 +91,7 @@ importers: version: 1.6.2 css-loader: specifier: 7.1.2 - version: 7.1.2(webpack@5.102.0) + version: 7.1.2(webpack@5.102.1) dayjs: specifier: 1.11.18 version: 1.11.18 @@ -102,8 +102,8 @@ importers: specifier: 2.20.0 version: 2.20.0 esbuild-loader: - specifier: 4.3.0 - version: 4.3.0(webpack@5.102.0) + specifier: 4.4.0 + version: 4.4.0(webpack@5.102.1) htmx.org: specifier: 2.0.7 version: 2.0.7 @@ -114,20 +114,20 @@ importers: specifier: 3.7.1 version: 3.7.1 katex: - specifier: 0.16.22 - version: 0.16.22 + specifier: 0.16.23 + version: 0.16.23 mermaid: specifier: 11.12.0 version: 11.12.0 mini-css-extract-plugin: specifier: 2.9.4 - version: 2.9.4(webpack@5.102.0) + version: 2.9.4(webpack@5.102.1) monaco-editor: - specifier: 0.53.0 - version: 0.53.0 + specifier: 0.54.0 + version: 0.54.0 monaco-editor-webpack-plugin: - specifier: 7.1.0 - version: 7.1.0(monaco-editor@0.53.0)(webpack@5.102.0) + specifier: 7.1.1 + version: 7.1.1(monaco-editor@0.54.0)(webpack@5.102.1) online-3d-viewer: specifier: 0.16.0 version: 0.16.0 @@ -142,13 +142,13 @@ importers: version: 8.5.6 postcss-loader: specifier: 8.2.0 - version: 8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.0) + version: 8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.1) sortablejs: specifier: 1.15.6 version: 1.15.6 swagger-ui-dist: - specifier: 5.29.1 - version: 5.29.1 + specifier: 5.29.4 + version: 5.29.4 tailwindcss: specifier: 3.4.17 version: 3.4.17 @@ -187,29 +187,29 @@ importers: version: 5.3.2(chart.js@4.5.0)(vue@3.5.22(typescript@5.9.3)) vue-loader: specifier: 17.4.2 - version: 17.4.2(vue@3.5.22(typescript@5.9.3))(webpack@5.102.0) + version: 17.4.2(vue@3.5.22(typescript@5.9.3))(webpack@5.102.1) webpack: - specifier: 5.102.0 - version: 5.102.0(webpack-cli@6.0.1) + specifier: 5.102.1 + version: 5.102.1(webpack-cli@6.0.1) webpack-cli: specifier: 6.0.1 - version: 6.0.1(webpack@5.102.0) + version: 6.0.1(webpack@5.102.1) wrap-ansi: specifier: 9.0.2 version: 9.0.2 devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.5.0 - version: 4.5.0(eslint@9.36.0(jiti@2.6.1)) + version: 4.5.0(eslint@9.37.0(jiti@2.6.1)) '@playwright/test': - specifier: 1.55.1 - version: 1.55.1 + specifier: 1.56.0 + version: 1.56.0 '@stylistic/eslint-plugin': specifier: 5.4.0 - version: 5.4.0(eslint@9.36.0(jiti@2.6.1)) + version: 5.4.0(eslint@9.37.0(jiti@2.6.1)) '@stylistic/stylelint-plugin': specifier: 4.0.0 - version: 4.0.0(stylelint@16.24.0(typescript@5.9.3)) + version: 4.0.0(stylelint@16.25.0(typescript@5.9.3)) '@types/codemirror': specifier: 5.60.16 version: 5.60.16 @@ -241,59 +241,59 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.45.0 - version: 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.46.0 + version: 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-vue': specifier: 6.0.1 - version: 6.0.1(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + version: 6.0.1(vite@7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@vitest/eslint-plugin': - specifier: 1.3.13 - version: 1.3.13(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.6.2)(happy-dom@19.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1)) + specifier: 1.3.16 + version: 1.3.16(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(happy-dom@20.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1)) eslint: - specifier: 9.36.0 - version: 9.36.0(jiti@2.6.1) + specifier: 9.37.0 + version: 9.37.0(jiti@2.6.1) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-array-func: specifier: 5.1.0 - version: 5.1.0(eslint@9.36.0(jiti@2.6.1)) + version: 5.1.0(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-github: specifier: 6.0.0 - version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) + version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-import-x: specifier: 4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)) + version: 4.16.1(@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-no-use-extend-native: specifier: 0.7.2 - version: 0.7.2(eslint@9.36.0(jiti@2.6.1)) + version: 0.7.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-playwright: specifier: 2.2.2 - version: 2.2.2(eslint@9.36.0(jiti@2.6.1)) + version: 2.2.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-regexp: specifier: 2.10.0 - version: 2.10.0(eslint@9.36.0(jiti@2.6.1)) + version: 2.10.0(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-sonarjs: specifier: 3.0.5 - version: 3.0.5(eslint@9.36.0(jiti@2.6.1)) + version: 3.0.5(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-unicorn: specifier: 61.0.2 - version: 61.0.2(eslint@9.36.0(jiti@2.6.1)) + version: 61.0.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-vue: specifier: 10.5.0 - version: 10.5.0(@stylistic/eslint-plugin@5.4.0(eslint@9.36.0(jiti@2.6.1)))(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.36.0(jiti@2.6.1))) + version: 10.5.0(@stylistic/eslint-plugin@5.4.0(eslint@9.37.0(jiti@2.6.1)))(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.37.0(jiti@2.6.1))) eslint-plugin-vue-scoped-css: specifier: 2.12.0 - version: 2.12.0(eslint@9.36.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.36.0(jiti@2.6.1))) + version: 2.12.0(eslint@9.37.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.37.0(jiti@2.6.1))) eslint-plugin-wc: specifier: 3.0.2 - version: 3.0.2(eslint@9.36.0(jiti@2.6.1)) + version: 3.0.2(eslint@9.37.0(jiti@2.6.1)) globals: specifier: 16.4.0 version: 16.4.0 happy-dom: - specifier: 19.0.2 - version: 19.0.2 + specifier: 20.0.2 + version: 20.0.2 markdownlint-cli: specifier: 0.45.0 version: 0.45.0 @@ -310,38 +310,38 @@ importers: specifier: 1.0.3 version: 1.0.3 stylelint: - specifier: 16.24.0 - version: 16.24.0(typescript@5.9.3) + specifier: 16.25.0 + version: 16.25.0(typescript@5.9.3) stylelint-config-recommended: specifier: 17.0.0 - version: 17.0.0(stylelint@16.24.0(typescript@5.9.3)) + version: 17.0.0(stylelint@16.25.0(typescript@5.9.3)) stylelint-declaration-block-no-ignored-properties: specifier: 2.8.0 - version: 2.8.0(stylelint@16.24.0(typescript@5.9.3)) + version: 2.8.0(stylelint@16.25.0(typescript@5.9.3)) stylelint-declaration-strict-value: specifier: 1.10.11 - version: 1.10.11(stylelint@16.24.0(typescript@5.9.3)) + version: 1.10.11(stylelint@16.25.0(typescript@5.9.3)) stylelint-value-no-unknown-custom-properties: specifier: 6.0.1 - version: 6.0.1(stylelint@16.24.0(typescript@5.9.3)) + version: 6.0.1(stylelint@16.25.0(typescript@5.9.3)) svgo: specifier: 4.0.0 version: 4.0.0 typescript-eslint: - specifier: 8.45.0 - version: 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.46.0 + version: 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) updates: - specifier: 16.7.4 - version: 16.7.4 + specifier: 16.8.0 + version: 16.8.0 vite-string-plugin: specifier: 1.4.6 version: 1.4.6 vitest: specifier: 3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.6.2)(happy-dom@19.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(happy-dom@20.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) vue-tsc: - specifier: 3.1.0 - version: 3.1.0(typescript@5.9.3) + specifier: 3.1.1 + version: 3.1.1(typescript@5.9.3) packages: @@ -352,8 +352,8 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@antfu/utils@9.2.1': - resolution: {integrity: sha512-TMilPqXyii1AsiEii6l6ubRzbo76p6oshUSYPaKsmXDavyMLqjzVDkcp3pHp5ELMUNJHATcEOGxKTTsX9yYhGg==} + '@antfu/utils@9.3.0': + resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} @@ -389,8 +389,8 @@ packages: '@cacheable/memory@2.0.3': resolution: {integrity: sha512-R3UKy/CKOyb1LZG/VRCTMcpiMDyLH7SH3JrraRdK6kf3GweWCOU3sgvE13W3TiDRbxnDKylzKJvhUAvWl9LQOA==} - '@cacheable/utils@2.0.3': - resolution: {integrity: sha512-m7Rce68cMHlAUjvWBy9Ru1Nmw5gU0SjGGtQDdhpe6E0xnbcvrIY0Epy//JU1VYYBUTzrG9jvgmTauULGKzOkWA==} + '@cacheable/utils@2.1.0': + resolution: {integrity: sha512-ZdxfOiaarMqMj+H7qwlt5EBKWaeGihSYVHdQv5lUsbn8MJJOTW82OIwirQ39U5tMZkNvy3bQE+ryzC+xTAb9/g==} '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -679,8 +679,8 @@ packages: resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.3.1': - resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + '@eslint/config-helpers@0.4.0': + resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.15.2': @@ -695,8 +695,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.36.0': - resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} + '@eslint/js@9.37.0': + resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -707,6 +707,10 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.4.0': + resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@github/browserslist-config@1.0.0': resolution: {integrity: sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==} @@ -775,8 +779,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@keyv/bigmap@1.0.2': - resolution: {integrity: sha512-KR03xkEZlAZNF4IxXgVXb+uNIVNvwdh8UwI0cnc7WI6a+aQcDp8GL80qVfeB4E5NpsKJzou5jU0r6yLSSbMOtA==} + '@keyv/bigmap@1.0.3': + resolution: {integrity: sha512-jUEkNlnE9tYzX2AIBeoSe1gVUvSOfIOQ5EFPL5Un8cFHGvjD9L/fxpxlS1tEivRLHgapO2RZJ3D93HYAa049pg==} engines: {node: '>= 18'} '@keyv/serialize@1.1.1': @@ -791,8 +795,8 @@ packages: '@mcaptcha/vanilla-glue@0.1.0-alpha-3': resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==} - '@mermaid-js/parser@0.6.2': - resolution: {integrity: sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==} + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -880,16 +884,16 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.55.1': - resolution: {integrity: sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==} + '@playwright/test@1.56.0': + resolution: {integrity: sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==} engines: {node: '>=18'} hasBin: true '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.18.0': - resolution: {integrity: sha512-4P8FS7slUp0vHE7zLtlfOQcgF99r0tdxpriT3ahp6iwwnf1hF3OlDWXEp5n6b6G5eIY1cNr1bCrjRL+DkRjFVw==} + '@primer/octicons@19.19.0': + resolution: {integrity: sha512-LBbL8nOl6FWMDy7riKB5ppHLtffY7loRq+CDGj0D5G1Xdo2mKlSOQy3rWy2RVE8SxxPFL+mj46C1nG+smKBEZA==} '@resvg/resvg-wasm@2.6.2': resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} @@ -898,113 +902,113 @@ packages: '@rolldown/pluginutils@1.0.0-beta.29': resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} - '@rollup/rollup-android-arm-eabi@4.52.3': - resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.3': - resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.3': - resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.3': - resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.3': - resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.3': - resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': - resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.3': - resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.3': - resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.3': - resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.3': - resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.3': - resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.3': - resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.3': - resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.3': - resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.3': - resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.3': - resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.3': - resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.3': - resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.3': - resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.3': - resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.3': - resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} cpu: [x64] os: [win32] @@ -1024,6 +1028,21 @@ packages: '@simonwep/pickr@1.9.0': resolution: {integrity: sha512-oEYvv15PyfZzjoAzvXYt3UyNGwzsrpFxLaZKzkOSd0WYBVwLd19iJerePDONxC1iF6+DpcswPdLIM2KzCJuYFg==} + '@solid-primitives/refs@1.1.2': + resolution: {integrity: sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/transition-group@1.1.2': + resolution: {integrity: sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/utils@6.3.2': + resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==} + peerDependencies: + solid-js: ^1.6.12 + '@stylistic/eslint-plugin@5.4.0': resolution: {integrity: sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1188,11 +1207,11 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@20.19.19': - resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==} + '@types/node@20.19.21': + resolution: {integrity: sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==} - '@types/node@24.6.2': - resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} + '@types/node@24.7.2': + resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==} '@types/pdfobject@2.2.5': resolution: {integrity: sha512-7gD5tqc/RUDq0PyoLemL0vEHxBYi+zY0WVaFAx/Y0jBsXFgot1vB9No1GhDZGwRGJMCIZbgAb74QG9MTyTNU/g==} @@ -1218,9 +1237,6 @@ packages: '@types/toastify-js@1.12.4': resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==} - '@types/trusted-types@1.0.6': - resolution: {integrity: sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1230,63 +1246,63 @@ packages: '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} - '@typescript-eslint/eslint-plugin@8.45.0': - resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} + '@typescript-eslint/eslint-plugin@8.46.0': + resolution: {integrity: sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.45.0 + '@typescript-eslint/parser': ^8.46.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.45.0': - resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} + '@typescript-eslint/parser@8.46.0': + resolution: {integrity: sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.45.0': - resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} + '@typescript-eslint/project-service@8.46.0': + resolution: {integrity: sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.45.0': - resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} + '@typescript-eslint/scope-manager@8.46.0': + resolution: {integrity: sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.45.0': - resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} + '@typescript-eslint/tsconfig-utils@8.46.0': + resolution: {integrity: sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.45.0': - resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} + '@typescript-eslint/type-utils@8.46.0': + resolution: {integrity: sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.45.0': - resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} + '@typescript-eslint/types@8.46.0': + resolution: {integrity: sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.45.0': - resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} + '@typescript-eslint/typescript-estree@8.46.0': + resolution: {integrity: sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.45.0': - resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} + '@typescript-eslint/utils@8.46.0': + resolution: {integrity: sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.45.0': - resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} + '@typescript-eslint/visitor-keys@8.46.0': + resolution: {integrity: sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -1391,8 +1407,8 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.3.13': - resolution: {integrity: sha512-QfzXd1+lCY3dIqPHOZlagA2bJYoWC5yAU3adv8Gks0rHAL6FpyXKYBiyMCuU6mRrbKUMphGqwDQobinOvYgJig==} + '@vitest/eslint-plugin@1.3.16': + resolution: {integrity: sha512-EvXGiZpz3L1G/pmebcmMe61UzqgR8LFwmm+QGgQEHcrTCFkMgl+c0mj2jneo38/CkHhofbK3zc3xafV6/SpzNw==} peerDependencies: eslint: '>= 8.57.0' typescript: '>= 5.0.0' @@ -1453,8 +1469,8 @@ packages: '@vue/compiler-ssr@3.5.22': resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} - '@vue/language-core@3.1.0': - resolution: {integrity: sha512-a7ns+X9vTbdmk7QLrvnZs8s4E1wwtxG/sELzr6F2j4pU+r/OoAv6jJGSz+5tVTU6e4+3rjepGhSP8jDmBBcb3w==} + '@vue/language-core@3.1.1': + resolution: {integrity: sha512-qjMY3Q+hUCjdH+jLrQapqgpsJ0rd/2mAY02lZoHG3VFJZZZKLjAlV+Oo9QmWIT4jh8+Rx8RUGUi++d7T9Wb6Mw==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -1645,8 +1661,8 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - asciinema-player@3.10.0: - resolution: {integrity: sha512-shoOK6F606nDKZxDVM7JuGSCAyWLePoGRFNlV+FqiP5Sqvyn0BlE7wlbjZyd2X4P1iRhv/HKfVNtnQIxmgphRA==} + asciinema-player@3.12.0: + resolution: {integrity: sha512-qKaqcN4gkssF5shAk0SOyREJfMAFmmek0cYUmx+qVDGTUKIEA2oU5mXCRFgszYAJqu2kyGlu20BYhr9mH7mmpQ==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} @@ -1664,8 +1680,8 @@ packages: engines: {node: '>= 4.5.0'} hasBin: true - axe-core@4.10.3: - resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + axe-core@4.11.0: + resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -1681,8 +1697,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.10: - resolution: {integrity: sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==} + baseline-browser-mapping@2.8.16: + resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} hasBin: true big.js@5.2.2: @@ -1732,8 +1748,8 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - cacheable@2.0.3: - resolution: {integrity: sha512-nZF80J3d8RMrroMSYm1E9pBllVDXWPuECZgEZxH+vusCY4MAXAJVrY0jutcHSgh3xYX3G2EUNnmtWGZVVjWCXw==} + cacheable@2.1.0: + resolution: {integrity: sha512-zzL1BxdnqwD69JRT0dihnawAcLkBMwAH+hZSKjUzeBbPedVhk3qYPjRw9VOMYWwt5xRih5xd8S+3kEdGohZm/g==} callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1743,8 +1759,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001746: - resolution: {integrity: sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==} + caniuse-lite@1.0.30001750: + resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -1805,8 +1821,8 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} - ci-info@4.3.0: - resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} citeproc@2.4.63: @@ -1882,8 +1898,8 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - core-js-compat@3.45.1: - resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} + core-js-compat@3.46.0: + resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} core-js@3.32.2: resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==} @@ -2194,6 +2210,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.1.7: + resolution: {integrity: sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==} + dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} @@ -2209,8 +2228,8 @@ packages: easymde@2.20.0: resolution: {integrity: sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==} - electron-to-chromium@1.5.228: - resolution: {integrity: sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==} + electron-to-chromium@1.5.234: + resolution: {integrity: sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==} emoji-regex@10.5.0: resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} @@ -2237,8 +2256,8 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - envinfo@7.15.0: - resolution: {integrity: sha512-chR+t7exF6y59kelhXw5I3849nTy7KIRO+ePdLMhCD+JRP/JvmkenDWP7QSFGlsHX+kxGxdDutOPrmj5j1HR6g==} + envinfo@7.17.0: + resolution: {integrity: sha512-GpfViocsFM7viwClFgxK26OtjMlKN67GCR5v6ASFkotxtpBWd9d+vNy+AH7F2E1TUkMDZ8P/dDPZX71/NG8xnQ==} engines: {node: '>=4'} hasBin: true @@ -2248,8 +2267,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild-loader@4.3.0: - resolution: {integrity: sha512-D7HeJNdkDKKMarPQO/3dlJT6RwN2YJO7ENU6RPlpOz5YxSHnUNi2yvW41Bckvi1EVwctIaLzlb0ni5ag2GINYA==} + esbuild-loader@4.4.0: + resolution: {integrity: sha512-4J+hXTpTtEdzUNLoY8ReqDNJx2NoldfiljRCiKbeYUuZmVaiJeDqFgyAzz8uOopaekwRoCcqBFyEroGQLFVZ1g==} peerDependencies: webpack: ^4.40.0 || ^5.0.0 @@ -2483,8 +2502,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.36.0: - resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} + eslint@9.37.0: + resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2643,8 +2662,8 @@ packages: resolution: {integrity: sha512-YCmOj+4YAeEB5Dd9jfp6ETdejMet4zSxXjNkgaa4npBEKRI9uDOGB5MmAdAgi2OoFGAKshYhCbmLq2DS03CgVA==} engines: {node: '>=18.0.0'} - get-tsconfig@4.10.1: - resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-tsconfig@4.12.0: + resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2714,8 +2733,8 @@ packages: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} - happy-dom@19.0.2: - resolution: {integrity: sha512-831CLbgDyjRbd2lApHZFsBDe56onuFcjsCBPodzWpzedTpeDr8CGZjs7iEIdNW1DVwSFRecfwzLpVyGBPamwGA==} + happy-dom@20.0.2: + resolution: {integrity: sha512-pYOyu624+6HDbY+qkjILpQGnpvZOusItCk+rvF5/V+6NkcgTKnbOldpIy22tBnxoaLtlM9nXgoqAcW29/B7CIw==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -2981,8 +3000,8 @@ packages: just-extend@5.1.1: resolution: {integrity: sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==} - katex@0.16.22: - resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} + katex@0.16.23: + resolution: {integrity: sha512-7VlC1hsEEolL9xNO05v9VjrvWZePkCVBJqj8ruICxYjZfHaHbaU53AlP+PODyFIXEnaEIEWi3wJy7FPZ95JAVg==} hasBin: true keyv@4.5.4: @@ -3039,8 +3058,8 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} loader-utils@2.0.4: @@ -3116,8 +3135,13 @@ packages: resolution: {integrity: sha512-xaSxkaU7wY/0852zGApM8LdlIfGCW8ETZ0Rr62IQtAnUMlMuifsg09vWJcNYeL4f0anvr8Vo4ZQar8jGpV0btQ==} engines: {node: '>=20'} - marked@16.3.0: - resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + + marked@16.4.0: + resolution: {integrity: sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==} engines: {node: '>= 20'} hasBin: true @@ -3270,14 +3294,14 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - monaco-editor-webpack-plugin@7.1.0: - resolution: {integrity: sha512-ZjnGINHN963JQkFqjjcBtn1XBtUATDZBMgNQhDQwd78w2ukRhFXAPNgWuacaQiDZsUr4h1rWv5Mv6eriKuOSzA==} + monaco-editor-webpack-plugin@7.1.1: + resolution: {integrity: sha512-WxdbFHS3Wtz4V9hzhe/Xog5hQRSMxmDLkEEYZwqMDHgJlkZo00HVFZR0j5d0nKypjTUkkygH3dDSXERLG4757A==} peerDependencies: monaco-editor: '>= 0.31.0' webpack: ^4.5.0 || 5.x - monaco-editor@0.53.0: - resolution: {integrity: sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==} + monaco-editor@0.54.0: + resolution: {integrity: sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==} moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} @@ -3299,8 +3323,8 @@ packages: nanopop@2.3.0: resolution: {integrity: sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==} - napi-postinstall@0.3.3: - resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true @@ -3328,8 +3352,8 @@ packages: encoding: optional: true - node-releases@2.0.21: - resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} nolyfill@1.0.44: resolution: {integrity: sha512-PoggwVLiJUn0MnodpftsiC7EuknW5+6v62ntTOQ6T6l7g2r6aoaOwgk0tQW2BxGLYw9bF298LL8jDFTmEFuzlA==} @@ -3388,8 +3412,8 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-manager-detector@1.3.0: - resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + package-manager-detector@1.4.0: + resolution: {integrity: sha512-rRZ+pR1Usc+ND9M2NkmCvE/LYJS+8ORVV9X0KuNSY/gFsp7RBHJM/ADh9LYq4Vvfq6QkKrW6/weuh8SMEtN5gw==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -3477,13 +3501,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.55.1: - resolution: {integrity: sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==} + playwright-core@1.56.0: + resolution: {integrity: sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==} engines: {node: '>=18'} hasBin: true - playwright@1.55.1: - resolution: {integrity: sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==} + playwright@1.56.0: + resolution: {integrity: sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==} engines: {node: '>=18'} hasBin: true @@ -3633,6 +3657,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qified@0.5.0: + resolution: {integrity: sha512-Zj6Q/Vc/SQ+Fzc87N90jJUzBzxD7MVQ2ZvGyMmYtnl2u1a07CejAhvtk4ZwASos+SiHKCAIylyGHJKIek75QBw==} + engines: {node: '>=20'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -3704,8 +3732,8 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rollup@4.52.3: - resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3728,8 +3756,8 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - schema-utils@4.3.2: - resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} scslre@0.3.0: @@ -3745,6 +3773,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -3792,6 +3825,12 @@ packages: solid-js@1.9.9: resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==} + solid-transition-group@0.2.3: + resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} + engines: {node: '>=18.0.0', pnpm: '>=8.6.0'} + peerDependencies: + solid-js: ^1.6.12 + sortablejs@1.15.6: resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==} @@ -3915,8 +3954,8 @@ packages: peerDependencies: stylelint: '>=16' - stylelint@16.24.0: - resolution: {integrity: sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==} + stylelint@16.25.0: + resolution: {integrity: sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==} engines: {node: '>=18.12.0'} hasBin: true @@ -3965,8 +4004,8 @@ packages: svgson@5.3.1: resolution: {integrity: sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==} - swagger-ui-dist@5.29.1: - resolution: {integrity: sha512-qyjpz0qgcomRr41a5Aye42o69TKwCeHM9F8htLGVeUMKekNS6qAqz9oS7CtSvgGJSppSNAYAIh7vrfrSdHj9zw==} + swagger-ui-dist@5.29.4: + resolution: {integrity: sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==} sync-fetch@0.4.5: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} @@ -3985,8 +4024,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tapable@2.2.3: - resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} terser-webpack-plugin@5.3.14: @@ -4095,8 +4134,8 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - typescript-eslint@8.45.0: - resolution: {integrity: sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==} + typescript-eslint@8.46.0: + resolution: {integrity: sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4122,8 +4161,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.13.0: - resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -4134,8 +4173,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@16.7.4: - resolution: {integrity: sha512-w8nMoQRFKL7iVTUBUF9pP9lxqe/vqQfxljKcCmY0pKwofXqGEUr3OUQznsGa62V6pvFvg097pm4jT4eDdPq6ow==} + updates@16.8.0: + resolution: {integrity: sha512-iKLehaYwcz9sdL/PIYX+V0quBl0hGXXqwLGPy/jlLOrXdFdz15QtE7kxtJATJhH2oPOi7+Y3VcL2rY90IU4L4g==} engines: {node: '>=20'} hasBin: true @@ -4160,8 +4199,8 @@ packages: vite-string-plugin@1.4.6: resolution: {integrity: sha512-Csjtny8/uVIynzlaRRj4RpHrPAakNwlH9jw6kgQ8tQhc2f0zzA6bCbAgWD0y84EgB8aLNrz7pZFUqSt3LOtk+w==} - vite@7.1.7: - resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} + vite@7.1.9: + resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4278,8 +4317,8 @@ packages: vue: optional: true - vue-tsc@3.1.0: - resolution: {integrity: sha512-fbMynMG7kXSnqZTRBSCh9ROYaVpXfCZbEO0gY3lqOjLbp361uuS88n6BDajiUriDIF+SGLWoinjvf6stS2J3Gg==} + vue-tsc@3.1.1: + resolution: {integrity: sha512-fyixKxFniOVgn+L/4+g8zCG6dflLLt01Agz9jl3TO45Bgk87NZJRmJVPsiK+ouq3LB91jJCbOV+pDkzYTxbI7A==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -4324,8 +4363,8 @@ packages: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} - webpack@5.102.0: - resolution: {integrity: sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==} + webpack@5.102.1: + resolution: {integrity: sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -4410,10 +4449,10 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: - package-manager-detector: 1.3.0 + package-manager-detector: 1.4.0 tinyexec: 1.0.1 - '@antfu/utils@9.2.1': {} + '@antfu/utils@9.3.0': {} '@babel/code-frame@7.27.1': dependencies: @@ -4440,17 +4479,19 @@ snapshots: '@cacheable/memoize@2.0.3': dependencies: - '@cacheable/utils': 2.0.3 + '@cacheable/utils': 2.1.0 '@cacheable/memory@2.0.3': dependencies: '@cacheable/memoize': 2.0.3 - '@cacheable/utils': 2.0.3 - '@keyv/bigmap': 1.0.2 + '@cacheable/utils': 2.1.0 + '@keyv/bigmap': 1.0.3 hookified: 1.12.1 keyv: 5.5.3 - '@cacheable/utils@2.0.3': {} + '@cacheable/utils@2.1.0': + dependencies: + keyv: 5.5.3 '@chevrotain/cst-dts-gen@11.0.3': dependencies: @@ -4640,24 +4681,24 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.5.0(eslint@9.36.0(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.5.0(eslint@9.37.0(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) ignore: 5.3.2 - '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@2.6.1))': dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.4.0(eslint@9.36.0(jiti@2.6.1))': + '@eslint/compat@1.4.0(eslint@9.37.0(jiti@2.6.1))': dependencies: '@eslint/core': 0.16.0 optionalDependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) '@eslint/config-array@0.21.0': dependencies: @@ -4667,7 +4708,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.3.1': {} + '@eslint/config-helpers@0.4.0': + dependencies: + '@eslint/core': 0.16.0 '@eslint/core@0.15.2': dependencies: @@ -4691,7 +4734,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.36.0': {} + '@eslint/js@9.37.0': {} '@eslint/object-schema@2.1.6': {} @@ -4700,6 +4743,11 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@eslint/plugin-kit@0.4.0': + dependencies: + '@eslint/core': 0.16.0 + levn: 0.4.1 + '@github/browserslist-config@1.0.0': {} '@github/combobox-nav@2.3.1': {} @@ -4731,7 +4779,7 @@ snapshots: '@iconify/utils@3.0.2': dependencies: '@antfu/install-pkg': 1.1.0 - '@antfu/utils': 9.2.1 + '@antfu/utils': 9.3.0 '@iconify/types': 2.0.0 debug: 4.4.3 globals: 15.15.0 @@ -4775,7 +4823,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@keyv/bigmap@1.0.2': + '@keyv/bigmap@1.0.3': dependencies: hookified: 1.12.1 @@ -4789,7 +4837,7 @@ snapshots: dependencies: '@mcaptcha/core-glue': 0.1.0-alpha-5 - '@mermaid-js/parser@0.6.2': + '@mermaid-js/parser@0.6.3': dependencies: langium: 3.3.1 @@ -4869,13 +4917,13 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.55.1': + '@playwright/test@1.56.0': dependencies: - playwright: 1.55.1 + playwright: 1.56.0 '@popperjs/core@2.11.8': {} - '@primer/octicons@19.18.0': + '@primer/octicons@19.19.0': dependencies: object-assign: 4.1.1 @@ -4883,70 +4931,70 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.29': {} - '@rollup/rollup-android-arm-eabi@4.52.3': + '@rollup/rollup-android-arm-eabi@4.52.4': optional: true - '@rollup/rollup-android-arm64@4.52.3': + '@rollup/rollup-android-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-arm64@4.52.3': + '@rollup/rollup-darwin-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-x64@4.52.3': + '@rollup/rollup-darwin-x64@4.52.4': optional: true - '@rollup/rollup-freebsd-arm64@4.52.3': + '@rollup/rollup-freebsd-arm64@4.52.4': optional: true - '@rollup/rollup-freebsd-x64@4.52.3': + '@rollup/rollup-freebsd-x64@4.52.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.3': + '@rollup/rollup-linux-arm-musleabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.3': + '@rollup/rollup-linux-arm64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.3': + '@rollup/rollup-linux-arm64-musl@4.52.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.3': + '@rollup/rollup-linux-loong64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.3': + '@rollup/rollup-linux-ppc64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.3': + '@rollup/rollup-linux-riscv64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.3': + '@rollup/rollup-linux-riscv64-musl@4.52.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.3': + '@rollup/rollup-linux-s390x-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.3': + '@rollup/rollup-linux-x64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-musl@4.52.3': + '@rollup/rollup-linux-x64-musl@4.52.4': optional: true - '@rollup/rollup-openharmony-arm64@4.52.3': + '@rollup/rollup-openharmony-arm64@4.52.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.3': + '@rollup/rollup-win32-arm64-msvc@4.52.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.3': + '@rollup/rollup-win32-ia32-msvc@4.52.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.3': + '@rollup/rollup-win32-x64-gnu@4.52.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.3': + '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true '@rtsao/scc@1.1.0': {} @@ -4963,17 +5011,30 @@ snapshots: core-js: 3.32.2 nanopop: 2.3.0 - '@stylistic/eslint-plugin@5.4.0(eslint@9.36.0(jiti@2.6.1))': + '@solid-primitives/refs@1.1.2(solid-js@1.9.9)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) - '@typescript-eslint/types': 8.45.0 - eslint: 9.36.0(jiti@2.6.1) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) + solid-js: 1.9.9 + + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.9)': + dependencies: + solid-js: 1.9.9 + + '@solid-primitives/utils@6.3.2(solid-js@1.9.9)': + dependencies: + solid-js: 1.9.9 + + '@stylistic/eslint-plugin@5.4.0(eslint@9.37.0(jiti@2.6.1))': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + '@typescript-eslint/types': 8.46.0 + eslint: 9.37.0(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 picomatch: 4.0.3 - '@stylistic/stylelint-plugin@4.0.0(stylelint@16.24.0(typescript@5.9.3))': + '@stylistic/stylelint-plugin@4.0.0(stylelint@16.25.0(typescript@5.9.3))': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 @@ -4982,11 +5043,11 @@ snapshots: postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 style-search: 0.1.0 - stylelint: 16.24.0(typescript@5.9.3) + stylelint: 16.25.0(typescript@5.9.3) '@swc/helpers@0.2.14': {} - '@techknowlogick/license-checker-webpack-plugin@0.3.0(webpack@5.102.0)': + '@techknowlogick/license-checker-webpack-plugin@0.3.0(webpack@5.102.1)': dependencies: glob: 7.2.3 lodash: 4.17.21 @@ -4995,7 +5056,7 @@ snapshots: spdx-expression-validate: 2.0.0 spdx-satisfies: 5.0.1 superstruct: 0.10.13 - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) webpack-sources: 1.4.3 wrap-ansi: 6.2.0 @@ -5169,13 +5230,13 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@20.19.19': + '@types/node@20.19.21': dependencies: undici-types: 6.21.0 - '@types/node@24.6.2': + '@types/node@24.7.2': dependencies: - undici-types: 7.13.0 + undici-types: 7.14.0 '@types/pdfobject@2.2.5': {} @@ -5195,8 +5256,6 @@ snapshots: '@types/toastify-js@1.12.4': {} - '@types/trusted-types@1.0.6': {} - '@types/trusted-types@2.0.7': optional: true @@ -5204,15 +5263,15 @@ snapshots: '@types/whatwg-mimetype@3.0.2': {} - '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.45.0 - '@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.45.0 - eslint: 9.36.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.0 + '@typescript-eslint/type-utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.0 + eslint: 9.37.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -5221,80 +5280,80 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.45.0 - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.45.0 + '@typescript-eslint/scope-manager': 8.46.0 + '@typescript-eslint/types': 8.46.0 + '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.0 debug: 4.4.3 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.45.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) - '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/tsconfig-utils': 8.46.0(typescript@5.9.3) + '@typescript-eslint/types': 8.46.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.45.0': + '@typescript-eslint/scope-manager@8.46.0': dependencies: - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/visitor-keys': 8.45.0 + '@typescript-eslint/types': 8.46.0 + '@typescript-eslint/visitor-keys': 8.46.0 - '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.46.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.0 + '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.45.0': {} + '@typescript-eslint/types@8.46.0': {} - '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.45.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/visitor-keys': 8.45.0 + '@typescript-eslint/project-service': 8.46.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.0(typescript@5.9.3) + '@typescript-eslint/types': 8.46.0 + '@typescript-eslint/visitor-keys': 8.46.0 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.2 + semver: 7.7.3 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.45.0 - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - eslint: 9.36.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.46.0 + '@typescript-eslint/types': 8.46.0 + '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.45.0': + '@typescript-eslint/visitor-keys@8.46.0': dependencies: - '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/types': 8.46.0 eslint-visitor-keys: 4.2.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -5356,20 +5415,20 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.1(vite@7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) - '@vitest/eslint-plugin@1.3.13(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.6.2)(happy-dom@19.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/eslint-plugin@1.3.16(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(happy-dom@20.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@typescript-eslint/scope-manager': 8.45.0 - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.36.0(jiti@2.6.1) + '@typescript-eslint/scope-manager': 8.46.0 + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.6.2)(happy-dom@19.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(happy-dom@20.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -5381,13 +5440,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -5457,7 +5516,7 @@ snapshots: '@vue/compiler-dom': 3.5.22 '@vue/shared': 3.5.22 - '@vue/language-core@3.1.0(typescript@5.9.3)': + '@vue/language-core@3.1.1(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.23 '@vue/compiler-dom': 3.5.22 @@ -5569,20 +5628,20 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.102.0)': + '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.102.1)': dependencies: - webpack: 5.102.0(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.102.0) + webpack: 5.102.1(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.102.1) - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.102.0)': + '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.102.1)': dependencies: - webpack: 5.102.0(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.102.0) + webpack: 5.102.1(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.102.1) - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.102.0)': + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.102.1)': dependencies: - webpack: 5.102.0(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.102.0) + webpack: 5.102.1(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack@5.102.1) '@xtuc/ieee754@1.2.0': {} @@ -5598,9 +5657,9 @@ snapshots: acorn@8.15.0: {} - add-asset-webpack-plugin@3.1.1(webpack@5.102.0): + add-asset-webpack-plugin@3.1.1(webpack@5.102.1): optionalDependencies: - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: @@ -5656,10 +5715,11 @@ snapshots: array-union@2.1.0: {} - asciinema-player@3.10.0: + asciinema-player@3.12.0: dependencies: '@babel/runtime': 7.28.4 solid-js: 1.9.9 + solid-transition-group: 0.2.3(solid-js@1.9.9) assertion-error@2.0.1: {} @@ -5669,7 +5729,7 @@ snapshots: atob@2.1.2: {} - axe-core@4.10.3: {} + axe-core@4.11.0: {} axobject-query@4.1.0: {} @@ -5679,7 +5739,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.8.10: {} + baseline-browser-mapping@2.8.16: {} big.js@5.2.2: {} @@ -5702,10 +5762,10 @@ snapshots: browserslist@4.26.3: dependencies: - baseline-browser-mapping: 2.8.10 - caniuse-lite: 1.0.30001746 - electron-to-chromium: 1.5.228 - node-releases: 2.0.21 + baseline-browser-mapping: 2.8.16 + caniuse-lite: 1.0.30001750 + electron-to-chromium: 1.5.234 + node-releases: 2.0.23 update-browserslist-db: 1.1.3(browserslist@4.26.3) buffer-from@1.1.2: {} @@ -5723,19 +5783,20 @@ snapshots: cac@6.7.14: {} - cacheable@2.0.3: + cacheable@2.1.0: dependencies: '@cacheable/memoize': 2.0.3 '@cacheable/memory': 2.0.3 - '@cacheable/utils': 2.0.3 + '@cacheable/utils': 2.1.0 hookified: 1.12.1 keyv: 5.5.3 + qified: 0.5.0 callsites@3.1.0: {} camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001746: {} + caniuse-lite@1.0.30001750: {} chai@5.3.3: dependencies: @@ -5805,7 +5866,7 @@ snapshots: chrome-trace-event@1.0.4: {} - ci-info@4.3.0: {} + ci-info@4.3.1: {} citeproc@2.4.63: {} @@ -5859,7 +5920,7 @@ snapshots: confbox@0.2.2: {} - core-js-compat@3.45.1: + core-js-compat@3.46.0: dependencies: browserslist: 4.26.3 @@ -5892,7 +5953,7 @@ snapshots: css-functions-list@3.2.3: {} - css-loader@7.1.2(webpack@5.102.0): + css-loader@7.1.2(webpack@5.102.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -5901,9 +5962,9 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.6) postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 - semver: 7.7.2 + semver: 7.7.3 optionalDependencies: - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) css-select@5.2.2: dependencies: @@ -6188,6 +6249,8 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.1.7: {} + dompurify@3.2.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6213,7 +6276,7 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - electron-to-chromium@1.5.228: {} + electron-to-chromium@1.5.234: {} emoji-regex@10.5.0: {} @@ -6226,13 +6289,13 @@ snapshots: enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.3 + tapable: 2.3.0 entities@4.5.0: {} env-paths@2.2.1: {} - envinfo@7.15.0: {} + envinfo@7.17.0: {} error-ex@1.3.4: dependencies: @@ -6240,12 +6303,12 @@ snapshots: es-module-lexer@1.7.0: {} - esbuild-loader@4.3.0(webpack@5.102.0): + esbuild-loader@4.4.0(webpack@5.102.1): dependencies: esbuild: 0.25.10 - get-tsconfig: 4.10.1 + get-tsconfig: 4.12.0 loader-utils: 2.0.4 - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) webpack-sources: 1.4.3 esbuild@0.25.10: @@ -6283,18 +6346,18 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.6.5(eslint@9.36.0(jiti@2.6.1)): + eslint-compat-utils@0.6.5(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) - semver: 7.7.2 + eslint: 9.37.0(jiti@2.6.1) + semver: 7.7.3 - eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: - get-tsconfig: 4.10.1 + get-tsconfig: 4.12.0 stable-hash-x: 0.2.0 optionalDependencies: unrs-resolver: 1.11.1 @@ -6307,111 +6370,111 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.10.1 + get-tsconfig: 4.12.0 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.36.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-array-func@5.1.0(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-array-func@5.1.0(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) - eslint-plugin-escompat@3.11.4(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-escompat@3.11.4(eslint@9.37.0(jiti@2.6.1)): dependencies: browserslist: 4.26.3 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) - eslint-plugin-eslint-comments@3.2.0(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-eslint-comments@3.2.0(eslint@9.37.0(jiti@2.6.1)): dependencies: escape-string-regexp: 1.0.5 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-filenames@1.3.2(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)): dependencies: - '@eslint/compat': 1.4.0(eslint@9.36.0(jiti@2.6.1)) + '@eslint/compat': 1.4.0(eslint@9.37.0(jiti@2.6.1)) '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.36.0 + '@eslint/js': 9.37.0 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 9.36.0(jiti@2.6.1) - eslint-config-prettier: 10.1.8(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-escompat: 3.11.4(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-eslint-comments: 3.2.0(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-filenames: 1.3.2(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-i18n-text: 1.0.1(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.36.0(jiti@2.6.1)) + eslint: 9.37.0(jiti@2.6.1) + eslint-config-prettier: 10.1.8(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-escompat: 3.11.4(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-eslint-comments: 3.2.0(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-filenames: 1.3.2(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-i18n-text: 1.0.1(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1))(prettier@3.6.2) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1))(prettier@3.6.2) eslint-rule-documentation: 1.0.23 globals: 16.4.0 jsx-ast-utils: 3.3.5 prettier: 3.6.2 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-i18n-text@1.0.1(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.37.0(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/types': 8.46.0 comment-parser: 1.4.1 debug: 4.4.3 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.0.3 - semver: 7.7.2 + semver: 7.7.3 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6420,9 +6483,9 @@ snapshots: array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.37.0(jiti@2.6.1)) hasown: '@nolyfill/hasown@1.0.44' is-core-module: '@nolyfill/is-core-module@1.0.39' is-glob: 4.0.3 @@ -6434,23 +6497,23 @@ snapshots: string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.37.0(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: '@nolyfill/array-includes@1.0.44' array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' ast-types-flow: 0.0.8 - axe-core: 4.10.3 + axe-core: 4.11.0 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) hasown: '@nolyfill/hasown@1.0.44' jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6461,46 +6524,46 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-no-use-extend-native@0.7.2(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-no-use-extend-native@0.7.2(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) is-get-set-prop: 2.0.0 is-js-type: 3.0.0 is-obj-prop: 2.0.0 is-proto-prop: 3.0.1 - eslint-plugin-playwright@2.2.2(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-playwright@2.2.2(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) globals: 13.24.0 - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1))(prettier@3.6.2): + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1))(prettier@3.6.2): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) prettier: 3.6.2 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@9.36.0(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@9.37.0(jiti@2.6.1)) - eslint-plugin-regexp@2.10.0(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-regexp@2.10.0(eslint@9.37.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 comment-parser: 1.4.1 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) jsdoc-type-pratt-parser: 4.8.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@3.0.5(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-sonarjs@3.0.5(eslint@9.37.0(jiti@2.6.1)): dependencies: '@eslint-community/regexpp': 4.12.1 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) functional-red-black-tree: 1.0.1 jsx-ast-utils-x: 0.1.0 lodash.merge: 4.6.2 @@ -6509,16 +6572,16 @@ snapshots: semver: 7.7.2 typescript: 5.9.3 - eslint-plugin-unicorn@61.0.2(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-unicorn@61.0.2(eslint@9.37.0(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.27.1 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) '@eslint/plugin-kit': 0.3.5 change-case: 5.4.4 - ci-info: 4.3.0 + ci-info: 4.3.1 clean-regexp: 1.0.0 - core-js-compat: 3.45.1 - eslint: 9.36.0(jiti@2.6.1) + core-js-compat: 3.46.0 + eslint: 9.37.0(jiti@2.6.1) esquery: 1.6.0 find-up-simple: 1.0.1 globals: 16.4.0 @@ -6528,41 +6591,41 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.12.0 - semver: 7.7.2 + semver: 7.7.3 strip-indent: 4.1.0 - eslint-plugin-vue-scoped-css@2.12.0(eslint@9.36.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.36.0(jiti@2.6.1))): + eslint-plugin-vue-scoped-css@2.12.0(eslint@9.37.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.37.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) - eslint: 9.36.0(jiti@2.6.1) - eslint-compat-utils: 0.6.5(eslint@9.36.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + eslint: 9.37.0(jiti@2.6.1) + eslint-compat-utils: 0.6.5(eslint@9.37.0(jiti@2.6.1)) lodash: 4.17.21 postcss: 8.5.6 postcss-safe-parser: 6.0.0(postcss@8.5.6) postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.0 postcss-styl: 0.12.3 - vue-eslint-parser: 10.2.0(eslint@9.36.0(jiti@2.6.1)) + vue-eslint-parser: 10.2.0(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-vue@10.5.0(@stylistic/eslint-plugin@5.4.0(eslint@9.36.0(jiti@2.6.1)))(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.36.0(jiti@2.6.1))): + eslint-plugin-vue@10.5.0(@stylistic/eslint-plugin@5.4.0(eslint@9.37.0(jiti@2.6.1)))(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.37.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) - eslint: 9.36.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + eslint: 9.37.0(jiti@2.6.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.1.2 - semver: 7.7.2 - vue-eslint-parser: 10.2.0(eslint@9.36.0(jiti@2.6.1)) + semver: 7.7.3 + vue-eslint-parser: 10.2.0(eslint@9.37.0(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.4.0(eslint@9.36.0(jiti@2.6.1)) - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.4.0(eslint@9.37.0(jiti@2.6.1)) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-wc@3.0.2(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-wc@3.0.2(eslint@9.37.0(jiti@2.6.1)): dependencies: - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 @@ -6582,16 +6645,16 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.36.0(jiti@2.6.1): + eslint@9.37.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.1 - '@eslint/core': 0.15.2 + '@eslint/config-helpers': 0.4.0 + '@eslint/core': 0.16.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.36.0 - '@eslint/plugin-kit': 0.3.5 + '@eslint/js': 9.37.0 + '@eslint/plugin-kit': 0.4.0 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -6725,7 +6788,7 @@ snapshots: flat-cache@6.1.17: dependencies: - cacheable: 2.0.3 + cacheable: 2.1.0 flatted: 3.3.3 hookified: 1.12.1 @@ -6752,7 +6815,7 @@ snapshots: get-set-props@0.2.0: {} - get-tsconfig@4.10.1: + get-tsconfig@4.12.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -6832,9 +6895,9 @@ snapshots: hammerjs@2.0.8: {} - happy-dom@19.0.2: + happy-dom@20.0.2: dependencies: - '@types/node': 20.19.19 + '@types/node': 20.19.21 '@types/whatwg-mimetype': 3.0.2 whatwg-mimetype: 3.0.0 @@ -6923,7 +6986,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 is-decimal@2.0.1: {} @@ -6986,7 +7049,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.6.2 + '@types/node': 24.7.2 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -7045,7 +7108,7 @@ snapshots: just-extend@5.1.1: {} - katex@0.16.22: + katex@0.16.23: dependencies: commander: 8.3.0 @@ -7100,7 +7163,7 @@ snapshots: dependencies: uc.micro: 2.1.0 - loader-runner@4.3.0: {} + loader-runner@4.3.1: {} loader-utils@2.0.4: dependencies: @@ -7190,7 +7253,9 @@ snapshots: transitivePeerDependencies: - supports-color - marked@16.3.0: {} + marked@14.0.0: {} + + marked@16.4.0: {} marked@4.3.0: {} @@ -7219,7 +7284,7 @@ snapshots: dependencies: '@braintree/sanitize-url': 7.1.1 '@iconify/utils': 3.0.2 - '@mermaid-js/parser': 0.6.2 + '@mermaid-js/parser': 0.6.3 '@types/d3': 7.4.3 cytoscape: 3.33.1 cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) @@ -7229,10 +7294,10 @@ snapshots: dagre-d3-es: 7.0.11 dayjs: 1.11.18 dompurify: 3.2.7 - katex: 0.16.22 + katex: 0.16.23 khroma: 2.1.0 lodash-es: 4.17.21 - marked: 16.3.0 + marked: 16.4.0 roughjs: 4.6.6 stylis: 4.3.6 ts-dedent: 2.2.0 @@ -7299,7 +7364,7 @@ snapshots: dependencies: '@types/katex': 0.16.7 devlop: 1.1.0 - katex: 0.16.22 + katex: 0.16.23 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -7423,11 +7488,11 @@ snapshots: dependencies: mime-db: 1.52.0 - mini-css-extract-plugin@2.9.4(webpack@5.102.0): + mini-css-extract-plugin@2.9.4(webpack@5.102.1): dependencies: - schema-utils: 4.3.2 - tapable: 2.2.3 - webpack: 5.102.0(webpack-cli@6.0.1) + schema-utils: 4.3.3 + tapable: 2.3.0 + webpack: 5.102.1(webpack-cli@6.0.1) minimatch@10.0.3: dependencies: @@ -7452,15 +7517,16 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - monaco-editor-webpack-plugin@7.1.0(monaco-editor@0.53.0)(webpack@5.102.0): + monaco-editor-webpack-plugin@7.1.1(monaco-editor@0.54.0)(webpack@5.102.1): dependencies: loader-utils: 2.0.4 - monaco-editor: 0.53.0 - webpack: 5.102.0(webpack-cli@6.0.1) + monaco-editor: 0.54.0 + webpack: 5.102.1(webpack-cli@6.0.1) - monaco-editor@0.53.0: + monaco-editor@0.54.0: dependencies: - '@types/trusted-types': 1.0.6 + dompurify: 3.1.7 + marked: 14.0.0 moo@0.5.2: {} @@ -7478,7 +7544,7 @@ snapshots: nanopop@2.3.0: {} - napi-postinstall@0.3.3: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -7492,7 +7558,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.21: {} + node-releases@2.0.23: {} nolyfill@1.0.44: {} @@ -7547,7 +7613,7 @@ snapshots: package-json-from-dist@1.0.1: {} - package-manager-detector@1.3.0: {} + package-manager-detector@1.4.0: {} parent-module@1.0.1: dependencies: @@ -7628,11 +7694,11 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 - playwright-core@1.55.1: {} + playwright-core@1.56.0: {} - playwright@1.55.1: + playwright@1.56.0: dependencies: - playwright-core: 1.55.1 + playwright-core: 1.56.0 optionalDependencies: fsevents: 2.3.2 @@ -7671,14 +7737,14 @@ snapshots: optionalDependencies: postcss: 8.5.6 - postcss-loader@8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.0): + postcss-loader@8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.1): dependencies: cosmiconfig: 9.0.0(typescript@5.9.3) jiti: 2.6.1 postcss: 8.5.6 - semver: 7.7.2 + semver: 7.7.3 optionalDependencies: - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) transitivePeerDependencies: - typescript @@ -7764,6 +7830,10 @@ snapshots: punycode@2.3.1: {} + qified@0.5.0: + dependencies: + hookified: 1.12.1 + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -7823,32 +7893,32 @@ snapshots: robust-predicates@3.0.2: {} - rollup@4.52.3: + rollup@4.52.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.3 - '@rollup/rollup-android-arm64': 4.52.3 - '@rollup/rollup-darwin-arm64': 4.52.3 - '@rollup/rollup-darwin-x64': 4.52.3 - '@rollup/rollup-freebsd-arm64': 4.52.3 - '@rollup/rollup-freebsd-x64': 4.52.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 - '@rollup/rollup-linux-arm-musleabihf': 4.52.3 - '@rollup/rollup-linux-arm64-gnu': 4.52.3 - '@rollup/rollup-linux-arm64-musl': 4.52.3 - '@rollup/rollup-linux-loong64-gnu': 4.52.3 - '@rollup/rollup-linux-ppc64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-musl': 4.52.3 - '@rollup/rollup-linux-s390x-gnu': 4.52.3 - '@rollup/rollup-linux-x64-gnu': 4.52.3 - '@rollup/rollup-linux-x64-musl': 4.52.3 - '@rollup/rollup-openharmony-arm64': 4.52.3 - '@rollup/rollup-win32-arm64-msvc': 4.52.3 - '@rollup/rollup-win32-ia32-msvc': 4.52.3 - '@rollup/rollup-win32-x64-gnu': 4.52.3 - '@rollup/rollup-win32-x64-msvc': 4.52.3 + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 roughjs@4.6.6: @@ -7875,7 +7945,7 @@ snapshots: sax@1.4.1: {} - schema-utils@4.3.2: + schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 ajv: 8.17.1 @@ -7892,6 +7962,8 @@ snapshots: semver@7.7.2: {} + semver@7.7.3: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -7932,6 +8004,12 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.3(seroval@1.3.2) + solid-transition-group@0.2.3(solid-js@1.9.9): + dependencies: + '@solid-primitives/refs': 1.1.2(solid-js@1.9.9) + '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.9) + solid-js: 1.9.9 + sortablejs@1.15.6: {} source-list-map@2.0.1: {} @@ -8027,25 +8105,25 @@ snapshots: style-search@0.1.0: {} - stylelint-config-recommended@17.0.0(stylelint@16.24.0(typescript@5.9.3)): + stylelint-config-recommended@17.0.0(stylelint@16.25.0(typescript@5.9.3)): dependencies: - stylelint: 16.24.0(typescript@5.9.3) + stylelint: 16.25.0(typescript@5.9.3) - stylelint-declaration-block-no-ignored-properties@2.8.0(stylelint@16.24.0(typescript@5.9.3)): + stylelint-declaration-block-no-ignored-properties@2.8.0(stylelint@16.25.0(typescript@5.9.3)): dependencies: - stylelint: 16.24.0(typescript@5.9.3) + stylelint: 16.25.0(typescript@5.9.3) - stylelint-declaration-strict-value@1.10.11(stylelint@16.24.0(typescript@5.9.3)): + stylelint-declaration-strict-value@1.10.11(stylelint@16.25.0(typescript@5.9.3)): dependencies: - stylelint: 16.24.0(typescript@5.9.3) + stylelint: 16.25.0(typescript@5.9.3) - stylelint-value-no-unknown-custom-properties@6.0.1(stylelint@16.24.0(typescript@5.9.3)): + stylelint-value-no-unknown-custom-properties@6.0.1(stylelint@16.25.0(typescript@5.9.3)): dependencies: postcss-value-parser: 4.2.0 resolve: 1.22.10 - stylelint: 16.24.0(typescript@5.9.3) + stylelint: 16.25.0(typescript@5.9.3) - stylelint@16.24.0(typescript@5.9.3): + stylelint@16.25.0(typescript@5.9.3): dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 @@ -8148,7 +8226,7 @@ snapshots: deep-rename-keys: 0.2.1 xml-reader: 2.4.3 - swagger-ui-dist@5.29.1: + swagger-ui-dist@5.29.4: dependencies: '@scarf/scarf': 1.4.0 @@ -8198,16 +8276,16 @@ snapshots: transitivePeerDependencies: - ts-node - tapable@2.2.3: {} + tapable@2.3.0: {} - terser-webpack-plugin@5.3.14(webpack@5.102.0): + terser-webpack-plugin@5.3.14(webpack@5.102.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.0 - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) terser@5.44.0: dependencies: @@ -8285,13 +8363,13 @@ snapshots: type-fest@0.20.2: {} - typescript-eslint@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.36.0(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8308,11 +8386,11 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.13.0: {} + undici-types@7.14.0: {} unrs-resolver@1.11.1: dependencies: - napi-postinstall: 0.3.3 + napi-postinstall: 0.3.4 optionalDependencies: '@unrs/resolver-binding-android-arm-eabi': 1.11.1 '@unrs/resolver-binding-android-arm64': 1.11.1 @@ -8340,7 +8418,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - updates@16.7.4: {} + updates@16.8.0: {} uri-js@4.4.1: dependencies: @@ -8352,13 +8430,13 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-node@3.2.4(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -8375,27 +8453,27 @@ snapshots: vite-string-plugin@1.4.6: {} - vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1): + vite@7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.3 + rollup: 4.52.4 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.6.2 + '@types/node': 24.7.2 fsevents: 2.3.3 jiti: 2.6.1 stylus: 0.57.0 terser: 5.44.0 yaml: 2.8.1 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.6.2)(happy-dom@19.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(happy-dom@20.0.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -8413,13 +8491,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.6.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.7.2)(jiti@2.6.1)(stylus@0.57.0)(terser@5.44.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.6.2 - happy-dom: 19.0.2 + '@types/node': 24.7.2 + happy-dom: 20.0.2 transitivePeerDependencies: - jiti - less @@ -8464,31 +8542,31 @@ snapshots: chart.js: 4.5.0 vue: 3.5.22(typescript@5.9.3) - vue-eslint-parser@10.2.0(eslint@9.36.0(jiti@2.6.1)): + vue-eslint-parser@10.2.0(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.36.0(jiti@2.6.1) + eslint: 9.37.0(jiti@2.6.1) eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 esquery: 1.6.0 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - supports-color - vue-loader@17.4.2(vue@3.5.22(typescript@5.9.3))(webpack@5.102.0): + vue-loader@17.4.2(vue@3.5.22(typescript@5.9.3))(webpack@5.102.1): dependencies: chalk: 4.1.2 hash-sum: 2.0.0 watchpack: 2.4.4 - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) optionalDependencies: vue: 3.5.22(typescript@5.9.3) - vue-tsc@3.1.0(typescript@5.9.3): + vue-tsc@3.1.1(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.23 - '@vue/language-core': 3.1.0(typescript@5.9.3) + '@vue/language-core': 3.1.1(typescript@5.9.3) typescript: 5.9.3 vue@3.5.22(typescript@5.9.3): @@ -8508,21 +8586,21 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-cli@6.0.1(webpack@5.102.0): + webpack-cli@6.0.1(webpack@5.102.1): dependencies: '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.102.0) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.102.0) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.102.0) + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.102.1) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.102.1) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.102.1) colorette: 2.0.20 commander: 12.1.0 cross-spawn: 7.0.6 - envinfo: 7.15.0 + envinfo: 7.17.0 fastest-levenshtein: 1.0.16 import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.102.0(webpack-cli@6.0.1) + webpack: 5.102.1(webpack-cli@6.0.1) webpack-merge: 6.0.1 webpack-merge@6.0.1: @@ -8538,7 +8616,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.102.0(webpack-cli@6.0.1): + webpack@5.102.1(webpack-cli@6.0.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -8557,16 +8635,16 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 + loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.3 - terser-webpack-plugin: 5.3.14(webpack@5.102.0) + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(webpack@5.102.1) watchpack: 2.4.4 webpack-sources: 3.3.3 optionalDependencies: - webpack-cli: 6.0.1(webpack@5.102.0) + webpack-cli: 6.0.1(webpack@5.102.1) transitivePeerDependencies: - '@swc/core' - esbuild diff --git a/public/assets/img/svg/gitea-running.svg b/public/assets/img/svg/gitea-running.svg new file mode 100644 index 0000000000..2320f8101c --- /dev/null +++ b/public/assets/img/svg/gitea-running.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-vscode.svg b/public/assets/img/svg/gitea-vscode.svg deleted file mode 100644 index 453b9befcc..0000000000 --- a/public/assets/img/svg/gitea-vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/assets/img/svg/octicon-comment-ai.svg b/public/assets/img/svg/octicon-comment-ai.svg new file mode 100644 index 0000000000..72aa8715a0 --- /dev/null +++ b/public/assets/img/svg/octicon-comment-ai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-vscode.svg b/public/assets/img/svg/octicon-vscode.svg new file mode 100644 index 0000000000..d226e3a574 --- /dev/null +++ b/public/assets/img/svg/octicon-vscode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index 55ba7862a9..86bab4b340 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -217,19 +217,19 @@ func (s *Service) UpdateTask( return nil, status.Errorf(codes.Internal, "load run: %v", err) } - // don't create commit status for cron job - if task.Job.Run.ScheduleID == 0 { - actions_service.CreateCommitStatus(ctx, task.Job) - } + actions_service.CreateCommitStatusForRunJobs(ctx, task.Job.Run, task.Job) if task.Status.IsDone() { notify_service.WorkflowJobStatusUpdate(ctx, task.Job.Run.Repo, task.Job.Run.TriggerUser, task.Job, task) } if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED { - if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil { + if err := actions_service.EmitJobsIfReadyByRun(task.Job.RunID); err != nil { log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err) } + if task.Job.Run.Status.IsDone() { + actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, task.Job) + } } return connect.NewResponse(&runnerv1.UpdateTaskResponse{ diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 66afede218..5f52ee8a43 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1423,6 +1423,7 @@ func Routes() *web.Router { m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) + m.Post("/diffpatch", mustEnableEditor, reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) m.Group("/contents", func() { m.Get("", repo.GetContentsList) m.Get("/*", repo.GetContents) @@ -1434,7 +1435,6 @@ func Routes() *web.Router { m.Put("", bind(api.UpdateFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.UpdateFile) m.Delete("", bind(api.DeleteFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.DeleteFile) }) - m.Post("/diffpatch", bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) }, mustEnableEditor, reqToken()) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) m.Group("/contents-ext", func() { diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index e9f5cf5d90..edb09fc08f 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -36,7 +36,7 @@ func ApplyDiffPatch(ctx *context.APIContext) { // in: body // required: true // schema: - // "$ref": "#/definitions/UpdateFileOptions" + // "$ref": "#/definitions/ApplyDiffPatchFileOptions" // responses: // "200": // "$ref": "#/responses/FileResponse" diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 4aba74b939..b80a9c14ba 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -121,6 +121,9 @@ type swaggerParameterBodies struct { // in:body GetFilesOptions api.GetFilesOptions + // in:body + ApplyDiffPatchFileOptions api.ApplyDiffPatchFileOptions + // in:body ChangeFilesOptions api.ChangeFilesOptions diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index 79989d8fbe..1dc40d6a75 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -636,6 +636,7 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest, ErrorDescription: "cannot proceed your request", }) + return } resp, tokenErr := oauth2_provider.NewAccessTokenResponse(ctx, authorizationCode.Grant, serverKey, clientKey) if tokenErr != nil { diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 3422128026..b409e887be 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -27,7 +27,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common" @@ -36,6 +35,7 @@ import ( notify_service "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/model" + "gopkg.in/yaml.v3" "xorm.io/builder" ) @@ -420,12 +420,45 @@ func Rerun(ctx *context_module.Context) { return } + // check run (workflow-level) concurrency + + job, jobs := getRunJobs(ctx, runIndex, jobIndex) + if ctx.Written() { + return + } + // reset run's start and stop time when it is done if run.Status.IsDone() { run.PreviousDuration = run.Duration() run.Started = 0 run.Stopped = 0 - if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil { + + vars, err := actions_model.GetVariablesOfRun(ctx, run) + if err != nil { + ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err)) + return + } + + if run.RawConcurrency != "" { + var rawConcurrency model.RawConcurrency + if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil { + ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err)) + return + } + + err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars) + if err != nil { + ctx.ServerError("EvaluateRunConcurrencyFillModel", err) + return + } + + run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run) + if err != nil { + ctx.ServerError("PrepareToStartRunWithConcurrency", err) + return + } + } + if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil { ctx.ServerError("UpdateRun", err) return } @@ -437,16 +470,12 @@ func Rerun(ctx *context_module.Context) { notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) } - job, jobs := getRunJobs(ctx, runIndex, jobIndex) - if ctx.Written() { - return - } - + isRunBlocked := run.Status == actions_model.StatusBlocked if jobIndexStr == "" { // rerun all jobs for _, j := range jobs { // if the job has needs, it should be set to "blocked" status to wait for other jobs - shouldBlock := len(j.Needs) > 0 - if err := rerunJob(ctx, j, shouldBlock); err != nil { + shouldBlockJob := len(j.Needs) > 0 || isRunBlocked + if err := rerunJob(ctx, j, shouldBlockJob); err != nil { ctx.ServerError("RerunJob", err) return } @@ -459,8 +488,8 @@ func Rerun(ctx *context_module.Context) { for _, j := range rerunJobs { // jobs other than the specified one should be set to "blocked" status - shouldBlock := j.JobID != job.JobID - if err := rerunJob(ctx, j, shouldBlock); err != nil { + shouldBlockJob := j.JobID != job.JobID || isRunBlocked + if err := rerunJob(ctx, j, shouldBlockJob); err != nil { ctx.ServerError("RerunJob", err) return } @@ -476,21 +505,43 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou } job.TaskID = 0 - job.Status = actions_model.StatusWaiting - if shouldBlock { - job.Status = actions_model.StatusBlocked - } + job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting) job.Started = 0 job.Stopped = 0 + job.ConcurrencyGroup = "" + job.ConcurrencyCancel = false + job.IsConcurrencyEvaluated = false + if err := job.LoadRun(ctx); err != nil { + return err + } + + vars, err := actions_model.GetVariablesOfRun(ctx, job.Run) + if err != nil { + return fmt.Errorf("get run %d variables: %w", job.Run.ID, err) + } + + if job.RawConcurrency != "" && !shouldBlock { + err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars) + if err != nil { + return fmt.Errorf("evaluate job concurrency: %w", err) + } + + job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job) + if err != nil { + return err + } + } + if err := db.WithTx(ctx, func(ctx context.Context) error { - _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped") + updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"} + _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...) return err }); err != nil { return err } - actions_service.CreateCommitStatus(ctx, job) + actions_service.CreateCommitStatusForRunJobs(ctx, job.Run, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) return nil @@ -518,52 +569,34 @@ func Logs(ctx *context_module.Context) { func Cancel(ctx *context_module.Context) { runIndex := getRunIndex(ctx) - _, jobs := getRunJobs(ctx, runIndex, -1) + firstJob, jobs := getRunJobs(ctx, runIndex, -1) if ctx.Written() { return } - var updatedjobs []*actions_model.ActionRunJob + var updatedJobs []*actions_model.ActionRunJob if err := db.WithTx(ctx, func(ctx context.Context) error { - for _, job := range jobs { - status := job.Status - if status.IsDone() { - continue - } - if job.TaskID == 0 { - job.Status = actions_model.StatusCancelled - job.Stopped = timeutil.TimeStampNow() - n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") - if err != nil { - return err - } - if n == 0 { - return errors.New("job has changed, try again") - } - if n > 0 { - updatedjobs = append(updatedjobs, job) - } - continue - } - if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil { - return err - } + cancelledJobs, err := actions_model.CancelJobs(ctx, jobs) + if err != nil { + return fmt.Errorf("cancel jobs: %w", err) } + updatedJobs = append(updatedJobs, cancelledJobs...) return nil }); err != nil { ctx.ServerError("StopTask", err) return } - actions_service.CreateCommitStatus(ctx, jobs...) + actions_service.CreateCommitStatusForRunJobs(ctx, firstJob.Run, jobs...) + actions_service.EmitJobsIfReadyByJobs(updatedJobs) - for _, job := range updatedjobs { + for _, job := range updatedJobs { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - if len(updatedjobs) > 0 { - job := updatedjobs[0] + if len(updatedJobs) > 0 { + job := updatedJobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) } ctx.JSONOK() @@ -579,40 +612,44 @@ func Approve(ctx *context_module.Context) { run := current.Run doer := ctx.Doer - var updatedjobs []*actions_model.ActionRunJob + var updatedJobs []*actions_model.ActionRunJob - if err := db.WithTx(ctx, func(ctx context.Context) error { + err := db.WithTx(ctx, func(ctx context.Context) (err error) { run.NeedApproval = false run.ApprovedBy = doer.ID if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil { return err } for _, job := range jobs { - if len(job.Needs) == 0 && job.Status.IsBlocked() { - job.Status = actions_model.StatusWaiting + job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job) + if err != nil { + return err + } + if job.Status == actions_model.StatusWaiting { n, err := actions_model.UpdateRunJob(ctx, job, nil, "status") if err != nil { return err } if n > 0 { - updatedjobs = append(updatedjobs, job) + updatedJobs = append(updatedJobs, job) } } } return nil - }); err != nil { + }) + if err != nil { ctx.ServerError("UpdateRunJob", err) return } - actions_service.CreateCommitStatus(ctx, jobs...) + actions_service.CreateCommitStatusForRunJobs(ctx, current.Run, jobs...) - if len(updatedjobs) > 0 { - job := updatedjobs[0] + if len(updatedJobs) > 0 { + job := updatedJobs[0] actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) } - for _, job := range updatedjobs { + for _, job := range updatedJobs { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 2a5ac10282..8c630cb35f 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -295,14 +295,14 @@ func EditFile(ctx *context.Context) { } defer dataRc.Close() - ctx.Data["FileSize"] = fInfo.fileSize + ctx.Data["FileSize"] = fInfo.blobOrLfsSize // Only some file types are editable online as text. if fInfo.isLFSFile() { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") } else if !fInfo.st.IsRepresentableAsText() { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") - } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + } else if fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file") } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 1b7e75f84e..69b93dd060 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -7,11 +7,10 @@ package repo import ( "bytes" "compress/gzip" - gocontext "context" "fmt" "net/http" "os" - "path/filepath" + "path" "regexp" "slices" "strconv" @@ -27,6 +26,7 @@ import ( "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -342,11 +342,11 @@ type serviceHandler struct { environ []string } -func (h *serviceHandler) getRepoDir() string { +func (h *serviceHandler) getStorageRepo() gitrepo.Repository { if h.isWiki { - return h.repo.WikiPath() + return h.repo.WikiStorageRepo() } - return h.repo.RepoPath() + return h.repo } func setHeaderNoCache(ctx *context.Context) { @@ -378,19 +378,10 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string ctx.Resp.WriteHeader(http.StatusBadRequest) return } - reqFile := filepath.Join(h.getRepoDir(), filepath.Clean(file)) - - fi, err := os.Stat(reqFile) - if os.IsNotExist(err) { - ctx.Resp.WriteHeader(http.StatusNotFound) - return - } + fs := gitrepo.GetRepoFS(h.getStorageRepo()) ctx.Resp.Header().Set("Content-Type", contentType) - ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10)) - // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat)) - http.ServeFile(ctx.Resp, ctx.Req, reqFile) + http.ServeFileFS(ctx.Resp, ctx.Req, fs, path.Clean(file)) } // one or more key=value pairs separated by colons @@ -416,6 +407,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { expectedContentType := fmt.Sprintf("application/x-git-%s-request", service) if ctx.Req.Header.Get("Content-Type") != expectedContentType { log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType) + // FIXME: why it's 401 if the content type is unexpected? ctx.Resp.WriteHeader(http.StatusUnauthorized) return } @@ -423,6 +415,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { cmd, err := prepareGitCmdWithAllowedService(service) if err != nil { log.Error("Failed to prepareGitCmdWithService: %v", err) + // FIXME: why it's 401 if the service type doesn't supported? ctx.Resp.WriteHeader(http.StatusUnauthorized) return } @@ -449,17 +442,14 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { } var stderr bytes.Buffer - if err := cmd.AddArguments("--stateless-rpc"). - AddDynamicArguments(h.getRepoDir()). - WithDir(h.getRepoDir()). + if err := gitrepo.RunCmd(ctx, h.getStorageRepo(), cmd.AddArguments("--stateless-rpc", "."). WithEnv(append(os.Environ(), h.environ...)). WithStderr(&stderr). WithStdin(reqBody). WithStdout(ctx.Resp). - WithUseContextTimeout(true). - Run(ctx); err != nil { + WithUseContextTimeout(true)); err != nil { if !git.IsErrCanceledOrKilled(err) { - log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String()) + log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getStorageRepo().RelativePath(), err, stderr.String()) } return } @@ -496,14 +486,6 @@ func getServiceType(ctx *context.Context) string { return "" } -func updateServerInfo(ctx gocontext.Context, dir string) []byte { - out, _, err := gitcmd.NewCommand("update-server-info").WithDir(dir).RunStdBytes(ctx) - if err != nil { - log.Error(fmt.Sprintf("%v - %s", err, string(out))) - } - return out -} - func packetWrite(str string) []byte { s := strconv.FormatInt(int64(len(str)+4), 16) if len(s)%4 != 0 { @@ -527,10 +509,8 @@ func GetInfoRefs(ctx *context.Context) { } h.environ = append(os.Environ(), h.environ...) - refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", "."). - WithEnv(h.environ). - WithDir(h.getRepoDir()). - RunStdBytes(ctx) + refs, _, err := gitrepo.RunCmdBytes(ctx, h.getStorageRepo(), cmd.AddArguments("--stateless-rpc", "--advertise-refs", "."). + WithEnv(h.environ)) if err != nil { log.Error(fmt.Sprintf("%v - %s", err, string(refs))) } @@ -541,7 +521,9 @@ func GetInfoRefs(ctx *context.Context) { _, _ = ctx.Resp.Write([]byte("0000")) _, _ = ctx.Resp.Write(refs) } else { - updateServerInfo(ctx, h.getRepoDir()) + if err := gitrepo.UpdateServerInfo(ctx, h.getStorageRepo()); err != nil { + log.Error("Failed to update server info: %v", err) + } h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs") } } diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index af6708e841..a558231df1 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -270,8 +270,7 @@ func LFSFileGet(ctx *context.Context) { // FIXME: there is no IsPlainText set, but template uses it ctx.Data["IsTextFile"] = st.IsText() ctx.Data["FileSize"] = meta.Size - // FIXME: the last field is the URL-base64-encoded filename, it should not be "direct" - ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct") + ctx.Data["RawFileLink"] = fmt.Sprintf("%s/%s/%s.git/info/lfs/objects/%s", setting.AppSubURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid)) switch { case st.IsRepresentableAsText(): if meta.Size >= setting.UI.MaxDisplayFileSize { diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 1a4f590e10..dd887d6edf 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -29,7 +29,6 @@ import ( "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" actions_service "code.gitea.io/gitea/services/actions" - asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/migrations" @@ -62,7 +61,7 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner) - signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) + signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository) ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled @@ -105,7 +104,7 @@ func SettingsPost(ctx *context.Context) { ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval - signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) + signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository) ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index e47bc56d08..d294934622 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -60,9 +60,9 @@ const ( ) type fileInfo struct { - fileSize int64 - lfsMeta *lfs.Pointer - st typesniffer.SniffedType + blobOrLfsSize int64 + lfsMeta *lfs.Pointer + st typesniffer.SniffedType } func (fi *fileInfo) isLFSFile() bool { @@ -81,7 +81,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b n, _ := util.ReadAtMost(dataRc, buf) buf = buf[:n] - fi = &fileInfo{fileSize: blob.Size(), st: typesniffer.DetectContentType(buf)} + fi = &fileInfo{blobOrLfsSize: blob.Size(), st: typesniffer.DetectContentType(buf)} // FIXME: what happens when README file is an image? if !fi.st.IsText() || !setting.LFS.StartServer { @@ -114,7 +114,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b } buf = buf[:n] fi.st = typesniffer.DetectContentType(buf) - fi.fileSize = blob.Size() + fi.blobOrLfsSize = meta.Pointer.Size fi.lfsMeta = &meta.Pointer return buf, dataRc, fi, nil } diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index afbbad4859..7f67034ada 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -226,7 +226,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { } ctx.Data["IsLFSFile"] = fInfo.isLFSFile() - ctx.Data["FileSize"] = fInfo.fileSize + ctx.Data["FileSize"] = fInfo.blobOrLfsSize ctx.Data["IsRepresentableAsText"] = fInfo.st.IsRepresentableAsText() ctx.Data["IsExecutable"] = entry.IsExecutable() ctx.Data["CanCopyContent"] = fInfo.st.IsRepresentableAsText() || fInfo.st.IsImage() @@ -243,7 +243,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) switch { - case fInfo.fileSize >= setting.UI.MaxDisplayFileSize: + case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize: ctx.Data["IsFileTooLarge"] = true case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader): // it also sets ctx.Data["FileContent"] and more diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 88d9fd8fe2..6b161df392 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -6,7 +6,6 @@ package repo import ( "errors" "fmt" - "html/template" "net/http" "path" "strconv" @@ -76,16 +75,24 @@ func prepareOpenWithEditorApps(ctx *context.Context) { } for _, app := range apps { schema, _, _ := strings.Cut(app.OpenURL, ":") - var iconHTML template.HTML - if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" { - iconHTML = svg.RenderHTML("gitea-"+schema, 16) - } else { - iconHTML = svg.RenderHTML("gitea-git", 16) // TODO: it could support user's customized icon in the future + + var iconName string + switch schema { + case "vscode": + iconName = "octicon-vscode" + case "vscodium": + iconName = "gitea-vscodium" + case "jetbrains": + iconName = "gitea-jetbrains" + default: + // TODO: it could support user's customized icon in the future + iconName = "gitea-git" } + tmplApps = append(tmplApps, map[string]any{ "DisplayName": app.DisplayName, "OpenURL": app.OpenURL, - "IconHTML": iconHTML, + "IconHTML": svg.RenderHTML(iconName, 16), }) } ctx.Data["OpenWithEditorApps"] = tmplApps diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index ba03febff3..edf38b7892 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -170,7 +170,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["FileIsText"] = fInfo.st.IsText() ctx.Data["FileTreePath"] = readmeFullPath - ctx.Data["FileSize"] = fInfo.fileSize + ctx.Data["FileSize"] = fInfo.blobOrLfsSize ctx.Data["IsLFSFile"] = fInfo.isLFSFile() if fInfo.isLFSFile() { @@ -182,7 +182,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil return } - if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + if fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize { // Pretend that this is a normal text file to display 'This file is too large to be shown' ctx.Data["IsFileTooLarge"] = true return diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 3c7aa0b1a5..e49bda1b16 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" notify_service "code.gitea.io/gitea/services/notify" ) @@ -36,13 +37,19 @@ func StopEndlessTasks(ctx context.Context) error { } func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.ActionRunJob) { - if len(jobs) > 0 { - CreateCommitStatus(ctx, jobs...) - for _, job := range jobs { - _ = job.LoadAttributes(ctx) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + if len(jobs) == 0 { + return + } + for _, job := range jobs { + if err := job.LoadAttributes(ctx); err != nil { + log.Error("Failed to load job attributes: %v", err) + continue } - job := jobs[0] + CreateCommitStatusForRunJobs(ctx, job.Run, job) + notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } + + if job := jobs[0]; job.Run != nil && job.Run.Repo != nil { notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } } @@ -50,15 +57,84 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { jobs, err := actions_model.CancelPreviousJobs(ctx, repoID, ref, workflowID, event) notifyWorkflowJobStatusUpdate(ctx, jobs) + EmitJobsIfReadyByJobs(jobs) return err } func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error { jobs, err := actions_model.CleanRepoScheduleTasks(ctx, repo) notifyWorkflowJobStatusUpdate(ctx, jobs) + EmitJobsIfReadyByJobs(jobs) return err } +func shouldBlockJobByConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (bool, error) { + if job.RawConcurrency != "" && !job.IsConcurrencyEvaluated { + // when the job depends on other jobs, we cannot evaluate its concurrency, so it should be blocked and will be evaluated again when its dependencies are done + return true, nil + } + + if job.ConcurrencyGroup == "" || job.ConcurrencyCancel { + return false, nil + } + + runs, jobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning}) + if err != nil { + return false, fmt.Errorf("GetConcurrentRunsAndJobs: %w", err) + } + + return len(runs) > 0 || len(jobs) > 0, nil +} + +// PrepareToStartJobWithConcurrency prepares a job to start by its evaluated concurrency group and cancelling previous jobs if necessary. +// It returns the new status of the job (either StatusBlocked or StatusWaiting) and any error encountered during the process. +func PrepareToStartJobWithConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (actions_model.Status, error) { + shouldBlock, err := shouldBlockJobByConcurrency(ctx, job) + if err != nil { + return actions_model.StatusBlocked, err + } + + // even if the current job is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group + jobs, err := actions_model.CancelPreviousJobsByJobConcurrency(ctx, job) + if err != nil { + return actions_model.StatusBlocked, fmt.Errorf("CancelPreviousJobsByJobConcurrency: %w", err) + } + notifyWorkflowJobStatusUpdate(ctx, jobs) + + return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), nil +} + +func shouldBlockRunByConcurrency(ctx context.Context, actionRun *actions_model.ActionRun) (bool, error) { + if actionRun.ConcurrencyGroup == "" || actionRun.ConcurrencyCancel { + return false, nil + } + + runs, jobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning}) + if err != nil { + return false, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + + return len(runs) > 0 || len(jobs) > 0, nil +} + +// PrepareToStartRunWithConcurrency prepares a run to start by its evaluated concurrency group and cancelling previous jobs if necessary. +// It returns the new status of the run (either StatusBlocked or StatusWaiting) and any error encountered during the process. +func PrepareToStartRunWithConcurrency(ctx context.Context, run *actions_model.ActionRun) (actions_model.Status, error) { + shouldBlock, err := shouldBlockRunByConcurrency(ctx, run) + if err != nil { + return actions_model.StatusBlocked, err + } + + // even if the current run is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group + jobs, err := actions_model.CancelPreviousJobsByRunConcurrency(ctx, run) + if err != nil { + return actions_model.StatusBlocked, fmt.Errorf("CancelPreviousJobsByRunConcurrency: %w", err) + } + notifyWorkflowJobStatusUpdate(ctx, jobs) + + return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), nil +} + func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { tasks, err := db.Find[actions_model.ActionTask](ctx, opts) if err != nil { @@ -95,6 +171,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { } notifyWorkflowJobStatusUpdate(ctx, jobs) + EmitJobsIfReadyByJobs(jobs) return nil } @@ -103,7 +180,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { func CancelAbandonedJobs(ctx context.Context) error { jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{ Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked}, - UpdatedBefore: timeutil.TimeStamp(time.Now().Add(-setting.Actions.AbandonedJobTimeout).Unix()), + UpdatedBefore: timeutil.TimeStampNow().AddDuration(-setting.Actions.AbandonedJobTimeout), }) if err != nil { log.Warn("find abandoned tasks: %v", err) @@ -114,6 +191,7 @@ func CancelAbandonedJobs(ctx context.Context) error { // Collect one job per run to send workflow run status update updatedRuns := map[int64]*actions_model.ActionRunJob{} + updatedJobs := []*actions_model.ActionRunJob{} for _, job := range jobs { job.Status = actions_model.StatusCancelled @@ -136,8 +214,12 @@ func CancelAbandonedJobs(ctx context.Context) error { log.Warn("cancel abandoned job %v: %v", job.ID, err) // go on } - CreateCommitStatus(ctx, job) + if job.Run == nil || job.Run.Repo == nil { + continue // error occurs during loading attributes, the following code that depends on "Run.Repo" will fail, so ignore and skip + } + CreateCommitStatusForRunJobs(ctx, job.Run, job) if updated { + updatedJobs = append(updatedJobs, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } } @@ -145,6 +227,7 @@ func CancelAbandonedJobs(ctx context.Context) error { for _, job := range updatedRuns { notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } + EmitJobsIfReadyByJobs(updatedJobs) return nil } diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index ef241e5091..d3f2b0f3cc 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -8,14 +8,15 @@ import ( "errors" "fmt" "path" + "strconv" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/commitstatus" - git "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" webhook_module "code.gitea.io/gitea/modules/webhook" commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" @@ -23,38 +24,46 @@ import ( "github.com/nektos/act/pkg/jobparser" ) -// CreateCommitStatus creates a commit status for the given job. +// CreateCommitStatusForRunJobs creates a commit status for the given job if it has a supported event and related commit. // It won't return an error failed, but will log it, because it's not critical. -func CreateCommitStatus(ctx context.Context, jobs ...*actions_model.ActionRunJob) { +func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.ActionRun, jobs ...*actions_model.ActionRunJob) { + // don't create commit status for cron job + if run.ScheduleID != 0 { + return + } + + event, commitID, err := getCommitStatusEventNameAndCommitID(run) + if err != nil { + log.Error("GetCommitStatusEventNameAndSHA: %v", err) + } + if event == "" || commitID == "" { + return // unsupported event, or no commit id, or error occurs, do nothing + } + + if err = run.LoadAttributes(ctx); err != nil { + log.Error("run.LoadAttributes: %v", err) + return + } + for _, job := range jobs { - if err := createCommitStatus(ctx, job); err != nil { + if err = createCommitStatus(ctx, run.Repo, event, commitID, run, job); err != nil { log.Error("Failed to create commit status for job %d: %v", job.ID, err) } } } -func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) error { - if err := job.LoadAttributes(ctx); err != nil { - return fmt.Errorf("load run: %w", err) - } - - run := job.Run - - var ( - sha string - event string - ) +func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, commitID string, _ error) { switch run.Event { case webhook_module.HookEventPush: event = "push" payload, err := run.GetPushEventPayload() if err != nil { - return fmt.Errorf("GetPushEventPayload: %w", err) + return "", "", fmt.Errorf("GetPushEventPayload: %w", err) } if payload.HeadCommit == nil { - return errors.New("head commit is missing in event payload") + return "", "", errors.New("head commit is missing in event payload") } - sha = payload.HeadCommit.ID + commitID = payload.HeadCommit.ID case // pull_request webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, @@ -69,32 +78,33 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er } payload, err := run.GetPullRequestEventPayload() if err != nil { - return fmt.Errorf("GetPullRequestEventPayload: %w", err) + return "", "", fmt.Errorf("GetPullRequestEventPayload: %w", err) } if payload.PullRequest == nil { - return errors.New("pull request is missing in event payload") + return "", "", errors.New("pull request is missing in event payload") } else if payload.PullRequest.Head == nil { - return errors.New("head of pull request is missing in event payload") + return "", "", errors.New("head of pull request is missing in event payload") } - sha = payload.PullRequest.Head.Sha + commitID = payload.PullRequest.Head.Sha case webhook_module.HookEventRelease: event = string(run.Event) - sha = run.CommitSHA - default: - return nil + commitID = run.CommitSHA + default: // do nothing, return empty } + return event, commitID, nil +} - repo := run.Repo +func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event, commitID string, run *actions_model.ActionRun, job *actions_model.ActionRunJob) error { // TODO: store workflow name as a field in ActionRun to avoid parsing runName := path.Base(run.WorkflowID) if wfs, err := jobparser.Parse(job.WorkflowPayload); err == nil && len(wfs) > 0 { runName = wfs[0].Name } - ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event) + ctxName := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event) state := toCommitStatus(job.Status) - if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil { + if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll); err == nil { for _, v := range statuses { - if v.Context == ctxname { + if v.Context == ctxName { if v.State == state { // no need to update return nil @@ -106,7 +116,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return fmt.Errorf("GetLatestCommitStatus: %w", err) } - description := "" + var description string switch job.Status { // TODO: if we want support description in different languages, we need to support i18n placeholders in it case actions_model.StatusSuccess: @@ -123,6 +133,8 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er description = "Waiting to run" case actions_model.StatusBlocked: description = "Blocked by required conditions" + default: + description = "Unknown status: " + strconv.Itoa(int(job.Status)) } index, err := getIndexOfJob(ctx, job) @@ -131,20 +143,16 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er } creator := user_model.NewActionsUser() - commitID, err := git.NewIDFromString(sha) - if err != nil { - return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err) - } status := git_model.CommitStatus{ - SHA: sha, + SHA: commitID, TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index), Description: description, - Context: ctxname, + Context: ctxName, CreatorID: creator.ID, State: state, } - return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &status) + return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID, &status) } func toCommitStatus(status actions_model.Status) commitstatus.CommitStatusState { diff --git a/services/actions/concurrency.go b/services/actions/concurrency.go new file mode 100644 index 0000000000..0908302709 --- /dev/null +++ b/services/actions/concurrency.go @@ -0,0 +1,115 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/json" + api "code.gitea.io/gitea/modules/structs" + + "github.com/nektos/act/pkg/jobparser" + act_model "github.com/nektos/act/pkg/model" + "gopkg.in/yaml.v3" +) + +// EvaluateRunConcurrencyFillModel evaluates the expressions in a run-level (workflow) concurrency, +// and fills the run's model fields with `concurrency.group` and `concurrency.cancel-in-progress`. +// Workflow-level concurrency doesn't depend on the job outputs, so it can always be evaluated if there is no syntax error. +// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency +func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, wfRawConcurrency *act_model.RawConcurrency, vars map[string]string) error { + if err := run.LoadAttributes(ctx); err != nil { + return fmt.Errorf("run LoadAttributes: %w", err) + } + + actionsRunCtx := GenerateGiteaContext(run, nil) + jobResults := map[string]*jobparser.JobResult{"": {}} + inputs, err := getInputsFromRun(run) + if err != nil { + return fmt.Errorf("get inputs: %w", err) + } + + rawConcurrency, err := yaml.Marshal(wfRawConcurrency) + if err != nil { + return fmt.Errorf("marshal raw concurrency: %w", err) + } + run.RawConcurrency = string(rawConcurrency) + run.ConcurrencyGroup, run.ConcurrencyCancel, err = jobparser.EvaluateConcurrency(wfRawConcurrency, "", nil, actionsRunCtx, jobResults, vars, inputs) + if err != nil { + return fmt.Errorf("evaluate concurrency: %w", err) + } + return nil +} + +func findJobNeedsAndFillJobResults(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*jobparser.JobResult, error) { + taskNeeds, err := FindTaskNeeds(ctx, job) + if err != nil { + return nil, fmt.Errorf("find task needs: %w", err) + } + jobResults := make(map[string]*jobparser.JobResult, len(taskNeeds)) + for jobID, taskNeed := range taskNeeds { + jobResult := &jobparser.JobResult{ + Result: taskNeed.Result.String(), + Outputs: taskNeed.Outputs, + } + jobResults[jobID] = jobResult + } + jobResults[job.JobID] = &jobparser.JobResult{ + Needs: job.Needs, + } + return jobResults, nil +} + +// EvaluateJobConcurrencyFillModel evaluates the expressions in a job-level concurrency, +// and fills the job's model fields with `concurrency.group` and `concurrency.cancel-in-progress`. +// Job-level concurrency may depend on other job's outputs (via `needs`): `concurrency.group: my-group-${{ needs.job1.outputs.out1 }}` +// If the needed jobs haven't been executed yet, this evaluation will also fail. +// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idconcurrency +func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, actionRunJob *actions_model.ActionRunJob, vars map[string]string) error { + if err := actionRunJob.LoadAttributes(ctx); err != nil { + return fmt.Errorf("job LoadAttributes: %w", err) + } + + var rawConcurrency act_model.RawConcurrency + if err := yaml.Unmarshal([]byte(actionRunJob.RawConcurrency), &rawConcurrency); err != nil { + return fmt.Errorf("unmarshal raw concurrency: %w", err) + } + + actionsJobCtx := GenerateGiteaContext(run, actionRunJob) + + jobResults, err := findJobNeedsAndFillJobResults(ctx, actionRunJob) + if err != nil { + return fmt.Errorf("find job needs and fill job results: %w", err) + } + + inputs, err := getInputsFromRun(run) + if err != nil { + return fmt.Errorf("get inputs: %w", err) + } + + workflowJob, err := actionRunJob.ParseJob() + if err != nil { + return fmt.Errorf("load job %d: %w", actionRunJob.ID, err) + } + + actionRunJob.ConcurrencyGroup, actionRunJob.ConcurrencyCancel, err = jobparser.EvaluateConcurrency(&rawConcurrency, actionRunJob.JobID, workflowJob, actionsJobCtx, jobResults, vars, inputs) + if err != nil { + return fmt.Errorf("evaluate concurrency: %w", err) + } + actionRunJob.IsConcurrencyEvaluated = true + return nil +} + +func getInputsFromRun(run *actions_model.ActionRun) (map[string]any, error) { + if run.Event != "workflow_dispatch" { + return map[string]any{}, nil + } + var payload api.WorkflowDispatchPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return payload.Inputs, nil +} diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index 47c9f59094..74a8a127ef 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -10,12 +10,14 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" - "github.com/nektos/act/pkg/jobparser" "xorm.io/builder" ) @@ -25,7 +27,7 @@ type jobUpdate struct { RunID int64 } -func EmitJobsIfReady(runID int64) error { +func EmitJobsIfReadyByRun(runID int64) error { err := jobEmitterQueue.Push(&jobUpdate{ RunID: runID, }) @@ -35,53 +37,77 @@ func EmitJobsIfReady(runID int64) error { return err } +func EmitJobsIfReadyByJobs(jobs []*actions_model.ActionRunJob) { + checkedRuns := make(container.Set[int64]) + for _, job := range jobs { + if !job.Status.IsDone() || checkedRuns.Contains(job.RunID) { + continue + } + if err := EmitJobsIfReadyByRun(job.RunID); err != nil { + log.Error("Check jobs of run %d: %v", job.RunID, err) + } + checkedRuns.Add(job.RunID) + } +} + func jobEmitterQueueHandler(items ...*jobUpdate) []*jobUpdate { ctx := graceful.GetManager().ShutdownContext() var ret []*jobUpdate for _, update := range items { - if err := checkJobsOfRun(ctx, update.RunID); err != nil { + if err := checkJobsByRunID(ctx, update.RunID); err != nil { + log.Error("check run %d: %v", update.RunID, err) ret = append(ret, update) } } return ret } -func checkJobsOfRun(ctx context.Context, runID int64) error { - jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: runID}) - if err != nil { - return err +func checkJobsByRunID(ctx context.Context, runID int64) error { + run, exist, err := db.GetByID[actions_model.ActionRun](ctx, runID) + if !exist { + return fmt.Errorf("run %d does not exist", runID) } - var updatedjobs []*actions_model.ActionRunJob + if err != nil { + return fmt.Errorf("get action run: %w", err) + } + var jobs, updatedJobs []*actions_model.ActionRunJob if err := db.WithTx(ctx, func(ctx context.Context) error { - idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs)) - for _, job := range jobs { - idToJobs[job.JobID] = append(idToJobs[job.JobID], job) + // check jobs of the current run + if js, ujs, err := checkJobsOfRun(ctx, run); err != nil { + return err + } else { + jobs = append(jobs, js...) + updatedJobs = append(updatedJobs, ujs...) } - - updates := newJobStatusResolver(jobs).Resolve() - for _, job := range jobs { - if status, ok := updates[job.ID]; ok { - job.Status = status - if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil { - return err - } else if n != 1 { - return fmt.Errorf("no affected for updating blocked job %v", job.ID) - } - updatedjobs = append(updatedjobs, job) - } + if js, ujs, err := checkRunConcurrency(ctx, run); err != nil { + return err + } else { + jobs = append(jobs, js...) + updatedJobs = append(updatedJobs, ujs...) } return nil }); err != nil { return err } - CreateCommitStatus(ctx, jobs...) - for _, job := range updatedjobs { + CreateCommitStatusForRunJobs(ctx, run, jobs...) + for _, job := range updatedJobs { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - if len(jobs) > 0 { + runJobs := make(map[int64][]*actions_model.ActionRunJob) + for _, job := range jobs { + runJobs[job.RunID] = append(runJobs[job.RunID], job) + } + runUpdatedJobs := make(map[int64][]*actions_model.ActionRunJob) + for _, uj := range updatedJobs { + runUpdatedJobs[uj.RunID] = append(runUpdatedJobs[uj.RunID], uj) + } + for runID, js := range runJobs { + if len(runUpdatedJobs[runID]) == 0 { + continue + } runUpdated := true - for _, job := range jobs { + for _, job := range js { if !job.Status.IsDone() { runUpdated = false break @@ -94,6 +120,118 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { return nil } +// findBlockedRunByConcurrency finds the blocked concurrent run in a repo and returns `nil, nil` when there is no blocked run. +func findBlockedRunByConcurrency(ctx context.Context, repoID int64, concurrencyGroup string) (*actions_model.ActionRun, error) { + if concurrencyGroup == "" { + return nil, nil + } + cRuns, cJobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, repoID, concurrencyGroup, []actions_model.Status{actions_model.StatusBlocked}) + if err != nil { + return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + + // There can be at most one blocked run or job + var concurrentRun *actions_model.ActionRun + if len(cRuns) > 0 { + concurrentRun = cRuns[0] + } else if len(cJobs) > 0 { + jobRun, exist, err := db.GetByID[actions_model.ActionRun](ctx, cJobs[0].RunID) + if !exist { + return nil, fmt.Errorf("run %d does not exist", cJobs[0].RunID) + } + if err != nil { + return nil, fmt.Errorf("get run by job %d: %w", cJobs[0].ID, err) + } + concurrentRun = jobRun + } + + return concurrentRun, nil +} + +func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) { + checkedConcurrencyGroup := make(container.Set[string]) + + // check run (workflow-level) concurrency + if run.ConcurrencyGroup != "" { + concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, run.ConcurrencyGroup) + if err != nil { + return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err) + } + if concurrentRun != nil && !concurrentRun.NeedApproval { + js, ujs, err := checkJobsOfRun(ctx, concurrentRun) + if err != nil { + return nil, nil, err + } + jobs = append(jobs, js...) + updatedJobs = append(updatedJobs, ujs...) + } + checkedConcurrencyGroup.Add(run.ConcurrencyGroup) + } + + // check job concurrency + runJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + if err != nil { + return nil, nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + } + for _, job := range runJobs { + if !job.Status.IsDone() { + continue + } + if job.ConcurrencyGroup == "" && checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) { + continue + } + concurrentRun, err := findBlockedRunByConcurrency(ctx, job.RepoID, job.ConcurrencyGroup) + if err != nil { + return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err) + } + if concurrentRun != nil && !concurrentRun.NeedApproval { + js, ujs, err := checkJobsOfRun(ctx, concurrentRun) + if err != nil { + return nil, nil, err + } + jobs = append(jobs, js...) + updatedJobs = append(updatedJobs, ujs...) + } + checkedConcurrencyGroup.Add(job.ConcurrencyGroup) + } + return jobs, updatedJobs, nil +} + +func checkJobsOfRun(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) { + jobs, err = db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + if err != nil { + return nil, nil, err + } + vars, err := actions_model.GetVariablesOfRun(ctx, run) + if err != nil { + return nil, nil, err + } + + if err = db.WithTx(ctx, func(ctx context.Context) error { + for _, job := range jobs { + job.Run = run + } + + updates := newJobStatusResolver(jobs, vars).Resolve(ctx) + for _, job := range jobs { + if status, ok := updates[job.ID]; ok { + job.Status = status + if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil { + return err + } else if n != 1 { + return fmt.Errorf("no affected for updating blocked job %v", job.ID) + } + updatedJobs = append(updatedJobs, job) + } + } + return nil + }); err != nil { + return nil, nil, err + } + + return jobs, updatedJobs, nil +} + func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, job *actions_model.ActionRunJob) { job.Run = nil if err := job.LoadAttributes(ctx); err != nil { @@ -107,9 +245,10 @@ type jobStatusResolver struct { statuses map[int64]actions_model.Status needs map[int64][]int64 jobMap map[int64]*actions_model.ActionRunJob + vars map[string]string } -func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver { +func newJobStatusResolver(jobs actions_model.ActionJobList, vars map[string]string) *jobStatusResolver { idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs)) jobMap := make(map[int64]*actions_model.ActionRunJob) for _, job := range jobs { @@ -131,13 +270,14 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver { statuses: statuses, needs: needs, jobMap: jobMap, + vars: vars, } } -func (r *jobStatusResolver) Resolve() map[int64]actions_model.Status { +func (r *jobStatusResolver) Resolve(ctx context.Context) map[int64]actions_model.Status { ret := map[int64]actions_model.Status{} for i := 0; i < len(r.statuses); i++ { - updated := r.resolve() + updated := r.resolve(ctx) if len(updated) == 0 { return ret } @@ -149,43 +289,86 @@ func (r *jobStatusResolver) Resolve() map[int64]actions_model.Status { return ret } -func (r *jobStatusResolver) resolve() map[int64]actions_model.Status { +func (r *jobStatusResolver) resolveCheckNeeds(id int64) (allDone, allSucceed bool) { + allDone, allSucceed = true, true + for _, need := range r.needs[id] { + needStatus := r.statuses[need] + if !needStatus.IsDone() { + allDone = false + } + if needStatus.In(actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped) { + allSucceed = false + } + } + return allDone, allSucceed +} + +func (r *jobStatusResolver) resolveJobHasIfCondition(actionRunJob *actions_model.ActionRunJob) (hasIf bool) { + // FIXME evaluate this on the server side + if job, err := actionRunJob.ParseJob(); err == nil { + return len(job.If.Value) > 0 + } + return hasIf +} + +func (r *jobStatusResolver) resolve(ctx context.Context) map[int64]actions_model.Status { ret := map[int64]actions_model.Status{} for id, status := range r.statuses { + actionRunJob := r.jobMap[id] if status != actions_model.StatusBlocked { continue } - allDone, allSucceed := true, true - for _, need := range r.needs[id] { - needStatus := r.statuses[need] - if !needStatus.IsDone() { - allDone = false - } - if needStatus.In(actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped) { - allSucceed = false + allDone, allSucceed := r.resolveCheckNeeds(id) + if !allDone { + continue + } + + // update concurrency and check whether the job can run now + err := updateConcurrencyEvaluationForJobWithNeeds(ctx, actionRunJob, r.vars) + if err != nil { + // The err can be caused by different cases: database error, or syntax error, or the needed jobs haven't completed + // At the moment there is no way to distinguish them. + // Actually, for most cases, the error is caused by "syntax error" / "the needed jobs haven't completed (skipped?)" + // TODO: if workflow or concurrency expression has syntax error, there should be a user error message, need to show it to end users + log.Debug("updateConcurrencyEvaluationForJobWithNeeds failed, this job will stay blocked: job: %d, err: %v", id, err) + continue + } + + shouldStartJob := true + if !allSucceed { + // Not all dependent jobs completed successfully: + // * if the job has "if" condition, it can be started, then the act_runner will evaluate the "if" condition. + // * otherwise, the job should be skipped. + shouldStartJob = r.resolveJobHasIfCondition(actionRunJob) + } + + newStatus := util.Iif(shouldStartJob, actions_model.StatusWaiting, actions_model.StatusSkipped) + if newStatus == actions_model.StatusWaiting { + newStatus, err = PrepareToStartJobWithConcurrency(ctx, actionRunJob) + if err != nil { + log.Error("ShouldBlockJobByConcurrency failed, this job will stay blocked: job: %d, err: %v", id, err) } } - if allDone { - if allSucceed { - ret[id] = actions_model.StatusWaiting - } else { - // Check if the job has an "if" condition - hasIf := false - if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 { - _, wfJob := wfJobs[0].Job() - hasIf = len(wfJob.If.Value) > 0 - } - if hasIf { - // act_runner will check the "if" condition - ret[id] = actions_model.StatusWaiting - } else { - // If the "if" condition is empty and not all dependent jobs completed successfully, - // the job should be skipped. - ret[id] = actions_model.StatusSkipped - } - } + if newStatus != actions_model.StatusBlocked { + ret[id] = newStatus } } return ret } + +func updateConcurrencyEvaluationForJobWithNeeds(ctx context.Context, actionRunJob *actions_model.ActionRunJob, vars map[string]string) error { + if setting.IsInTesting && actionRunJob.RepoID == 0 { + return nil // for testing purpose only, no repo, no evaluation + } + + err := EvaluateJobConcurrencyFillModel(ctx, actionRunJob.Run, actionRunJob, vars) + if err != nil { + return fmt.Errorf("evaluate job concurrency: %w", err) + } + + if _, err := actions_model.UpdateRunJob(ctx, actionRunJob, nil, "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"); err != nil { + return fmt.Errorf("update run job: %w", err) + } + return nil +} diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go index 58c2dc3b24..a2152fb270 100644 --- a/services/actions/job_emitter_test.go +++ b/services/actions/job_emitter_test.go @@ -129,8 +129,8 @@ jobs: } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := newJobStatusResolver(tt.jobs) - assert.Equal(t, tt.want, r.Resolve()) + r := newJobStatusResolver(tt.jobs, nil) + assert.Equal(t, tt.want, r.Resolve(t.Context())) }) } } diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index d0d2572b0b..d17955b029 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -27,9 +27,7 @@ import ( api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" - notify_service "code.gitea.io/gitea/services/notify" - "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" ) @@ -346,66 +344,10 @@ func handleWorkflows( run.NeedApproval = need - if err := run.LoadAttributes(ctx); err != nil { - log.Error("LoadAttributes: %v", err) + if err := PrepareRunAndInsert(ctx, dwf.Content, run, nil); err != nil { + log.Error("PrepareRunAndInsert: %v", err) continue } - - vars, err := actions_model.GetVariablesOfRun(ctx, run) - if err != nil { - log.Error("GetVariablesOfRun: %v", err) - continue - } - - giteaCtx := GenerateGiteaContext(run, nil) - - jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext())) - if err != nil { - log.Error("jobparser.Parse: %v", err) - continue - } - - if len(jobs) > 0 && jobs[0].RunName != "" { - run.Title = jobs[0].RunName - } - - // cancel running jobs if the event is push or pull_request_sync - if run.Event == webhook_module.HookEventPush || - run.Event == webhook_module.HookEventPullRequestSync { - if err := CancelPreviousJobs( - ctx, - run.RepoID, - run.Ref, - run.WorkflowID, - run.Event, - ); err != nil { - log.Error("CancelPreviousJobs: %v", err) - } - } - - if err := actions_model.InsertRun(ctx, run, jobs); err != nil { - log.Error("InsertRun: %v", err) - continue - } - - alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - log.Error("FindRunJobs: %v", err) - continue - } - CreateCommitStatus(ctx, alljobs...) - if len(alljobs) > 0 { - job := alljobs[0] - err := job.LoadRun(ctx) - if err != nil { - log.Error("LoadRun: %v", err) - continue - } - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } - for _, job := range alljobs { - notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil) - } } return nil } @@ -560,24 +502,6 @@ func handleSchedules( Content: dwf.Content, } - vars, err := actions_model.GetVariablesOfRun(ctx, run.ToActionRun()) - if err != nil { - log.Error("GetVariablesOfRun: %v", err) - continue - } - - giteaCtx := GenerateGiteaContext(run.ToActionRun(), nil) - - jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext())) - if err != nil { - log.Error("jobparser.Parse: %v", err) - continue - } - - if len(jobs) > 0 && jobs[0].RunName != "" { - run.Title = jobs[0].RunName - } - crons = append(crons, run) } diff --git a/services/actions/run.go b/services/actions/run.go new file mode 100644 index 0000000000..90413e9bc2 --- /dev/null +++ b/services/actions/run.go @@ -0,0 +1,178 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + notify_service "code.gitea.io/gitea/services/notify" + + "github.com/nektos/act/pkg/jobparser" + "gopkg.in/yaml.v3" +) + +// PrepareRunAndInsert prepares a run and inserts it into the database +// It parses the workflow content, evaluates concurrency if needed, and inserts the run and its jobs into the database. +// The title will be cut off at 255 characters if it's longer than 255 characters. +func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model.ActionRun, inputsWithDefaults map[string]any) error { + if err := run.LoadAttributes(ctx); err != nil { + return fmt.Errorf("LoadAttributes: %w", err) + } + + vars, err := actions_model.GetVariablesOfRun(ctx, run) + if err != nil { + return fmt.Errorf("GetVariablesOfRun: %w", err) + } + + wfRawConcurrency, err := jobparser.ReadWorkflowRawConcurrency(content) + if err != nil { + return fmt.Errorf("ReadWorkflowRawConcurrency: %w", err) + } + + if wfRawConcurrency != nil { + err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars) + if err != nil { + return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err) + } + } + + giteaCtx := GenerateGiteaContext(run, nil) + + jobs, err := jobparser.Parse(content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()), jobparser.WithInputs(inputsWithDefaults)) + if err != nil { + return fmt.Errorf("parse workflow: %w", err) + } + + if len(jobs) > 0 && jobs[0].RunName != "" { + run.Title = jobs[0].RunName + } + + if err = InsertRun(ctx, run, jobs, vars); err != nil { + return fmt.Errorf("InsertRun: %w", err) + } + + // Load the newly inserted jobs with all fields from database (the job models in InsertRun are partial, so load again) + allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + if err != nil { + return fmt.Errorf("FindRunJob: %w", err) + } + + CreateCommitStatusForRunJobs(ctx, run, allJobs...) + + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) + for _, job := range allJobs { + notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) + } + + return nil +} + +// InsertRun inserts a run +// The title will be cut off at 255 characters if it's longer than 255 characters. +func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow, vars map[string]string) error { + return db.WithTx(ctx, func(ctx context.Context) error { + index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) + if err != nil { + return err + } + run.Index = index + run.Title = util.EllipsisDisplayString(run.Title, 255) + + // check run (workflow-level) concurrency + run.Status, err = PrepareToStartRunWithConcurrency(ctx, run) + if err != nil { + return err + } + + if err := db.Insert(ctx, run); err != nil { + return err + } + + if err := run.LoadRepo(ctx); err != nil { + return err + } + + if err := actions_model.UpdateRepoRunsNumbers(ctx, run.Repo); err != nil { + return err + } + + runJobs := make([]*actions_model.ActionRunJob, 0, len(jobs)) + var hasWaitingJobs bool + for _, v := range jobs { + id, job := v.Job() + needs := job.Needs() + if err := v.SetJob(id, job.EraseNeeds()); err != nil { + return err + } + payload, _ := v.Marshal() + + shouldBlockJob := len(needs) > 0 || run.NeedApproval || run.Status == actions_model.StatusBlocked + + job.Name = util.EllipsisDisplayString(job.Name, 255) + runJob := &actions_model.ActionRunJob{ + RunID: run.ID, + RepoID: run.RepoID, + OwnerID: run.OwnerID, + CommitSHA: run.CommitSHA, + IsForkPullRequest: run.IsForkPullRequest, + Name: job.Name, + WorkflowPayload: payload, + JobID: id, + Needs: needs, + RunsOn: job.RunsOn(), + Status: util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting), + } + // check job concurrency + if job.RawConcurrency != nil { + rawConcurrency, err := yaml.Marshal(job.RawConcurrency) + if err != nil { + return fmt.Errorf("marshal raw concurrency: %w", err) + } + runJob.RawConcurrency = string(rawConcurrency) + + // do not evaluate job concurrency when it requires `needs`, the jobs with `needs` will be evaluated later by job emitter + if len(needs) == 0 { + err = EvaluateJobConcurrencyFillModel(ctx, run, runJob, vars) + if err != nil { + return fmt.Errorf("evaluate job concurrency: %w", err) + } + } + + // If a job needs other jobs ("needs" is not empty), its status is set to StatusBlocked at the entry of the loop + // No need to check job concurrency for a blocked job (it will be checked by job emitter later) + if runJob.Status == actions_model.StatusWaiting { + runJob.Status, err = PrepareToStartJobWithConcurrency(ctx, runJob) + if err != nil { + return fmt.Errorf("prepare to start job with concurrency: %w", err) + } + } + } + + hasWaitingJobs = hasWaitingJobs || runJob.Status == actions_model.StatusWaiting + if err := db.Insert(ctx, runJob); err != nil { + return err + } + + runJobs = append(runJobs, runJob) + } + + run.Status = actions_model.AggregateJobStatus(runJobs) + if err := actions_model.UpdateRun(ctx, run, "status"); err != nil { + return err + } + + // if there is a job in the waiting status, increase tasks version. + if hasWaitingJobs { + if err := actions_model.IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { + return err + } + } + + return nil + }) +} diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index c029c5a1a2..037bf5cddd 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -15,9 +15,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" - notify_service "code.gitea.io/gitea/services/notify" - - "github.com/nektos/act/pkg/jobparser" ) // StartScheduleTasks start the task @@ -53,20 +50,6 @@ func startTasks(ctx context.Context) error { // Loop through each spec and create a schedule task for it for _, row := range specs { - // cancel running jobs if the event is push - if row.Schedule.Event == webhook_module.HookEventPush { - // cancel running jobs of the same workflow - if err := CancelPreviousJobs( - ctx, - row.RepoID, - row.Schedule.Ref, - row.Schedule.WorkflowID, - webhook_module.HookEventSchedule, - ); err != nil { - log.Error("CancelPreviousJobs: %v", err) - } - } - if row.Repo.IsArchived { // Skip if the repo is archived continue @@ -133,34 +116,12 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) Status: actions_model.StatusWaiting, } - vars, err := actions_model.GetVariablesOfRun(ctx, run) - if err != nil { - log.Error("GetVariablesOfRun: %v", err) - return err - } - - // Parse the workflow specification from the cron schedule - workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars)) - if err != nil { - return err - } - + // FIXME cron.Content might be outdated if the workflow file has been changed. + // Load the latest sha from default branch // Insert the action run and its associated jobs into the database - if err := actions_model.InsertRun(ctx, run, workflows); err != nil { + if err := PrepareRunAndInsert(ctx, cron.Content, run, nil); err != nil { return err } - allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - log.Error("FindRunJobs: %v", err) - } - err = run.LoadAttributes(ctx) - if err != nil { - log.Error("LoadAttributes: %v", err) - } - notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) - for _, job := range allJobs { - notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) - } // Return nil if no errors occurred return nil diff --git a/services/actions/task.go b/services/actions/task.go index 6a547c1c12..cf2164f456 100644 --- a/services/actions/task.go +++ b/services/actions/task.go @@ -97,7 +97,7 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv return nil, false, nil } - CreateCommitStatus(ctx, job) + CreateCommitStatusForRunJobs(ctx, job.Run, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, actionTask) return task, true, nil diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 40b34194e9..25801d6fa1 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -8,7 +8,6 @@ import ( "strings" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -16,13 +15,11 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/reqctx" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - notify_service "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" @@ -98,7 +95,6 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } // find workflow from commit - var workflows []*jobparser.SingleWorkflow var entry *git.TreeEntry run := &actions_model.ActionRun{ @@ -152,24 +148,6 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } } - giteaCtx := GenerateGiteaContext(run, nil) - - workflows, err = jobparser.Parse(content, jobparser.WithGitContext(giteaCtx.ToGitHubContext()), jobparser.WithInputs(inputsWithDefaults)) - if err != nil { - return err - } - - if len(workflows) > 0 && workflows[0].RunName != "" { - run.Title = workflows[0].RunName - } - - if len(workflows) == 0 { - return util.ErrorWrapLocale( - util.NewNotExistErrorf("workflow %q doesn't exist", workflowID), - "actions.workflow.not_found", workflowID, - ) - } - // ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context // https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch @@ -187,38 +165,9 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } run.EventPayload = string(eventPayload) - // cancel running jobs of the same workflow - if err := CancelPreviousJobs( - ctx, - run.RepoID, - run.Ref, - run.WorkflowID, - run.Event, - ); err != nil { - log.Error("CancelRunningJobs: %v", err) - } - // Insert the action run and its associated jobs into the database - if err := actions_model.InsertRun(ctx, run, workflows); err != nil { - return fmt.Errorf("InsertRun: %w", err) - } - - allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - log.Error("FindRunJobs: %v", err) - } - CreateCommitStatus(ctx, allJobs...) - if len(allJobs) > 0 { - job := allJobs[0] - err := job.LoadRun(ctx) - if err != nil { - log.Error("LoadRun: %v", err) - } else { - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } - } - for _, job := range allJobs { - notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil) + if err := PrepareRunAndInsert(ctx, content, run, inputsWithDefaults); err != nil { + return fmt.Errorf("PrepareRun: %w", err) } return nil } diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 61b9e56d95..1ed05ba287 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -17,7 +17,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" @@ -109,54 +108,9 @@ func IsErrWontSign(err error) bool { return ok } -// SigningKey returns the KeyID and git Signature for the repo -func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Signature) { - if setting.Repository.Signing.SigningKey == "none" { - return nil, nil - } - - if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { - // Can ignore the error here as it means that commit.gpgsign is not set - value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repoPath).RunStdString(ctx) - sign, valid := git.ParseBool(strings.TrimSpace(value)) - if !sign || !valid { - return nil, nil - } - - format, _, _ := gitcmd.NewCommand("config", "--default", git.SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repoPath).RunStdString(ctx) - signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repoPath).RunStdString(ctx) - signingName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repoPath).RunStdString(ctx) - signingEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repoPath).RunStdString(ctx) - - if strings.TrimSpace(signingKey) == "" { - return nil, nil - } - - return &git.SigningKey{ - KeyID: strings.TrimSpace(signingKey), - Format: strings.TrimSpace(format), - }, &git.Signature{ - Name: strings.TrimSpace(signingName), - Email: strings.TrimSpace(signingEmail), - } - } - - if setting.Repository.Signing.SigningKey == "" { - return nil, nil - } - - return &git.SigningKey{ - KeyID: setting.Repository.Signing.SigningKey, - Format: setting.Repository.Signing.SigningFormat, - }, &git.Signature{ - Name: setting.Repository.Signing.SigningName, - Email: setting.Repository.Signing.SigningEmail, - } -} - // PublicSigningKey gets the public signing key within a provided repository directory func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) { - signingKey, _ := SigningKey(ctx, repoPath) + signingKey, _ := git.GetSigningKey(ctx, repoPath) if signingKey == nil { return "", "", nil } @@ -181,7 +135,7 @@ func PublicSigningKey(ctx context.Context, repoPath string) (content, format str // SignInitialCommit determines if we should sign the initial commit to this repository func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) - signingKey, sig := SigningKey(ctx, repoPath) + signingKey, sig := git.GetSigningKey(ctx, repoPath) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -216,9 +170,8 @@ Loop: // SignWikiCommit determines if we should sign the commits to this repository wiki func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { - repoWikiPath := repo.WikiPath() rules := signingModeFromStrings(setting.Repository.Signing.Wiki) - signingKey, sig := SigningKey(ctx, repoWikiPath) + signingKey, sig := gitrepo.GetSigningKey(ctx, repo.WikiStorageRepo()) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -271,7 +224,7 @@ Loop: // SignCRUDAction determines if we should sign a CRUD commit to this repository func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) - signingKey, sig := SigningKey(ctx, repoPath) + signingKey, sig := git.GetSigningKey(ctx, repoPath) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -335,7 +288,7 @@ func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model. } repo := pr.BaseRepo - signingKey, signer := SigningKey(ctx, repo.RepoPath()) + signingKey, signer := gitrepo.GetSigningKey(ctx, repo) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } diff --git a/services/issue/comments.go b/services/issue/comments.go index 9442701029..3ce2e2a5e1 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -15,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" git_service "code.gitea.io/gitea/services/git" notify_service "code.gitea.io/gitea/services/notify" @@ -151,15 +152,15 @@ func DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_m } // LoadCommentPushCommits Load push commits -func LoadCommentPushCommits(ctx context.Context, c *issues_model.Comment) (err error) { +func LoadCommentPushCommits(ctx context.Context, c *issues_model.Comment) error { if c.Content == "" || c.Commits != nil || c.Type != issues_model.CommentTypePullRequestPush { return nil } var data issues_model.PushActionContent - err = json.Unmarshal([]byte(c.Content), &data) - if err != nil { - return err + if err := json.Unmarshal([]byte(c.Content), &data); err != nil { + log.Debug("Unmarshal: %v", err) // no need to show 500 error to end user when the JSON is broken + return nil } c.IsForcePush = data.IsForcePush @@ -168,9 +169,15 @@ func LoadCommentPushCommits(ctx context.Context, c *issues_model.Comment) (err e if len(data.CommitIDs) != 2 { return nil } - c.OldCommit = data.CommitIDs[0] - c.NewCommit = data.CommitIDs[1] + c.OldCommit, c.NewCommit = data.CommitIDs[0], data.CommitIDs[1] } else { + if err := c.LoadIssue(ctx); err != nil { + return err + } + if err := c.Issue.LoadRepo(ctx); err != nil { + return err + } + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, c.Issue.Repo) if err != nil { return err @@ -179,10 +186,11 @@ func LoadCommentPushCommits(ctx context.Context, c *issues_model.Comment) (err e c.Commits, err = git_service.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) if err != nil { - return err + log.Debug("ConvertFromGitCommit: %v", err) // no need to show 500 error to end user when the commit does not exist + } else { + c.CommitsNum = int64(len(c.Commits)) } - c.CommitsNum = int64(len(c.Commits)) } - return err + return nil } diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 4b19112d3c..da58bbd1b6 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -249,8 +249,6 @@ func checkRecoverableSyncError(stderrMessage string) bool { // runSync returns true if sync finished without error. func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) { - repoPath := m.Repo.RepoPath() - wikiPath := m.Repo.WikiPath() timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo) @@ -311,7 +309,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo // If there is still an error (or there always was an error) if err != nil { log.Error("SyncMirrors [repo: %-v]: failed to update mirror repository:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) - desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage) + desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", m.Repo.RelativePath(), stderrMessage) if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } @@ -320,7 +318,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo } output := stderrBuilder.String() - if err := git.WriteCommitGraph(ctx, repoPath); err != nil { + if err := gitrepo.WriteCommitGraph(ctx, m.Repo); err != nil { log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err) } @@ -394,14 +392,14 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo // If there is still an error (or there always was an error) if err != nil { log.Error("SyncMirrors [repo: %-v Wiki]: failed to update mirror repository wiki:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) - desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage) + desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", m.Repo.WikiStorageRepo().RelativePath(), stderrMessage) if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return nil, false } - if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { + if err := gitrepo.WriteCommitGraph(ctx, m.Repo.WikiStorageRepo()); err != nil { log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err) } } diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 9a832a1350..b61345e830 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -124,14 +124,12 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { performPush := func(repo *repo_model.Repository, isWiki bool) error { var storageRepo gitrepo.Repository = repo - path := repo.RepoPath() if isWiki { storageRepo = repo.WikiStorageRepo() - path = repo.WikiPath() } remoteURL, err := gitrepo.GitRemoteGetURL(ctx, storageRepo, m.RemoteName) if err != nil { - log.Error("GetRemoteURL(%s) Error %v", path, err) + log.Error("GetRemoteURL(%s) Error %v", storageRepo.RelativePath(), err) return errors.New("Unexpected error") } @@ -152,17 +150,17 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { } } - log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName) + log.Trace("Pushing %s mirror[%d] remote %s", storageRepo.RelativePath(), m.ID, m.RemoteName) envs := proxy.EnvWithProxy(remoteURL.URL) - if err := git.Push(ctx, path, git.PushOptions{ + if err := gitrepo.Push(ctx, storageRepo, git.PushOptions{ Remote: m.RemoteName, Force: true, Mirror: true, Timeout: timeout, Env: envs, }); err != nil { - log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err) + log.Error("Error pushing %s mirror[%d] remote %s: %v", storageRepo.RelativePath(), m.ID, m.RemoteName, err) return util.SanitizeErrorCredentialURLs(err) } diff --git a/services/pull/merge.go b/services/pull/merge.go index f1ad8fa17d..1e8e9d444b 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -248,6 +248,11 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U } defer releaser() defer func() { + // This is a duplicated call to AddTestPullRequestTask (it will also be called by the post-receive hook, via a push queue). + // This call will do some operations (push to base repo, sync commit divergence, add PR conflict check queue task, etc) + // immediately instead of waiting for the "push queue"'s task. The code is from https://github.com/go-gitea/gitea/pull/7082. + // But it's really questionable whether it's worth to do it ahead without waiting for the "push queue" task to run. + // TODO: DUPLICATE-PR-TASK: maybe can try to remove this in 1.26 to see if there is any issue. go AddTestPullRequestTask(TestPullRequestOptions{ RepoID: pr.BaseRepo.ID, Doer: doer, diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index 7dedf0d2a0..07935ac16d 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -32,6 +32,9 @@ type mergeContext struct { env []string } +// PrepareGitCmd prepares a git command with the correct directory, environment, and output buffers +// This function can only be called with gitcmd.Run() +// Do NOT use it with gitcmd.RunStd*() functions, otherwise it will panic func (ctx *mergeContext) PrepareGitCmd(cmd *gitcmd.Command) *gitcmd.Command { ctx.outbuf.Reset() ctx.errbuf.Reset() @@ -73,7 +76,11 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque } if expectedHeadCommitID != "" { - trackingCommitID, _, err := mergeCtx.PrepareGitCmd(gitcmd.NewCommand("show-ref", "--hash").AddDynamicArguments(git.BranchPrefix + trackingBranch)).RunStdString(ctx) + trackingCommitID, _, err := gitcmd.NewCommand("show-ref", "--hash"). + AddDynamicArguments(git.BranchPrefix + trackingBranch). + WithEnv(mergeCtx.env). + WithDir(mergeCtx.tmpBasePath). + RunStdString(ctx) if err != nil { defer cancel() log.Error("failed to get sha of head branch in %-v: show-ref[%s] --hash refs/heads/tracking: %v", mergeCtx.pr, mergeCtx.tmpBasePath, err) diff --git a/services/pull/pull.go b/services/pull/pull.go index 310aeaf525..72f571ec8e 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -409,10 +409,8 @@ type TestPullRequestOptions struct { func AddTestPullRequestTask(opts TestPullRequestOptions) { log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch) graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { - // There is no sensible way to shut this down ":-(" - // If you don't let it run all the way then you will lose data - // TODO: graceful: AddTestPullRequestTask needs to become a queue! - + // this function does a lot of operations to various models, if the process gets killed in the middle, + // there is no way to recover at the moment. The best workaround is to let end user push again. repo, err := repo_model.GetRepositoryByID(ctx, opts.RepoID) if err != nil { log.Error("GetRepositoryByID: %v", err) @@ -437,11 +435,15 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) { continue } - StartPullRequestCheckImmediately(ctx, pr) + // create push comment before check pull request status, + // then when the status is mergeable, the comment is already in database, to make testing easy and stable comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID, opts.IsForcePush) if err == nil && comment != nil { notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment) } + // The caller can be in a goroutine or a "push queue", "conflict check" can be time-consuming, + // and the concurrency should be limited, so the conflict check will be done in another queue + StartPullRequestCheckImmediately(ctx, pr) } if opts.IsSync { diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 597a4aa48c..4f7a504b11 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -37,6 +37,9 @@ type prTmpRepoContext struct { errbuf *strings.Builder // any use should be preceded by a Reset and preferably after use } +// PrepareGitCmd prepares a git command with the correct directory, environment, and output buffers +// This function can only be called with gitcmd.Run() +// Do NOT use it with gitcmd.RunStd*() functions, otherwise it will panic func (ctx *prTmpRepoContext) PrepareGitCmd(cmd *gitcmd.Command) *gitcmd.Command { ctx.outbuf.Reset() ctx.errbuf.Reset() diff --git a/services/pull/update.go b/services/pull/update.go index cce3937451..436e3b52a6 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -63,6 +63,9 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. } defer func() { + // The code is from https://github.com/go-gitea/gitea/pull/9784, + // it seems a simple copy-paste from https://github.com/go-gitea/gitea/pull/7082 without a real reason. + // TODO: DUPLICATE-PR-TASK: search and see another TODO comment for more details go AddTestPullRequestTask(TestPullRequestOptions{ RepoID: pr.BaseRepo.ID, Doer: doer, diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 9873d5deeb..acac6fd9ad 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -28,22 +28,23 @@ import ( ) func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) { - wikiPath := repo.WikiPath() - wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr) - if wikiRemotePath == "" { + wikiRemoteURL := repo_module.WikiRemoteURL(ctx, opts.CloneAddr) + if wikiRemoteURL == "" { return "", nil } - if err := util.RemoveAll(wikiPath); err != nil { - return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", wikiPath, err) + storageRepo := repo.WikiStorageRepo() + + if err := gitrepo.DeleteRepository(ctx, storageRepo); err != nil { + return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", storageRepo.RelativePath(), err) } cleanIncompleteWikiPath := func() { - if err := util.RemoveAll(wikiPath); err != nil { - log.Error("Failed to remove incomplete wiki dir %q, err: %v", wikiPath, err) + if err := gitrepo.DeleteRepository(ctx, storageRepo); err != nil { + log.Error("Failed to remove incomplete wiki dir %q, err: %v", storageRepo.RelativePath(), err) } } - if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ + if err := gitrepo.CloneExternalRepo(ctx, wikiRemoteURL, storageRepo, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, @@ -54,15 +55,15 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration. return "", err } - if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { + if err := gitrepo.WriteCommitGraph(ctx, storageRepo); err != nil { cleanIncompleteWikiPath() return "", err } - defaultBranch, err := gitrepo.GetDefaultBranch(ctx, repo.WikiStorageRepo()) + defaultBranch, err := gitrepo.GetDefaultBranch(ctx, storageRepo) if err != nil { cleanIncompleteWikiPath() - return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err) + return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", storageRepo.RelativePath(), err) } return defaultBranch, nil diff --git a/services/repository/transfer.go b/services/repository/transfer.go index c30a540137..98307a447a 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -107,16 +107,18 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } if repoRenamed { - if err := util.Rename(repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name)); err != nil { + oldRelativePath, newRelativePath := repo_model.RelativePath(newOwnerName, repo.Name), repo_model.RelativePath(oldOwnerName, repo.Name) + if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { log.Critical("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name), err) + oldRelativePath, newRelativePath, err) } } if wikiRenamed { - if err := util.Rename(repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name)); err != nil { + oldRelativePath, newRelativePath := repo_model.RelativeWikiPath(newOwnerName, repo.Name), repo_model.RelativeWikiPath(oldOwnerName, repo.Name) + if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { log.Critical("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name), err) + oldRelativePath, newRelativePath, err) } } @@ -289,12 +291,12 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName repoRenamed = true // Rename remote wiki repository to new path and delete local copy. - wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) - if isExist, err := util.IsExist(wikiPath); err != nil { - log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) + wikiStorageRepo := repo_model.StorageRepo(repo_model.RelativeWikiPath(oldOwner.Name, repo.Name)) + if isExist, err := gitrepo.IsRepositoryExist(ctx, wikiStorageRepo); err != nil { + log.Error("Unable to check if %s exists. Error: %v", wikiStorageRepo.RelativePath(), err) return err } else if isExist { - if err := util.Rename(wikiPath, repo_model.WikiPath(newOwner.Name, repo.Name)); err != nil { + if err := gitrepo.RenameRepository(ctx, wikiStorageRepo, repo_model.StorageRepo(repo_model.RelativeWikiPath(newOwner.Name, repo.Name))); err != nil { return fmt.Errorf("rename repository wiki: %w", err) } wikiRenamed = true diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 25c33b57fc..25f836dd5d 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -120,7 +120,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model cloneOpts.Branch = repo.DefaultWikiBranch } - if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil { + if err := gitrepo.CloneRepoToLocal(ctx, repo.WikiStorageRepo(), basePath, cloneOpts); err != nil { log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) } @@ -269,7 +269,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } defer cleanup() - if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{ + if err := gitrepo.CloneRepoToLocal(ctx, repo.WikiStorageRepo(), basePath, git.CloneRepoOptions{ Bare: true, Shared: true, Branch: repo.DefaultWikiBranch, diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 806347c720..080b2cd3d6 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -252,9 +252,9 @@ {{end}} {{if .CacheConn}}
{{ctx.Locale.Tr "admin.config.cache_conn"}}
-
{{.CacheConn}}
+
{{.CacheConn}}
{{ctx.Locale.Tr "admin.config.cache_item_ttl"}}
-
{{.CacheItemTTL}}
+
{{.CacheItemTTL}}
{{end}}
{{ctx.Locale.Tr "admin.config.cache_test"}}
@@ -275,7 +275,7 @@
{{ctx.Locale.Tr "admin.config.session_provider"}}
{{.SessionConfig.Provider}}
{{ctx.Locale.Tr "admin.config.provider_config"}}
-
{{if .SessionConfig.ProviderConfig}}{{.SessionConfig.ProviderConfig}}{{else}}-{{end}}
+
{{if .SessionConfig.ProviderConfig}}{{.SessionConfig.ProviderConfig}}{{else}}-{{end}}
{{ctx.Locale.Tr "admin.config.cookie_name"}}
{{.SessionConfig.CookieName}}
{{ctx.Locale.Tr "admin.config.gc_interval_time"}}
@@ -301,7 +301,7 @@
{{ctx.Locale.Tr "admin.config.git_max_diff_files"}}
{{.Git.MaxGitDiffFiles}}
{{ctx.Locale.Tr "admin.config.git_gc_args"}}
-
{{.Git.GCArgs}}
+
{{.Git.GCArgs}}
@@ -330,7 +330,7 @@ {{if .Loggers.access.IsEnabled}}
{{ctx.Locale.Tr "admin.config.access_log_template"}}
-
{{$.AccessLogTemplate}}
+
{{$.AccessLogTemplate}}
{{end}} {{range $loggerName, $loggerDetail := .Loggers}} diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl index db7ed81c79..356c517935 100644 --- a/templates/admin/stacktrace-row.tmpl +++ b/templates/admin/stacktrace-row.tmpl @@ -46,8 +46,8 @@
{{svg "octicon-dot-fill" 16}}
-
{{.Function}}
-
{{.File}}:{{.Line}}
+
{{.Function}}
+
{{.File}}:{{.Line}}
{{end}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 6d4c1fb2db..6380a72498 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -2,7 +2,7 @@ {{if .Details}}
{{.Summary}} - {{.Details | SanitizeHTML}} + {{.Details | SanitizeHTML}}
{{else}}
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 692808a32d..21bc287643 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -65,7 +65,9 @@
- {{$.Project.RenderedContent}} +
+ {{$.Project.RenderedContent}} +
diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index 23df61a43c..1e8ab4c16b 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -28,9 +28,9 @@
{{if $run.IsRefDeleted}} - {{$run.PrettyRef}} + {{$run.PrettyRef}} {{else}} - {{$run.PrettyRef}} + {{$run.PrettyRef}} {{end}}
{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince $run.Updated}}
diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl index f2020bc160..055bc714c9 100644 --- a/templates/repo/actions/status.tmpl +++ b/templates/repo/actions/status.tmpl @@ -12,11 +12,11 @@ {{else if eq .status "cancelled"}} {{svg "octicon-stop" $size (printf "text grey %s" $className)}} {{else if eq .status "waiting"}} - {{svg "octicon-clock" $size (printf "text yellow %s" $className)}} + {{svg "octicon-circle" $size (printf "text grey %s" $className)}} {{else if eq .status "blocked"}} {{svg "octicon-blocked" $size (printf "text yellow %s" $className)}} {{else if eq .status "running"}} - {{svg "octicon-meter" $size (printf "text yellow circular-spin %s" $className)}} + {{svg "gitea-running" $size (printf "text yellow circular-spin %s" $className)}} {{else}}{{/*failure, unknown*/}} {{svg "octicon-x-circle-fill" $size (printf "text red %s" $className)}} {{end}} diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl index 38133bde2b..664f1442f9 100644 --- a/templates/repo/file_info.tmpl +++ b/templates/repo/file_info.tmpl @@ -11,11 +11,11 @@ {{end}} {{if ne .FileSize nil}}
- {{FileSize .FileSize}}{{if .IsLFSFile}}LFS{{end}} + {{FileSize .FileSize}}{{if .IsLFSFile}}LFS{{end}}
{{end}} {{if .LFSLock}} -
+
{{svg "octicon-lock" 16 "tw-mr-1"}} {{.LFSLockOwner}}
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index cd1b168401..e9f4afda26 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -39,7 +39,7 @@ {{.LineNums}} -
    {{.FileContent}}
+
{{.FileContent}}
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index 953ba69670..d27c1fb8b1 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -68,7 +68,7 @@ {{range $key, $val := .RequestInfo.Headers}}{{$key}}: {{$val}} {{end}}
{{ctx.Locale.Tr "repo.settings.webhook.payload"}}
-
{{or .RequestInfo.Body .PayloadContent}}
+
{{or .RequestInfo.Body .PayloadContent}}
{{else}} - {{end}} @@ -79,7 +79,7 @@
{{range $key, $val := .ResponseInfo.Headers}}{{$key}}: {{$val}}
 {{end}}
{{ctx.Locale.Tr "repo.settings.webhook.body"}}
-
{{.ResponseInfo.Body}}
+
{{.ResponseInfo.Body}}
{{else}} - {{end}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0df8356fd9..325f0b78a0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7844,7 +7844,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/UpdateFileOptions" + "$ref": "#/definitions/ApplyDiffPatchFileOptions" } } ], @@ -21645,6 +21645,54 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ApplyDiffPatchFileOptions": { + "description": "ApplyDiffPatchFileOptions options for applying a diff patch\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", + "type": "object", + "required": [ + "content" + ], + "properties": { + "author": { + "$ref": "#/definitions/Identity" + }, + "branch": { + "description": "branch (optional) is the base branch for the changes. If not supplied, the default branch is used", + "type": "string", + "x-go-name": "BranchName" + }, + "committer": { + "$ref": "#/definitions/Identity" + }, + "content": { + "type": "string", + "x-go-name": "Content" + }, + "dates": { + "$ref": "#/definitions/CommitDateOptions" + }, + "force_push": { + "description": "force_push (optional) will do a force-push if the new branch already exists", + "type": "boolean", + "x-go-name": "ForcePush" + }, + "message": { + "description": "message (optional) is the commit message of the changes. If not supplied, a default message will be used", + "type": "string", + "x-go-name": "Message" + }, + "new_branch": { + "description": "new_branch (optional) will make a new branch from base branch for the changes. If not supplied, the changes will be committed to the base branch", + "type": "string", + "x-go-name": "NewBranchName" + }, + "signoff": { + "description": "Add a Signed-off-by trailer by the committer at the end of the commit log message.", + "type": "boolean", + "x-go-name": "Signoff" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Attachment": { "description": "Attachment a generic attachment", "type": "object", diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index e44a838b25..bb45ea58c5 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -21,8 +21,8 @@
-

{{ctx.Locale.Tr "settings.gpg_token_help"}}

-

{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}

+ {{ctx.Locale.Tr "settings.gpg_token_help"}} +
{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}
@@ -89,8 +89,8 @@
-

{{ctx.Locale.Tr "settings.gpg_token_help"}}

-

{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}

+ {{ctx.Locale.Tr "settings.gpg_token_help"}} +
{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}

diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl index 9d62d4ab08..02bcc937ff 100644 --- a/templates/user/settings/keys_ssh.tmpl +++ b/templates/user/settings/keys_ssh.tmpl @@ -77,16 +77,15 @@
-

{{ctx.Locale.Tr "settings.ssh_token_help"}}

-

echo -n '{{$.TokenToSign}}' | ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey

+ {{ctx.Locale.Tr "settings.ssh_token_help"}} +
echo -n '{{$.TokenToSign}}' | ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey
Windows PowerShell -

cmd /c "<NUL set /p=`"{{$.TokenToSign}}`"| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey"

+
cmd /c "<NUL set /p=`"{{$.TokenToSign}}`"| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey"
-
Windows CMD -

set /p={{$.TokenToSign}}| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey

+
set /p={{$.TokenToSign}}| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey

diff --git a/tests/integration/actions_concurrency_test.go b/tests/integration/actions_concurrency_test.go new file mode 100644 index 0000000000..cc61368260 --- /dev/null +++ b/tests/integration/actions_concurrency_test.go @@ -0,0 +1,1709 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + webhook_module "code.gitea.io/gitea/modules/webhook" + actions_service "code.gitea.io/gitea/services/actions" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/stretchr/testify/assert" +) + +func TestWorkflowConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + // add a variable for test + req := NewRequestWithJSON(t, "POST", + fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/myvar", user2.Name, repo.Name), &api.CreateVariableOption{ + Value: "abc123", + }). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + wf1TreePath := ".gitea/workflows/concurrent-workflow-1.yml" + wf1FileContent := `name: concurrent-workflow-1 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-1.yml' +concurrency: + group: workflow-main-abc123-user2 +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow1' +` + wf2TreePath := ".gitea/workflows/concurrent-workflow-2.yml" + wf2FileContent := `name: concurrent-workflow-2 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-2.yml' +concurrency: + group: workflow-${{ gitea.ref_name }}-${{ vars.myvar }}-${{ gitea.event.pusher.username }} +jobs: + wf2-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow2' +` + wf3TreePath := ".gitea/workflows/concurrent-workflow-3.yml" + wf3FileContent := `name: concurrent-workflow-3 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-3.yml' +concurrency: + group: workflow-main-abc${{ 123 }}-${{ gitea.event.pusher.username }} +jobs: + wf3-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow3' +` + // push workflow1 + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + // fetch and exec workflow1 + task := runner.fetchTask(t) + _, _, run := getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-1.yml", run.WorkflowID) + runner.fetchNoTask(t) + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // push workflow2 + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf2TreePath, wf2FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf2TreePath, opts2) + // fetch workflow2 + task = runner.fetchTask(t) + _, _, run = getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-2.yml", run.WorkflowID) + + // push workflow3 + opts3 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf3TreePath, wf3FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf3TreePath, opts3) + runner.fetchNoTask(t) + + // exec workflow2 + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // fetch and exec workflow3 + task = runner.fetchTask(t) + _, _, run = getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-3.yml", run.WorkflowID) + runner.fetchNoTask(t) + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + }) +} + +func TestWorkflowConcurrencyShort(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + // add a variable for test + req := NewRequestWithJSON(t, "POST", + fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/myvar", user2.Name, repo.Name), &api.CreateVariableOption{ + Value: "abc123", + }). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + wf1TreePath := ".gitea/workflows/concurrent-workflow-1.yml" + wf1FileContent := `name: concurrent-workflow-1 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-1.yml' +concurrency: workflow-main-abc123-user2 +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow1' +` + wf2TreePath := ".gitea/workflows/concurrent-workflow-2.yml" + wf2FileContent := `name: concurrent-workflow-2 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-2.yml' +concurrency: workflow-${{ gitea.ref_name }}-${{ vars.myvar }}-${{ gitea.event.pusher.username }} +jobs: + wf2-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow2' +` + wf3TreePath := ".gitea/workflows/concurrent-workflow-3.yml" + wf3FileContent := `name: concurrent-workflow-3 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-3.yml' +concurrency: workflow-main-abc${{ 123 }}-${{ gitea.event.pusher.username }} +jobs: + wf3-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow3' +` + // push workflow1 + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + // fetch and exec workflow1 + task := runner.fetchTask(t) + _, _, run := getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-1.yml", run.WorkflowID) + runner.fetchNoTask(t) + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // push workflow2 + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf2TreePath, wf2FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf2TreePath, opts2) + // fetch workflow2 + task = runner.fetchTask(t) + _, _, run = getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-2.yml", run.WorkflowID) + + // push workflow3 + opts3 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf3TreePath, wf3FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf3TreePath, opts3) + runner.fetchNoTask(t) + + // exec workflow2 + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // fetch and exec workflow3 + task = runner.fetchTask(t) + _, _, run = getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-3.yml", run.WorkflowID) + runner.fetchNoTask(t) + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + }) +} + +func TestWorkflowConcurrencyShortJson(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + // add a variable for test + req := NewRequestWithJSON(t, "POST", + fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/myvar", user2.Name, repo.Name), &api.CreateVariableOption{ + Value: "abc123", + }). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + wf1TreePath := ".gitea/workflows/concurrent-workflow-1.yml" + wf1FileContent := `name: concurrent-workflow-1 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-1.yml' +concurrency: |- + ${{ fromjson('{ + "group": "workflow-main-abc123-user2", + "cancel-in-progress": false + }') }} +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow1' +` + wf2TreePath := ".gitea/workflows/concurrent-workflow-2.yml" + wf2FileContent := `name: concurrent-workflow-2 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-2.yml' +concurrency: |- + ${{ fromjson('{ + "group": "workflow-main-abc123-user2", + "cancel-in-progress": false + }') }} +jobs: + wf2-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow2' +` + wf3TreePath := ".gitea/workflows/concurrent-workflow-3.yml" + wf3FileContent := `name: concurrent-workflow-3 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-3.yml' +concurrency: |- + ${{ fromjson('{ + "group": "workflow-main-abc123-user2", + "cancel-in-progress": false + }') }} +jobs: + wf3-job: + runs-on: ubuntu-latest + steps: + - run: echo 'job from workflow3' +` + // push workflow1 + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + // fetch and exec workflow1 + task := runner.fetchTask(t) + _, _, run := getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-1.yml", run.WorkflowID) + runner.fetchNoTask(t) + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // push workflow2 + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf2TreePath, wf2FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf2TreePath, opts2) + // fetch workflow2 + task = runner.fetchTask(t) + _, _, run = getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-2.yml", run.WorkflowID) + + // push workflow3 + opts3 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf3TreePath, wf3FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf3TreePath, opts3) + runner.fetchNoTask(t) + + // exec workflow2 + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // fetch and exec workflow3 + task = runner.fetchTask(t) + _, _, run = getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "workflow-main-abc123-user2", run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-3.yml", run.WorkflowID) + runner.fetchNoTask(t) + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + }) +} + +func TestPullRequestWorkflowConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // user2 is the owner of the base repo + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user2Session := loginUser(t, user2.Name) + user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + // user4 is the owner of the forked repo + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-concurrency", false) + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID}) + user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(user2APICtx)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + // init the workflow + wfTreePath := ".gitea/workflows/pull.yml" + wfFileContent := `name: Pull Request +on: pull_request +concurrency: + group: pull-request-test + cancel-in-progress: ${{ !startsWith(gitea.head_ref, 'do-not-cancel/') }} +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'test the pull' +` + opts1 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wfTreePath, wfFileContent) + createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts1) + // user2 creates a pull request + doAPICreateFile(user2APICtx, "user2-fix.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "bugfix/aaa", + Message: "create user2-fix.txt", + Author: api.Identity{ + Name: user4.Name, + Email: user4.Email, + }, + Committer: api.Identity{ + Name: user4.Name, + Email: user4.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("user2-fix")), + })(t) + doAPICreatePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, "bugfix/aaa")(t) + pr1Task1 := runner.fetchTask(t) + _, _, pr1Run1 := getTaskAndJobAndRunByTaskID(t, pr1Task1.Id) + assert.Equal(t, "pull-request-test", pr1Run1.ConcurrencyGroup) + assert.True(t, pr1Run1.ConcurrencyCancel) + assert.Equal(t, actions_model.StatusRunning, pr1Run1.Status) + + // user4 forks the repo + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name), + &api.CreateForkOption{ + Name: util.ToPointer("actions-concurrency-fork"), + }).AddTokenAuth(user4Token) + resp := MakeRequest(t, req, http.StatusAccepted) + var apiForkRepo api.Repository + DecodeJSON(t, resp, &apiForkRepo) + forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID}) + user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(user4APICtx)(t) + + // user4 creates a pull request from branch "bugfix/bbb" + doAPICreateFile(user4APICtx, "user4-fix.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "bugfix/bbb", + Message: "create user4-fix.txt", + Author: api.Identity{ + Name: user4.Name, + Email: user4.Email, + }, + Committer: api.Identity{ + Name: user4.Name, + Email: user4.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("user4-fix")), + })(t) + doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":bugfix/bbb")(t) + // cannot fetch the task because an approval is required + runner.fetchNoTask(t) + // user2 approves the run + pr2Run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID}) + req = NewRequestWithValues(t, "POST", + fmt.Sprintf("/%s/%s/actions/runs/%d/approve", baseRepo.OwnerName, baseRepo.Name, pr2Run1.Index), + map[string]string{ + "_csrf": GetUserCSRFToken(t, user2Session), + }) + user2Session.MakeRequest(t, req, http.StatusOK) + // fetch the task and the previous task has been cancelled + pr2Task1 := runner.fetchTask(t) + _, _, pr2Run1 = getTaskAndJobAndRunByTaskID(t, pr2Task1.Id) + assert.Equal(t, "pull-request-test", pr2Run1.ConcurrencyGroup) + assert.True(t, pr2Run1.ConcurrencyCancel) + assert.Equal(t, actions_model.StatusRunning, pr2Run1.Status) + pr1Run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: pr1Run1.ID}) + assert.Equal(t, actions_model.StatusCancelled, pr1Run1.Status) + + // user4 creates another pull request from branch "do-not-cancel/ccc" + doAPICreateFile(user4APICtx, "user4-fix2.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "do-not-cancel/ccc", + Message: "create user4-fix2.txt", + Author: api.Identity{ + Name: user4.Name, + Email: user4.Email, + }, + Committer: api.Identity{ + Name: user4.Name, + Email: user4.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("user4-fix2")), + })(t) + doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":do-not-cancel/ccc")(t) + // cannot fetch the task because cancel-in-progress is false + runner.fetchNoTask(t) + runner.execTask(t, pr2Task1, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + pr2Run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: pr2Run1.ID}) + assert.Equal(t, actions_model.StatusSuccess, pr2Run1.Status) + // fetch the task + pr3Task1 := runner.fetchTask(t) + _, _, pr3Run1 := getTaskAndJobAndRunByTaskID(t, pr3Task1.Id) + assert.Equal(t, "pull-request-test", pr3Run1.ConcurrencyGroup) + assert.False(t, pr3Run1.ConcurrencyCancel) + assert.Equal(t, actions_model.StatusRunning, pr3Run1.Status) + }) +} + +func TestJobConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner1 := newMockRunner() + runner1.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner-1", []string{"runner1"}, false) + runner2 := newMockRunner() + runner2.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner-2", []string{"runner2"}, false) + + // add a variable for test + req := NewRequestWithJSON(t, "POST", + fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/version_var", user2.Name, repo.Name), &api.CreateVariableOption{ + Value: "v1.23.0", + }). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + wf1TreePath := ".gitea/workflows/concurrent-workflow-1.yml" + wf1FileContent := `name: concurrent-workflow-1 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-1.yml' +jobs: + wf1-job1: + runs-on: runner1 + concurrency: + group: job-main-${{ vars.version_var }} + steps: + - run: echo 'wf1-job1' +` + wf2TreePath := ".gitea/workflows/concurrent-workflow-2.yml" + wf2FileContent := `name: concurrent-workflow-2 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-2.yml' +jobs: + wf2-job1: + runs-on: runner2 + outputs: + version: ${{ steps.version_step.outputs.app_version }} + steps: + - id: version_step + run: echo "app_version=v1.23.0" >> "$GITHUB_OUTPUT" + wf2-job2: + runs-on: runner1 + needs: [wf2-job1] + concurrency: + group: job-main-${{ needs.wf2-job1.outputs.version }} + steps: + - run: echo 'wf2-job2' +` + wf3TreePath := ".gitea/workflows/concurrent-workflow-3.yml" + wf3FileContent := `name: concurrent-workflow-3 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-3.yml' +jobs: + wf3-job1: + runs-on: runner1 + concurrency: + group: job-main-${{ vars.version_var }} + cancel-in-progress: ${{ vars.version_var == 'v1.23.0' }} + steps: + - run: echo 'wf3-job1' +` + + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf2TreePath, wf2FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf2TreePath, opts2) + + // fetch wf1-job1 + wf1Job1Task := runner1.fetchTask(t) + _, wf1Job1ActionJob, _ := getTaskAndJobAndRunByTaskID(t, wf1Job1Task.Id) + assert.Equal(t, "job-main-v1.23.0", wf1Job1ActionJob.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusRunning, wf1Job1ActionJob.Status) + // fetch and exec wf2-job1 + wf2Job1Task := runner2.fetchTask(t) + runner2.execTask(t, wf2Job1Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "version": "v1.23.0", + }, + }) + // cannot fetch wf2-job2 because wf1-job1 is running + runner1.fetchNoTask(t) + // exec wf1-job1 + runner1.execTask(t, wf1Job1Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + // fetch wf2-job2 + wf2Job2Task := runner1.fetchTask(t) + _, wf2Job2ActionJob, wf2Run := getTaskAndJobAndRunByTaskID(t, wf2Job2Task.Id) + assert.Equal(t, "job-main-v1.23.0", wf2Job2ActionJob.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusRunning, wf2Job2ActionJob.Status) + // push workflow3 to trigger wf3-job1 + opts3 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf3TreePath, wf3FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf3TreePath, opts3) + // fetch wf3-job1 + wf3Job1Task := runner1.fetchTask(t) + _, wf3Job1ActionJob, _ := getTaskAndJobAndRunByTaskID(t, wf3Job1Task.Id) + assert.Equal(t, "job-main-v1.23.0", wf3Job1ActionJob.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusRunning, wf3Job1ActionJob.Status) + // wf2-job2 has been cancelled + _, wf2Job2ActionJob, _ = getTaskAndJobAndRunByTaskID(t, wf2Job2Task.Id) + assert.Equal(t, actions_model.StatusCancelled, wf2Job2ActionJob.Status) + + // rerun wf2 + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, wf2Run.Index), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + // (rerun1) cannot fetch wf2-job2 + runner1.fetchNoTask(t) + // (rerun1) fetch and exec wf2-job1 + wf2Job1Rerun1Task := runner2.fetchTask(t) + _, _, wf2Rerun1Run := getTaskAndJobAndRunByTaskID(t, wf2Job1Rerun1Task.Id) + assert.Equal(t, wf2Rerun1Run.ID, wf2Run.ID) + runner2.execTask(t, wf2Job1Rerun1Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + outputs: map[string]string{ + "version": "v1.24.0", + }, + }) + // (rerun1) fetch and exec wf2-job2 + wf2Job2Rerun1Task := runner1.fetchTask(t) + runner1.execTask(t, wf2Job2Rerun1Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + _, wf2Job2Rerun1Job, _ := getTaskAndJobAndRunByTaskID(t, wf2Job2Rerun1Task.Id) + assert.Equal(t, "job-main-v1.24.0", wf2Job2Rerun1Job.ConcurrencyGroup) + + // rerun wf2-job2 + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, wf2Run.Index, 1), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + // (rerun2) fetch and exec wf2-job2 + wf2Job2Rerun2Task := runner1.fetchTask(t) + runner1.execTask(t, wf2Job2Rerun2Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + _, wf2Job2Rerun2Job, _ := getTaskAndJobAndRunByTaskID(t, wf2Job2Rerun2Task.Id) + assert.Equal(t, "job-main-v1.24.0", wf2Job2Rerun2Job.ConcurrencyGroup) + }) +} + +func TestMatrixConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + linuxRunner := newMockRunner() + linuxRunner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-linux-runner", []string{"linux-runner"}, false) + windowsRunner := newMockRunner() + windowsRunner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-windows-runner", []string{"windows-runner"}, false) + darwinRunner := newMockRunner() + darwinRunner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-darwin-runner", []string{"darwin-runner"}, false) + + wf1TreePath := ".gitea/workflows/concurrent-workflow-1.yml" + wf1FileContent := `name: concurrent-workflow-1 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-1.yml' +jobs: + wf1-job: + runs-on: ${{ matrix.os }}-runner + strategy: + matrix: + os: [windows, linux] + concurrency: + group: job-os-${{ matrix.os }} + steps: + - run: echo 'wf1' +` + + wf2TreePath := ".gitea/workflows/concurrent-workflow-2.yml" + wf2FileContent := `name: concurrent-workflow-2 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-2.yml' +jobs: + wf2-job: + runs-on: ${{ matrix.os }}-runner + strategy: + matrix: + os: [darwin, windows, linux] + concurrency: + group: job-os-${{ matrix.os }} + steps: + - run: echo 'wf2' +` + + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + + job1WinTask := windowsRunner.fetchTask(t) + job1LinuxTask := linuxRunner.fetchTask(t) + windowsRunner.fetchNoTask(t) + linuxRunner.fetchNoTask(t) + _, job1WinJob, _ := getTaskAndJobAndRunByTaskID(t, job1WinTask.Id) + assert.Equal(t, "wf1-job (windows)", job1WinJob.Name) + assert.Equal(t, "job-os-windows", job1WinJob.ConcurrencyGroup) + _, job1LinuxJob, _ := getTaskAndJobAndRunByTaskID(t, job1LinuxTask.Id) + assert.Equal(t, "wf1-job (linux)", job1LinuxJob.Name) + assert.Equal(t, "job-os-linux", job1LinuxJob.ConcurrencyGroup) + + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf2TreePath, wf2FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf2TreePath, opts2) + job2DarwinTask := darwinRunner.fetchTask(t) + _, job2DarwinJob, _ := getTaskAndJobAndRunByTaskID(t, job2DarwinTask.Id) + assert.Equal(t, "wf2-job (darwin)", job2DarwinJob.Name) + assert.Equal(t, "job-os-darwin", job2DarwinJob.ConcurrencyGroup) + + windowsRunner.execTask(t, job1WinTask, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + linuxRunner.execTask(t, job1LinuxTask, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + job2WinTask := windowsRunner.fetchTask(t) + job2LinuxTask := linuxRunner.fetchTask(t) + _, job2WinJob, _ := getTaskAndJobAndRunByTaskID(t, job2WinTask.Id) + assert.Equal(t, "wf2-job (windows)", job2WinJob.Name) + assert.Equal(t, "job-os-windows", job2WinJob.ConcurrencyGroup) + _, job2LinuxJob, _ := getTaskAndJobAndRunByTaskID(t, job2LinuxTask.Id) + assert.Equal(t, "wf2-job (linux)", job2LinuxJob.Name) + assert.Equal(t, "job-os-linux", job2LinuxJob.ConcurrencyGroup) + }) +} + +func TestWorkflowDispatchConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + wf1TreePath := ".gitea/workflows/workflow-dispatch-concurrency.yml" + wf1FileContent := `name: workflow-dispatch-concurrency +on: + workflow_dispatch: + inputs: + appVersion: + description: 'APP version' + required: true + default: 'v1.23' + type: choice + options: + - v1.21 + - v1.22 + - v1.23 + cancel: + description: 'Cancel running workflows' + required: false + type: boolean + default: false +concurrency: + group: workflow-dispatch-${{ inputs.appVersion }} + cancel-in-progress: ${{ inputs.cancel }} +jobs: + job: + runs-on: ubuntu-latest + steps: + - run: echo 'workflow dispatch job' +` + + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + + // run the workflow with appVersion=v1.21 and cancel=false + urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "workflow-dispatch-concurrency.yml") + req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.21", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task1 := runner.fetchTask(t) + _, _, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id) + assert.Equal(t, "workflow-dispatch-v1.21", run1.ConcurrencyGroup) + + // run the workflow with appVersion=v1.22 and cancel=false + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task2 := runner.fetchTask(t) + _, _, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run2.ConcurrencyGroup) + + // run the workflow with appVersion=v1.22 and cancel=false again + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + runner.fetchNoTask(t) // cannot fetch task because task2 is not completed + + // run the workflow with appVersion=v1.22 and cancel=true + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + "cancel": "on", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task4 := runner.fetchTask(t) + _, _, run4 := getTaskAndJobAndRunByTaskID(t, task4.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run4.ConcurrencyGroup) + _, _, run2 = getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, actions_model.StatusCancelled, run2.Status) + }) +} + +func TestWorkflowDispatchRerunAllJobsConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + wf1TreePath := ".gitea/workflows/workflow-dispatch-concurrency.yml" + wf1FileContent := `name: workflow-dispatch-concurrency +on: + workflow_dispatch: + inputs: + appVersion: + description: 'APP version' + required: true + default: 'v1.23' + type: choice + options: + - v1.21 + - v1.22 + - v1.23 + cancel: + description: 'Cancel running workflows' + required: false + type: boolean + default: false +concurrency: + group: workflow-dispatch-${{ inputs.appVersion }} + cancel-in-progress: ${{ inputs.cancel }} +jobs: + job: + runs-on: ubuntu-latest + steps: + - run: echo 'workflow dispatch job' +` + + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + + // run the workflow with appVersion=v1.21 and cancel=false + urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "workflow-dispatch-concurrency.yml") + req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.21", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task1 := runner.fetchTask(t) + _, _, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id) + assert.Equal(t, "workflow-dispatch-v1.21", run1.ConcurrencyGroup) + + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task2 := runner.fetchTask(t) + _, _, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run2.ConcurrencyGroup) + + // run the workflow with appVersion=v1.22 and cancel=false again + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + runner.fetchNoTask(t) // cannot fetch task because task2 is not completed + + // run the workflow with appVersion=v1.22 and cancel=true + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + "cancel": "on", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task4 := runner.fetchTask(t) + _, _, run4 := getTaskAndJobAndRunByTaskID(t, task4.Id) + assert.Equal(t, actions_model.StatusRunning, run4.Status) + assert.Equal(t, "workflow-dispatch-v1.22", run4.ConcurrencyGroup) + _, _, run2 = getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, actions_model.StatusCancelled, run2.Status) + + runner.execTask(t, task4, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // rerun cancel true scenario + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.Index), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run4.Index), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + task5 := runner.fetchTask(t) + _, _, run4_1 := getTaskAndJobAndRunByTaskID(t, task5.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run4_1.ConcurrencyGroup) + assert.Equal(t, run4.ID, run4_1.ID) + _, _, run2_1 := getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, actions_model.StatusCancelled, run2_1.Status) + + runner.execTask(t, task5, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_CANCELLED, + }) + + // rerun cancel false scenario + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.Index), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + run2_2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID}) + assert.Equal(t, actions_model.StatusWaiting, run2_2.Status) + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run2.Index+1), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + task6 := runner.fetchTask(t) + _, _, run3 := getTaskAndJobAndRunByTaskID(t, task6.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run3.ConcurrencyGroup) + + run2_2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2_2.ID}) + assert.Equal(t, actions_model.StatusCancelled, run2_2.Status) // cancelled by run3 + }) +} + +func TestWorkflowDispatchRerunSingleJobConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + wf1TreePath := ".gitea/workflows/workflow-dispatch-concurrency.yml" + wf1FileContent := `name: workflow-dispatch-concurrency +on: + workflow_dispatch: + inputs: + appVersion: + description: 'APP version' + required: true + default: 'v1.23' + type: choice + options: + - v1.21 + - v1.22 + - v1.23 + cancel: + description: 'Cancel running workflows' + required: false + type: boolean + default: false +concurrency: + group: workflow-dispatch-${{ inputs.appVersion }} + cancel-in-progress: ${{ inputs.cancel }} +jobs: + job: + runs-on: ubuntu-latest + steps: + - run: echo 'workflow dispatch job' +` + + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + + // run the workflow with appVersion=v1.21 and cancel=false + urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "workflow-dispatch-concurrency.yml") + req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.21", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task1 := runner.fetchTask(t) + _, _, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id) + assert.Equal(t, "workflow-dispatch-v1.21", run1.ConcurrencyGroup) + + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task2 := runner.fetchTask(t) + _, _, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run2.ConcurrencyGroup) + + // run the workflow with appVersion=v1.22 and cancel=false again + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + runner.fetchNoTask(t) // cannot fetch task because task2 is not completed + + // run the workflow with appVersion=v1.22 and cancel=true + req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "ref": "refs/heads/main", + "appVersion": "v1.22", + "cancel": "on", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + task4 := runner.fetchTask(t) + _, _, run4 := getTaskAndJobAndRunByTaskID(t, task4.Id) + assert.Equal(t, actions_model.StatusRunning, run4.Status) + assert.Equal(t, "workflow-dispatch-v1.22", run4.ConcurrencyGroup) + _, _, run2 = getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, actions_model.StatusCancelled, run2.Status) + + runner.execTask(t, task4, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // rerun cancel true scenario + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run2.Index, 1), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run4.Index, 1), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + task5 := runner.fetchTask(t) + _, _, run4_1 := getTaskAndJobAndRunByTaskID(t, task5.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run4_1.ConcurrencyGroup) + assert.Equal(t, run4.ID, run4_1.ID) + _, _, run2_1 := getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, actions_model.StatusCancelled, run2_1.Status) + + runner.execTask(t, task5, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_CANCELLED, + }) + + // rerun cancel false scenario + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run2.Index, 1), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + run2_2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID}) + assert.Equal(t, actions_model.StatusWaiting, run2_2.Status) + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, apiRepo.Name, run2.Index+1, 1), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + _ = session.MakeRequest(t, req, http.StatusOK) + + task6 := runner.fetchTask(t) + _, _, run3 := getTaskAndJobAndRunByTaskID(t, task6.Id) + assert.Equal(t, "workflow-dispatch-v1.22", run3.ConcurrencyGroup) + + run2_2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2_2.ID}) + assert.Equal(t, actions_model.StatusCancelled, run2_2.Status) // cancelled by run3 + }) +} + +func TestScheduleConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + wf1TreePath := ".gitea/workflows/schedule-concurrency.yml" + wf1FileContent := `name: schedule-concurrency +on: + push: + schedule: + - cron: '@every 1m' +concurrency: + group: schedule-concurrency + cancel-in-progress: ${{ gitea.event_name == 'push' }} +jobs: + job: + runs-on: ubuntu-latest + steps: + - run: echo 'schedule workflow' +` + + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + + // fetch the task triggered by push + task1 := runner.fetchTask(t) + _, _, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id) + assert.Equal(t, "schedule-concurrency", run1.ConcurrencyGroup) + assert.True(t, run1.ConcurrencyCancel) + assert.Equal(t, string(webhook_module.HookEventPush), run1.TriggerEvent) + assert.Equal(t, actions_model.StatusRunning, run1.Status) + + // trigger the task by schedule + spec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID}) + spec.Next = timeutil.TimeStampNow() // manually update "Next" + assert.NoError(t, actions_model.UpdateScheduleSpec(t.Context(), spec, "next")) + assert.NoError(t, actions_service.StartScheduleTasks(t.Context())) + runner.fetchNoTask(t) // cannot fetch because task1 is not completed + runner.execTask(t, task1, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + _, _, run1 = getTaskAndJobAndRunByTaskID(t, task1.Id) + assert.Equal(t, actions_model.StatusSuccess, run1.Status) + task2 := runner.fetchTask(t) + _, _, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, "schedule-concurrency", run2.ConcurrencyGroup) + assert.False(t, run2.ConcurrencyCancel) + assert.Equal(t, string(webhook_module.HookEventSchedule), run2.TriggerEvent) + assert.Equal(t, actions_model.StatusRunning, run2.Status) + + // trigger the task by schedule again + spec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID}) + spec.Next = timeutil.TimeStampNow() // manually update "Next" + assert.NoError(t, actions_model.UpdateScheduleSpec(t.Context(), spec, "next")) + assert.NoError(t, actions_service.StartScheduleTasks(t.Context())) + runner.fetchNoTask(t) // cannot fetch because task2 is not completed + run3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, Status: actions_model.StatusBlocked}) + assert.Equal(t, "schedule-concurrency", run3.ConcurrencyGroup) + assert.False(t, run3.ConcurrencyCancel) + assert.Equal(t, string(webhook_module.HookEventSchedule), run3.TriggerEvent) + + // trigger the task by push + doAPICreateFile(httpContext, "doc.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "main", + Message: "create doc.txt", + Author: api.Identity{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: api.Identity{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("doc")), + })(t) + + task4 := runner.fetchTask(t) + _, _, run4 := getTaskAndJobAndRunByTaskID(t, task4.Id) + assert.Equal(t, "schedule-concurrency", run4.ConcurrencyGroup) + assert.True(t, run4.ConcurrencyCancel) + assert.Equal(t, string(webhook_module.HookEventPush), run4.TriggerEvent) + run3 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run3.ID}) + assert.Equal(t, actions_model.StatusCancelled, run3.Status) + }) +} + +func TestWorkflowAndJobConcurrency(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner1 := newMockRunner() + runner1.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner-1", []string{"runner1"}, false) + runner2 := newMockRunner() + runner2.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner-2", []string{"runner2"}, false) + + wf1TreePath := ".gitea/workflows/concurrent-workflow-1.yml" + wf1FileContent := `name: concurrent-workflow-1 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-1.yml' +concurrency: + group: workflow-group-1 +jobs: + wf1-job1: + runs-on: runner1 + concurrency: + group: job-group-1 + steps: + - run: echo 'wf1-job1' + wf1-job2: + runs-on: runner2 + concurrency: + group: job-group-2 + steps: + - run: echo 'wf1-job2' +` + wf2TreePath := ".gitea/workflows/concurrent-workflow-2.yml" + wf2FileContent := `name: concurrent-workflow-2 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-2.yml' +concurrency: + group: workflow-group-1 +jobs: + wf2-job1: + runs-on: runner1 + concurrency: + group: job-group-1 + steps: + - run: echo 'wf2-job1' + wf2-job2: + runs-on: runner2 + concurrency: + group: job-group-2 + steps: + - run: echo 'wf2-job2' +` + wf3TreePath := ".gitea/workflows/concurrent-workflow-3.yml" + wf3FileContent := `name: concurrent-workflow-3 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-3.yml' +concurrency: + group: workflow-group-2 +jobs: + wf3-job1: + runs-on: runner1 + concurrency: + group: job-group-1 + steps: + - run: echo 'wf3-job1' +` + + wf4TreePath := ".gitea/workflows/concurrent-workflow-4.yml" + wf4FileContent := `name: concurrent-workflow-4 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-4.yml' +concurrency: + group: workflow-group-2 +jobs: + wf4-job1: + runs-on: runner2 + concurrency: + group: job-group-2 + cancel-in-progress: true + steps: + - run: echo 'wf4-job1' +` + + // push workflow 1 + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + + // fetch wf1-job1 and wf1-job2 + w1j1Task := runner1.fetchTask(t) + w1j2Task := runner2.fetchTask(t) + _, w1j1Job, w1Run := getTaskAndJobAndRunByTaskID(t, w1j1Task.Id) + assert.Equal(t, "job-group-1", w1j1Job.ConcurrencyGroup) + assert.Equal(t, "workflow-group-1", w1Run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-1.yml", w1Run.WorkflowID) + assert.Equal(t, actions_model.StatusRunning, w1j1Job.Status) + _, w1j2Job, _ := getTaskAndJobAndRunByTaskID(t, w1j2Task.Id) + assert.Equal(t, "job-group-2", w1j2Job.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusRunning, w1j2Job.Status) + + // push workflow 2 + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf2TreePath, wf2FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf2TreePath, opts2) + // cannot fetch wf2-job1 and wf2-job2 because workflow-2 is blocked by workflow-1's concurrency group "workflow-group-1" + runner1.fetchNoTask(t) + runner2.fetchNoTask(t) + // query wf2-job1 from db and check its status + w2Run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, WorkflowID: "concurrent-workflow-2.yml"}) + w2j1Job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: w2Run.ID, JobID: "wf2-job1"}) + assert.Equal(t, actions_model.StatusBlocked, w2j1Job.Status) + + // push workflow 3 + opts3 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf3TreePath, wf3FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf3TreePath, opts3) + // cannot fetch wf3-job1 because it is blocked by wf1-job1's concurrency group "job-group-1" + runner1.fetchNoTask(t) + // query wf3-job1 from db and check its status + w3Run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, WorkflowID: "concurrent-workflow-3.yml"}) + w3j1Job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: w3Run.ID, JobID: "wf3-job1"}) + assert.Equal(t, actions_model.StatusBlocked, w3j1Job.Status) + // wf2-job1 is cancelled by wf3-job1 + w2j1Job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: w2j1Job.ID}) + assert.Equal(t, actions_model.StatusCancelled, w2j1Job.Status) + + // exec wf1-job1 + runner1.execTask(t, w1j1Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // fetch wf3-job1 + assert.Equal(t, actions_model.StatusBlocked, w3j1Job.Status) + w3j1Task := runner1.fetchTask(t) + _, w3j1Job, w3Run = getTaskAndJobAndRunByTaskID(t, w3j1Task.Id) + assert.Equal(t, "job-group-1", w3j1Job.ConcurrencyGroup) + assert.Equal(t, "workflow-group-2", w3Run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-3.yml", w3Run.WorkflowID) + + // exec wf1-job2 + runner2.execTask(t, w1j2Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // fetch wf2-job2 + w2j2Task := runner2.fetchTask(t) + _, w2j2Job, w2Run := getTaskAndJobAndRunByTaskID(t, w2j2Task.Id) + assert.Equal(t, "job-group-2", w2j2Job.ConcurrencyGroup) + assert.Equal(t, "workflow-group-1", w2Run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-2.yml", w2Run.WorkflowID) + assert.Equal(t, actions_model.StatusRunning, w2j2Job.Status) + + // push workflow-4 + opts4 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf4TreePath, wf4FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf4TreePath, opts4) + // cannot fetch wf4-job1 because it is blocked by workflow-3's concurrency group "workflow-group-2" + runner2.fetchNoTask(t) + + // exec wf3-job1 + runner1.execTask(t, w3j1Task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // fetch wf4-job1 + w4j1Task := runner2.fetchTask(t) + // all tasks have been fetched + runner1.fetchNoTask(t) + runner2.fetchNoTask(t) + + _, w2j2Job, w2Run = getTaskAndJobAndRunByTaskID(t, w2j2Task.Id) + // wf2-job2 is cancelled because wf4-job1's cancel-in-progress is true + assert.Equal(t, actions_model.StatusCancelled, w2j2Job.Status) + assert.Equal(t, actions_model.StatusCancelled, w2Run.Status) + _, w4j1Job, w4Run := getTaskAndJobAndRunByTaskID(t, w4j1Task.Id) + assert.Equal(t, "job-group-2", w4j1Job.ConcurrencyGroup) + assert.Equal(t, "workflow-group-2", w4Run.ConcurrencyGroup) + assert.Equal(t, "concurrent-workflow-4.yml", w4Run.WorkflowID) + }) +} + +func TestCancelConcurrentRun(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user2Session := loginUser(t, user2.Name) + user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, user2Token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + user2APICtx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(user2APICtx)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + // init the workflow + wfTreePath := ".gitea/workflows/run.yml" + wfFileContent := `name: Cancel Run +on: push +concurrency: + group: cancel-run-group + cancel-in-progress: false +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'test' +` + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wfTreePath, wfFileContent) + createWorkflowFile(t, user2Token, repo.OwnerName, repo.Name, wfTreePath, opts1) + + // fetch and check the first task + task1 := runner.fetchTask(t) + _, _, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id) + assert.Equal(t, "cancel-run-group", run1.ConcurrencyGroup) + assert.False(t, run1.ConcurrencyCancel) + assert.Equal(t, actions_model.StatusRunning, run1.Status) + + // push another file to trigger the workflow again + doAPICreateFile(user2APICtx, "file1.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + Message: "create file1.txt", + Author: api.Identity{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: api.Identity{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("file1")), + })(t) + + // cannot fetch the second task because the first task is not completed + runner.fetchNoTask(t) + + // cancel the first run + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user2.Name, repo.Name, run1.Index), map[string]string{ + "_csrf": GetUserCSRFToken(t, user2Session), + }) + user2Session.MakeRequest(t, req, http.StatusOK) + + // the first run has been cancelled + run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run1.ID}) + assert.Equal(t, actions_model.StatusCancelled, run1.Status) + + // fetch and check the second task + task2 := runner.fetchTask(t) + _, _, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, "cancel-run-group", run2.ConcurrencyGroup) + assert.False(t, run2.ConcurrencyCancel) + assert.Equal(t, actions_model.StatusRunning, run2.Status) + }) +} + +func TestAbandonConcurrentRun(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user2Session := loginUser(t, user2.Name) + user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, user2Token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + user2APICtx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(user2APICtx)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + wf1TreePath := ".gitea/workflows/workflow-1.yml" + wf1FileContent := `name: Workflow-1 +on: + push: + paths: + - '.gitea/workflows/workflow-1.yml' +concurrency: + group: test-group +jobs: + wf1-job1: + runs-on: ubuntu-latest + steps: + - run: echo 'wf1-job1' + wf1-job2: + runs-on: customized-runner + steps: + - run: echo 'wf1-job1' +` + + wf2TreePath := ".gitea/workflows/workflow-2.yml" + wf2FileContent := `name: Workflow-2 +on: + push: + paths: + - '.gitea/workflows/workflow-2.yml' +concurrency: + group: test-group +jobs: + wf2-job1: + runs-on: ubuntu-latest + steps: + - run: echo 'wf2-job1' +` + // push workflow1 + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent) + createWorkflowFile(t, user2Token, repo.OwnerName, repo.Name, wf1TreePath, opts1) + + // fetch wf1-job1 + w1j1Task := runner.fetchTask(t) + _, _, run1 := getTaskAndJobAndRunByTaskID(t, w1j1Task.Id) + assert.Equal(t, "test-group", run1.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusRunning, run1.Status) + // query wf1-job2 from db and check its status + w1j2Job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run1.ID, JobID: "wf1-job2"}) + // wf1-job2 is waiting but no runner will run it + assert.Equal(t, actions_model.StatusWaiting, w1j2Job.Status) + + time.Sleep(time.Second) + now := time.Now() + time.Sleep(time.Second) + + // push workflow2 + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf2TreePath, wf2FileContent) + createWorkflowFile(t, user2Token, repo.OwnerName, repo.Name, wf2TreePath, opts2) + + // query run2 from db and check its status + run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, WorkflowID: "workflow-2.yml"}) + // run2 is blocked because it is blocked by workflow1's concurrency group "test-group" + assert.Equal(t, actions_model.StatusBlocked, run2.Status) + + // mock time + fakeNow := now.Add(setting.Actions.AbandonedJobTimeout) + timeutil.MockSet(fakeNow) + defer timeutil.MockUnset() + + // call CancelAbandonedJobs manually + assert.NoError(t, actions_service.CancelAbandonedJobs(t.Context())) + + // check the status of wf1-job2 + w1j2Job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: w1j2Job.ID}) + assert.Equal(t, actions_model.StatusCancelled, w1j2Job.Status) + // check the status of run1 + run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run1.ID}) + assert.Equal(t, actions_model.StatusCancelled, run1.Status) + + // fetch wf2-job1 and check + w2j1Task := runner.fetchTask(t) + _, w2j1Job, run2 := getTaskAndJobAndRunByTaskID(t, w2j1Task.Id) + assert.Equal(t, "test-group", run2.ConcurrencyGroup) + assert.Equal(t, "wf2-job1", w2j1Job.JobID) + assert.Equal(t, actions_model.StatusRunning, run2.Status) + assert.Equal(t, actions_model.StatusRunning, w2j1Job.Status) + }) +} + +func TestRunAndJobWithSameConcurrencyGroup(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-concurrency", false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(httpContext)(t) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false) + + wf1TreePath := ".gitea/workflows/concurrent-workflow-1.yml" + wf1FileContent := `name: concurrent-workflow-1 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-1.yml' +jobs: + wf1-job: + runs-on: ubuntu-latest + concurrency: + group: test-group + steps: + - run: echo 'wf1-job' +` + wf2TreePath := ".gitea/workflows/concurrent-workflow-2.yml" + wf2FileContent := `name: concurrent-workflow-2 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-2.yml' +concurrency: + group: test-group +jobs: + wf2-job: + runs-on: ubuntu-latest + steps: + - run: echo 'wf2-job' +` + wf3TreePath := ".gitea/workflows/concurrent-workflow-3.yml" + wf3FileContent := `name: concurrent-workflow-3 +on: + push: + paths: + - '.gitea/workflows/concurrent-workflow-3.yml' +jobs: + wf3-job: + runs-on: ubuntu-latest + concurrency: + group: test-group + cancel-in-progress: true + steps: + - run: echo 'wf3-job' +` + // push workflow1 + opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf1TreePath, wf1FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1) + // fetch run1 + task := runner.fetchTask(t) + _, job1, run1 := getTaskAndJobAndRunByTaskID(t, task.Id) + assert.Equal(t, "test-group", job1.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusRunning, run1.Status) + + // push workflow2 + opts2 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf2TreePath, wf2FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf2TreePath, opts2) + // cannot fetch run2 because run1 is still running + runner.fetchNoTask(t) + run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, WorkflowID: "concurrent-workflow-2.yml"}) + assert.Equal(t, "test-group", run2.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusBlocked, run2.Status) + + // exec run1 + runner.execTask(t, task, &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + }) + + // fetch run2 + task2 := runner.fetchTask(t) + _, _, run2 = getTaskAndJobAndRunByTaskID(t, task2.Id) + assert.Equal(t, actions_model.StatusRunning, run2.Status) + + // push workflow3 + opts3 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wf3TreePath, wf3FileContent) + createWorkflowFile(t, token, user2.Name, repo.Name, wf3TreePath, opts3) + // fetch run3 + task3 := runner.fetchTask(t) + _, job3, run3 := getTaskAndJobAndRunByTaskID(t, task3.Id) + assert.Equal(t, "test-group", job3.ConcurrencyGroup) + assert.Equal(t, actions_model.StatusRunning, run3.Status) + + // run2 should be cancelled by run3 + run2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID}) + assert.Equal(t, actions_model.StatusCancelled, run2.Status) + }) +} diff --git a/tests/integration/api_repo_file_diffpatch_test.go b/tests/integration/api_repo_file_diffpatch_test.go new file mode 100644 index 0000000000..e463027ed3 --- /dev/null +++ b/tests/integration/api_repo_file_diffpatch_test.go @@ -0,0 +1,89 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func getApplyDiffPatchFileOptions() *api.ApplyDiffPatchFileOptions { + return &api.ApplyDiffPatchFileOptions{ + FileOptions: api.FileOptions{ + BranchName: "master", + }, + Content: `diff --git a/patch-file-1.txt b/patch-file-1.txt +new file mode 100644 +index 0000000000..aaaaaaaaaa +--- /dev/null ++++ b/patch-file-1.txt +@@ -0,0 +1 @@ ++File 1 +`, + } +} + +func TestAPIApplyDiffPatchFileOptions(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16 + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo + repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo + + session2 := loginUser(t, user2.Name) + token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + session4 := loginUser(t, user4.Name) + token4 := getTokenForLoggedInUser(t, session4, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/diffpatch", getApplyDiffPatchFileOptions()).AddTokenAuth(token2) + resp := MakeRequest(t, req, http.StatusCreated) + var fileResponse api.FileResponse + DecodeJSON(t, resp, &fileResponse) + assert.Nil(t, fileResponse.Content) + assert.NotEmpty(t, fileResponse.Commit.HTMLURL) + req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/raw/patch-file-1.txt") + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "File 1\n", resp.Body.String()) + + // Test creating a file in repo1 by user4 who does not have write access + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo16.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token4) + MakeRequest(t, req, http.StatusNotFound) + + // Tests a repo with no token given so will fail + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo16.Name), getApplyDiffPatchFileOptions()) + MakeRequest(t, req, http.StatusNotFound) + + // Test using access token for a private repo that the user of the token owns + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo16.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusCreated) + + // Test using org repo "org3/repo3" where user2 is a collaborator + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", org3.Name, repo3.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusCreated) + + // Test using org repo "org3/repo3" with no user token + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", org3.Name, repo3.Name), getApplyDiffPatchFileOptions()) + MakeRequest(t, req, http.StatusNotFound) + + // Test using repo "user2/repo1" where user4 is a NOT collaborator + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo1.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token4) + MakeRequest(t, req, http.StatusForbidden) + }) +} diff --git a/tests/integration/git_lfs_ssh_test.go b/tests/integration/git_lfs_ssh_test.go index 4ca1ffece5..d2f34ef10b 100644 --- a/tests/integration/git_lfs_ssh_test.go +++ b/tests/integration/git_lfs_ssh_test.go @@ -5,6 +5,8 @@ package integration import ( "net/url" + "os" + "path/filepath" "slices" "strings" "sync" @@ -23,7 +25,8 @@ import ( func TestGitLFSSSH(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - dstPath := t.TempDir() + localRepoForUpload := filepath.Join(t.TempDir(), "test-upload") + localRepoForDownload := filepath.Join(t.TempDir(), "test-download") apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) var mu sync.Mutex @@ -37,7 +40,7 @@ func TestGitLFSSSH(t *testing.T) { withKeyFile(t, "my-testing-key", func(keyFile string) { t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile)) cloneURL := createSSHUrl(apiTestContext.GitPath(), u) - t.Run("Clone", doGitClone(dstPath, cloneURL)) + t.Run("CloneOrigin", doGitClone(localRepoForUpload, cloneURL)) cfg, err := setting.CfgProvider.PrepareSaving() require.NoError(t, err) @@ -46,10 +49,15 @@ func TestGitLFSSSH(t *testing.T) { require.NoError(t, cfg.Save()) _, _, cmdErr := gitcmd.NewCommand("config", "lfs.sshtransfer", "always"). - WithDir(dstPath). + WithDir(localRepoForUpload). RunStdString(t.Context()) assert.NoError(t, cmdErr) - lfsCommitAndPushTest(t, dstPath, 10) + pushedFiles := lfsCommitAndPushTest(t, localRepoForUpload, 10) + + t.Run("CloneLFS", doGitClone(localRepoForDownload, cloneURL)) + content, err := os.ReadFile(filepath.Join(localRepoForDownload, pushedFiles[0])) + assert.NoError(t, err) + assert.Len(t, content, 10) }) countBatch := slices.ContainsFunc(routerCalls, func(s string) bool { @@ -58,12 +66,16 @@ func TestGitLFSSSH(t *testing.T) { countUpload := slices.ContainsFunc(routerCalls, func(s string) bool { return strings.Contains(s, "PUT /api/internal/repo/user2/repo1.git/info/lfs/objects/") }) + countDownload := slices.ContainsFunc(routerCalls, func(s string) bool { + return strings.Contains(s, "GET /api/internal/repo/user2/repo1.git/info/lfs/objects/") + }) nonAPIRequests := slices.ContainsFunc(routerCalls, func(s string) bool { fields := strings.Fields(s) return !strings.HasPrefix(fields[1], "/api/") }) assert.NotZero(t, countBatch) assert.NotZero(t, countUpload) + assert.NotZero(t, countDownload) assert.Zero(t, nonAPIRequests) }) } diff --git a/tests/integration/lfs_view_test.go b/tests/integration/lfs_view_test.go index 3140f491c9..bbfbb0e6ec 100644 --- a/tests/integration/lfs_view_test.go +++ b/tests/integration/lfs_view_test.go @@ -72,6 +72,8 @@ func TestLFSRender(t *testing.T) { fileInfo := doc.Find("div.file-info-entry").First().Text() assert.Contains(t, fileInfo, "LFS") + fileSize := doc.Find("div.file-info-entry > .file-info-size").Text() + assert.Equal(t, "2.0 KiB", fileSize) // find new file view container fileViewContainer := doc.Find("[data-global-init=initRepoFileView]") diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 7670aebab5..3345216838 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -43,7 +43,13 @@ import ( "github.com/stretchr/testify/assert" ) -func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle, deleteBranch bool) *httptest.ResponseRecorder { +type MergeOptions struct { + Style repo_model.MergeStyle + HeadCommitID string + DeleteBranch bool +} + +func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeOptions MergeOptions) *httptest.ResponseRecorder { req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum)) resp := session.MakeRequest(t, req, http.StatusOK) @@ -51,11 +57,12 @@ func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum strin link := path.Join(user, repo, "pulls", pullnum, "merge") options := map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - "do": string(mergeStyle), + "_csrf": htmlDoc.GetCSRF(), + "do": string(mergeOptions.Style), + "head_commit_id": mergeOptions.HeadCommitID, } - if deleteBranch { + if mergeOptions.DeleteBranch { options["delete_branch_after_merge"] = "on" } @@ -69,6 +76,14 @@ func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum strin assert.Equal(t, fmt.Sprintf("/%s/%s/pulls/%s", user, repo, pullnum), respJSON.Redirect) + pullnumInt, err := strconv.ParseInt(pullnum, 10, 64) + assert.NoError(t, err) + repository, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), user, repo) + assert.NoError(t, err) + pull, err := issues_model.GetPullRequestByIndex(t.Context(), repository.ID, pullnumInt) + assert.NoError(t, err) + assert.True(t, pull.HasMerged) + return resp } @@ -102,7 +117,10 @@ func TestPullMerge(t *testing.T) { elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: false, + }) hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) @@ -124,7 +142,10 @@ func TestPullRebase(t *testing.T) { elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase, false) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleRebase, + DeleteBranch: false, + }) hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) @@ -146,7 +167,10 @@ func TestPullRebaseMerge(t *testing.T) { elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge, false) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleRebaseMerge, + DeleteBranch: false, + }) hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) @@ -169,7 +193,42 @@ func TestPullSquash(t *testing.T) { elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash, false) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleSquash, + DeleteBranch: false, + }) + + hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) + assert.NoError(t, err) + assert.Len(t, hookTasks, hookTasksLenBefore+1) + }) +} + +func TestPullSquashWithHeadCommitID(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + hookTasks, err := webhook.HookTasks(t.Context(), 1, 1) // Retrieve previous hook number + assert.NoError(t, err) + hookTasksLenBefore := len(hookTasks) + + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") + testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n") + + resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) + headBranch, err := git_model.GetBranch(t.Context(), repo1.ID, "master") + assert.NoError(t, err) + assert.NotNil(t, headBranch) + + elem := strings.Split(test.RedirectURL(resp), "/") + assert.Equal(t, "pulls", elem[3]) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleSquash, + DeleteBranch: false, + HeadCommitID: headBranch.CommitID, + }) hookTasks, err = webhook.HookTasks(t.Context(), 1, 1) assert.NoError(t, err) @@ -187,7 +246,10 @@ func TestPullCleanUpAfterMerge(t *testing.T) { elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: false, + }) // Check PR branch deletion resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4]) @@ -556,7 +618,10 @@ func TestPullRetargetChildOnBranchDelete(t *testing.T) { elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/") assert.Equal(t, "pulls", elemChildPR[3]) - testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true) + testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: true, + }) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"}) @@ -592,7 +657,10 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) { defer test.MockVariableValue(&setting.Repository.PullRequest.RetargetChildrenOnMerge, false)() - testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true) + testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: true, + }) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"}) @@ -624,7 +692,10 @@ func TestPullRequestMergedWithNoPermissionDeleteBranch(t *testing.T) { // user2 has no permission to delete branch of repo user1/repo1 session2 := loginUser(t, "user2") - testPullMerge(t, session2, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true) + testPullMerge(t, session2, elemBasePR[1], elemBasePR[2], elemBasePR[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: true, + }) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user4", Name: "repo1"}) branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"}) @@ -672,7 +743,10 @@ func TestPullMergeIndexerNotifier(t *testing.T) { // merge the pull request elem := strings.Split(test.RedirectURL(createPullResp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: false, + }) // check if the issue is closed issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 8e9a5d7b93..ff6a0ecfb9 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -229,7 +229,10 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) { resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "master", "This is a pull title") elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, user1Session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) + testPullMerge(t, user1Session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: false, + }) // Grab the CSRF token. req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4])) diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go index d5025decba..7781fd0511 100644 --- a/tests/integration/repo_activity_test.go +++ b/tests/integration/repo_activity_test.go @@ -27,7 +27,10 @@ func TestRepoActivity(t *testing.T) { resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title") elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) - testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) + testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{ + Style: repo_model.MergeStyleMerge, + DeleteBranch: false, + }) testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n") testPullCreate(t, session, "user1", "repo1", false, "master", "feat/better_readme", "This is a pull title") diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 379cf56802..666ae44c08 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -218,13 +218,19 @@ func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "merged-pr", http.StatusSeeOther) prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "merged-pr", "merged pr") testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "merged-pr", fmt.Sprintf("new-commit-%s.txt", headRepo.Name), "new-commit") - testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, repo_model.MergeStyleRebaseMerge, false) + testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, MergeOptions{ + Style: repo_model.MergeStyleRebaseMerge, + DeleteBranch: false, + }) // create merged PR with deleted branch testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "merged-pr-deleted", http.StatusSeeOther) prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "merged-pr-deleted", "merged pr with deleted branch") testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "merged-pr-deleted", fmt.Sprintf("new-commit-%s-2.txt", headRepo.Name), "new-commit") - testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, repo_model.MergeStyleRebaseMerge, true) + testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, MergeOptions{ + Style: repo_model.MergeStyleRebaseMerge, + DeleteBranch: true, + }) } func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath string, expected []string) { diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index d12addb127..cd6b0df122 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -257,10 +257,12 @@ func testViewFileInRepo(t *testing.T) { description := htmlDoc.doc.Find(".repo-description") repoTopics := htmlDoc.doc.Find("#repo-topics") repoSummary := htmlDoc.doc.Find(".repository-summary") + fileSize := htmlDoc.Find("div.file-info-entry > .file-info-size").Text() assert.Equal(t, 0, description.Length()) assert.Equal(t, 0, repoTopics.Length()) assert.Equal(t, 0, repoSummary.Length()) + assert.Equal(t, "30 B", fileSize) } // TestBlameFileInRepo repo description, topics and summary should not be displayed when running blame on a file diff --git a/web_src/css/base.css b/web_src/css/base.css index 9cef92019d..8b77e55fa3 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -101,11 +101,13 @@ samp, font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */ } -code { +/* there are many blocks in non-markup(.markup code) / non-code-diff(code.code-inner) containers (for example: translation strings, etc), +so we need to make have default global styles, ".markup code" has its own styles and doesn't conflict, but `.code-inner` is special. +TODO: in the future, we should use `div` instead of `code` for `.code-inner` because it is a container for highlighted code line */ +code:not(.code-inner) { padding: 1px 4px; border-radius: var(--border-radius); background-color: var(--color-label-bg); - color: var(--color-label-text); } b, diff --git a/web_src/css/form.css b/web_src/css/form.css index 757edf7297..197c0f5af2 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -228,6 +228,12 @@ textarea:focus, color: var(--color-text-light-1); } +.form .help pre.command-block { + white-space: pre-wrap; + overflow-wrap: anywhere; + margin: 0.25em 0 0.25em 1em; +} + .m-captcha-style { width: 100%; height: 5em; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 9f4fa51881..c70937147a 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1582,6 +1582,7 @@ tbody.commit-list { display: flex; align-items: center; width: max-content; + gap: 0.25em; } .file-info-entry + .file-info-entry { diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue index bc3b99ab89..22f79384e3 100644 --- a/web_src/js/components/ActionRunStatus.vue +++ b/web_src/js/components/ActionRunStatus.vue @@ -21,10 +21,10 @@ withDefaults(defineProps<{ - - + + - + diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index b90aef7411..6e6733c7d0 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -601,7 +601,8 @@ export default defineComponent({
-
+ +