diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbf2a331dd480..7a23448e0ab55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -267,26 +267,10 @@ with the rest of the summary matching the original PR. Similarly for frontports --- -The below is a script that may be helpful in creating backports. YMMV. +A command to help create backports can be found in `contrib/backport` and can be installed (from inside the gitea repo root directory) using: ```bash -#!/bin/sh -PR="$1" -SHA="$2" -VERSION="$3" - -if [ -z "$SHA" ]; then - SHA=$(gh api /repos/go-gitea/gitea/pulls/$PR -q '.merge_commit_sha') -fi - -if [ -z "$VERSION" ]; then - VERSION="v1.16" -fi - -echo git checkout origin/release/"$VERSION" -b backport-$PR-$VERSION -git checkout origin/release/"$VERSION" -b backport-$PR-$VERSION -git cherry-pick $SHA && git commit --amend && git push zeripath backport-$PR-$VERSION && xdg-open https://github.com/go-gitea/gitea/compare/release/"$VERSION"...zeripath:backport-$PR-$VERSION - +go install contrib/backport/backport.go ``` ## Developer Certificate of Origin (DCO) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 7f5b647796da0..8789bd3f066e7 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -4,6 +4,11 @@ "path": "cloud.google.com/go/compute/metadata/LICENSE", "licenseText": "\n 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": "code.gitea.io/actions-proto-go", + "path": "code.gitea.io/actions-proto-go/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2022 The Gitea Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + }, { "name": "code.gitea.io/gitea/modules/lfs", "path": "code.gitea.io/gitea/modules/lfs/LICENSE", @@ -69,6 +74,11 @@ "path": "github.com/NYTimes/gziphandler/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 2016-2017 The New York Times Company\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/ProtonMail/go-crypto", + "path": "github.com/ProtonMail/go-crypto/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/RoaringBitmap/roaring", "path": "github.com/RoaringBitmap/roaring/LICENSE", @@ -199,6 +209,11 @@ "path": "github.com/bradfitz/gomemcache/memcache/LICENSE", "licenseText": "\n 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/bufbuild/connect-go", + "path": "github.com/bufbuild/connect-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 2021-2022 Buf Technologies, Inc.\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/buildkite/terminal-to-html/v3", "path": "github.com/buildkite/terminal-to-html/v3/LICENSE", @@ -224,6 +239,11 @@ "path": "github.com/chi-middleware/proxy/LICENSE", "licenseText": "Copyright (c) 2020 Lauris BH\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": "github.com/cloudflare/circl", + "path": "github.com/cloudflare/circl/LICENSE", + "licenseText": "Copyright (c) 2019 Cloudflare. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Cloudflare nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n========================================================================\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/couchbase/go-couchbase", "path": "github.com/couchbase/go-couchbase/LICENSE", @@ -304,11 +324,21 @@ "path": "github.com/emersion/go-sasl/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 emersion\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" }, + { + "name": "github.com/emirpasic/gods", + "path": "github.com/emirpasic/gods/LICENSE", + "licenseText": "Copyright (c) 2015, Emir Pasic\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n-------------------------------------------------------------------------------\n\nAVL Tree:\n\nCopyright (c) 2017 Benjamin Scher Purcell \u003cbenjapurcell@gmail.com\u003e\n\nPermission to use, copy, modify, and distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n" + }, { "name": "github.com/ethantkoenig/rupture", "path": "github.com/ethantkoenig/rupture/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2018 Ethan Koenig\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" }, + { + "name": "github.com/fatih/color", + "path": "github.com/fatih/color/LICENSE.md", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Fatih Arslan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject 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, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + }, { "name": "github.com/felixge/fgprof", "path": "github.com/felixge/fgprof/LICENSE.txt", @@ -369,6 +399,26 @@ "path": "github.com/go-fed/httpsig/LICENSE", "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, go-fed\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/go-git/gcfg", + "path": "github.com/go-git/gcfg/LICENSE", + "licenseText": "Copyright (c) 2012 Péter Surányi. Portions Copyright (c) 2009 The Go\nAuthors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "github.com/go-git/go-billy/v5", + "path": "github.com/go-git/go-billy/v5/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 2017 Sourced Technologies S.L.\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/go-git/go-git/v5", + "path": "github.com/go-git/go-git/v5/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 2018 Sourced Technologies, S.L.\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/go-ini/ini", + "path": "github.com/go-ini/ini/LICENSE", + "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright 2014 Unknwon\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/go-ldap/ldap/v3", "path": "github.com/go-ldap/ldap/v3/LICENSE", @@ -539,11 +589,21 @@ "path": "github.com/huandu/xstrings/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Huan Du\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\n" }, + { + "name": "github.com/imdario/mergo", + "path": "github.com/imdario/mergo/LICENSE", + "licenseText": "Copyright (c) 2013 Dario Castañé. All rights reserved.\nCopyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/jaytaylor/html2text", "path": "github.com/jaytaylor/html2text/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\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\n" }, + { + "name": "github.com/jbenet/go-context/io", + "path": "github.com/jbenet/go-context/io/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Juan Batiz-Benet\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": "github.com/jhillyerd/enmime", "path": "github.com/jhillyerd/enmime/LICENSE", @@ -564,6 +624,11 @@ "path": "github.com/kballard/go-shellquote/LICENSE", "licenseText": "Copyright (C) 2014 Kevin Ballard\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE\nOR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, + { + "name": "github.com/kevinburke/ssh_config", + "path": "github.com/kevinburke/ssh_config/LICENSE", + "licenseText": "Copyright (c) 2017 Kevin Burke.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n===================\n\nThe lexer and parser borrow heavily from github.com/pelletier/go-toml. The\nlicense for that project is copied below.\n\nThe MIT License (MIT)\n\nCopyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton\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" + }, { "name": "github.com/keybase/go-crypto", "path": "github.com/keybase/go-crypto/LICENSE", @@ -624,6 +689,11 @@ "path": "github.com/markbates/goth/LICENSE.txt", "licenseText": "Copyright (c) 2014 Mark Bates\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, + { + "name": "github.com/mattn/go-colorable", + "path": "github.com/mattn/go-colorable/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Yasuhiro Matsumoto\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" + }, { "name": "github.com/mattn/go-isatty", "path": "github.com/mattn/go-isatty/LICENSE", @@ -689,6 +759,11 @@ "path": "github.com/modern-go/reflect2/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" }, + { + "name": "github.com/nektos/act/pkg", + "path": "github.com/nektos/act/pkg/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2022 The Gitea Authors\nCopyright (c) 2019\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" + }, { "name": "github.com/nfnt/resize", "path": "github.com/nfnt/resize/LICENSE", @@ -764,11 +839,21 @@ "path": "github.com/prometheus/procfs/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" }, + { + "name": "github.com/rhysd/actionlint", + "path": "github.com/rhysd/actionlint/LICENSE.txt", + "licenseText": "the MIT License\n\nCopyright (c) 2021 rhysd\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 copies\nof the Software, and to permit persons to whom the Software is furnished to do so,\nsubject 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 IMPLIED,\nINCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR\nPURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR\nTHE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n" + }, { "name": "github.com/rivo/uniseg", "path": "github.com/rivo/uniseg/LICENSE.txt", "licenseText": "MIT License\n\nCopyright (c) 2019 Oliver Kuederle\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" }, + { + "name": "github.com/robfig/cron", + "path": "github.com/robfig/cron/LICENSE", + "licenseText": "Copyright (C) 2012 Rob Figueiredo\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject 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, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + }, { "name": "github.com/rs/xid", "path": "github.com/rs/xid/LICENSE", @@ -789,6 +874,16 @@ "path": "github.com/sergi/go-diff/diffmatchpatch/LICENSE", "licenseText": "Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, 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\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n" }, + { + "name": "github.com/sirupsen/logrus", + "path": "github.com/sirupsen/logrus/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Simon Eskildsen\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": "github.com/skeema/knownhosts", + "path": "github.com/skeema/knownhosts/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" + }, { "name": "github.com/ssor/bom", "path": "github.com/ssor/bom/LICENSE", @@ -844,6 +939,11 @@ "path": "github.com/xanzy/go-gitlab/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" }, + { + "name": "github.com/xanzy/ssh-agent", + "path": "github.com/xanzy/ssh-agent/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" + }, { "name": "github.com/yohcop/openid-go", "path": "github.com/yohcop/openid-go/LICENSE", @@ -904,6 +1004,11 @@ "path": "golang.org/x/oauth2/LICENSE", "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "golang.org/x/sync", + "path": "golang.org/x/sync/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "golang.org/x/sys", "path": "golang.org/x/sys/LICENSE", @@ -919,6 +1024,16 @@ "path": "golang.org/x/time/rate/LICENSE", "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "google.golang.org/genproto/googleapis/rpc/status", + "path": "google.golang.org/genproto/googleapis/rpc/status/LICENSE", + "licenseText": "\n 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": "google.golang.org/grpc", + "path": "google.golang.org/grpc/LICENSE", + "licenseText": "\n 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": "google.golang.org/protobuf", "path": "google.golang.org/protobuf/LICENSE", @@ -934,6 +1049,11 @@ "path": "gopkg.in/ini.v1/LICENSE", "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright 2014 Unknwon\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": "gopkg.in/warnings.v0", + "path": "gopkg.in/warnings.v0/LICENSE", + "licenseText": "Copyright (c) 2016 Péter Surányi.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "gopkg.in/yaml.v2", "path": "gopkg.in/yaml.v2/LICENSE", diff --git a/cmd/admin.go b/cmd/admin.go index a662011f9c079..8f8c68f9810c9 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -180,6 +180,11 @@ var ( Name: "raw", Usage: "Display only the token value", }, + cli.StringFlag{ + Name: "scopes", + Value: "", + Usage: "Comma separated list of scopes to apply to access token", + }, }, Action: runGenerateAccessToken, } @@ -698,9 +703,15 @@ func runGenerateAccessToken(c *cli.Context) error { return err } + accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize() + if err != nil { + return err + } + t := &auth_model.AccessToken{ - Name: c.String("token-name"), - UID: user.ID, + Name: c.String("token-name"), + UID: user.ID, + Scope: accessTokenScope, } if err := auth_model.NewAccessToken(t); err != nil { diff --git a/cmd/hook.go b/cmd/hook.go index 228b79f7f014a..a62ffdde5fe67 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -185,6 +185,7 @@ Gitea or set your environment appropriately.`, "") userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64) + actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64) hookOptions := private.HookOptions{ UserID: userID, @@ -194,6 +195,7 @@ Gitea or set your environment appropriately.`, "") GitPushOptions: pushOptions(), PullRequestID: prID, DeployKeyID: deployKeyID, + ActionPerm: int(actionPerm), } scanner := bufio.NewScanner(os.Stdin) diff --git a/contrib/backport/README b/contrib/backport/README new file mode 100644 index 0000000000000..1e84c1bb9743f --- /dev/null +++ b/contrib/backport/README @@ -0,0 +1,41 @@ +`backport` +========== + +`backport` is a command to help create backports of PRs. It backports a +provided PR from main on to a released version. + +It will create a backport branch, cherry-pick the PR's merge commit, adjust +the commit message and then push this back up to your fork's remote. + +The default version will read from `docs/config.yml`. You can override this +using the option `--version`. + +The upstream branches will be fetched, using the remote `origin`. This can +be overrided using `--upstream`, and fetching can be avoided using +`--no-fetch`. + +By default the branch created will be called `backport-$PR-$VERSION`. You +can override this using the option `--backport-branch`. This branch will +be created from `--release-branch` which is `release/$(VERSION)` +by default and will be pulled from `$(UPSTREAM)`. + +The merge-commit as determined by the github API will be used as the SHA to +cherry-pick. You can override this using `--cherry-pick`. + +The commit message will be amended to add the `Backport` header. +`--no-amend-message` can be set to stop this from happening. + +If cherry-pick is successful the backported branch will be pushed up to your +fork using your remote. These will be determined using `git remote -v`. You +can set your fork name using `--fork-user` and your remote name using +`--remote`. You can avoid pushing using `--no-push`. + +If the push is successful, `xdg-open` will be called to open a backport url. +You can stop this using `--no-xdg-open`. + +Installation +============ + +```bash +go install contrib/backport/backport.go +``` diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go new file mode 100644 index 0000000000000..c35bf9e30f67e --- /dev/null +++ b/contrib/backport/backport.go @@ -0,0 +1,438 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "os/signal" + "path" + "strconv" + "strings" + "syscall" + + "github.com/google/go-github/v45/github" + "github.com/urfave/cli" + "gopkg.in/yaml.v3" +) + +const defaultVersion = "v1.18" // to backport to + +func main() { + app := cli.NewApp() + app.Name = "backport" + app.Usage = "Backport provided PR-number on to the current or previous released version" + app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version` + app.ArgsUsage = "" + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "version", + Usage: "Version branch to backport on to", + }, + cli.StringFlag{ + Name: "upstream", + Value: "origin", + Usage: "Upstream remote for the Gitea upstream", + }, + cli.StringFlag{ + Name: "release-branch", + Value: "", + Usage: "Release branch to backport on. Will default to release/", + }, + cli.StringFlag{ + Name: "cherry-pick", + Usage: "SHA to cherry-pick as backport", + }, + cli.StringFlag{ + Name: "backport-branch", + Usage: "Backport branch to backport on to (default: backport--", + }, + cli.StringFlag{ + Name: "remote", + Value: "", + Usage: "Remote for your fork of the Gitea upstream", + }, + cli.StringFlag{ + Name: "fork-user", + Value: "", + Usage: "Forked user name on Github", + }, + cli.BoolFlag{ + Name: "no-fetch", + Usage: "Set this flag to prevent fetch of remote branches", + }, + cli.BoolFlag{ + Name: "no-amend-message", + Usage: "Set this flag to prevent automatic amendment of the commit message", + }, + cli.BoolFlag{ + Name: "no-push", + Usage: "Set this flag to prevent pushing the backport up to your fork", + }, + cli.BoolFlag{ + Name: "no-xdg-open", + Usage: "Set this flag to not use xdg-open to open the PR URL", + }, + } + cli.AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} +USAGE: + {{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if len .Authors}} +AUTHOR: + {{range .Authors}}{{ . }}{{end}} + {{end}}{{if .Commands}} +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + + app.Action = runBackport + + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err) + } +} + +func runBackport(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + version := c.String("version") + if version == "" { + version = readVersion() + } + if version == "" { + version = defaultVersion + } + + upstream := c.String("upstream") + if upstream == "" { + upstream = "origin" + } + + forkUser := c.String("fork-user") + remote := c.String("remote") + if remote == "" && !c.Bool("--no-push") { + var err error + remote, forkUser, err = determineRemote(ctx, forkUser) + if err != nil { + return err + } + } + + upstreamReleaseBranch := c.String("release-branch") + if upstreamReleaseBranch == "" { + upstreamReleaseBranch = path.Join("release", version) + } + + localReleaseBranch := path.Join(upstream, upstreamReleaseBranch) + + args := c.Args() + if len(args) == 0 { + return fmt.Errorf("no PR number provided\nProvide a PR number to backport") + } else if len(args) != 1 { + return fmt.Errorf("multiple PRs provided %v\nOnly a single PR can be backported at a time", args) + } + + pr := args[0] + + backportBranch := c.String("backport-branch") + if backportBranch == "" { + backportBranch = "backport-" + pr + "-" + version + } + + fmt.Printf("* Backporting %s to %s as %s\n", pr, localReleaseBranch, backportBranch) + + sha := c.String("cherry-pick") + if sha == "" { + var err error + sha, err = determineSHAforPR(ctx, pr) + if err != nil { + return err + } + } + if sha == "" { + return fmt.Errorf("unable to determine sha for cherry-pick of %s", pr) + } + + if !c.Bool("no-fetch") { + if err := fetchRemoteAndMain(ctx, upstream, upstreamReleaseBranch); err != nil { + return err + } + } + + if err := checkoutBackportBranch(ctx, backportBranch, localReleaseBranch); err != nil { + return err + } + + if err := cherrypick(ctx, sha); err != nil { + return err + } + + if !c.Bool("no-amend-message") { + if err := amendCommit(ctx, pr); err != nil { + return err + } + } + + if !c.Bool("no-push") { + url := "https://github.com/go-gitea/gitea/compare/" + upstreamReleaseBranch + "..." + forkUser + ":" + backportBranch + + if err := gitPushUp(ctx, remote, backportBranch); err != nil { + return err + } + + if !c.Bool("no-xdg-open") { + if err := xdgOpen(ctx, url); err != nil { + return err + } + } else { + fmt.Printf("* Navigate to %s to open PR\n", url) + } + } + return nil +} + +func xdgOpen(ctx context.Context, url string) error { + fmt.Printf("* `xdg-open %s`\n", url) + out, err := exec.CommandContext(ctx, "xdg-open", url).Output() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", string(out)) + return fmt.Errorf("unable to xdg-open to %s: %w", url, err) + } + return nil +} + +func gitPushUp(ctx context.Context, remote, backportBranch string) error { + fmt.Printf("* `git push -u %s %s`\n", remote, backportBranch) + out, err := exec.CommandContext(ctx, "git", "push", "-u", remote, backportBranch).Output() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", string(out)) + return fmt.Errorf("unable to push up to %s: %w", remote, err) + } + return nil +} + +func amendCommit(ctx context.Context, pr string) error { + fmt.Printf("* Amending commit to prepend `Backport #%s` to body\n", pr) + out, err := exec.CommandContext(ctx, "git", "log", "-1", "--pretty=format:%B").Output() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", string(out)) + return fmt.Errorf("unable to get last log message: %w", err) + } + + parts := strings.SplitN(string(out), "\n", 2) + + if len(parts) != 2 { + return fmt.Errorf("unable to interpret log message:\n%s", string(out)) + } + subject, body := parts[0], parts[1] + if !strings.HasSuffix(subject, " (#"+pr+")") { + subject = subject + " (#" + pr + ")" + } + + out, err = exec.CommandContext(ctx, "git", "commit", "--amend", "-m", subject+"\n\nBackport #"+pr+"\n"+body).Output() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", string(out)) + return fmt.Errorf("unable to amend last log message: %w", err) + } + return nil +} + +func cherrypick(ctx context.Context, sha string) error { + // Check if a CHERRY_PICK_HEAD exists + if _, err := os.Stat(".git/CHERRY_PICK_HEAD"); err == nil { + // Assume that we are in the middle of cherry-pick - continue it + fmt.Println("* Attempting git cherry-pick --continue") + out, err := exec.CommandContext(ctx, "git", "cherry-pick", "--continue").Output() + if err != nil { + fmt.Fprintf(os.Stderr, "git cherry-pick --continue failed:\n%s\n", string(out)) + return fmt.Errorf("unable to continue cherry-pick: %w", err) + } + return nil + } + + fmt.Printf("* Attempting git cherry-pick %s\n", sha) + out, err := exec.CommandContext(ctx, "git", "cherry-pick", sha).Output() + if err != nil { + fmt.Fprintf(os.Stderr, "git cherry-pick %s failed:\n%s\n", sha, string(out)) + return fmt.Errorf("git cherry-pick %s failed: %w", sha, err) + } + return nil +} + +func checkoutBackportBranch(ctx context.Context, backportBranch, releaseBranch string) error { + out, err := exec.CommandContext(ctx, "git", "branch", "--show-current").Output() + if err != nil { + return fmt.Errorf("unable to check current branch %w", err) + } + + currentBranch := strings.TrimSpace(string(out)) + fmt.Printf("* Current branch is %s\n", currentBranch) + if currentBranch == backportBranch { + fmt.Printf("* Current branch is %s - not checking out\n", currentBranch) + return nil + } + + if _, err := exec.CommandContext(ctx, "git", "rev-list", "-1", backportBranch).Output(); err == nil { + fmt.Printf("* Branch %s already exists. Checking it out...\n", backportBranch) + return exec.CommandContext(ctx, "git", "checkout", "-f", backportBranch).Run() + } + + fmt.Printf("* `git checkout -b %s %s`\n", backportBranch, releaseBranch) + return exec.CommandContext(ctx, "git", "checkout", "-b", backportBranch, releaseBranch).Run() +} + +func fetchRemoteAndMain(ctx context.Context, remote, releaseBranch string) error { + fmt.Printf("* `git fetch %s main`\n", remote) + out, err := exec.CommandContext(ctx, "git", "fetch", remote, "main").Output() + if err != nil { + fmt.Println(string(out)) + return fmt.Errorf("unable to fetch %s from %s: %w", "main", remote, err) + } + fmt.Println(string(out)) + + fmt.Printf("* `git fetch %s %s`\n", remote, releaseBranch) + out, err = exec.CommandContext(ctx, "git", "fetch", remote, releaseBranch).Output() + if err != nil { + fmt.Println(string(out)) + return fmt.Errorf("unable to fetch %s from %s: %w", releaseBranch, remote, err) + } + fmt.Println(string(out)) + + return nil +} + +func determineRemote(ctx context.Context, forkUser string) (string, string, error) { + out, err := exec.CommandContext(ctx, "git", "remote", "-v").Output() + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to list git remotes:\n%s\n", string(out)) + return "", "", fmt.Errorf("unable to determine forked remote: %w", err) + } + lines := strings.Split(string(out), "\n") + for _, line := range lines { + fields := strings.Split(line, "\t") + name, remote := fields[0], fields[1] + // only look at pushers + if !strings.HasSuffix(remote, " (push)") { + continue + } + // only look at github.com pushes + if !strings.Contains(remote, "github.com") { + continue + } + // ignore go-gitea/gitea + if strings.Contains(remote, "go-gitea/gitea") { + continue + } + if !strings.Contains(remote, forkUser) { + continue + } + if strings.HasPrefix(remote, "git@github.com:") { + forkUser = strings.TrimPrefix(remote, "git@github.com:") + } else if strings.HasPrefix(remote, "https://github.com/") { + forkUser = strings.TrimPrefix(remote, "https://github.com/") + } else if strings.HasPrefix(remote, "https://www.github.com/") { + forkUser = strings.TrimPrefix(remote, "https://www.github.com/") + } else if forkUser == "" { + return "", "", fmt.Errorf("unable to extract forkUser from remote %s: %s", name, remote) + } + idx := strings.Index(forkUser, "/") + if idx >= 0 { + forkUser = forkUser[:idx] + } + return name, forkUser, nil + } + return "", "", fmt.Errorf("unable to find appropriate remote in:\n%s", string(out)) +} + +func readVersion() string { + bs, err := os.ReadFile("docs/config.yaml") + if err != nil { + if err == os.ErrNotExist { + log.Println("`docs/config.yaml` not present") + return "" + } + fmt.Fprintf(os.Stderr, "Unable to read `docs/config.yaml`: %v\n", err) + return "" + } + + type params struct { + Version string + } + type docConfig struct { + Params params + } + dc := &docConfig{} + if err := yaml.Unmarshal(bs, dc); err != nil { + fmt.Fprintf(os.Stderr, "Unable to read `docs/config.yaml`: %v\n", err) + return "" + } + + if dc.Params.Version == "" { + fmt.Fprintf(os.Stderr, "No version in `docs/config.yaml`") + return "" + } + + version := dc.Params.Version + if version[0] != 'v' { + version = "v" + version + } + + split := strings.SplitN(version, ".", 3) + + return strings.Join(split[:2], ".") +} + +func determineSHAforPR(ctx context.Context, prStr string) (string, error) { + prNum, err := strconv.Atoi(prStr) + if err != nil { + return "", err + } + + client := github.NewClient(http.DefaultClient) + + pr, _, err := client.PullRequests.Get(ctx, "go-gitea", "gitea", prNum) + if err != nil { + return "", err + } + + if pr.Merged == nil || !*pr.Merged { + return "", fmt.Errorf("PR #%d is not yet merged - cannot determine sha to backport", prNum) + } + + if pr.MergeCommitSHA != nil { + return *pr.MergeCommitSHA, nil + } + + return "", nil +} + +func installSignals() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(context.Background()) + go func() { + // install notify + signalChannel := make(chan os.Signal, 1) + + signal.Notify( + signalChannel, + syscall.SIGINT, + syscall.SIGTERM, + ) + select { + case <-signalChannel: + case <-ctx.Done(): + } + cancel() + signal.Reset() + }() + + return ctx, cancel +} diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b5559b1a954e1..77efe1417ba96 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2458,6 +2458,8 @@ ROUTER = console ;LIMIT_SIZE_COMPOSER = -1 ;; Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_CONAN = -1 +;; Maximum size of a Conda upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +;LIMIT_SIZE_CONDA = -1 ;; Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_CONTAINER = -1 ;; Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) @@ -2550,3 +2552,19 @@ ROUTER = console ;PROXY_URL = ;; Comma separated list of host names requiring proxy. Glob patterns (*) are accepted; use ** to match all hosts. ;PROXY_HOSTS = + +; [actions] +;; Enable/Disable actions capabilities +;ENABLED = false +;; Default address to get action plugins, e.g. the default value means downloading from "https://gitea.com/actions/checkout" for "uses: actions/checkout@v3" +;DEFAULT_ACTIONS_URL = https://gitea.com + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; settings for action logs, will override storage setting +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[storage.actions_log] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; storage type +;STORAGE_TYPE = local diff --git a/docker/rootless/etc/templates/app.ini b/docker/rootless/etc/templates/app.ini index f106f01f5eb44..0057635062c25 100644 --- a/docker/rootless/etc/templates/app.ini +++ b/docker/rootless/etc/templates/app.ini @@ -37,7 +37,7 @@ PROVIDER_CONFIG = $GITEA_WORK_DIR/data/sessions [picture] AVATAR_UPLOAD_PATH = $GITEA_WORK_DIR/data/avatars -REPOSITORY_AVATAR_UPLOAD_PATH = $GITEA_WORK_DIR/data/gitea/repo-avatars +REPOSITORY_AVATAR_UPLOAD_PATH = $GITEA_WORK_DIR/data/repo-avatars [attachment] PATH = $GITEA_WORK_DIR/data/attachments diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 295fa713a4e69..441bb824ad208 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -1214,6 +1214,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_COMPOSER`: **-1**: Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_CONAN`: **-1**: Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +- `LIMIT_SIZE_CONDA`: **-1**: Maximum size of a Conda upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_CONTAINER`: **-1**: Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_GENERIC`: **-1**: Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) - `LIMIT_SIZE_HELM`: **-1**: Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) @@ -1314,6 +1315,41 @@ PROXY_URL = socks://127.0.0.1:1080 PROXY_HOSTS = *.github.com ``` +## Actions (`actions`) + +- `ENABLED`: **false**: Enable/Disable actions capabilities +- `DEFAULT_ACTIONS_URL`: **https://gitea.com**: Default address to get action plugins, e.g. the default value means downloading from "https://gitea.com/actions/checkout" for "uses: actions/checkout@v3" + +`DEFAULT_ACTIONS_URL` indicates where should we find the relative path action plugin. i.e. when use an action in a workflow file like + +```yaml +name: versions +on: + push: + branches: + - main + - releases/* +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 +``` + +Now we need to know how to get actions/checkout, this configuration is the default git server to get it. That means we will get the repository via git clone ${DEFAULT_ACTIONS_URL}/actions/checkout and fetch tag v3. + +To help people who don't want to mirror these actions in their git instances, the default value is https://gitea.com +To help people run actions totally in their network, they can change the value and copy all necessary action repositories into their git server. + +Of course we should support the form in future PRs like + +```yaml +steps: + - uses: gitea.com/actions/checkout@v3 +``` + +although Github don't support this form. + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer. diff --git a/docs/content/doc/developers/guidelines-backend.en-us.md b/docs/content/doc/developers/guidelines-backend.en-us.md index 913898be83dbb..f35111522fa63 100644 --- a/docs/content/doc/developers/guidelines-backend.en-us.md +++ b/docs/content/doc/developers/guidelines-backend.en-us.md @@ -54,7 +54,7 @@ To maintain understandable code and avoid circular dependencies it is important ### Package Dependencies -Since Golang don't support import cycles, we have to decide the package dependencies carefully. There are some levels between those packages. Below is the ideal package dependencies direction. +Since Golang doesn't support import cycles, we have to decide the package dependencies carefully. There are some levels between those packages. Below is the ideal package dependencies direction. `cmd` -> `routers` -> `services` -> `models` -> `modules` diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 1ecbf1068b571..9eaae36a69b73 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -56,7 +56,7 @@ _Symbols used in table:_ | Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Repository Tokens with write rights | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | | RSS Feeds | ✓ | ✘ | ✓ | ✘ | ✘ | ✘ | ✘ | -| Built-in CI/CD | [✘](https://github.com/go-gitea/gitea/issues/13539) | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Built-in CI/CD | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | Subgroups: groups within groups | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | | Interaction with other instances | [/](https://github.com/go-gitea/gitea/issues/18240) | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ | | Mermaid diagrams in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | diff --git a/docs/content/doc/features/comparison.zh-cn.md b/docs/content/doc/features/comparison.zh-cn.md index c5bec58cb85b2..55014dab7bf5b 100644 --- a/docs/content/doc/features/comparison.zh-cn.md +++ b/docs/content/doc/features/comparison.zh-cn.md @@ -49,7 +49,7 @@ _表格中的符号含义:_ | 内置容器 Registry | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | 外部 Git 镜像 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | | WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | -| 内置 CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 内置 CI/CD | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | 子组织:组织内的组织 | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | #### 代码管理 diff --git a/docs/content/doc/features/comparison.zh-tw.md b/docs/content/doc/features/comparison.zh-tw.md index 4da9c74ec8cc8..bde17794412e7 100644 --- a/docs/content/doc/features/comparison.zh-tw.md +++ b/docs/content/doc/features/comparison.zh-tw.md @@ -51,7 +51,7 @@ menu: | 內建 Container Registry | [✘](https://github.com/go-gitea/gitea/issues/2316) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | 對外部 Git 鏡像 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | | FIDO (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| 內建 CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 內建 CI/CD | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | 子群組: 群組中的群組 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | ## 程式碼管理 diff --git a/docs/content/doc/packages/conda.en-us.md b/docs/content/doc/packages/conda.en-us.md new file mode 100644 index 0000000000000..8b828475904e0 --- /dev/null +++ b/docs/content/doc/packages/conda.en-us.md @@ -0,0 +1,85 @@ +--- +date: "2022-12-28T00:00:00+00:00" +title: "Conda Packages Repository" +slug: "packages/conda" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Conda" + weight: 25 + identifier: "conda" +--- + +# Conda Packages Repository + +Publish [Conda](https://docs.conda.io/en/latest/) packages for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Conda package registry, you need to use [conda](https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html). + +## Configuring the package registry + +To register the package registry and provide credentials, edit your `.condarc` file: + +```yaml +channel_alias: https://gitea.example.com/api/packages/{owner}/conda +channels: + - https://gitea.example.com/api/packages/{owner}/conda +default_channels: + - https://gitea.example.com/api/packages/{owner}/conda +``` + +| Placeholder | Description | +| ------------ | ----------- | +| `owner` | The owner of the package. | + +See the [official documentation](https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html) for explanations of the individual settings. + +If you need to provide credentials, you may embed them as part of the channel url (`https://user:password@gitea.example.com/...`). + +## Publish a package + +To publish a package, perform a HTTP PUT operation with the package content in the request body. + +``` +PUT https://gitea.example.com/api/packages/{owner}/conda/{channel}/{filename} +``` + +| Placeholder | Description | +| ------------ | ----------- | +| `owner` | The owner of the package. | +| `channel` | The [channel](https://conda.io/projects/conda/en/latest/user-guide/concepts/channels.html) of the package. (optional) | +| `filename` | The name of the file. | + +Example request using HTTP Basic authentication: + +```shell +curl --user your_username:your_password_or_token \ + --upload-file path/to/package-1.0.conda \ + https://gitea.example.com/api/packages/testuser/conda/package-1.0.conda +``` + +You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. + +## Install a package + +To install a package from the package registry, execute one of the following commands: + +```shell +conda install {package_name} +conda install {package_name}={package_version} +conda install -c {channel} {package_name} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `package_name` | The package name. | +| `package_version` | The package version. | +| `channel` | The channel of the package. (optional) | diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md index b12155e14da78..9a736c1e5641e 100644 --- a/docs/content/doc/packages/overview.en-us.md +++ b/docs/content/doc/packages/overview.en-us.md @@ -28,6 +28,7 @@ The following package managers are currently supported: | ---- | -------- | -------------- | | [Composer]({{< relref "doc/packages/composer.en-us.md" >}}) | PHP | `composer` | | [Conan]({{< relref "doc/packages/conan.en-us.md" >}}) | C++ | `conan` | +| [Conda]({{< relref "doc/packages/conda.en-us.md" >}}) | - | `conda` | | [Container]({{< relref "doc/packages/container.en-us.md" >}}) | - | any OCI compliant client | | [Generic]({{< relref "doc/packages/generic.en-us.md" >}}) | - | any HTTP client | | [Helm]({{< relref "doc/packages/helm.en-us.md" >}}) | - | any HTTP client, `cm-push` | diff --git a/docs/content/doc/packages/storage.en-us.md b/docs/content/doc/packages/storage.en-us.md index b04a7595f74bd..8b05b8a45fb4b 100644 --- a/docs/content/doc/packages/storage.en-us.md +++ b/docs/content/doc/packages/storage.en-us.md @@ -71,7 +71,7 @@ The patterns are case-insensitive which matches the behaviour of the package reg ### How the cleanup rules work -The cleanup rules are part of the [clean up job]({{< relref "doc/advanced/config-cheat-sheet.en-us.md#cron---cleanup-expired-packages-croncleanup_packages" >}}) and run periodicly. +The cleanup rules are part of the [clean up job]({{< relref "doc/advanced/config-cheat-sheet.en-us.md#cron---cleanup-expired-packages-croncleanup_packages" >}}) and run periodically. The cleanup rule: diff --git a/docs/content/doc/secrets/overview.en-us.md b/docs/content/doc/secrets/overview.en-us.md index 1a88d6cfbcc6b..21fb65f98d3d2 100644 --- a/docs/content/doc/secrets/overview.en-us.md +++ b/docs/content/doc/secrets/overview.en-us.md @@ -1,6 +1,6 @@ --- date: "2022-12-19T21:26:00+08:00" -title: "Encrypted secrets" +title: "Secrets" slug: "secrets/overview" draft: false toc: false @@ -12,24 +12,24 @@ menu: identifier: "overview" --- -# Encrypted secrets +# Secrets -Encrypted secrets allow you to store sensitive information in your organization or repository. +Secrets allow you to store sensitive information in your user, organization or repository. Secrets are available on Gitea 1.19+. # Naming your secrets The following rules apply to secret names: -Secret names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed. +- Secret names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed. -Secret names must not start with the `GITHUB_` and `GITEA_` prefix. +- Secret names must not start with the `GITHUB_` and `GITEA_` prefix. -Secret names must not start with a number. +- Secret names must not start with a number. -Secret names are not case-sensitive. +- Secret names are not case-sensitive. -Secret names must be unique at the level they are created at. +- Secret names must be unique at the level they are created at. For example, a secret created at the repository level must have a unique name in that repository, and a secret created at the organization level must have a unique name at that level. diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index 5f05bc4c3be3e..d9d397df31b3d 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -382,7 +382,7 @@ Currently there are a check list below: Sometimes if you moved or renamed your Gitea binary when upgrade and you haven't run `Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.)` on your Admin Panel. Then all pull/push via SSH will not be work. This check will help you to check if it works well. -For contributors, if you want to add more checks, you can wrie ad new function like `func(ctx *cli.Context) ([]string, error)` and +For contributors, if you want to add more checks, you can write a new function like `func(ctx *cli.Context) ([]string, error)` and append it to `doctor.go`. ```go diff --git a/docs/content/doc/usage/linked-references.en-us.md b/docs/content/doc/usage/linked-references.en-us.md index bb40acdcd089e..df2813792868a 100644 --- a/docs/content/doc/usage/linked-references.en-us.md +++ b/docs/content/doc/usage/linked-references.en-us.md @@ -24,7 +24,7 @@ in search for references. These references will be shown as links in the Issue V and, in some cases, produce certain _actions_. Likewise, commit messages are parsed when they are listed, and _actions_ -are can be triggered when they are pushed to the main branch. +can be triggered when they are pushed to the main branch. To prevent the creation of unintended references, there are certain rules for them to be recognized. For example, they should not be included inside code diff --git a/go.mod b/go.mod index f1d7773110761..a929508e0de0d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module code.gitea.io/gitea go 1.18 require ( + code.gitea.io/actions-proto-go v0.2.0 code.gitea.io/gitea-vet v0.2.2 code.gitea.io/sdk/gitea v0.15.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 @@ -17,6 +18,7 @@ require ( github.com/PuerkitoBio/goquery v1.8.0 github.com/alecthomas/chroma/v2 v2.4.0 github.com/blevesearch/bleve/v2 v2.3.6 + github.com/bufbuild/connect-go v1.3.1 github.com/buildkite/terminal-to-html/v3 v3.7.0 github.com/caddyserver/certmagic v0.17.2 github.com/chi-middleware/proxy v1.1.1 @@ -24,6 +26,7 @@ require ( github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 github.com/djherbis/buffer v1.2.0 github.com/djherbis/nio/v3 v3.0.1 + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 github.com/dustin/go-humanize v1.0.0 github.com/editorconfig/editorconfig-core-go/v2 v2.5.1 github.com/emersion/go-imap v1.2.1 @@ -74,6 +77,7 @@ require ( github.com/microcosm-cc/bluemonday v1.0.21 github.com/minio/minio-go/v7 v7.0.46 github.com/msteinert/pam v1.1.0 + github.com/nektos/act v0.0.0 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/niklasfasching/go-org v1.6.5 github.com/oliamb/cutter v0.2.2 @@ -102,6 +106,8 @@ require ( golang.org/x/sys v0.3.0 golang.org/x/text v0.5.0 golang.org/x/tools v0.1.12 + google.golang.org/grpc v1.47.0 + google.golang.org/protobuf v1.28.1 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 @@ -156,14 +162,15 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.7.0 // indirect - github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-ap/errors v0.0.0-20221205040414-01c1adfc98ea // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/inflect v0.19.0 // indirect @@ -205,6 +212,7 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/markbates/going v1.0.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mholt/acmez v1.0.4 // indirect @@ -229,7 +237,9 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/rhysd/actionlint v1.6.22 // indirect github.com/rivo/uniseg v0.4.3 // indirect + github.com/robfig/cron v1.2.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rs/xid v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -259,9 +269,10 @@ require ( go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/mod v0.7.0 // indirect + golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -273,6 +284,8 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142 replace github.com/blevesearch/zapx/v15 v15.3.6 => github.com/zeripath/zapx/v15 v15.3.6-alignment-fix +replace github.com/nektos/act => gitea.com/gitea/act v0.234.2-0.20230131074955-e46ede1b1744 + exclude github.com/gofrs/uuid v3.2.0+incompatible exclude github.com/gofrs/uuid v4.0.0+incompatible diff --git a/go.sum b/go.sum index aed72dfb8112b..864ddff1cdadb 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +code.gitea.io/actions-proto-go v0.2.0 h1:nYh9nhhfk67YA4wVNLsCzd//RCvXnljwXClJ33+HPVk= +code.gitea.io/actions-proto-go v0.2.0/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/gitea-vet v0.2.2 h1:TEOV/Glf38iGmKzKP0EB++Z5OSL4zGg3RrAvlwaMuvk= code.gitea.io/gitea-vet v0.2.2/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= @@ -68,6 +70,8 @@ codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsi dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= +gitea.com/gitea/act v0.234.2-0.20230131074955-e46ede1b1744 h1:cqzKmGlX0wynSXO04NILpL25eBGwogDrKpkkbwmIpj4= +gitea.com/gitea/act v0.234.2-0.20230131074955-e46ede1b1744/go.mod h1:2C/WbTalu1VPNgbVaZJaZDzlOtAKqkXJhdOClxkMy14= gitea.com/go-chi/binding v0.0.0-20221013104517-b29891619681 h1:MMSPgnVULVwV9kEBgvyEUhC9v/uviZ55hPJEMjpbNR4= gitea.com/go-chi/binding v0.0.0-20221013104517-b29891619681/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0= @@ -225,6 +229,8 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/bufbuild/connect-go v1.3.1 h1:doJP6Q8Ypg6haUT2IAZJPWHUN9rAUp+F9MfK7yhu1zs= +github.com/bufbuild/connect-go v1.3.1/go.mod h1:9iNvh/NOsfhNBUH5CtvXeVUskQO1xsrEviH7ZArwZ3I= github.com/buildkite/terminal-to-html/v3 v3.7.0 h1:chdLUSpiOj/A4v3dzxyOqixXI6aw7IDA6Dk77FXsvNU= github.com/buildkite/terminal-to-html/v3 v3.7.0/go.mod h1:g0ME1XqbkBSgXR9YmlIHcJIjzaMyWW+HbsG0rPb5puo= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -352,6 +358,7 @@ github.com/ethantkoenig/rupture v1.0.1 h1:6aAXghmvtnngMgQzy7SMGdicMvkV86V4n9fT0m github.com/ethantkoenig/rupture v1.0.1/go.mod h1:Sjqo/nbffZp1pVVXNGhpugIjsWmuS9KiIB4GtpEBur4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -404,6 +411,8 @@ github.com/go-git/go-git/v5 v5.5.2/go.mod h1:BE5hUJ5yaV2YMxhmaP4l6RBQ08kMxKSPD4B 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-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -881,7 +890,9 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -889,6 +900,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -1082,11 +1095,15 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rhysd/actionlint v1.6.22 h1:cAEf2PGNwJXhdcTVF2xS/0ORqWS+ueUHwjQYsqFsGSk= +github.com/rhysd/actionlint v1.6.22/go.mod h1:gIKOdxtV40mBOcD0ZR8EBa8NqjEXToAZioroS3oedMg= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1479,8 +1496,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1824,6 +1842,7 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 h1:4SPz2GL2CXJt28MTF8V6Ap/9ZiVbQlJeGSd9qtA7DLs= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1861,6 +1880,7 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/models/actions/run.go b/models/actions/run.go new file mode 100644 index 0000000000000..14d191c814e34 --- /dev/null +++ b/models/actions/run.go @@ -0,0 +1,274 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + 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/json" + 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" +) + +// ActionRun represents a run of a workflow file +type ActionRun struct { + ID int64 + Title string + RepoID int64 `xorm:"index unique(repo_index)"` + Repo *repo_model.Repository `xorm:"-"` + OwnerID int64 `xorm:"index"` + WorkflowID string `xorm:"index"` // the name of workflow file + Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository + TriggerUserID int64 + TriggerUser *user_model.User `xorm:"-"` + Ref string + CommitSHA string + IsForkPullRequest bool + Event webhook_module.HookEventType + EventPayload string `xorm:"LONGTEXT"` + Status Status `xorm:"index"` + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` +} + +func init() { + db.RegisterModel(new(ActionRun)) + db.RegisterModel(new(ActionRunIndex)) +} + +func (run *ActionRun) HTMLURL() string { + if run.Repo == nil { + return "" + } + return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.Index) +} + +func (run *ActionRun) Link() string { + if run.Repo == nil { + return "" + } + return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) +} + +// RefLink return the url of run's ref +func (run *ActionRun) RefLink() string { + refName := git.RefName(run.Ref) + if refName.RefGroup() == "pull" { + return run.Repo.Link() + "/pulls/" + refName.ShortName() + } + return git.RefURL(run.Repo.Link(), run.Ref) +} + +// PrettyRef return #id for pull ref or ShortName for others +func (run *ActionRun) PrettyRef() string { + refName := git.RefName(run.Ref) + if refName.RefGroup() == "pull" { + return "#" + strings.TrimSuffix(strings.TrimPrefix(run.Ref, git.PullPrefix), "/head") + } + return refName.ShortName() +} + +// LoadAttributes load Repo TriggerUser if not loaded +func (run *ActionRun) LoadAttributes(ctx context.Context) error { + if run == nil { + return nil + } + + if run.Repo == nil { + repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + if err != nil { + return err + } + run.Repo = repo + } + if err := run.Repo.LoadAttributes(ctx); err != nil { + return err + } + + if run.TriggerUser == nil { + u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID) + if err != nil { + return err + } + run.TriggerUser = u + } + + return nil +} + +func (run *ActionRun) Duration() time.Duration { + return calculateDuration(run.Started, run.Stopped, run.Status) +} + +func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { + if run.Event == webhook_module.HookEventPush { + var payload api.PushPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil + } + return nil, fmt.Errorf("event %s is not a push event", run.Event) +} + +func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { + _, err := db.GetEngine(ctx).ID(repo.ID). + SetExpr("num_action_runs", + builder.Select("count(*)").From("action_run"). + Where(builder.Eq{"repo_id": repo.ID}), + ). + SetExpr("num_closed_action_runs", + builder.Select("count(*)").From("action_run"). + Where(builder.Eq{ + "repo_id": repo.ID, + }.And( + builder.In("status", + StatusSuccess, + StatusFailure, + StatusCancelled, + StatusSkipped, + ), + ), + ), + ). + Update(repo) + return err +} + +// InsertRun inserts a run +func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { + ctx, commiter, err := db.TxContext(ctx) + if err != nil { + return err + } + defer commiter.Close() + + index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) + if err != nil { + return err + } + run.Index = index + + if run.Status.IsUnknown() { + run.Status = StatusWaiting + } + + if err := db.Insert(ctx, run); err != nil { + return err + } + + if run.Repo == nil { + repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + if err != nil { + return err + } + run.Repo = repo + } + + if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { + return err + } + + runJobs := make([]*ActionRunJob, 0, len(jobs)) + for _, v := range jobs { + id, job := v.Job() + needs := job.Needs() + job.EraseNeeds() + payload, _ := v.Marshal() + status := StatusWaiting + if len(needs) > 0 { + status = StatusBlocked + } + 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 + } + + return commiter.Commit() +} + +func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { + var run ActionRun + has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist) + } + + return &run, nil +} + +func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error) { + run := &ActionRun{ + RepoID: repoID, + Index: index, + } + has, err := db.GetEngine(ctx).Get(run) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("run with index %d %d: %w", repoID, index, util.ErrNotExist) + } + + return run, nil +} + +func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { + sess := db.GetEngine(ctx).ID(run.ID) + if len(cols) > 0 { + sess.Cols(cols...) + } + _, err := sess.Update(run) + + if run.Status != 0 || util.SliceContains(cols, "status") { + if run.RepoID == 0 { + run, err = GetRunByID(ctx, run.ID) + if err != nil { + return err + } + } + if run.Repo == nil { + repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + if err != nil { + return err + } + run.Repo = repo + } + if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { + return err + } + } + + return err +} + +type ActionRunIndex db.ResourceIndex diff --git a/models/actions/run_job.go b/models/actions/run_job.go new file mode 100644 index 0000000000000..0002e507704d5 --- /dev/null +++ b/models/actions/run_job.go @@ -0,0 +1,163 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +// ActionRunJob represents a job of a run +type ActionRunJob struct { + ID int64 + RunID int64 `xorm:"index"` + Run *ActionRun `xorm:"-"` + RepoID int64 `xorm:"index"` + 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"` +} + +func init() { + db.RegisterModel(new(ActionRunJob)) +} + +func (job *ActionRunJob) Duration() time.Duration { + return calculateDuration(job.Started, job.Stopped, job.Status) +} + +func (job *ActionRunJob) LoadRun(ctx context.Context) error { + if job.Run == nil { + run, err := GetRunByID(ctx, job.RunID) + if err != nil { + return err + } + job.Run = run + } + return nil +} + +// LoadAttributes load Run if not loaded +func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { + if job == nil { + return nil + } + + if err := job.LoadRun(ctx); err != nil { + return err + } + + return job.Run.LoadAttributes(ctx) +} + +func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { + var job ActionRunJob + has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("run job with id %d: %w", id, util.ErrNotExist) + } + + return &job, nil +} + +func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error) { + var jobs []*ActionRunJob + if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil { + return nil, err + } + return jobs, nil +} + +func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) { + e := db.GetEngine(ctx) + + sess := e.ID(job.ID) + if len(cols) > 0 { + sess.Cols(cols...) + } + + if cond != nil { + sess.Where(cond) + } + + affected, err := sess.Update(job) + if err != nil { + return 0, err + } + + if affected == 0 || (!util.SliceContains(cols, "status") && job.Status == 0) { + return affected, nil + } + + if job.RunID == 0 { + var err error + if job, err = GetRunJobByID(ctx, job.ID); err != nil { + return affected, err + } + } + + jobs, err := GetRunJobsByRunID(ctx, job.RunID) + if err != nil { + return affected, err + } + + runStatus := aggregateJobStatus(jobs) + + run := &ActionRun{ + ID: job.RunID, + Status: runStatus, + } + if runStatus.IsDone() { + run.Stopped = timeutil.TimeStampNow() + } + return affected, UpdateRun(ctx, run) +} + +func aggregateJobStatus(jobs []*ActionRunJob) Status { + allDone := true + allWaiting := true + hasFailure := false + for _, job := range jobs { + if !job.Status.IsDone() { + allDone = false + } + if job.Status != StatusWaiting { + allWaiting = false + } + if job.Status == StatusFailure || job.Status == StatusCancelled { + hasFailure = true + } + } + if allDone { + if hasFailure { + return StatusFailure + } + return StatusSuccess + } + if allWaiting { + return StatusWaiting + } + return StatusRunning +} diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go new file mode 100644 index 0000000000000..047bf6441004a --- /dev/null +++ b/models/actions/run_job_list.go @@ -0,0 +1,99 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +type ActionJobList []*ActionRunJob + +func (jobs ActionJobList) GetRunIDs() []int64 { + ids := make(container.Set[int64], len(jobs)) + for _, j := range jobs { + if j.RunID == 0 { + continue + } + ids.Add(j.RunID) + } + return ids.Values() +} + +func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { + runIDs := jobs.GetRunIDs() + runs := make(map[int64]*ActionRun, len(runIDs)) + if err := db.GetEngine(ctx).In("id", runIDs).Find(&runs); err != nil { + return err + } + for _, j := range jobs { + if j.RunID > 0 && j.Run == nil { + j.Run = runs[j.RunID] + } + } + if withRepo { + var runsList RunList = make([]*ActionRun, 0, len(runs)) + for _, r := range runs { + runsList = append(runsList, r) + } + return runsList.LoadRepos() + } + return nil +} + +func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) error { + return jobs.LoadRuns(ctx, withRepo) +} + +type FindRunJobOptions struct { + db.ListOptions + RunID int64 + RepoID int64 + OwnerID int64 + CommitSHA string + Statuses []Status + UpdatedBefore timeutil.TimeStamp +} + +func (opts FindRunJobOptions) toConds() builder.Cond { + cond := builder.NewCond() + if opts.RunID > 0 { + cond = cond.And(builder.Eq{"run_id": opts.RunID}) + } + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.CommitSHA != "" { + cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) + } + if len(opts.Statuses) > 0 { + cond = cond.And(builder.In("status", opts.Statuses)) + } + if opts.UpdatedBefore > 0 { + cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) + } + return cond +} + +func FindRunJobs(ctx context.Context, opts FindRunJobOptions) (ActionJobList, int64, error) { + e := db.GetEngine(ctx).Where(opts.toConds()) + if opts.PageSize > 0 && opts.Page >= 1 { + e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + var tasks ActionJobList + total, err := e.FindAndCount(&tasks) + return tasks, total, err +} + +func CountRunJobs(ctx context.Context, opts FindRunJobOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionRunJob)) +} diff --git a/models/actions/run_list.go b/models/actions/run_list.go new file mode 100644 index 0000000000000..f9d8417227948 --- /dev/null +++ b/models/actions/run_list.go @@ -0,0 +1,107 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +type RunList []*ActionRun + +// GetUserIDs returns a slice of user's id +func (runs RunList) GetUserIDs() []int64 { + ids := make(container.Set[int64], len(runs)) + for _, run := range runs { + ids.Add(run.TriggerUserID) + } + return ids.Values() +} + +func (runs RunList) GetRepoIDs() []int64 { + ids := make(container.Set[int64], len(runs)) + for _, run := range runs { + ids.Add(run.RepoID) + } + return ids.Values() +} + +func (runs RunList) LoadTriggerUser(ctx context.Context) error { + userIDs := runs.GetUserIDs() + users := make(map[int64]*user_model.User, len(userIDs)) + if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil { + return err + } + for _, run := range runs { + if run.TriggerUserID == user_model.ActionsUserID { + run.TriggerUser = user_model.NewActionsUser() + } else { + run.TriggerUser = users[run.TriggerUserID] + } + } + return nil +} + +func (runs RunList) LoadRepos() error { + repoIDs := runs.GetRepoIDs() + repos, err := repo_model.GetRepositoriesMapByIDs(repoIDs) + if err != nil { + return err + } + for _, run := range runs { + run.Repo = repos[run.RepoID] + } + return nil +} + +type FindRunOptions struct { + db.ListOptions + RepoID int64 + OwnerID int64 + IsClosed util.OptionalBool + WorkflowFileName string +} + +func (opts FindRunOptions) toConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.IsClosed.IsFalse() { + cond = cond.And(builder.Eq{"status": StatusWaiting}.Or( + builder.Eq{"status": StatusRunning})) + } else if opts.IsClosed.IsTrue() { + cond = cond.And( + builder.Neq{"status": StatusWaiting}.And( + builder.Neq{"status": StatusRunning})) + } + if opts.WorkflowFileName != "" { + cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowFileName}) + } + return cond +} + +func FindRuns(ctx context.Context, opts FindRunOptions) (RunList, int64, error) { + e := db.GetEngine(ctx).Where(opts.toConds()) + if opts.PageSize > 0 && opts.Page >= 1 { + e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + var runs RunList + total, err := e.Desc("id").FindAndCount(&runs) + return runs, total, err +} + +func CountRuns(ctx context.Context, opts FindRunOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionRun)) +} diff --git a/models/actions/runner.go b/models/actions/runner.go new file mode 100644 index 0000000000000..4efe105b08845 --- /dev/null +++ b/models/actions/runner.go @@ -0,0 +1,252 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "xorm.io/builder" +) + +// ActionRunner represents runner machines +type ActionRunner struct { + ID int64 + UUID string `xorm:"CHAR(36) UNIQUE"` + Name string `xorm:"VARCHAR(255)"` + OwnerID int64 `xorm:"index"` // org level runner, 0 means system + Owner *user_model.User `xorm:"-"` + RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global + Repo *repo_model.Repository `xorm:"-"` + Description string `xorm:"TEXT"` + Base int // 0 native 1 docker 2 virtual machine + RepoRange string // glob match which repositories could use this runner + + Token string `xorm:"-"` + TokenHash string `xorm:"UNIQUE"` // sha256 of token + TokenSalt string + // TokenLastEight string `xorm:"token_last_eight"` // it's unnecessary because we don't find runners by token + + LastOnline timeutil.TimeStamp `xorm:"index"` + LastActive timeutil.TimeStamp `xorm:"index"` + + // Store OS and Artch. + AgentLabels []string + // Store custom labes use defined. + CustomLabels []string + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + Deleted timeutil.TimeStamp `xorm:"deleted"` +} + +func (r *ActionRunner) OwnType() string { + if r.RepoID != 0 { + return fmt.Sprintf("Repo(%s)", r.Repo.FullName()) + } + if r.OwnerID != 0 { + return fmt.Sprintf("Org(%s)", r.Owner.Name) + } + return "Global" +} + +func (r *ActionRunner) Status() runnerv1.RunnerStatus { + if time.Since(r.LastOnline.AsTime()) > time.Minute { + return runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE + } + if time.Since(r.LastActive.AsTime()) > 10*time.Second { + return runnerv1.RunnerStatus_RUNNER_STATUS_IDLE + } + return runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE +} + +func (r *ActionRunner) StatusName() string { + return strings.ToLower(strings.TrimPrefix(r.Status().String(), "RUNNER_STATUS_")) +} + +func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { + return lang.Tr("actions.runners.status." + r.StatusName()) +} + +func (r *ActionRunner) IsOnline() bool { + status := r.Status() + if status == runnerv1.RunnerStatus_RUNNER_STATUS_IDLE || status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE { + return true + } + return false +} + +// AllLabels returns agent and custom labels +func (r *ActionRunner) AllLabels() []string { + return append(r.AgentLabels, r.CustomLabels...) +} + +// Editable checks if the runner is editable by the user +func (r *ActionRunner) Editable(ownerID, repoID int64) bool { + if ownerID == 0 && repoID == 0 { + return true + } + if ownerID > 0 && r.OwnerID == ownerID { + return true + } + return repoID > 0 && r.RepoID == repoID +} + +// LoadAttributes loads the attributes of the runner +func (r *ActionRunner) LoadAttributes(ctx context.Context) error { + if r.OwnerID > 0 { + var user user_model.User + has, err := db.GetEngine(ctx).ID(r.OwnerID).Get(&user) + if err != nil { + return err + } + if has { + r.Owner = &user + } + } + if r.RepoID > 0 { + var repo repo_model.Repository + has, err := db.GetEngine(ctx).ID(r.RepoID).Get(&repo) + if err != nil { + return err + } + if has { + r.Repo = &repo + } + } + return nil +} + +func (r *ActionRunner) GenerateToken() (err error) { + r.Token, r.TokenSalt, r.TokenHash, _, err = generateSaltedToken() + return err +} + +func init() { + db.RegisterModel(&ActionRunner{}) +} + +type FindRunnerOptions struct { + db.ListOptions + RepoID int64 + OwnerID int64 + Sort string + Filter string + WithAvailable bool // not only runners belong to, but also runners can be used +} + +func (opts FindRunnerOptions) toCond() builder.Cond { + cond := builder.NewCond() + + if opts.RepoID > 0 { + c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID}) + if opts.WithAvailable { + c = c.Or(builder.Eq{"owner_id": builder.Select("owner_id").From("repository").Where(builder.Eq{"id": opts.RepoID})}) + c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) + } + cond = cond.And(c) + } + if opts.OwnerID > 0 { + c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID}) + if opts.WithAvailable { + c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) + } + cond = cond.And(c) + } + + if opts.Filter != "" { + cond = cond.And(builder.Like{"name", opts.Filter}) + } + return cond +} + +func (opts FindRunnerOptions) toOrder() string { + switch opts.Sort { + case "online": + return "last_online DESC" + case "offline": + return "last_online ASC" + case "alphabetically": + return "name ASC" + } + return "last_online DESC" +} + +func CountRunners(ctx context.Context, opts FindRunnerOptions) (int64, error) { + return db.GetEngine(ctx). + Where(opts.toCond()). + Count(ActionRunner{}) +} + +func FindRunners(ctx context.Context, opts FindRunnerOptions) (runners RunnerList, err error) { + sess := db.GetEngine(ctx). + Where(opts.toCond()). + OrderBy(opts.toOrder()) + if opts.Page > 0 { + sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + return runners, sess.Find(&runners) +} + +// GetRunnerByUUID returns a runner via uuid +func GetRunnerByUUID(ctx context.Context, uuid string) (*ActionRunner, error) { + var runner ActionRunner + has, err := db.GetEngine(ctx).Where("uuid=?", uuid).Get(&runner) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("runner with uuid %s: %w", uuid, util.ErrNotExist) + } + return &runner, nil +} + +// GetRunnerByID returns a runner via id +func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { + var runner ActionRunner + has, err := db.GetEngine(ctx).Where("id=?", id).Get(&runner) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("runner with id %d: %w", id, util.ErrNotExist) + } + return &runner, nil +} + +// UpdateRunner updates runner's information. +func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { + e := db.GetEngine(ctx) + var err error + if len(cols) == 0 { + _, err = e.ID(r.ID).AllCols().Update(r) + } else { + _, err = e.ID(r.ID).Cols(cols...).Update(r) + } + return err +} + +// DeleteRunner deletes a runner by given ID. +func DeleteRunner(ctx context.Context, id int64) error { + if _, err := GetRunnerByID(ctx, id); err != nil { + return err + } + + _, err := db.GetEngine(ctx).Delete(&ActionRunner{ID: id}) + return err +} + +// CreateRunner creates new runner. +func CreateRunner(ctx context.Context, t *ActionRunner) error { + _, err := db.GetEngine(ctx).Insert(t) + return err +} diff --git a/models/actions/runner_list.go b/models/actions/runner_list.go new file mode 100644 index 0000000000000..87f0886b470d5 --- /dev/null +++ b/models/actions/runner_list.go @@ -0,0 +1,77 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" +) + +type RunnerList []*ActionRunner + +// GetUserIDs returns a slice of user's id +func (runners RunnerList) GetUserIDs() []int64 { + ids := make(container.Set[int64], len(runners)) + for _, runner := range runners { + if runner.OwnerID == 0 { + continue + } + ids.Add(runner.OwnerID) + } + return ids.Values() +} + +func (runners RunnerList) LoadOwners(ctx context.Context) error { + userIDs := runners.GetUserIDs() + users := make(map[int64]*user_model.User, len(userIDs)) + if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil { + return err + } + for _, runner := range runners { + if runner.OwnerID > 0 && runner.Owner == nil { + runner.Owner = users[runner.OwnerID] + } + } + return nil +} + +func (runners RunnerList) getRepoIDs() []int64 { + repoIDs := make(container.Set[int64], len(runners)) + for _, runner := range runners { + if runner.RepoID == 0 { + continue + } + if _, ok := repoIDs[runner.RepoID]; !ok { + repoIDs[runner.RepoID] = struct{}{} + } + } + return repoIDs.Values() +} + +func (runners RunnerList) LoadRepos(ctx context.Context) error { + repoIDs := runners.getRepoIDs() + repos := make(map[int64]*repo_model.Repository, len(repoIDs)) + if err := db.GetEngine(ctx).In("id", repoIDs).Find(&repos); err != nil { + return err + } + + for _, runner := range runners { + if runner.RepoID > 0 && runner.Repo == nil { + runner.Repo = repos[runner.RepoID] + } + } + return nil +} + +func (runners RunnerList) LoadAttributes(ctx context.Context) error { + if err := runners.LoadOwners(ctx); err != nil { + return err + } + + return runners.LoadRepos(ctx) +} diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go new file mode 100644 index 0000000000000..fabd6c644c8da --- /dev/null +++ b/models/actions/runner_token.go @@ -0,0 +1,86 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// ActionRunnerToken represents runner tokens +type ActionRunnerToken struct { + ID int64 + Token string `xorm:"UNIQUE"` + OwnerID int64 `xorm:"index"` // org level runner, 0 means system + Owner *user_model.User `xorm:"-"` + RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global + Repo *repo_model.Repository `xorm:"-"` + IsActive bool + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + Deleted timeutil.TimeStamp `xorm:"deleted"` +} + +func init() { + db.RegisterModel(new(ActionRunnerToken)) +} + +// GetRunnerToken returns a action runner via token +func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, error) { + var runnerToken ActionRunnerToken + has, err := db.GetEngine(ctx).Where("token=?", token).Get(&runnerToken) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist) + } + return &runnerToken, nil +} + +// UpdateRunnerToken updates runner token information. +func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string) (err error) { + e := db.GetEngine(ctx) + + if len(cols) == 0 { + _, err = e.ID(r.ID).AllCols().Update(r) + } else { + _, err = e.ID(r.ID).Cols(cols...).Update(r) + } + return err +} + +// NewRunnerToken creates a new runner token +func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { + token, err := util.CryptoRandomString(40) + if err != nil { + return nil, err + } + runnerToken := &ActionRunnerToken{ + OwnerID: ownerID, + RepoID: repoID, + IsActive: false, + Token: token, + } + _, err = db.GetEngine(ctx).Insert(runnerToken) + return runnerToken, err +} + +// GetUnactivatedRunnerToken returns a unactivated runner token +func GetUnactivatedRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { + var runnerToken ActionRunnerToken + has, err := db.GetEngine(ctx).Where("owner_id=? AND repo_id=? AND is_active=?", ownerID, repoID, false).OrderBy("id DESC").Get(&runnerToken) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("runner token: %w", util.ErrNotExist) + } + return &runnerToken, nil +} diff --git a/models/actions/status.go b/models/actions/status.go new file mode 100644 index 0000000000000..059cf9bc09515 --- /dev/null +++ b/models/actions/status.go @@ -0,0 +1,100 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "code.gitea.io/gitea/modules/translation" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" +) + +// Status represents the status of ActionRun, ActionRunJob, ActionTask, or ActionTaskStep +type Status int + +const ( + StatusUnknown Status = iota // 0, consistent with runnerv1.Result_RESULT_UNSPECIFIED + StatusSuccess // 1, consistent with runnerv1.Result_RESULT_SUCCESS + StatusFailure // 2, consistent with runnerv1.Result_RESULT_FAILURE + StatusCancelled // 3, consistent with runnerv1.Result_RESULT_CANCELLED + StatusSkipped // 4, consistent with runnerv1.Result_RESULT_SKIPPED + StatusWaiting // 5, isn't a runnerv1.Result + StatusRunning // 6, isn't a runnerv1.Result + StatusBlocked // 7, isn't a runnerv1.Result +) + +var statusNames = map[Status]string{ + StatusUnknown: "unknown", + StatusWaiting: "waiting", + StatusRunning: "running", + StatusSuccess: "success", + StatusFailure: "failure", + StatusCancelled: "cancelled", + StatusSkipped: "skipped", + StatusBlocked: "blocked", +} + +// String returns the string name of the Status +func (s Status) String() string { + return statusNames[s] +} + +// LocaleString returns the locale string name of the Status +func (s Status) LocaleString(lang translation.Locale) string { + return lang.Tr("actions.status." + s.String()) +} + +// IsDone returns whether the Status is final +func (s Status) IsDone() bool { + return s.In(StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped) +} + +// HasRun returns whether the Status is a result of running +func (s Status) HasRun() bool { + return s.In(StatusSuccess, StatusFailure) +} + +func (s Status) IsUnknown() bool { + return s == StatusUnknown +} + +func (s Status) IsSuccess() bool { + return s == StatusSuccess +} + +func (s Status) IsFailure() bool { + return s == StatusFailure +} + +func (s Status) IsCancelled() bool { + return s == StatusCancelled +} + +func (s Status) IsSkipped() bool { + return s == StatusSkipped +} + +func (s Status) IsWaiting() bool { + return s == StatusWaiting +} + +func (s Status) IsRunning() bool { + return s == StatusRunning +} + +// In returns whether s is one of the given statuses +func (s Status) In(statuses ...Status) bool { + for _, v := range statuses { + if s == v { + return true + } + } + return false +} + +func (s Status) AsResult() runnerv1.Result { + if s.IsDone() { + return runnerv1.Result(s) + } + return runnerv1.Result_RESULT_UNSPECIFIED +} diff --git a/models/actions/task.go b/models/actions/task.go new file mode 100644 index 0000000000000..5b6206c346425 --- /dev/null +++ b/models/actions/task.go @@ -0,0 +1,504 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "crypto/subtle" + "fmt" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + lru "github.com/hashicorp/golang-lru" + "github.com/nektos/act/pkg/jobparser" + "google.golang.org/protobuf/types/known/timestamppb" + "xorm.io/builder" +) + +// ActionTask represents a distribution of job +type ActionTask struct { + ID int64 + JobID int64 + Job *ActionRunJob `xorm:"-"` + Steps []*ActionTaskStep `xorm:"-"` + Attempt int64 + RunnerID int64 `xorm:"index"` + Status Status `xorm:"index"` + Started timeutil.TimeStamp `xorm:"index"` + Stopped timeutil.TimeStamp + + RepoID int64 `xorm:"index"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` + IsForkPullRequest bool + + Token string `xorm:"-"` + TokenHash string `xorm:"UNIQUE"` // sha256 of token + TokenSalt string + TokenLastEight string `xorm:"index token_last_eight"` + + LogFilename string // file name of log + LogInStorage bool // read log from database or from storage + LogLength int64 // lines count + LogSize int64 // blob size + LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset + LogExpired bool // files that are too old will be deleted + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` +} + +var successfulTokenTaskCache *lru.Cache + +func init() { + db.RegisterModel(new(ActionTask), func() error { + if setting.SuccessfulTokensCacheSize > 0 { + var err error + successfulTokenTaskCache, err = lru.New(setting.SuccessfulTokensCacheSize) + if err != nil { + return fmt.Errorf("unable to allocate Task cache: %v", err) + } + } else { + successfulTokenTaskCache = nil + } + return nil + }) +} + +func (task *ActionTask) Duration() time.Duration { + return calculateDuration(task.Started, task.Stopped, task.Status) +} + +func (task *ActionTask) IsStopped() bool { + return task.Stopped > 0 +} + +func (task *ActionTask) GetRunLink() string { + if task.Job == nil || task.Job.Run == nil { + return "" + } + return task.Job.Run.Link() +} + +func (task *ActionTask) GetCommitLink() string { + if task.Job == nil || task.Job.Run == nil || task.Job.Run.Repo == nil { + return "" + } + return task.Job.Run.Repo.CommitLink(task.CommitSHA) +} + +func (task *ActionTask) GetRepoName() string { + if task.Job == nil || task.Job.Run == nil || task.Job.Run.Repo == nil { + return "" + } + return task.Job.Run.Repo.FullName() +} + +func (task *ActionTask) GetRepoLink() string { + if task.Job == nil || task.Job.Run == nil || task.Job.Run.Repo == nil { + return "" + } + return task.Job.Run.Repo.Link() +} + +func (task *ActionTask) LoadJob(ctx context.Context) error { + if task.Job == nil { + job, err := GetRunJobByID(ctx, task.JobID) + if err != nil { + return err + } + task.Job = job + } + return nil +} + +// LoadAttributes load Job Steps if not loaded +func (task *ActionTask) LoadAttributes(ctx context.Context) error { + if task == nil { + return nil + } + if err := task.LoadJob(ctx); err != nil { + return err + } + + if err := task.Job.LoadAttributes(ctx); err != nil { + return err + } + + if task.Steps == nil { // be careful, an empty slice (not nil) also means loaded + steps, err := GetTaskStepsByTaskID(ctx, task.ID) + if err != nil { + return err + } + task.Steps = steps + } + + return nil +} + +func (task *ActionTask) GenerateToken() (err error) { + task.Token, task.TokenSalt, task.TokenHash, task.TokenLastEight, err = generateSaltedToken() + return err +} + +func GetTaskByID(ctx context.Context, id int64) (*ActionTask, error) { + var task ActionTask + has, err := db.GetEngine(ctx).Where("id=?", id).Get(&task) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("task with id %d: %w", id, util.ErrNotExist) + } + + return &task, nil +} + +func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, error) { + errNotExist := fmt.Errorf("task with token %q: %w", token, util.ErrNotExist) + if token == "" { + return nil, errNotExist + } + // A token is defined as being SHA1 sum these are 40 hexadecimal bytes long + if len(token) != 40 { + return nil, errNotExist + } + for _, x := range []byte(token) { + if x < '0' || (x > '9' && x < 'a') || x > 'f' { + return nil, errNotExist + } + } + + lastEight := token[len(token)-8:] + + if id := getTaskIDFromCache(token); id > 0 { + task := &ActionTask{ + TokenLastEight: lastEight, + } + // Re-get the task from the db in case it has been deleted in the intervening period + has, err := db.GetEngine(ctx).ID(id).Get(task) + if err != nil { + return nil, err + } + if has { + return task, nil + } + successfulTokenTaskCache.Remove(token) + } + + var tasks []*ActionTask + err := db.GetEngine(ctx).Where("token_last_eight = ? AND status = ?", lastEight, StatusRunning).Find(&tasks) + if err != nil { + return nil, err + } else if len(tasks) == 0 { + return nil, errNotExist + } + + for _, t := range tasks { + tempHash := auth_model.HashToken(token, t.TokenSalt) + if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 { + if successfulTokenTaskCache != nil { + successfulTokenTaskCache.Add(token, t.ID) + } + return t, nil + } + } + return nil, errNotExist +} + +func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { + dbCtx, commiter, err := db.TxContext(ctx) + if err != nil { + return nil, false, err + } + defer commiter.Close() + ctx = dbCtx.WithContext(ctx) + + e := db.GetEngine(ctx) + + jobCond := builder.NewCond() + if runner.RepoID != 0 { + jobCond = builder.Eq{"repo_id": runner.RepoID} + } else if runner.OwnerID != 0 { + jobCond = builder.In("repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": runner.OwnerID})) + } + if jobCond.IsValid() { + jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond)) + } + + var jobs []*ActionRunJob + if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("id").Find(&jobs); err != nil { + return nil, false, err + } + + // TODO: a more efficient way to filter labels + var job *ActionRunJob + labels := runner.AgentLabels + labels = append(labels, runner.CustomLabels...) + log.Trace("runner labels: %v", labels) + for _, v := range jobs { + if isSubset(labels, v.RunsOn) { + job = v + break + } + } + if job == nil { + return nil, false, nil + } + if err := job.LoadAttributes(ctx); err != nil { + return nil, false, err + } + + now := timeutil.TimeStampNow() + job.Attempt++ + job.Started = now + job.Status = StatusRunning + + task := &ActionTask{ + JobID: job.ID, + Attempt: job.Attempt, + RunnerID: runner.ID, + Started: now, + Status: StatusRunning, + RepoID: job.RepoID, + OwnerID: job.OwnerID, + CommitSHA: job.CommitSHA, + IsForkPullRequest: job.IsForkPullRequest, + } + if err := task.GenerateToken(); err != nil { + return nil, false, err + } + + var workflowJob *jobparser.Job + if gots, err := jobparser.Parse(job.WorkflowPayload); err != nil { + return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) + } else if len(gots) != 1 { + return nil, false, fmt.Errorf("workflow of job %d: not signle workflow", job.ID) + } else { + _, workflowJob = gots[0].Job() + } + + if _, err := e.Insert(task); err != nil { + return nil, false, err + } + + task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID) + if _, err := e.ID(task.ID).Cols("log_filename").Update(task); err != nil { + return nil, false, err + } + + if len(workflowJob.Steps) > 0 { + steps := make([]*ActionTaskStep, len(workflowJob.Steps)) + for i, v := range workflowJob.Steps { + steps[i] = &ActionTaskStep{ + Name: v.String(), + TaskID: task.ID, + Index: int64(i), + RepoID: task.RepoID, + Status: StatusWaiting, + } + } + if _, err := e.Insert(steps); err != nil { + return nil, false, err + } + task.Steps = steps + } + + job.TaskID = task.ID + if n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}); err != nil { + return nil, false, err + } else if n != 1 { + return nil, false, nil + } + + if job.Run.Status.IsWaiting() { + job.Run.Status = StatusRunning + job.Run.Started = now + if err := UpdateRun(ctx, job.Run, "status", "started"); err != nil { + return nil, false, err + } + } + + task.Job = job + + if err := commiter.Commit(); err != nil { + return nil, false, err + } + + return task, true, nil +} + +func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { + sess := db.GetEngine(ctx).ID(task.ID) + if len(cols) > 0 { + sess.Cols(cols...) + } + _, err := sess.Update(task) + return err +} + +func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionTask, error) { + stepStates := map[int64]*runnerv1.StepState{} + for _, v := range state.Steps { + stepStates[v.Id] = v + } + + ctx, commiter, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer commiter.Close() + + e := db.GetEngine(ctx) + + task := &ActionTask{} + if has, err := e.ID(state.Id).Get(task); err != nil { + return nil, err + } else if !has { + return nil, util.ErrNotExist + } + + if state.Result != runnerv1.Result_RESULT_UNSPECIFIED { + task.Status = Status(state.Result) + task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix()) + if _, err := UpdateRunJob(ctx, &ActionRunJob{ + ID: task.JobID, + Status: task.Status, + Stopped: task.Stopped, + }, nil); err != nil { + return nil, err + } + } + + if _, err := e.ID(task.ID).Update(task); err != nil { + return nil, err + } + + if err := task.LoadAttributes(ctx); err != nil { + return nil, err + } + + for _, step := range task.Steps { + var result runnerv1.Result + if v, ok := stepStates[step.Index]; ok { + result = v.Result + step.LogIndex = v.LogIndex + step.LogLength = v.LogLength + step.Started = convertTimestamp(v.StartedAt) + step.Stopped = convertTimestamp(v.StoppedAt) + } + if result != runnerv1.Result_RESULT_UNSPECIFIED { + step.Status = Status(result) + } else if step.Started != 0 { + step.Status = StatusRunning + } + if _, err := e.ID(step.ID).Update(step); err != nil { + return nil, err + } + } + + if err := commiter.Commit(); err != nil { + return nil, err + } + + return task, nil +} + +func StopTask(ctx context.Context, taskID int64, status Status) error { + if !status.IsDone() { + return fmt.Errorf("cannot stop task with status %v", status) + } + e := db.GetEngine(ctx) + + task := &ActionTask{} + if has, err := e.ID(taskID).Get(task); err != nil { + return err + } else if !has { + return util.ErrNotExist + } + if task.Status.IsDone() { + return nil + } + + now := timeutil.TimeStampNow() + task.Status = status + task.Stopped = now + if _, err := UpdateRunJob(ctx, &ActionRunJob{ + ID: task.JobID, + Status: task.Status, + Stopped: task.Stopped, + }, nil); err != nil { + return err + } + + if _, err := e.ID(task.ID).Update(task); err != nil { + return err + } + + if err := task.LoadAttributes(ctx); err != nil { + return err + } + + for _, step := range task.Steps { + if !step.Status.IsDone() { + step.Status = status + if step.Started == 0 { + step.Started = now + } + step.Stopped = now + } + if _, err := e.ID(step.ID).Update(step); err != nil { + return err + } + } + + return nil +} + +func isSubset(set, subset []string) bool { + m := make(container.Set[string], len(set)) + for _, v := range set { + m.Add(v) + } + + for _, v := range subset { + if !m.Contains(v) { + return false + } + } + return true +} + +func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp { + if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 { + return timeutil.TimeStamp(0) + } + return timeutil.TimeStamp(timestamp.AsTime().Unix()) +} + +func logFileName(repoFullName string, taskID int64) string { + return fmt.Sprintf("%s/%02x/%d.log", repoFullName, taskID%256, taskID) +} + +func getTaskIDFromCache(token string) int64 { + if successfulTokenTaskCache == nil { + return 0 + } + tInterface, ok := successfulTokenTaskCache.Get(token) + if !ok { + return 0 + } + t, ok := tInterface.(int64) + if !ok { + return 0 + } + return t +} diff --git a/models/actions/task_list.go b/models/actions/task_list.go new file mode 100644 index 0000000000000..1f6b16772b8f5 --- /dev/null +++ b/models/actions/task_list.go @@ -0,0 +1,105 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +type TaskList []*ActionTask + +func (tasks TaskList) GetJobIDs() []int64 { + ids := make(container.Set[int64], len(tasks)) + for _, t := range tasks { + if t.JobID == 0 { + continue + } + ids.Add(t.JobID) + } + return ids.Values() +} + +func (tasks TaskList) LoadJobs(ctx context.Context) error { + jobIDs := tasks.GetJobIDs() + jobs := make(map[int64]*ActionRunJob, len(jobIDs)) + if err := db.GetEngine(ctx).In("id", jobIDs).Find(&jobs); err != nil { + return err + } + for _, t := range tasks { + if t.JobID > 0 && t.Job == nil { + t.Job = jobs[t.JobID] + } + } + + // TODO: Replace with "ActionJobList(maps.Values(jobs))" once available + var jobsList ActionJobList = make([]*ActionRunJob, 0, len(jobs)) + for _, j := range jobs { + jobsList = append(jobsList, j) + } + return jobsList.LoadAttributes(ctx, true) +} + +func (tasks TaskList) LoadAttributes(ctx context.Context) error { + return tasks.LoadJobs(ctx) +} + +type FindTaskOptions struct { + db.ListOptions + RepoID int64 + OwnerID int64 + CommitSHA string + Status Status + UpdatedBefore timeutil.TimeStamp + StartedBefore timeutil.TimeStamp + RunnerID int64 + IDOrderDesc bool +} + +func (opts FindTaskOptions) toConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.CommitSHA != "" { + cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) + } + if opts.Status > StatusUnknown { + cond = cond.And(builder.Eq{"status": opts.Status}) + } + if opts.UpdatedBefore > 0 { + cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) + } + if opts.StartedBefore > 0 { + cond = cond.And(builder.Lt{"started": opts.StartedBefore}) + } + if opts.RunnerID > 0 { + cond = cond.And(builder.Eq{"runner_id": opts.RunnerID}) + } + return cond +} + +func FindTasks(ctx context.Context, opts FindTaskOptions) (TaskList, error) { + e := db.GetEngine(ctx).Where(opts.toConds()) + if opts.PageSize > 0 && opts.Page >= 1 { + e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + if opts.IDOrderDesc { + e.OrderBy("id DESC") + } + var tasks TaskList + return tasks, e.Find(&tasks) +} + +func CountTasks(ctx context.Context, opts FindTaskOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionTask)) +} diff --git a/models/actions/task_step.go b/models/actions/task_step.go new file mode 100644 index 0000000000000..3af1fe3f5a821 --- /dev/null +++ b/models/actions/task_step.go @@ -0,0 +1,41 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" +) + +// ActionTaskStep represents a step of ActionTask +type ActionTaskStep struct { + ID int64 + Name string `xorm:"VARCHAR(255)"` + TaskID int64 `xorm:"index unique(task_index)"` + Index int64 `xorm:"index unique(task_index)"` + RepoID int64 `xorm:"index"` + Status Status `xorm:"index"` + LogIndex int64 + LogLength int64 + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` +} + +func (step *ActionTaskStep) Duration() time.Duration { + return calculateDuration(step.Started, step.Stopped, step.Status) +} + +func init() { + db.RegisterModel(new(ActionTaskStep)) +} + +func GetTaskStepsByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskStep, error) { + var steps []*ActionTaskStep + return steps, db.GetEngine(ctx).Where("task_id=?", taskID).OrderBy("`index` ASC").Find(&steps) +} diff --git a/models/actions/utils.go b/models/actions/utils.go new file mode 100644 index 0000000000000..12657942fc24f --- /dev/null +++ b/models/actions/utils.go @@ -0,0 +1,84 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "io" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +func generateSaltedToken() (string, string, string, string, error) { + salt, err := util.CryptoRandomString(10) + if err != nil { + return "", "", "", "", err + } + buf, err := util.CryptoRandomBytes(20) + if err != nil { + return "", "", "", "", err + } + token := hex.EncodeToString(buf) + hash := auth_model.HashToken(token, salt) + return token, salt, hash, token[len(token)-8:], nil +} + +/* +LogIndexes is the index for mapping log line number to buffer offset. +Because it uses varint encoding, it is impossible to predict its size. +But we can make a simple estimate with an assumption that each log line has 200 byte, then: +| lines | file size | index size | +|-----------|---------------------|--------------------| +| 100 | 20 KiB(20000) | 258 B(258) | +| 1000 | 195 KiB(200000) | 2.9 KiB(2958) | +| 10000 | 1.9 MiB(2000000) | 34 KiB(34715) | +| 100000 | 19 MiB(20000000) | 386 KiB(394715) | +| 1000000 | 191 MiB(200000000) | 4.1 MiB(4323626) | +| 10000000 | 1.9 GiB(2000000000) | 47 MiB(49323626) | +| 100000000 | 19 GiB(20000000000) | 490 MiB(513424280) | +*/ +type LogIndexes []int64 + +func (indexes *LogIndexes) FromDB(b []byte) error { + reader := bytes.NewReader(b) + for { + v, err := binary.ReadVarint(reader) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return fmt.Errorf("binary ReadVarint: %w", err) + } + *indexes = append(*indexes, v) + } +} + +func (indexes *LogIndexes) ToDB() ([]byte, error) { + buf, i := make([]byte, binary.MaxVarintLen64*len(*indexes)), 0 + for _, v := range *indexes { + n := binary.PutVarint(buf[i:], v) + i += n + } + return buf[:i], nil +} + +var timeSince = time.Since + +func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time.Duration { + if started == 0 { + return 0 + } + s := started.AsTime() + if status.IsDone() { + return stopped.AsTime().Sub(s) + } + return timeSince(s).Truncate(time.Second) +} diff --git a/models/actions/utils_test.go b/models/actions/utils_test.go new file mode 100644 index 0000000000000..98c048d4ef899 --- /dev/null +++ b/models/actions/utils_test.go @@ -0,0 +1,90 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "math" + "testing" + "time" + + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLogIndexes_ToDB(t *testing.T) { + tests := []struct { + indexes LogIndexes + }{ + { + indexes: []int64{1, 2, 0, -1, -2, math.MaxInt64, math.MinInt64}, + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got, err := tt.indexes.ToDB() + require.NoError(t, err) + + indexes := LogIndexes{} + require.NoError(t, indexes.FromDB(got)) + + assert.Equal(t, tt.indexes, indexes) + }) + } +} + +func Test_calculateDuration(t *testing.T) { + oldTimeSince := timeSince + defer func() { + timeSince = oldTimeSince + }() + + timeSince = func(t time.Time) time.Duration { + return timeutil.TimeStamp(1000).AsTime().Sub(t) + } + type args struct { + started timeutil.TimeStamp + stopped timeutil.TimeStamp + status Status + } + tests := []struct { + name string + args args + want time.Duration + }{ + { + name: "unknown", + args: args{ + started: 0, + stopped: 0, + status: StatusUnknown, + }, + want: 0, + }, + { + name: "running", + args: args{ + started: 500, + stopped: 0, + status: StatusRunning, + }, + want: 500 * time.Second, + }, + { + name: "done", + args: args{ + started: 500, + stopped: 600, + status: StatusSuccess, + }, + want: 100 * time.Second, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, calculateDuration(tt.args.started, tt.args.stopped, tt.args.status), "calculateDuration(%v, %v, %v)", tt.args.started, tt.args.stopped, tt.args.status) + }) + } +} diff --git a/models/activities/notification.go b/models/activities/notification.go index 321d543c736ae..f153eb0589aa5 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -151,7 +151,7 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo } for i := range users { notify = append(notify, &Notification{ - UserID: users[i].ID, + UserID: i, RepoID: repo.ID, Status: NotificationStatusUnread, UpdatedBy: doer.ID, diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go new file mode 100644 index 0000000000000..bac1cb9eb608a --- /dev/null +++ b/models/dbfs/dbfile.go @@ -0,0 +1,357 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package dbfs + +import ( + "context" + "errors" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models/db" +) + +var defaultFileBlockSize int64 = 32 * 1024 + +type File interface { + io.ReadWriteCloser + io.Seeker +} + +type file struct { + ctx context.Context + metaID int64 + fullPath string + blockSize int64 + + allowRead bool + allowWrite bool + offset int64 +} + +var _ File = (*file)(nil) + +func (f *file) readAt(fileMeta *dbfsMeta, offset int64, p []byte) (n int, err error) { + if offset >= fileMeta.FileSize { + return 0, io.EOF + } + + blobPos := int(offset % f.blockSize) + blobOffset := offset - int64(blobPos) + blobRemaining := int(f.blockSize) - blobPos + needRead := len(p) + if needRead > blobRemaining { + needRead = blobRemaining + } + if blobOffset+int64(blobPos)+int64(needRead) > fileMeta.FileSize { + needRead = int(fileMeta.FileSize - blobOffset - int64(blobPos)) + } + if needRead <= 0 { + return 0, io.EOF + } + var fileData dbfsData + ok, err := db.GetEngine(f.ctx).Where("meta_id = ? AND blob_offset = ?", f.metaID, blobOffset).Get(&fileData) + if err != nil { + return 0, err + } + blobData := fileData.BlobData + if !ok { + blobData = nil + } + + canCopy := len(blobData) - blobPos + if canCopy <= 0 { + canCopy = 0 + } + realRead := needRead + if realRead > canCopy { + realRead = canCopy + } + if realRead > 0 { + copy(p[:realRead], fileData.BlobData[blobPos:blobPos+realRead]) + } + for i := realRead; i < needRead; i++ { + p[i] = 0 + } + return needRead, nil +} + +func (f *file) Read(p []byte) (n int, err error) { + if f.metaID == 0 || !f.allowRead { + return 0, os.ErrInvalid + } + + fileMeta, err := findFileMetaByID(f.ctx, f.metaID) + if err != nil { + return 0, err + } + n, err = f.readAt(fileMeta, f.offset, p) + f.offset += int64(n) + return n, err +} + +func (f *file) Write(p []byte) (n int, err error) { + if f.metaID == 0 || !f.allowWrite { + return 0, os.ErrInvalid + } + + fileMeta, err := findFileMetaByID(f.ctx, f.metaID) + if err != nil { + return 0, err + } + + needUpdateSize := false + written := 0 + for len(p) > 0 { + blobPos := int(f.offset % f.blockSize) + blobOffset := f.offset - int64(blobPos) + blobRemaining := int(f.blockSize) - blobPos + needWrite := len(p) + if needWrite > blobRemaining { + needWrite = blobRemaining + } + buf := make([]byte, f.blockSize) + readBytes, err := f.readAt(fileMeta, blobOffset, buf) + if err != nil && !errors.Is(err, io.EOF) { + return written, err + } + copy(buf[blobPos:blobPos+needWrite], p[:needWrite]) + if blobPos+needWrite > readBytes { + buf = buf[:blobPos+needWrite] + } else { + buf = buf[:readBytes] + } + + fileData := dbfsData{ + MetaID: fileMeta.ID, + BlobOffset: blobOffset, + BlobData: buf, + } + if res, err := db.GetEngine(f.ctx).Exec("UPDATE dbfs_data SET revision=revision+1, blob_data=? WHERE meta_id=? AND blob_offset=?", buf, fileMeta.ID, blobOffset); err != nil { + return written, err + } else if updated, err := res.RowsAffected(); err != nil { + return written, err + } else if updated == 0 { + if _, err = db.GetEngine(f.ctx).Insert(&fileData); err != nil { + return written, err + } + } + written += needWrite + f.offset += int64(needWrite) + if f.offset > fileMeta.FileSize { + fileMeta.FileSize = f.offset + needUpdateSize = true + } + p = p[needWrite:] + } + + fileMetaUpdate := dbfsMeta{ + ModifyTimestamp: timeToFileTimestamp(time.Now()), + } + if needUpdateSize { + fileMetaUpdate.FileSize = f.offset + } + if _, err := db.GetEngine(f.ctx).ID(fileMeta.ID).Update(fileMetaUpdate); err != nil { + return written, err + } + return written, nil +} + +func (f *file) Seek(n int64, whence int) (int64, error) { + if f.metaID == 0 { + return 0, os.ErrInvalid + } + + newOffset := f.offset + switch whence { + case io.SeekStart: + newOffset = n + case io.SeekCurrent: + newOffset += n + case io.SeekEnd: + size, err := f.size() + if err != nil { + return f.offset, err + } + newOffset = size + n + default: + return f.offset, os.ErrInvalid + } + if newOffset < 0 { + return f.offset, os.ErrInvalid + } + f.offset = newOffset + return newOffset, nil +} + +func (f *file) Close() error { + return nil +} + +func timeToFileTimestamp(t time.Time) int64 { + return t.UnixMicro() +} + +func (f *file) loadMetaByPath() (*dbfsMeta, error) { + var fileMeta dbfsMeta + if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil { + return nil, err + } else if ok { + f.metaID = fileMeta.ID + f.blockSize = fileMeta.BlockSize + return &fileMeta, nil + } + return nil, nil +} + +func (f *file) open(flag int) (err error) { + // see os.OpenFile for flag values + if flag&os.O_WRONLY != 0 { + f.allowWrite = true + } else if flag&os.O_RDWR != 0 { + f.allowRead = true + f.allowWrite = true + } else /* O_RDONLY */ { + f.allowRead = true + } + + if f.allowWrite { + if flag&os.O_CREATE != 0 { + if flag&os.O_EXCL != 0 { + // file must not exist. + if f.metaID != 0 { + return os.ErrExist + } + } else { + // create a new file if none exists. + if f.metaID == 0 { + if err = f.createEmpty(); err != nil { + return err + } + } + } + } + if flag&os.O_TRUNC != 0 { + if err = f.truncate(); err != nil { + return err + } + } + if flag&os.O_APPEND != 0 { + if _, err = f.Seek(0, io.SeekEnd); err != nil { + return err + } + } + return nil + } + + // read only mode + if f.metaID == 0 { + return os.ErrNotExist + } + return nil +} + +func (f *file) createEmpty() error { + if f.metaID != 0 { + return os.ErrExist + } + now := time.Now() + _, err := db.GetEngine(f.ctx).Insert(&dbfsMeta{ + FullPath: f.fullPath, + BlockSize: f.blockSize, + CreateTimestamp: timeToFileTimestamp(now), + ModifyTimestamp: timeToFileTimestamp(now), + }) + if err != nil { + return err + } + if _, err = f.loadMetaByPath(); err != nil { + return err + } + return nil +} + +func (f *file) truncate() error { + if f.metaID == 0 { + return os.ErrNotExist + } + return db.WithTx(f.ctx, func(ctx context.Context) error { + if _, err := db.GetEngine(ctx).Exec("UPDATE dbfs_meta SET file_size = 0 WHERE id = ?", f.metaID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Delete(&dbfsData{MetaID: f.metaID}); err != nil { + return err + } + return nil + }) +} + +func (f *file) renameTo(newPath string) error { + if f.metaID == 0 { + return os.ErrNotExist + } + newPath = buildPath(newPath) + return db.WithTx(f.ctx, func(ctx context.Context) error { + if _, err := db.GetEngine(ctx).Exec("UPDATE dbfs_meta SET full_path = ? WHERE id = ?", newPath, f.metaID); err != nil { + return err + } + return nil + }) +} + +func (f *file) delete() error { + if f.metaID == 0 { + return os.ErrNotExist + } + return db.WithTx(f.ctx, func(ctx context.Context) error { + if _, err := db.GetEngine(ctx).Delete(&dbfsMeta{ID: f.metaID}); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Delete(&dbfsData{MetaID: f.metaID}); err != nil { + return err + } + return nil + }) +} + +func (f *file) size() (int64, error) { + if f.metaID == 0 { + return 0, os.ErrNotExist + } + fileMeta, err := findFileMetaByID(f.ctx, f.metaID) + if err != nil { + return 0, err + } + return fileMeta.FileSize, nil +} + +func findFileMetaByID(ctx context.Context, metaID int64) (*dbfsMeta, error) { + var fileMeta dbfsMeta + if ok, err := db.GetEngine(ctx).Where("id = ?", metaID).Get(&fileMeta); err != nil { + return nil, err + } else if ok { + return &fileMeta, nil + } + return nil, nil +} + +func buildPath(path string) string { + path = filepath.Clean(path) + path = strings.ReplaceAll(path, "\\", "/") + path = strings.TrimPrefix(path, "/") + return strconv.Itoa(strings.Count(path, "/")) + ":" + path +} + +func newDbFile(ctx context.Context, path string) (*file, error) { + path = buildPath(path) + f := &file{ctx: ctx, fullPath: path, blockSize: defaultFileBlockSize} + if _, err := f.loadMetaByPath(); err != nil { + return nil, err + } + return f, nil +} diff --git a/models/dbfs/dbfs.go b/models/dbfs/dbfs.go new file mode 100644 index 0000000000000..89d3dc7cbc911 --- /dev/null +++ b/models/dbfs/dbfs.go @@ -0,0 +1,73 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package dbfs + +import ( + "context" + "os" + + "code.gitea.io/gitea/models/db" +) + +type dbfsMeta struct { + ID int64 `xorm:"pk autoincr"` + FullPath string `xorm:"VARCHAR(500) UNIQUE NOT NULL"` + BlockSize int64 `xorm:"BIGINT NOT NULL"` + FileSize int64 `xorm:"BIGINT NOT NULL"` + CreateTimestamp int64 `xorm:"BIGINT NOT NULL"` + ModifyTimestamp int64 `xorm:"BIGINT NOT NULL"` +} + +type dbfsData struct { + ID int64 `xorm:"pk autoincr"` + Revision int64 `xorm:"BIGINT NOT NULL"` + MetaID int64 `xorm:"BIGINT index(meta_offset) NOT NULL"` + BlobOffset int64 `xorm:"BIGINT index(meta_offset) NOT NULL"` + BlobSize int64 `xorm:"BIGINT NOT NULL"` + BlobData []byte `xorm:"BLOB NOT NULL"` +} + +func init() { + db.RegisterModel(new(dbfsMeta)) + db.RegisterModel(new(dbfsData)) +} + +func OpenFile(ctx context.Context, name string, flag int) (File, error) { + f, err := newDbFile(ctx, name) + if err != nil { + return nil, err + } + err = f.open(flag) + if err != nil { + _ = f.Close() + return nil, err + } + return f, nil +} + +func Open(ctx context.Context, name string) (File, error) { + return OpenFile(ctx, name, os.O_RDONLY) +} + +func Create(ctx context.Context, name string) (File, error) { + return OpenFile(ctx, name, os.O_RDWR|os.O_CREATE|os.O_TRUNC) +} + +func Rename(ctx context.Context, oldPath, newPath string) error { + f, err := newDbFile(ctx, oldPath) + if err != nil { + return err + } + defer f.Close() + return f.renameTo(newPath) +} + +func Remove(ctx context.Context, name string) error { + f, err := newDbFile(ctx, name) + if err != nil { + return err + } + defer f.Close() + return f.delete() +} diff --git a/models/dbfs/dbfs_test.go b/models/dbfs/dbfs_test.go new file mode 100644 index 0000000000000..30aa6463c59ee --- /dev/null +++ b/models/dbfs/dbfs_test.go @@ -0,0 +1,179 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package dbfs + +import ( + "bufio" + "io" + "os" + "testing" + + "code.gitea.io/gitea/models/db" + + "github.com/stretchr/testify/assert" + + _ "github.com/mattn/go-sqlite3" +) + +func changeDefaultFileBlockSize(n int64) (restore func()) { + old := defaultFileBlockSize + defaultFileBlockSize = n + return func() { + defaultFileBlockSize = old + } +} + +func TestDbfsBasic(t *testing.T) { + defer changeDefaultFileBlockSize(4)() + + // test basic write/read + f, err := OpenFile(db.DefaultContext, "test.txt", os.O_RDWR|os.O_CREATE) + assert.NoError(t, err) + + n, err := f.Write([]byte("0123456789")) // blocks: 0123 4567 89 + assert.NoError(t, err) + assert.EqualValues(t, 10, n) + + _, err = f.Seek(0, io.SeekStart) + assert.NoError(t, err) + + buf, err := io.ReadAll(f) + assert.NoError(t, err) + assert.EqualValues(t, 10, n) + assert.EqualValues(t, "0123456789", string(buf)) + + // write some new data + _, err = f.Seek(1, io.SeekStart) + assert.NoError(t, err) + _, err = f.Write([]byte("bcdefghi")) // blocks: 0bcd efgh i9 + assert.NoError(t, err) + + // read from offset + buf, err = io.ReadAll(f) + assert.NoError(t, err) + assert.EqualValues(t, "9", string(buf)) + + // read all + _, err = f.Seek(0, io.SeekStart) + assert.NoError(t, err) + buf, err = io.ReadAll(f) + assert.NoError(t, err) + assert.EqualValues(t, "0bcdefghi9", string(buf)) + + // write to new size + _, err = f.Seek(-1, io.SeekEnd) + assert.NoError(t, err) + _, err = f.Write([]byte("JKLMNOP")) // blocks: 0bcd efgh iJKL MNOP + assert.NoError(t, err) + _, err = f.Seek(0, io.SeekStart) + assert.NoError(t, err) + buf, err = io.ReadAll(f) + assert.NoError(t, err) + assert.EqualValues(t, "0bcdefghiJKLMNOP", string(buf)) + + // write beyond EOF and fill with zero + _, err = f.Seek(5, io.SeekCurrent) + assert.NoError(t, err) + _, err = f.Write([]byte("xyzu")) // blocks: 0bcd efgh iJKL MNOP 0000 0xyz u + assert.NoError(t, err) + _, err = f.Seek(0, io.SeekStart) + assert.NoError(t, err) + buf, err = io.ReadAll(f) + assert.NoError(t, err) + assert.EqualValues(t, "0bcdefghiJKLMNOP\x00\x00\x00\x00\x00xyzu", string(buf)) + + // write to the block with zeros + _, err = f.Seek(-6, io.SeekCurrent) + assert.NoError(t, err) + _, err = f.Write([]byte("ABCD")) // blocks: 0bcd efgh iJKL MNOP 000A BCDz u + assert.NoError(t, err) + _, err = f.Seek(0, io.SeekStart) + assert.NoError(t, err) + buf, err = io.ReadAll(f) + assert.NoError(t, err) + assert.EqualValues(t, "0bcdefghiJKLMNOP\x00\x00\x00ABCDzu", string(buf)) + + assert.NoError(t, f.Close()) + + // test rename + err = Rename(db.DefaultContext, "test.txt", "test2.txt") + assert.NoError(t, err) + + _, err = OpenFile(db.DefaultContext, "test.txt", os.O_RDONLY) + assert.Error(t, err) + + f, err = OpenFile(db.DefaultContext, "test2.txt", os.O_RDONLY) + assert.NoError(t, err) + assert.NoError(t, f.Close()) + + // test remove + err = Remove(db.DefaultContext, "test2.txt") + assert.NoError(t, err) + + _, err = OpenFile(db.DefaultContext, "test2.txt", os.O_RDONLY) + assert.Error(t, err) +} + +func TestDbfsReadWrite(t *testing.T) { + defer changeDefaultFileBlockSize(4)() + + f1, err := OpenFile(db.DefaultContext, "test.log", os.O_RDWR|os.O_CREATE) + assert.NoError(t, err) + defer f1.Close() + + f2, err := OpenFile(db.DefaultContext, "test.log", os.O_RDONLY) + assert.NoError(t, err) + defer f2.Close() + + _, err = f1.Write([]byte("line 1\n")) + assert.NoError(t, err) + + f2r := bufio.NewReader(f2) + + line, err := f2r.ReadString('\n') + assert.NoError(t, err) + assert.EqualValues(t, "line 1\n", line) + _, err = f2r.ReadString('\n') + assert.ErrorIs(t, err, io.EOF) + + _, err = f1.Write([]byte("line 2\n")) + assert.NoError(t, err) + + line, err = f2r.ReadString('\n') + assert.NoError(t, err) + assert.EqualValues(t, "line 2\n", line) + _, err = f2r.ReadString('\n') + assert.ErrorIs(t, err, io.EOF) +} + +func TestDbfsSeekWrite(t *testing.T) { + defer changeDefaultFileBlockSize(4)() + + f, err := OpenFile(db.DefaultContext, "test2.log", os.O_RDWR|os.O_CREATE) + assert.NoError(t, err) + defer f.Close() + + n, err := f.Write([]byte("111")) + assert.NoError(t, err) + + _, err = f.Seek(int64(n), io.SeekStart) + assert.NoError(t, err) + + _, err = f.Write([]byte("222")) + assert.NoError(t, err) + + _, err = f.Seek(int64(n), io.SeekStart) + assert.NoError(t, err) + + _, err = f.Write([]byte("333")) + assert.NoError(t, err) + + fr, err := OpenFile(db.DefaultContext, "test2.log", os.O_RDONLY) + assert.NoError(t, err) + defer f.Close() + + buf, err := io.ReadAll(fr) + assert.NoError(t, err) + assert.EqualValues(t, "111333", string(buf)) +} diff --git a/models/dbfs/main_test.go b/models/dbfs/main_test.go new file mode 100644 index 0000000000000..7a820b2d83e18 --- /dev/null +++ b/models/dbfs/main_test.go @@ -0,0 +1,23 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package dbfs + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" +) + +func init() { + setting.SetCustomPathAndConf("", "", "") + setting.LoadForTest() +} + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + GiteaRootPath: filepath.Join("..", ".."), + }) +} diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index ea47a33f1c4db..dd434d78a980c 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -140,3 +140,14 @@ num_members: 1 includes_all_repositories: false can_create_org_repo: false + +- + id: 14 + org_id: 3 + lower_name: teamcreaterepo + name: teamCreateRepo + authorize: 2 # write + num_repos: 0 + num_members: 1 + includes_all_repositories: false + can_create_org_repo: true diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml index 845741effddf8..de4f29d977cbb 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -93,3 +93,9 @@ org_id: 19 team_id: 6 uid: 31 + +- + id: 17 + org_id: 3 + team_id: 14 + uid: 2 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 3afed37df9655..63a5e0f890552 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -104,7 +104,7 @@ num_following: 0 num_stars: 0 num_repos: 3 - num_teams: 4 + num_teams: 5 num_members: 3 visibility: 0 repo_admin_change_team_access: false diff --git a/models/issues/comment.go b/models/issues/comment.go index 91dc128277eee..9ad538fcc6c2a 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -355,7 +355,7 @@ func (c *Comment) LoadPoster(ctx context.Context) (err error) { return nil } - c.Poster, err = user_model.GetUserByID(ctx, c.PosterID) + c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID) if err != nil { if user_model.IsErrUserNotExist(err) { c.PosterID = -1 diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 2b55bc212feca..0411d44531ee1 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -29,32 +29,13 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { return nil } - posterIDs := comments.getPosterIDs() - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return err - } - left -= limit - posterIDs = posterIDs[limit:] + posterMaps, err := getPosters(ctx, comments.getPosterIDs()) + if err != nil { + return err } for _, comment := range comments { - if comment.PosterID <= 0 { - continue - } - var ok bool - if comment.Poster, ok = posterMaps[comment.PosterID]; !ok { - comment.Poster = user_model.NewGhostUser() - } + comment.Poster = getPoster(comment.PosterID, posterMaps) } return nil } diff --git a/models/issues/issue.go b/models/issues/issue.go index 50c9b77119974..78cac900523f9 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -235,7 +235,7 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) { // LoadPoster loads poster func (issue *Issue) LoadPoster(ctx context.Context) (err error) { if issue.Poster == nil { - issue.Poster, err = user_model.GetUserByID(ctx, issue.PosterID) + issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) if err != nil { issue.PosterID = -1 issue.Poster = user_model.NewGhostUser() diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index e22e48c0bb4bd..6ddadd27ed442 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -86,7 +86,18 @@ func (issues IssueList) loadPosters(ctx context.Context) error { return nil } - posterIDs := issues.getPosterIDs() + posterMaps, err := getPosters(ctx, issues.getPosterIDs()) + if err != nil { + return err + } + + for _, issue := range issues { + issue.Poster = getPoster(issue.PosterID, posterMaps) + } + return nil +} + +func getPosters(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) { posterMaps := make(map[int64]*user_model.User, len(posterIDs)) left := len(posterIDs) for left > 0 { @@ -98,22 +109,26 @@ func (issues IssueList) loadPosters(ctx context.Context) error { In("id", posterIDs[:limit]). Find(&posterMaps) if err != nil { - return err + return nil, err } left -= limit posterIDs = posterIDs[limit:] } + return posterMaps, nil +} - for _, issue := range issues { - if issue.PosterID <= 0 { - continue - } - var ok bool - if issue.Poster, ok = posterMaps[issue.PosterID]; !ok { - issue.Poster = user_model.NewGhostUser() - } +func getPoster(posterID int64, posterMaps map[int64]*user_model.User) *user_model.User { + if posterID == user_model.ActionsUserID { + return user_model.NewActionsUser() } - return nil + if posterID <= 0 { + return nil + } + poster, ok := posterMaps[posterID] + if !ok { + return user_model.NewGhostUser() + } + return poster } func (issues IssueList) getIssueIDs() []int64 { diff --git a/models/issues/pull.go b/models/issues/pull.go index 93b227f3fd5ce..6ff6502e4eb56 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -394,6 +394,11 @@ func (pr *PullRequest) IsAncestor() bool { return pr.Status == PullRequestStatusAncestor } +// IsFromFork return true if this PR is from a fork. +func (pr *PullRequest) IsFromFork() bool { + return pr.HeadRepoID != pr.BaseRepoID +} + // SetMerged sets a pull request to merged and closes the corresponding issue func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) { if pr.HasMerged { diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 12dbff107d15d..007c2fd90333a 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -173,8 +173,9 @@ func (prs PullRequestList) loadAttributes(ctx context.Context) error { for i := range issues { set[issues[i].ID] = issues[i] } - for i := range prs { - prs[i].Issue = set[prs[i].IssueID] + for _, pr := range prs { + pr.Issue = set[pr.IssueID] + pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync } return nil } diff --git a/models/issues/review.go b/models/issues/review.go index 7e1a39bb5b8ba..fe123d73986bd 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -158,7 +158,7 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) { if r.ReviewerID == 0 || r.Reviewer != nil { return } - r.Reviewer, err = user_model.GetUserByID(ctx, r.ReviewerID) + r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) return err } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 2058fcec0fe05..15600f057cfb1 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -453,6 +453,8 @@ var migrations = []Migration{ NewMigration("Add updated unix to LFSMetaObject", v1_19.AddUpdatedUnixToLFSMetaObject), // v239 -> v240 NewMigration("Add scope for access_token", v1_19.AddScopeForAccessTokens), + // v240 -> v241 + NewMigration("Add actions tables", v1_19.AddActionsTables), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_19/v240.go b/models/migrations/v1_19/v240.go new file mode 100644 index 0000000000000..4505f86299556 --- /dev/null +++ b/models/migrations/v1_19/v240.go @@ -0,0 +1,176 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_19 //nolint + +import ( + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddActionsTables(x *xorm.Engine) error { + type ActionRunner struct { + ID int64 + UUID string `xorm:"CHAR(36) UNIQUE"` + Name string `xorm:"VARCHAR(255)"` + OwnerID int64 `xorm:"index"` // org level runner, 0 means system + RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global + Description string `xorm:"TEXT"` + Base int // 0 native 1 docker 2 virtual machine + RepoRange string // glob match which repositories could use this runner + + Token string `xorm:"-"` + TokenHash string `xorm:"UNIQUE"` // sha256 of token + TokenSalt string + // TokenLastEight string `xorm:"token_last_eight"` // it's unnecessary because we don't find runners by token + + LastOnline timeutil.TimeStamp `xorm:"index"` + LastActive timeutil.TimeStamp `xorm:"index"` + + // Store OS and Artch. + AgentLabels []string + // Store custom labes use defined. + CustomLabels []string + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + Deleted timeutil.TimeStamp `xorm:"deleted"` + } + + type ActionRunnerToken struct { + ID int64 + Token string `xorm:"UNIQUE"` + OwnerID int64 `xorm:"index"` // org level runner, 0 means system + RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global + IsActive bool + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + Deleted timeutil.TimeStamp `xorm:"deleted"` + } + + type ActionRun struct { + ID int64 + Title string + RepoID int64 `xorm:"index unique(repo_index)"` + OwnerID int64 `xorm:"index"` + WorkflowID string `xorm:"index"` // the name of workflow file + Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository + TriggerUserID int64 + Ref string + CommitSHA string + Event string + IsForkPullRequest bool + EventPayload string `xorm:"LONGTEXT"` + Status int `xorm:"index"` + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + } + + type ActionRunJob struct { + ID int64 + RunID int64 `xorm:"index"` + RepoID int64 `xorm:"index"` + 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 int `xorm:"index"` + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` + } + + type Repository struct { + NumActionRuns int `xorm:"NOT NULL DEFAULT 0"` + NumClosedActionRuns int `xorm:"NOT NULL DEFAULT 0"` + } + + type ActionRunIndex db.ResourceIndex + + type ActionTask struct { + ID int64 + JobID int64 + Attempt int64 + RunnerID int64 `xorm:"index"` + Status int `xorm:"index"` + Started timeutil.TimeStamp `xorm:"index"` + Stopped timeutil.TimeStamp + + RepoID int64 `xorm:"index"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` + IsForkPullRequest bool + + TokenHash string `xorm:"UNIQUE"` // sha256 of token + TokenSalt string + TokenLastEight string `xorm:"index token_last_eight"` + + LogFilename string // file name of log + LogInStorage bool // read log from database or from storage + LogLength int64 // lines count + LogSize int64 // blob size + LogIndexes []int64 `xorm:"LONGBLOB"` // line number to offset + LogExpired bool // files that are too old will be deleted + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` + } + + type ActionTaskStep struct { + ID int64 + Name string `xorm:"VARCHAR(255)"` + TaskID int64 `xorm:"index unique(task_index)"` + Index int64 `xorm:"index unique(task_index)"` + RepoID int64 `xorm:"index"` + Status int `xorm:"index"` + LogIndex int64 + LogLength int64 + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + } + + type dbfsMeta struct { + ID int64 `xorm:"pk autoincr"` + FullPath string `xorm:"VARCHAR(500) UNIQUE NOT NULL"` + BlockSize int64 `xorm:"BIGINT NOT NULL"` + FileSize int64 `xorm:"BIGINT NOT NULL"` + CreateTimestamp int64 `xorm:"BIGINT NOT NULL"` + ModifyTimestamp int64 `xorm:"BIGINT NOT NULL"` + } + + type dbfsData struct { + ID int64 `xorm:"pk autoincr"` + Revision int64 `xorm:"BIGINT NOT NULL"` + MetaID int64 `xorm:"BIGINT index(meta_offset) NOT NULL"` + BlobOffset int64 `xorm:"BIGINT index(meta_offset) NOT NULL"` + BlobSize int64 `xorm:"BIGINT NOT NULL"` + BlobData []byte `xorm:"BLOB NOT NULL"` + } + + return x.Sync( + new(ActionRunner), + new(ActionRunnerToken), + new(ActionRun), + new(ActionRunJob), + new(Repository), + new(ActionRunIndex), + new(ActionTask), + new(ActionTaskStep), + new(dbfsMeta), + new(dbfsData), + ) +} diff --git a/models/organization/org.go b/models/organization/org.go index 9d9e9cda46a48..05eaead60bf09 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -397,13 +397,14 @@ func (org *Organization) GetOrgUserMaxAuthorizeLevel(uid int64) (perm.AccessMode } // GetUsersWhoCanCreateOrgRepo returns users which are able to create repo in organization -func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) ([]*user_model.User, error) { - users := make([]*user_model.User, 0, 10) +func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*user_model.User, error) { + // Use a map, in order to de-duplicate users. + users := make(map[int64]*user_model.User) return users, db.GetEngine(ctx). Join("INNER", "`team_user`", "`team_user`.uid=`user`.id"). Join("INNER", "`team`", "`team`.id=`team_user`.team_id"). Where(builder.Eq{"team.can_create_org_repo": true}.Or(builder.Eq{"team.authorize": perm.AccessModeOwner})). - And("team_user.org_id = ?", orgID).Asc("`user`.name").Find(&users) + And("team_user.org_id = ?", orgID).Find(&users) } // SearchOrganizationsOptions options to filter organizations diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 2f821e3a4c1fa..0a38365924ed6 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -91,11 +91,12 @@ func TestUser_GetTeams(t *testing.T) { org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) teams, err := org.LoadTeams() assert.NoError(t, err) - if assert.Len(t, teams, 4) { + if assert.Len(t, teams, 5) { assert.Equal(t, int64(1), teams[0].ID) assert.Equal(t, int64(2), teams[1].ID) assert.Equal(t, int64(12), teams[2].ID) - assert.Equal(t, int64(7), teams[3].ID) + assert.Equal(t, int64(14), teams[3].ID) + assert.Equal(t, int64(7), teams[4].ID) } } @@ -292,7 +293,7 @@ func TestUser_GetUserTeamIDs(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, teamIDs) } - testSuccess(2, []int64{1, 2}) + testSuccess(2, []int64{1, 2, 14}) testSuccess(4, []int64{2}) testSuccess(unittest.NonexistentID, []int64{}) } @@ -447,7 +448,7 @@ func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) { users, err = organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, 7) assert.NoError(t, err) assert.Len(t, users, 1) - assert.EqualValues(t, 5, users[0].ID) + assert.NotNil(t, users[5]) } func TestUser_RemoveOrgRepo(t *testing.T) { diff --git a/models/packages/conda/search.go b/models/packages/conda/search.go new file mode 100644 index 0000000000000..887441e3b2bd2 --- /dev/null +++ b/models/packages/conda/search.go @@ -0,0 +1,63 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package conda + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + conda_module "code.gitea.io/gitea/modules/packages/conda" + + "xorm.io/builder" +) + +type FileSearchOptions struct { + OwnerID int64 + Channel string + Subdir string + Filename string +} + +// SearchFiles gets all files matching the search options +func SearchFiles(ctx context.Context, opts *FileSearchOptions) ([]*packages.PackageFile, error) { + var cond builder.Cond = builder.Eq{ + "package.type": packages.TypeConda, + "package.owner_id": opts.OwnerID, + "package_version.is_internal": false, + } + + if opts.Filename != "" { + cond = cond.And(builder.Eq{ + "package_file.lower_name": strings.ToLower(opts.Filename), + }) + } + + var versionPropsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypePackage, + "package_property.name": conda_module.PropertyChannel, + "package_property.value": opts.Channel, + } + + cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(versionPropsCond).From("package_property"))) + + var filePropsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeFile, + "package_property.name": conda_module.PropertySubdir, + "package_property.value": opts.Subdir, + } + + cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(filePropsCond).From("package_property"))) + + sess := db.GetEngine(ctx). + Select("package_file.*"). + Table("package_file"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(cond) + + pfs := make([]*packages.PackageFile, 0, 10) + return pfs, sess.Find(&pfs) +} diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 34f1cad87dc45..3b36ee22661ca 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/composer" "code.gitea.io/gitea/modules/packages/conan" + "code.gitea.io/gitea/modules/packages/conda" "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/packages/helm" "code.gitea.io/gitea/modules/packages/maven" @@ -132,6 +133,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc metadata = &composer.Metadata{} case TypeConan: metadata = &conan.Metadata{} + case TypeConda: + metadata = &conda.VersionMetadata{} case TypeContainer: metadata = &container.Metadata{} case TypeGeneric: diff --git a/models/packages/package.go b/models/packages/package.go index a804f35de3526..0015953d81d8c 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -32,6 +32,7 @@ type Type string const ( TypeComposer Type = "composer" TypeConan Type = "conan" + TypeConda Type = "conda" TypeContainer Type = "container" TypeGeneric Type = "generic" TypeHelm Type = "helm" @@ -47,6 +48,7 @@ const ( var TypeList = []Type{ TypeComposer, TypeConan, + TypeConda, TypeContainer, TypeGeneric, TypeHelm, @@ -66,6 +68,8 @@ func (pt Type) Name() string { return "Composer" case TypeConan: return "Conan" + case TypeConda: + return "Conda" case TypeContainer: return "Container" case TypeGeneric: @@ -97,6 +101,8 @@ func (pt Type) SVGName() string { return "gitea-composer" case TypeConan: return "gitea-conan" + case TypeConda: + return "gitea-conda" case TypeContainer: return "octicon-container" case TypeGeneric: diff --git a/models/repo.go b/models/repo.go index e95887077c955..38dc3f1ab184e 100644 --- a/models/repo.go +++ b/models/repo.go @@ -11,6 +11,7 @@ import ( _ "image/jpeg" // Needed for jpeg support + actions_model "code.gitea.io/gitea/models/actions" activities_model "code.gitea.io/gitea/models/activities" admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -26,6 +27,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" @@ -52,6 +54,12 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { defer committer.Close() sess := db.GetEngine(ctx) + // Query the action tasks of this repo, they will be needed after they have been deleted to remove the logs + tasks, err := actions_model.FindTasks(ctx, actions_model.FindTaskOptions{RepoID: repoID}) + if err != nil { + return fmt.Errorf("find actions tasks of repo %v: %w", repoID, err) + } + // In case is a organization. org, err := user_model.GetUserByID(ctx, uid) if err != nil { @@ -152,6 +160,11 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { &repo_model.Watch{RepoID: repoID}, &webhook.Webhook{RepoID: repoID}, &secret_model.Secret{RepoID: repoID}, + &actions_model.ActionTaskStep{RepoID: repoID}, + &actions_model.ActionTask{RepoID: repoID}, + &actions_model.ActionRunJob{RepoID: repoID}, + &actions_model.ActionRun{RepoID: repoID}, + &actions_model.ActionRunner{RepoID: repoID}, ); err != nil { return fmt.Errorf("deleteBeans: %w", err) } @@ -315,6 +328,15 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { } } + // Finally, delete action logs after the actions have already been deleted to avoid new log files + for _, task := range tasks { + err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + log.Error("remove log file %q: %v", task.LogFilename, err) + // go on + } + } + return nil } diff --git a/models/repo/repo.go b/models/repo/repo.go index e5e1ac43b41fd..831eb22dc5a50 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -141,6 +141,9 @@ type Repository struct { NumProjects int `xorm:"NOT NULL DEFAULT 0"` NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"` NumOpenProjects int `xorm:"-"` + NumActionRuns int `xorm:"NOT NULL DEFAULT 0"` + NumClosedActionRuns int `xorm:"NOT NULL DEFAULT 0"` + NumOpenActionRuns int `xorm:"-"` IsPrivate bool `xorm:"INDEX"` IsEmpty bool `xorm:"INDEX"` @@ -233,6 +236,7 @@ func (repo *Repository) AfterLoad() { repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects + repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns } // LoadAttributes loads attributes of the repository. diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index e20d03e2c5ae3..ee450a46c4b3f 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -174,7 +174,7 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { r.Config = new(PullRequestsConfig) case unit.TypeIssues: r.Config = new(IssuesConfig) - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages: + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages, unit.TypeActions: fallthrough default: r.Config = new(UnitConfig) diff --git a/models/unit/unit.go b/models/unit/unit.go index c4743dbdb4079..bcd0572ab9ecf 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -27,6 +27,7 @@ const ( TypeExternalTracker // 7 ExternalTracker TypeProjects // 8 Kanban board TypePackages // 9 Packages + TypeActions // 10 Actions ) // Value returns integer value for unit type @@ -54,6 +55,8 @@ func (u Type) String() string { return "TypeProjects" case TypePackages: return "TypePackages" + case TypeActions: + return "TypeActions" } return fmt.Sprintf("Unknown Type %d", u) } @@ -77,6 +80,7 @@ var ( TypeExternalTracker, TypeProjects, TypePackages, + TypeActions, } // DefaultRepoUnits contains the default unit types @@ -288,6 +292,15 @@ var ( perm.AccessModeRead, } + UnitActions = Unit{ + TypeActions, + "actions.actions", + "/actions", + "actions.unit.desc", + 7, + perm.AccessModeOwner, + } + // Units contains all the units Units = map[Type]Unit{ TypeCode: UnitCode, @@ -299,6 +312,7 @@ var ( TypeExternalWiki: UnitExternalWiki, TypeProjects: UnitProjects, TypePackages: UnitPackages, + TypeActions: UnitActions, } ) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index f3127006b8d34..7e327f2bd22ea 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -104,6 +104,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) { setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages") + setting.Actions.Storage.Path = filepath.Join(setting.AppDataPath, "actions_log") + setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home") setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost" diff --git a/models/user/user.go b/models/user/user.go index a2c54a4429fd9..0917bea7540df 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -559,32 +559,6 @@ func GetUserSalt() (string, error) { return hex.EncodeToString(rBytes), nil } -// NewGhostUser creates and returns a fake user for someone has deleted their account. -func NewGhostUser() *User { - return &User{ - ID: -1, - Name: "Ghost", - LowerName: "ghost", - } -} - -// NewReplaceUser creates and returns a fake user for external user -func NewReplaceUser(name string) *User { - return &User{ - ID: -1, - Name: name, - LowerName: strings.ToLower(name), - } -} - -// IsGhost check if user is fake user for a deleted account -func (u *User) IsGhost() bool { - if u == nil { - return false - } - return u.ID == -1 && u.Name == "Ghost" -} - var ( reservedUsernames = []string{ ".", @@ -622,6 +596,7 @@ var ( "swagger.v1.json", "user", "v2", + "gitea-actions", } reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom"} @@ -1013,6 +988,20 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) { return u, nil } +// GetPossibleUserByID returns the user if id > 0 or return system usrs if id < 0 +func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) { + switch id { + case -1: + return NewGhostUser(), nil + case ActionsUserID: + return NewActionsUser(), nil + case 0: + return nil, ErrUserNotExist{} + default: + return GetUserByID(ctx, id) + } +} + // GetUserByNameCtx returns user by given name. func GetUserByName(ctx context.Context, name string) (*User, error) { if len(name) == 0 { diff --git a/models/user/user_system.go b/models/user/user_system.go new file mode 100644 index 0000000000000..f54f4e3ffb5bb --- /dev/null +++ b/models/user/user_system.go @@ -0,0 +1,64 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "strings" + + "code.gitea.io/gitea/modules/structs" +) + +// NewGhostUser creates and returns a fake user for someone has deleted their account. +func NewGhostUser() *User { + return &User{ + ID: -1, + Name: "Ghost", + LowerName: "ghost", + } +} + +// IsGhost check if user is fake user for a deleted account +func (u *User) IsGhost() bool { + if u == nil { + return false + } + return u.ID == -1 && u.Name == "Ghost" +} + +// NewReplaceUser creates and returns a fake user for external user +func NewReplaceUser(name string) *User { + return &User{ + ID: -1, + Name: name, + LowerName: strings.ToLower(name), + } +} + +const ( + ActionsUserID = -2 + ActionsUserName = "gitea-actions" + ActionsFullName = "Gitea Actions" + ActionsEmail = "teabot@gitea.io" +) + +// NewActionsUser creates and returns a fake user for running the actions. +func NewActionsUser() *User { + return &User{ + ID: ActionsUserID, + Name: ActionsUserName, + LowerName: ActionsUserName, + IsActive: true, + FullName: ActionsFullName, + Email: ActionsEmail, + KeepEmailPrivate: true, + LoginName: ActionsUserName, + Type: UserTypeIndividual, + AllowCreateOrganization: true, + Visibility: structs.VisibleTypePublic, + } +} + +func (u *User) IsActions() bool { + return u != nil && u.ID == ActionsUserID +} diff --git a/modules/actions/log.go b/modules/actions/log.go new file mode 100644 index 0000000000000..3868101992a95 --- /dev/null +++ b/modules/actions/log.go @@ -0,0 +1,163 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "strings" + "time" + + "code.gitea.io/gitea/models/dbfs" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/storage" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + MaxLineSize = 64 * 1024 + DBFSPrefix = "actions_log/" + + timeFormat = "2006-01-02T15:04:05.0000000Z07:00" + defaultBufSize = MaxLineSize +) + +func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runnerv1.LogRow) ([]int, error) { + name := DBFSPrefix + filename + f, err := dbfs.OpenFile(ctx, name, os.O_WRONLY|os.O_CREATE) + if err != nil { + return nil, fmt.Errorf("dbfs OpenFile %q: %w", name, err) + } + defer f.Close() + if _, err := f.Seek(offset, io.SeekStart); err != nil { + return nil, fmt.Errorf("dbfs Seek %q: %w", name, err) + } + + writer := bufio.NewWriterSize(f, defaultBufSize) + + ns := make([]int, 0, len(rows)) + for _, row := range rows { + n, err := writer.WriteString(FormatLog(row.Time.AsTime(), row.Content) + "\n") + if err != nil { + return nil, err + } + ns = append(ns, n) + } + + if err := writer.Flush(); err != nil { + return nil, err + } + return ns, nil +} + +func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) { + f, err := openLogs(ctx, inStorage, filename) + if err != nil { + return nil, err + } + defer f.Close() + + if _, err := f.Seek(offset, io.SeekStart); err != nil { + return nil, fmt.Errorf("file seek: %w", err) + } + + scanner := bufio.NewScanner(f) + maxLineSize := len(timeFormat) + MaxLineSize + 1 + scanner.Buffer(make([]byte, maxLineSize), maxLineSize) + + var rows []*runnerv1.LogRow + for scanner.Scan() && (int64(len(rows)) < limit || limit < 0) { + t, c, err := ParseLog(scanner.Text()) + if err != nil { + return nil, fmt.Errorf("parse log %q: %w", scanner.Text(), err) + } + rows = append(rows, &runnerv1.LogRow{ + Time: timestamppb.New(t), + Content: c, + }) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("scan: %w", err) + } + + return rows, nil +} + +func TransferLogs(ctx context.Context, filename string) (func(), error) { + name := DBFSPrefix + filename + remove := func() { + if err := dbfs.Remove(ctx, name); err != nil { + log.Warn("dbfs remove %q: %v", name, err) + } + } + f, err := dbfs.Open(ctx, name) + if err != nil { + return nil, fmt.Errorf("dbfs open %q: %w", name, err) + } + defer f.Close() + + if _, err := storage.Actions.Save(filename, f, -1); err != nil { + return nil, fmt.Errorf("storage save %q: %w", filename, err) + } + return remove, nil +} + +func RemoveLogs(ctx context.Context, inStorage bool, filename string) error { + if !inStorage { + name := DBFSPrefix + filename + err := dbfs.Remove(ctx, name) + if err != nil { + return fmt.Errorf("dbfs remove %q: %w", name, err) + } + return nil + } + err := storage.Actions.Delete(filename) + if err != nil { + return fmt.Errorf("storage delete %q: %w", filename, err) + } + return nil +} + +func openLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) { + if !inStorage { + name := DBFSPrefix + filename + f, err := dbfs.Open(ctx, name) + if err != nil { + return nil, fmt.Errorf("dbfs open %q: %w", name, err) + } + return f, nil + } + f, err := storage.Actions.Open(filename) + if err != nil { + return nil, fmt.Errorf("storage open %q: %w", filename, err) + } + return f, nil +} + +func FormatLog(timestamp time.Time, content string) string { + // Content shouldn't contain new line, it will break log indexes, other control chars are safe. + content = strings.ReplaceAll(content, "\n", `\n`) + if len(content) > MaxLineSize { + content = content[:MaxLineSize] + } + return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), content) +} + +func ParseLog(in string) (time.Time, string, error) { + index := strings.IndexRune(in, ' ') + if index < 0 { + return time.Time{}, "", fmt.Errorf("invalid log: %q", in) + } + timestamp, err := time.Parse(timeFormat, in[:index]) + if err != nil { + return time.Time{}, "", err + } + return timestamp, in[index+1:], nil +} diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go new file mode 100644 index 0000000000000..cbbc0b357d21f --- /dev/null +++ b/modules/actions/task_state.go @@ -0,0 +1,101 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + actions_model "code.gitea.io/gitea/models/actions" +) + +const ( + preStepName = "Set up job" + postStepName = "Complete job" +) + +// FullSteps returns steps with "Set up job" and "Complete job" +func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { + if len(task.Steps) == 0 { + return fullStepsOfEmptySteps(task) + } + + firstStep := task.Steps[0] + var logIndex int64 + + preStep := &actions_model.ActionTaskStep{ + Name: preStepName, + LogLength: task.LogLength, + Started: task.Started, + Status: actions_model.StatusRunning, + } + + if firstStep.Status.HasRun() || firstStep.Status.IsRunning() { + preStep.LogLength = firstStep.LogIndex + preStep.Stopped = firstStep.Started + preStep.Status = actions_model.StatusSuccess + } else if task.Status.IsDone() { + preStep.Stopped = task.Stopped + preStep.Status = actions_model.StatusFailure + } + logIndex += preStep.LogLength + + var lastHasRunStep *actions_model.ActionTaskStep + for _, step := range task.Steps { + if step.Status.HasRun() { + lastHasRunStep = step + } + logIndex += step.LogLength + } + if lastHasRunStep == nil { + lastHasRunStep = preStep + } + + postStep := &actions_model.ActionTaskStep{ + Name: postStepName, + Status: actions_model.StatusWaiting, + } + if task.Status.IsDone() { + postStep.LogIndex = logIndex + postStep.LogLength = task.LogLength - postStep.LogIndex + postStep.Status = task.Status + postStep.Started = lastHasRunStep.Stopped + postStep.Stopped = task.Stopped + } + ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2) + ret = append(ret, preStep) + ret = append(ret, task.Steps...) + ret = append(ret, postStep) + + return ret +} + +func fullStepsOfEmptySteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { + preStep := &actions_model.ActionTaskStep{ + Name: preStepName, + LogLength: task.LogLength, + Started: task.Started, + Stopped: task.Stopped, + Status: actions_model.StatusRunning, + } + + postStep := &actions_model.ActionTaskStep{ + Name: postStepName, + LogIndex: task.LogLength, + Started: task.Stopped, + Stopped: task.Stopped, + Status: actions_model.StatusWaiting, + } + + if task.Status.IsDone() { + preStep.Status = task.Status + if preStep.Status.IsSuccess() { + postStep.Status = actions_model.StatusSuccess + } else { + postStep.Status = actions_model.StatusCancelled + } + } + + return []*actions_model.ActionTaskStep{ + preStep, + postStep, + } +} diff --git a/modules/actions/task_state_test.go b/modules/actions/task_state_test.go new file mode 100644 index 0000000000000..3a599fbcbd2b0 --- /dev/null +++ b/modules/actions/task_state_test.go @@ -0,0 +1,112 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + + "github.com/stretchr/testify/assert" +) + +func TestFullSteps(t *testing.T) { + tests := []struct { + name string + task *actions_model.ActionTask + want []*actions_model.ActionTaskStep + }{ + { + name: "regular", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, + }, + Status: actions_model.StatusSuccess, + Started: 10000, + Stopped: 10100, + LogLength: 100, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010}, + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, + {Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 10100}, + }, + }, + { + name: "failed step", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 20, Started: 10010, Stopped: 10020}, + {Status: actions_model.StatusFailure, LogIndex: 30, LogLength: 60, Started: 10020, Stopped: 10090}, + {Status: actions_model.StatusCancelled, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + Status: actions_model.StatusFailure, + Started: 10000, + Stopped: 10100, + LogLength: 100, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010}, + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 20, Started: 10010, Stopped: 10020}, + {Status: actions_model.StatusFailure, LogIndex: 30, LogLength: 60, Started: 10020, Stopped: 10090}, + {Status: actions_model.StatusCancelled, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + {Name: postStepName, Status: actions_model.StatusFailure, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 10100}, + }, + }, + { + name: "first step is running", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusRunning, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 0}, + }, + Status: actions_model.StatusRunning, + Started: 10000, + Stopped: 10100, + LogLength: 100, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010}, + {Status: actions_model.StatusRunning, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 0}, + {Name: postStepName, Status: actions_model.StatusWaiting, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + }, + { + name: "first step has canceled", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusCancelled, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + Status: actions_model.StatusFailure, + Started: 10000, + Stopped: 10100, + LogLength: 100, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusFailure, LogIndex: 0, LogLength: 100, Started: 10000, Stopped: 10100}, + {Status: actions_model.StatusCancelled, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + {Name: postStepName, Status: actions_model.StatusFailure, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100}, + }, + }, + { + name: "empty steps", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{}, + Status: actions_model.StatusSuccess, + Started: 10000, + Stopped: 10100, + LogLength: 100, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 100, Started: 10000, Stopped: 10100}, + {Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, FullSteps(tt.task), "FullSteps(%v)", tt.task) + }) + } +} diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go new file mode 100644 index 0000000000000..c1c8e71f53b5c --- /dev/null +++ b/modules/actions/workflows.go @@ -0,0 +1,221 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "bytes" + "io" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" + + "github.com/gobwas/glob" + "github.com/nektos/act/pkg/jobparser" + "github.com/nektos/act/pkg/model" +) + +func ListWorkflows(commit *git.Commit) (git.Entries, error) { + tree, err := commit.SubTree(".gitea/workflows") + if _, ok := err.(git.ErrNotExist); ok { + tree, err = commit.SubTree(".github/workflows") + } + if _, ok := err.(git.ErrNotExist); ok { + return nil, nil + } + if err != nil { + return nil, err + } + + entries, err := tree.ListEntriesRecursiveFast() + if err != nil { + return nil, err + } + + ret := make(git.Entries, 0, len(entries)) + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml") { + ret = append(ret, entry) + } + } + return ret, nil +} + +func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) (map[string][]byte, error) { + entries, err := ListWorkflows(commit) + if err != nil { + return nil, err + } + + workflows := make(map[string][]byte, len(entries)) + for _, entry := range entries { + f, err := entry.Blob().DataAsync() + if err != nil { + return nil, err + } + content, err := io.ReadAll(f) + _ = f.Close() + if err != nil { + return nil, err + } + workflow, err := model.ReadWorkflow(bytes.NewReader(content)) + if err != nil { + log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) + continue + } + events, err := jobparser.ParseRawOn(&workflow.RawOn) + if err != nil { + log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) + continue + } + for _, evt := range events { + if evt.Name != triggedEvent.Event() { + continue + } + + if detectMatched(commit, triggedEvent, payload, evt) { + workflows[entry.Name()] = content + } + } + } + + return workflows, nil +} + +func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { + if len(evt.Acts) == 0 { + return true + } + + switch triggedEvent { + case webhook_module.HookEventCreate: + fallthrough + case webhook_module.HookEventDelete: + fallthrough + case webhook_module.HookEventFork: + log.Warn("unsupported event %q", triggedEvent.Event()) + return false + case webhook_module.HookEventPush: + pushPayload := payload.(*api.PushPayload) + matchTimes := 0 + // all acts conditions should be satisfied + for cond, vals := range evt.Acts { + switch cond { + case "branches", "tags": + for _, val := range vals { + if glob.MustCompile(val, '/').Match(pushPayload.Ref) { + matchTimes++ + break + } + } + case "paths": + filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) + if err != nil { + log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) + } else { + for _, val := range vals { + matched := false + for _, file := range filesChanged { + if glob.MustCompile(val, '/').Match(file) { + matched = true + break + } + } + if matched { + matchTimes++ + break + } + } + } + default: + log.Warn("unsupported condition %q", cond) + } + } + return matchTimes == len(evt.Acts) + + case webhook_module.HookEventIssues: + fallthrough + case webhook_module.HookEventIssueAssign: + fallthrough + case webhook_module.HookEventIssueLabel: + fallthrough + case webhook_module.HookEventIssueMilestone: + fallthrough + case webhook_module.HookEventIssueComment: + fallthrough + case webhook_module.HookEventPullRequest: + prPayload := payload.(*api.PullRequestPayload) + matchTimes := 0 + // all acts conditions should be satisfied + for cond, vals := range evt.Acts { + switch cond { + case "types": + for _, val := range vals { + if glob.MustCompile(val, '/').Match(string(prPayload.Action)) { + matchTimes++ + break + } + } + case "branches": + for _, val := range vals { + if glob.MustCompile(val, '/').Match(prPayload.PullRequest.Base.Ref) { + matchTimes++ + break + } + } + case "paths": + filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) + if err != nil { + log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) + } else { + for _, val := range vals { + matched := false + for _, file := range filesChanged { + if glob.MustCompile(val, '/').Match(file) { + matched = true + break + } + } + if matched { + matchTimes++ + break + } + } + } + default: + log.Warn("unsupported condition %q", cond) + } + } + return matchTimes == len(evt.Acts) + case webhook_module.HookEventPullRequestAssign: + fallthrough + case webhook_module.HookEventPullRequestLabel: + fallthrough + case webhook_module.HookEventPullRequestMilestone: + fallthrough + case webhook_module.HookEventPullRequestComment: + fallthrough + case webhook_module.HookEventPullRequestReviewApproved: + fallthrough + case webhook_module.HookEventPullRequestReviewRejected: + fallthrough + case webhook_module.HookEventPullRequestReviewComment: + fallthrough + case webhook_module.HookEventPullRequestSync: + fallthrough + case webhook_module.HookEventWiki: + fallthrough + case webhook_module.HookEventRepository: + fallthrough + case webhook_module.HookEventRelease: + fallthrough + case webhook_module.HookEventPackage: + fallthrough + default: + log.Warn("unsupported event %q", triggedEvent.Event()) + } + return false +} diff --git a/modules/auth/webauthn/webauthn.go b/modules/auth/webauthn/webauthn.go index d08f7bf7cc715..937da872ca1af 100644 --- a/modules/auth/webauthn/webauthn.go +++ b/modules/auth/webauthn/webauthn.go @@ -28,7 +28,7 @@ func Init() { Config: &webauthn.Config{ RPDisplayName: setting.AppName, RPID: setting.Domain, - RPOrigin: appURL, + RPOrigins: []string{appURL}, AuthenticatorSelection: protocol.AuthenticatorSelection{ UserVerification: "discouraged", }, diff --git a/modules/auth/webauthn/webauthn_test.go b/modules/auth/webauthn/webauthn_test.go index 1beeb64cd6ca0..15a8d7182833d 100644 --- a/modules/auth/webauthn/webauthn_test.go +++ b/modules/auth/webauthn/webauthn_test.go @@ -15,11 +15,11 @@ func TestInit(t *testing.T) { setting.Domain = "domain" setting.AppName = "AppName" setting.AppURL = "https://domain/" - rpOrigin := "https://domain" + rpOrigin := []string{"https://domain"} Init() assert.Equal(t, setting.Domain, WebAuthn.Config.RPID) assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName) - assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigin) + assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins) } diff --git a/modules/context/context.go b/modules/context/context.go index 6273093060a12..84f40ce063ce8 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -805,6 +805,7 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["DisableStars"] = setting.Repository.DisableStars + ctx.Data["EnableActions"] = setting.Actions.Enabled ctx.Data["ManifestData"] = setting.ManifestData @@ -812,6 +813,7 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled() ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled() ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled() + ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled() ctx.Data["locale"] = locale ctx.Data["AllLangs"] = translation.AllLangs() diff --git a/modules/context/repo.go b/modules/context/repo.go index a5ade21e43528..38c1d84541e92 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -1043,6 +1043,7 @@ func UnitTypes() func(ctx *Context) { ctx.Data["UnitTypeExternalTracker"] = unit_model.TypeExternalTracker ctx.Data["UnitTypeProjects"] = unit_model.TypeProjects ctx.Data["UnitTypePackages"] = unit_model.TypePackages + ctx.Data["UnitTypeActions"] = unit_model.TypeActions } } diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 1247a81fea3cf..d7d9ce0b7ebf9 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -21,12 +21,12 @@ func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDire if setting.IsProd { if maxAge == 0 { - directives = append(directives, "no-store") + directives = append(directives, "max-age=0", "private", "must-revalidate") } else { directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds()))) } } else { - directives = append(directives, "no-store") + directives = append(directives, "max-age=0", "private", "must-revalidate") // to remind users they are using non-prod setting. h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) diff --git a/modules/markup/html.go b/modules/markup/html.go index 73ae768d76c0a..bcb38f99eb4a8 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -358,12 +358,19 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output } func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node) { - // Add user-content- to IDs if they don't already have them + // Add user-content- to IDs and "#" links if they don't already have them for idx, attr := range node.Attr { - if attr.Key == "id" && !(strings.HasPrefix(attr.Val, "user-content-") || blackfridayExtRegex.MatchString(attr.Val)) { + val := strings.TrimPrefix(attr.Val, "#") + notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val)) + + if attr.Key == "id" && notHasPrefix { node.Attr[idx].Val = "user-content-" + attr.Val } + if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix { + node.Attr[idx].Val = "#user-content-" + val + } + if attr.Key == "class" && attr.Val == "emoji" { textProcs = nil } diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 2300b68f7822a..da2a7ab3a085e 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -10,6 +10,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification/action" "code.gitea.io/gitea/modules/notification/base" "code.gitea.io/gitea/modules/notification/indexer" @@ -106,6 +107,13 @@ func NotifyAutoMergePullRequest(ctx context.Context, doer *user_model.User, pr * // NotifyNewPullRequest notifies new pull request to notifiers func NotifyNewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) { + if err := pr.LoadIssue(ctx); err != nil { + log.Error("%v", err) + return + } + if err := pr.Issue.LoadPoster(ctx); err != nil { + return + } for _, notifier := range notifiers { notifier.NotifyNewPullRequest(ctx, pr, mentions) } @@ -120,6 +128,10 @@ func NotifyPullRequestSynchronized(ctx context.Context, doer *user_model.User, p // NotifyPullRequestReview notifies new pull request review func NotifyPullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { + if err := review.LoadReviewer(ctx); err != nil { + log.Error("%v", err) + return + } for _, notifier := range notifiers { notifier.NotifyPullRequestReview(ctx, pr, review, comment, mentions) } @@ -127,6 +139,10 @@ func NotifyPullRequestReview(ctx context.Context, pr *issues_model.PullRequest, // NotifyPullRequestCodeComment notifies new pull request code comment func NotifyPullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) { + if err := comment.LoadPoster(ctx); err != nil { + log.Error("LoadPoster: %v", err) + return + } for _, notifier := range notifiers { notifier.NotifyPullRequestCodeComment(ctx, pr, comment, mentions) } @@ -169,6 +185,10 @@ func NotifyDeleteComment(ctx context.Context, doer *user_model.User, c *issues_m // NotifyNewRelease notifies new release to notifiers func NotifyNewRelease(ctx context.Context, rel *repo_model.Release) { + if err := rel.LoadAttributes(ctx); err != nil { + log.Error("LoadPublisher: %v", err) + return + } for _, notifier := range notifiers { notifier.NotifyNewRelease(ctx, rel) } diff --git a/modules/packages/conda/metadata.go b/modules/packages/conda/metadata.go new file mode 100644 index 0000000000000..02dbf313ba2d8 --- /dev/null +++ b/modules/packages/conda/metadata.go @@ -0,0 +1,243 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package conda + +import ( + "archive/tar" + "archive/zip" + "compress/bzip2" + "io" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/klauspost/compress/zstd" +) + +var ( + ErrInvalidStructure = util.SilentWrap{Message: "package structure is invalid", Err: util.ErrInvalidArgument} + ErrInvalidName = util.SilentWrap{Message: "package name is invalid", Err: util.ErrInvalidArgument} + ErrInvalidVersion = util.SilentWrap{Message: "package version is invalid", Err: util.ErrInvalidArgument} +) + +const ( + PropertyName = "conda.name" + PropertyChannel = "conda.channel" + PropertySubdir = "conda.subdir" + PropertyMetadata = "conda.metdata" +) + +// Package represents a Conda package +type Package struct { + Name string + Version string + Subdir string + VersionMetadata *VersionMetadata + FileMetadata *FileMetadata +} + +// VersionMetadata represents the metadata of a Conda package +type VersionMetadata struct { + Description string `json:"description,omitempty"` + Summary string `json:"summary,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + DocumentationURL string `json:"documentation_url,omitempty"` + License string `json:"license,omitempty"` + LicenseFamily string `json:"license_family,omitempty"` +} + +// FileMetadata represents the metadata of a Conda package file +type FileMetadata struct { + IsCondaPackage bool `json:"is_conda"` + Architecture string `json:"architecture,omitempty"` + NoArch string `json:"noarch,omitempty"` + Build string `json:"build,omitempty"` + BuildNumber int64 `json:"build_number,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` + Platform string `json:"platform,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + +type index struct { + Name string `json:"name"` + Version string `json:"version"` + Architecture string `json:"arch"` + NoArch string `json:"noarch"` + Build string `json:"build"` + BuildNumber int64 `json:"build_number"` + Dependencies []string `json:"depends"` + License string `json:"license"` + LicenseFamily string `json:"license_family"` + Platform string `json:"platform"` + Subdir string `json:"subdir"` + Timestamp int64 `json:"timestamp"` +} + +type about struct { + Description string `json:"description"` + Summary string `json:"summary"` + ProjectURL string `json:"home"` + RepositoryURL string `json:"dev_url"` + DocumentationURL string `json:"doc_url"` +} + +type ReaderAndReaderAt interface { + io.Reader + io.ReaderAt +} + +// ParsePackageBZ2 parses the Conda package file compressed with bzip2 +func ParsePackageBZ2(r io.Reader) (*Package, error) { + gzr := bzip2.NewReader(r) + + return parsePackageTar(gzr) +} + +// ParsePackageConda parses the Conda package file compressed with zip and zstd +func ParsePackageConda(r io.ReaderAt, size int64) (*Package, error) { + zr, err := zip.NewReader(r, size) + if err != nil { + return nil, err + } + + for _, file := range zr.File { + if strings.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") { + f, err := zr.Open(file.Name) + if err != nil { + return nil, err + } + defer f.Close() + + dec, err := zstd.NewReader(f) + if err != nil { + return nil, err + } + defer dec.Close() + + p, err := parsePackageTar(dec) + if p != nil { + p.FileMetadata.IsCondaPackage = true + } + return p, err + } + } + + return nil, ErrInvalidStructure +} + +func parsePackageTar(r io.Reader) (*Package, error) { + var i *index + var a *about + + tr := tar.NewReader(r) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hdr.Typeflag != tar.TypeReg { + continue + } + + if hdr.Name == "info/index.json" { + if err := json.NewDecoder(tr).Decode(&i); err != nil { + return nil, err + } + + if !checkName(i.Name) { + return nil, ErrInvalidName + } + + if !checkVersion(i.Version) { + return nil, ErrInvalidVersion + } + + if a != nil { + break // stop loop if both files were found + } + } else if hdr.Name == "info/about.json" { + if err := json.NewDecoder(tr).Decode(&a); err != nil { + return nil, err + } + + if !validation.IsValidURL(a.ProjectURL) { + a.ProjectURL = "" + } + if !validation.IsValidURL(a.RepositoryURL) { + a.RepositoryURL = "" + } + if !validation.IsValidURL(a.DocumentationURL) { + a.DocumentationURL = "" + } + + if i != nil { + break // stop loop if both files were found + } + } + } + + if i == nil { + return nil, ErrInvalidStructure + } + if a == nil { + a = &about{} + } + + return &Package{ + Name: i.Name, + Version: i.Version, + Subdir: i.Subdir, + VersionMetadata: &VersionMetadata{ + License: i.License, + LicenseFamily: i.LicenseFamily, + Description: a.Description, + Summary: a.Summary, + ProjectURL: a.ProjectURL, + RepositoryURL: a.RepositoryURL, + DocumentationURL: a.DocumentationURL, + }, + FileMetadata: &FileMetadata{ + Architecture: i.Architecture, + NoArch: i.NoArch, + Build: i.Build, + BuildNumber: i.BuildNumber, + Dependencies: i.Dependencies, + Platform: i.Platform, + Timestamp: i.Timestamp, + }, + }, nil +} + +// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393 +func checkName(name string) bool { + if name == "" { + return false + } + if name != strings.ToLower(name) { + return false + } + return !checkBadCharacters(name, "!") +} + +// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403 +func checkVersion(version string) bool { + if version == "" { + return false + } + return !checkBadCharacters(version, "-") +} + +func checkBadCharacters(s, additional string) bool { + if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") { + return true + } + return strings.ContainsAny(s, additional) +} diff --git a/modules/packages/conda/metadata_test.go b/modules/packages/conda/metadata_test.go new file mode 100644 index 0000000000000..2038ca370ce93 --- /dev/null +++ b/modules/packages/conda/metadata_test.go @@ -0,0 +1,150 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package conda + +import ( + "archive/tar" + "archive/zip" + "bytes" + "io" + "testing" + + "github.com/dsnet/compress/bzip2" + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + description = "Package Description" + projectURL = "https://gitea.io" + repositoryURL = "https://gitea.io/gitea/gitea" + documentationURL = "https://docs.gitea.io" +) + +func TestParsePackage(t *testing.T) { + createArchive := func(files map[string][]byte) *bytes.Buffer { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + for filename, content := range files { + hdr := &tar.Header{ + Name: filename, + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) + } + tw.Close() + return &buf + } + + t.Run("MissingIndexFile", func(t *testing.T) { + buf := createArchive(map[string][]byte{"dummy.txt": {}}) + + p, err := parsePackageTar(buf) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidStructure) + }) + + t.Run("MissingAboutFile", func(t *testing.T) { + buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"1.0"}`)}) + + p, err := parsePackageTar(buf) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.Equal(t, "name", p.Name) + assert.Equal(t, "1.0", p.Version) + assert.Empty(t, p.VersionMetadata.ProjectURL) + }) + + t.Run("InvalidName", func(t *testing.T) { + for _, name := range []string{"", "name!", "nAMe"} { + buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"` + name + `","version":"1.0"}`)}) + + p, err := parsePackageTar(buf) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + } + }) + + t.Run("InvalidVersion", func(t *testing.T) { + for _, version := range []string{"", "1.0-2"} { + buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"` + version + `"}`)}) + + p, err := parsePackageTar(buf) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + } + }) + + t.Run("Valid", func(t *testing.T) { + buf := createArchive(map[string][]byte{ + "info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `","subdir":"linux-64"}`), + "info/about.json": []byte(`{"description":"` + description + `","dev_url":"` + repositoryURL + `","doc_url":"` + documentationURL + `","home":"` + projectURL + `"}`), + }) + + p, err := parsePackageTar(buf) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, "linux-64", p.Subdir) + assert.Equal(t, description, p.VersionMetadata.Description) + assert.Equal(t, projectURL, p.VersionMetadata.ProjectURL) + assert.Equal(t, repositoryURL, p.VersionMetadata.RepositoryURL) + assert.Equal(t, documentationURL, p.VersionMetadata.DocumentationURL) + }) + + t.Run(".tar.bz2", func(t *testing.T) { + tarArchive := createArchive(map[string][]byte{ + "info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`), + }) + + var buf bytes.Buffer + bw, _ := bzip2.NewWriter(&buf, nil) + io.Copy(bw, tarArchive) + bw.Close() + + br := bytes.NewReader(buf.Bytes()) + + p, err := ParsePackageBZ2(br) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.False(t, p.FileMetadata.IsCondaPackage) + }) + + t.Run(".conda", func(t *testing.T) { + tarArchive := createArchive(map[string][]byte{ + "info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`), + }) + + var infoBuf bytes.Buffer + zsw, _ := zstd.NewWriter(&infoBuf) + io.Copy(zsw, tarArchive) + zsw.Close() + + var buf bytes.Buffer + zpw := zip.NewWriter(&buf) + w, _ := zpw.Create("info-x.tar.zst") + w.Write(infoBuf.Bytes()) + zpw.Close() + + br := bytes.NewReader(buf.Bytes()) + + p, err := ParsePackageConda(br, int64(br.Len())) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.True(t, p.FileMetadata.IsCondaPackage) + }) +} diff --git a/modules/private/hook.go b/modules/private/hook.go index 027014270adec..9533eaae5966a 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -57,6 +57,7 @@ type HookOptions struct { PullRequestID int64 DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user. IsWiki bool + ActionPerm int } // SSHLogOption ssh log options diff --git a/modules/references/references.go b/modules/references/references.go index 1022e5af3d177..68662425cccf1 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -35,12 +35,12 @@ var ( // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`) // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository - // e.g. gogits/gogs#12345 + // e.g. org/repo#12345 crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) // crossReferenceCommitPattern matches a string that references a commit in a different repository // e.g. go-gitea/gitea@d8a994ef, go-gitea/gitea@d8a994ef243349f321568f9e36d5c3f444b99cae (7-40 characters) crossReferenceCommitPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)@([0-9a-f]{7,40})(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) - // spaceTrimmedPattern let's us find the trailing space + // spaceTrimmedPattern let's find the trailing space spaceTrimmedPattern = regexp.MustCompile(`(?:.*[0-9a-zA-Z-_])\s`) // timeLogPattern matches string for time tracking timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) @@ -365,7 +365,7 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe Owner: content[m[2]:m[3]], Name: content[m[4]:m[5]], CommitSha: content[m[6]:m[7]], - RefLocation: &RefSpan{Start: m[0], End: m[1]}, + RefLocation: &RefSpan{Start: m[2], End: m[7]}, } } diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 1159e14b37ada..75e002c99a3f8 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -352,7 +352,7 @@ func TestFindRenderizableCommitCrossReference(t *testing.T) { Owner: "go-gitea", Name: "gitea", CommitSha: "abcd1234", - RefLocation: &RefSpan{Start: 4, End: 29}, + RefLocation: &RefSpan{Start: 5, End: 28}, }, }, } diff --git a/modules/repository/env.go b/modules/repository/env.go index 646bf35cc674f..30edd1c9e326c 100644 --- a/modules/repository/env.go +++ b/modules/repository/env.go @@ -27,6 +27,7 @@ const ( EnvPRID = "GITEA_PR_ID" EnvIsInternal = "GITEA_INTERNAL_PUSH" EnvAppURL = "GITEA_ROOT_URL" + EnvActionPerm = "GITEA_ACTION_PERM" ) // InternalPushingEnvironment returns an os environment to switch off hooks on push diff --git a/modules/setting/actions.go b/modules/setting/actions.go new file mode 100644 index 0000000000000..5c83a73aeb1ae --- /dev/null +++ b/modules/setting/actions.go @@ -0,0 +1,29 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "code.gitea.io/gitea/modules/log" +) + +// Actions settings +var ( + Actions = struct { + Storage // how the created logs should be stored + Enabled bool + DefaultActionsURL string `ini:"DEFAULT_ACTIONS_URL"` + }{ + Enabled: false, + DefaultActionsURL: "https://gitea.com", + } +) + +func newActions() { + sec := Cfg.Section("actions") + if err := sec.MapTo(&Actions); err != nil { + log.Fatal("Failed to map Actions settings: %v", err) + } + + Actions.Storage = getStorage("actions_log", "", nil) +} diff --git a/modules/setting/packages.go b/modules/setting/packages.go index 120fbb5bda8e7..d0cd80aa0358d 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -27,6 +27,7 @@ var ( LimitTotalOwnerSize int64 LimitSizeComposer int64 LimitSizeConan int64 + LimitSizeConda int64 LimitSizeContainer int64 LimitSizeGeneric int64 LimitSizeHelm int64 @@ -66,6 +67,7 @@ func newPackages() { Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN") + Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA") Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER") Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC") Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 92861fb528677..f591727ca3421 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1074,6 +1074,8 @@ func loadFromConf(allowEmpty bool, extraConfig string) { newPackages() + newActions() + if err = Cfg.Section("ui").MapTo(&UI); err != nil { log.Fatal("Failed to map UI settings: %v", err) } else if err = Cfg.Section("markdown").MapTo(&Markdown); err != nil { diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 671e0ce56529e..d8998b19225fe 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -125,6 +125,9 @@ var ( // Packages represents packages storage Packages ObjectStorage = uninitializedStorage + + // Actions represents actions storage + Actions ObjectStorage = uninitializedStorage ) // Init init the stoarge @@ -136,6 +139,7 @@ func Init() error { initLFS, initRepoArchives, initPackages, + initActions, } { if err := f(); err != nil { return err @@ -204,3 +208,13 @@ func initPackages() (err error) { Packages, err = NewStorage(setting.Packages.Storage.Type, &setting.Packages.Storage) return err } + +func initActions() (err error) { + if !setting.Actions.Enabled { + Actions = discardStorage("Actions isn't enabled") + return nil + } + log.Info("Initialising Actions storage with type: %s", setting.Actions.Storage.Type) + Actions, err = NewStorage(setting.Actions.Storage.Type, &setting.Actions.Storage) + return err +} diff --git a/modules/structs/commit_status.go b/modules/structs/commit_status.go index dfde79190c3e1..7e3b629b7ae20 100644 --- a/modules/structs/commit_status.go +++ b/modules/structs/commit_status.go @@ -18,6 +18,8 @@ const ( CommitStatusFailure CommitStatusState = "failure" // CommitStatusWarning is for when the CommitStatus is Warning CommitStatusWarning CommitStatusState = "warning" + // CommitStatusRunning is for when the CommitStatus is Running + CommitStatusRunning CommitStatusState = "running" ) // NoBetterThan returns true if this State is no better than the given State diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 7b997b49d9e7d..a390d9459298d 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -472,6 +472,9 @@ func NewFuncMap() []template.FuncMap { curBranch, ) }, + "RefShortName": func(ref string) string { + return git.RefName(ref).ShortName() + }, }} } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 74ba85ca90161..8465660cc0756 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1131,6 +1131,7 @@ editor.commit_directly_to_this_branch = Commit directly to the %[3]s pulls.merged_by = by %[3]s was merged %[1]s pulls.merged_by_fake = by %[2]s was merged %[1]s @@ -1870,8 +1873,10 @@ settings.pulls.allow_manual_merge = Enable Mark PR as manually merged settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur) settings.pulls.allow_rebase_update = Enable updating pull request branch by rebase settings.pulls.default_delete_branch_after_merge = Delete pull request branch after merge by default +settings.releases_desc = Enable Repository Releases settings.packages_desc = Enable Repository Packages Registry settings.projects_desc = Enable Repository Projects +settings.actions_desc = Enable Repository Actions settings.admin_settings = Administrator Settings settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) settings.admin_code_indexer = Code Indexer @@ -2427,7 +2432,7 @@ teams.leave.detail = Leave %s? teams.can_create_org_repo = Create repositories teams.can_create_org_repo_helper = Members can create new repositories in organization. Creator will get administrator access to the new repository. teams.none_access = No Access -teams.none_access_helper = Members cannot view or do any other action on this unit. +teams.none_access_helper = Members cannot view or do any other action on this unit. It has no effect for public repositories. teams.general_access = General Access teams.general_access_helper = Members permissions will be decided by below permission table. teams.read_access = Read @@ -2566,6 +2571,9 @@ dashboard.delete_old_actions.started = Delete all old actions from database star dashboard.update_checker = Update checker dashboard.delete_old_system_notices = Delete all old system notices from database dashboard.gc_lfs = Garbage collect LFS meta objects +dashboard.stop_zombie_tasks = Stop zombie tasks +dashboard.stop_endless_tasks = Stop endless tasks +dashboard.cancel_abandoned_jobs = Cancel abandoned jobs users.user_manage_panel = User Account Management users.new_account = Create User Account @@ -3151,6 +3159,11 @@ conan.details.repository = Repository conan.registry = Setup this registry from the command line: conan.install = To install the package using Conan, run the following command: conan.documentation = For more information on the Conan registry, see the documentation. +conda.registry = Setup this registry as a Conda repository in your .condarc file: +conda.install = To install the package using Conda, run the following command: +conda.documentation = For more information on the Conda registry, see the documentation. +conda.details.repository_site = Repository Site +conda.details.documentation_site = Documentation Site container.details.type = Image Type container.details.platform = Platform container.details.repository_site = Repository Site @@ -3246,6 +3259,62 @@ creation.value_placeholder = Input any content. Whitespace at the start and end creation.success = The secret '%s' has been added. creation.failed = Failed to add secret. deletion = Remove secret -deletion.description = Removing a secret will revoke its access to repositories. Continue? +deletion.description = Removing a secret is permanent and cannot be undone. Continue? deletion.success = The secret has been removed. deletion.failed = Failed to remove secret. + +[actions] +actions = Actions + +unit.desc = Manage actions + +status.unknown = "Unknown" +status.waiting = "Waiting" +status.running = "Running" +status.success = "Success" +status.failure = "Failure" +status.cancelled = "Cancelled" +status.skipped = "Skipped" +status.blocked = "Blocked" + +runners = Runners +runners.runner_manage_panel = Runners Management +runners.new = Create new Runner +runners.new_notice = How to start a runner +runners.status = Status +runners.id = ID +runners.name = Name +runners.owner_type = Type +runners.description = Description +runners.labels = Labels +runners.last_online = Last Online Time +runners.agent_labels = Agent Labels +runners.custom_labels = Custom Labels +runners.custom_labels_helper = Custom labels are labels that are added manually by an administrator. A comma separates labels, whitespace at the start and end of each label is ignored. +runners.runner_title = Runner +runners.task_list = Recent tasks on this runner +runners.task_list.run = Run +runners.task_list.status = Status +runners.task_list.repository = Repository +runners.task_list.commit = Commit +runners.task_list.done_at = Done At +runners.edit_runner = Edit Runner +runners.update_runner = Update Changes +runners.update_runner_success = Runner updated successfully +runners.update_runner_failed = Failed to update runner +runners.delete_runner = Delete this runner +runners.delete_runner_success = Runner deleted successfully +runners.delete_runner_failed = Failed to delete runner +runners.delete_runner_header = Confirm to delete this runner +runners.delete_runner_notice = If a task is running on this runner, it will be terminated and mark as failed. It may break building workflow. +runners.none = No runners available +runners.status.unspecified = Unknown +runners.status.idle = Idle +runners.status.active = Active +runners.status.offline = Offline + +runs.all_workflows = All Workflows +runs.open_tab = %d Open +runs.closed_tab = %d Closed +runs.commit = Commit +runs.pushed_by = Pushed by diff --git a/package-lock.json b/package-lock.json index 60f0a0f8e6235..940bff2726e46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@primer/octicons": "17.10.2", "@vue/compiler-sfc": "3.2.45", "add-asset-webpack-plugin": "2.0.1", + "ansi-to-html": "0.7.2", "asciinema-player": "3.0.1", "css-loader": "6.7.3", "dropzone": "6.0.0-beta.2", @@ -2014,6 +2015,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "dependencies": { + "entities": "^2.2.0" + }, + "bin": { + "ansi-to-html": "bin/ansi-to-html" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ansi-to-html/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -11286,6 +11309,21 @@ "color-convert": "^2.0.1" } }, + "ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "requires": { + "entities": "^2.2.0" + }, + "dependencies": { + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + } + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", diff --git a/package.json b/package.json index 842804211cf86..4a3407bed987d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@primer/octicons": "17.10.2", "@vue/compiler-sfc": "3.2.45", "add-asset-webpack-plugin": "2.0.1", + "ansi-to-html": "0.7.2", "asciinema-player": "3.0.1", "css-loader": "6.7.3", "dropzone": "6.0.0-beta.2", diff --git a/public/img/svg/gitea-conda.svg b/public/img/svg/gitea-conda.svg new file mode 100644 index 0000000000000..cd4817adf2e30 --- /dev/null +++ b/public/img/svg/gitea-conda.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/actions/actions.go b/routers/api/actions/actions.go new file mode 100644 index 0000000000000..bdcac412068ab --- /dev/null +++ b/routers/api/actions/actions.go @@ -0,0 +1,25 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "net/http" + + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/actions/ping" + "code.gitea.io/gitea/routers/api/actions/runner" +) + +func Routes(_ context.Context, prefix string) *web.Route { + m := web.NewRoute() + + path, handler := ping.NewPingServiceHandler() + m.Post(path+"*", http.StripPrefix(prefix, handler).ServeHTTP) + + path, handler = runner.NewRunnerServiceHandler() + m.Post(path+"*", http.StripPrefix(prefix, handler).ServeHTTP) + + return m +} diff --git a/routers/api/actions/ping/ping.go b/routers/api/actions/ping/ping.go new file mode 100644 index 0000000000000..55219fe12bc67 --- /dev/null +++ b/routers/api/actions/ping/ping.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package ping + +import ( + "context" + "fmt" + "net/http" + + "code.gitea.io/gitea/modules/log" + + pingv1 "code.gitea.io/actions-proto-go/ping/v1" + "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" + "github.com/bufbuild/connect-go" +) + +func NewPingServiceHandler() (string, http.Handler) { + return pingv1connect.NewPingServiceHandler(&Service{}) +} + +var _ pingv1connect.PingServiceHandler = (*Service)(nil) + +type Service struct { + pingv1connect.UnimplementedPingServiceHandler +} + +func (s *Service) Ping( + ctx context.Context, + req *connect.Request[pingv1.PingRequest], +) (*connect.Response[pingv1.PingResponse], error) { + log.Trace("Content-Type: %s", req.Header().Get("Content-Type")) + log.Trace("User-Agent: %s", req.Header().Get("User-Agent")) + res := connect.NewResponse(&pingv1.PingResponse{ + Data: fmt.Sprintf("Hello, %s!", req.Msg.Data), + }) + return res, nil +} diff --git a/routers/api/actions/ping/ping_test.go b/routers/api/actions/ping/ping_test.go new file mode 100644 index 0000000000000..f39e94a1f30b8 --- /dev/null +++ b/routers/api/actions/ping/ping_test.go @@ -0,0 +1,61 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package ping + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + pingv1 "code.gitea.io/actions-proto-go/ping/v1" + "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" + "github.com/bufbuild/connect-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + mux := http.NewServeMux() + mux.Handle(pingv1connect.NewPingServiceHandler( + &Service{}, + )) + MainServiceTest(t, mux) +} + +func MainServiceTest(t *testing.T, h http.Handler) { + t.Parallel() + server := httptest.NewUnstartedServer(h) + server.EnableHTTP2 = true + server.StartTLS() + defer server.Close() + + connectClient := pingv1connect.NewPingServiceClient( + server.Client(), + server.URL, + ) + + grpcClient := pingv1connect.NewPingServiceClient( + server.Client(), + server.URL, + connect.WithGRPC(), + ) + + grpcWebClient := pingv1connect.NewPingServiceClient( + server.Client(), + server.URL, + connect.WithGRPCWeb(), + ) + + clients := []pingv1connect.PingServiceClient{connectClient, grpcClient, grpcWebClient} + t.Run("ping request", func(t *testing.T) { + for _, client := range clients { + result, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ + Data: "foobar", + })) + require.NoError(t, err) + assert.Equal(t, "Hello, foobar!", result.Msg.Data) + } + }) +} diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go new file mode 100644 index 0000000000000..8c0bd86ecb6c6 --- /dev/null +++ b/routers/api/actions/runner/interceptor.go @@ -0,0 +1,79 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "context" + "crypto/subtle" + "errors" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "github.com/bufbuild/connect-go" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + uuidHeaderKey = "x-runner-uuid" + tokenHeaderKey = "x-runner-token" +) + +var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) { + methodName := getMethodName(request) + if methodName == "Register" { + return unaryFunc(ctx, request) + } + uuid := request.Header().Get(uuidHeaderKey) + token := request.Header().Get(tokenHeaderKey) + runner, err := actions_model.GetRunnerByUUID(ctx, uuid) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + return nil, status.Error(codes.Unauthenticated, "unregistered runner") + } + return nil, status.Error(codes.Internal, err.Error()) + } + if subtle.ConstantTimeCompare([]byte(runner.TokenHash), []byte(auth_model.HashToken(token, runner.TokenSalt))) != 1 { + return nil, status.Error(codes.Unauthenticated, "unregistered runner") + } + + cols := []string{"last_online"} + runner.LastOnline = timeutil.TimeStampNow() + if methodName == "UpdateTask" || methodName == "UpdateLog" { + runner.LastActive = timeutil.TimeStampNow() + cols = append(cols, "last_active") + } + if err := actions_model.UpdateRunner(ctx, runner, cols...); err != nil { + log.Error("can't update runner status: %v", err) + } + + ctx = context.WithValue(ctx, runnerCtxKey{}, runner) + return unaryFunc(ctx, request) + } +})) + +func getMethodName(req connect.AnyRequest) string { + splits := strings.Split(req.Spec().Procedure, "/") + if len(splits) > 0 { + return splits[len(splits)-1] + } + return "" +} + +type runnerCtxKey struct{} + +func GetRunner(ctx context.Context) *actions_model.ActionRunner { + if v := ctx.Value(runnerCtxKey{}); v != nil { + if r, ok := v.(*actions_model.ActionRunner); ok { + return r + } + } + return nil +} diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go new file mode 100644 index 0000000000000..7dbab9da0a2b9 --- /dev/null +++ b/routers/api/actions/runner/runner.go @@ -0,0 +1,221 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "context" + "errors" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + actions_service "code.gitea.io/gitea/services/actions" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" + "github.com/bufbuild/connect-go" + gouuid "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func NewRunnerServiceHandler() (string, http.Handler) { + return runnerv1connect.NewRunnerServiceHandler( + &Service{}, + connect.WithCompressMinBytes(1024), + withRunner, + ) +} + +var _ runnerv1connect.RunnerServiceClient = (*Service)(nil) + +type Service struct { + runnerv1connect.UnimplementedRunnerServiceHandler +} + +// Register for new runner. +func (s *Service) Register( + ctx context.Context, + req *connect.Request[runnerv1.RegisterRequest], +) (*connect.Response[runnerv1.RegisterResponse], error) { + if req.Msg.Token == "" || req.Msg.Name == "" { + return nil, errors.New("missing runner token, name") + } + + runnerToken, err := actions_model.GetRunnerToken(ctx, req.Msg.Token) + if err != nil { + return nil, errors.New("runner token not found") + } + + if runnerToken.IsActive { + return nil, errors.New("runner token has already activated") + } + + // create new runner + runner := &actions_model.ActionRunner{ + UUID: gouuid.New().String(), + Name: req.Msg.Name, + OwnerID: runnerToken.OwnerID, + RepoID: runnerToken.RepoID, + AgentLabels: req.Msg.AgentLabels, + CustomLabels: req.Msg.CustomLabels, + } + if err := runner.GenerateToken(); err != nil { + return nil, errors.New("can't generate token") + } + + // create new runner + if err := actions_model.CreateRunner(ctx, runner); err != nil { + return nil, errors.New("can't create new runner") + } + + // update token status + runnerToken.IsActive = true + if err := actions_model.UpdateRunnerToken(ctx, runnerToken, "is_active"); err != nil { + return nil, errors.New("can't update runner token status") + } + + res := connect.NewResponse(&runnerv1.RegisterResponse{ + Runner: &runnerv1.Runner{ + Id: runner.ID, + Uuid: runner.UUID, + Token: runner.Token, + Name: runner.Name, + AgentLabels: runner.AgentLabels, + CustomLabels: runner.CustomLabels, + }, + }) + + return res, nil +} + +// FetchTask assigns a task to the runner +func (s *Service) FetchTask( + ctx context.Context, + req *connect.Request[runnerv1.FetchTaskRequest], +) (*connect.Response[runnerv1.FetchTaskResponse], error) { + runner := GetRunner(ctx) + + var task *runnerv1.Task + if t, ok, err := pickTask(ctx, runner); err != nil { + log.Error("pick task failed: %v", err) + return nil, status.Errorf(codes.Internal, "pick task: %v", err) + } else if ok { + task = t + } + + res := connect.NewResponse(&runnerv1.FetchTaskResponse{ + Task: task, + }) + return res, nil +} + +// UpdateTask updates the task status. +func (s *Service) UpdateTask( + ctx context.Context, + req *connect.Request[runnerv1.UpdateTaskRequest], +) (*connect.Response[runnerv1.UpdateTaskResponse], error) { + { + // to debug strange runner behaviors, it could be removed if all problems have been solved. + stateMsg, _ := json.Marshal(req.Msg.State) + log.Trace("update task with state: %s", stateMsg) + } + + // Get Task first + task, err := actions_model.GetTaskByID(ctx, req.Msg.State.Id) + if err != nil { + return nil, status.Errorf(codes.Internal, "can't find the task: %v", err) + } + if task.Status.IsCancelled() { + return connect.NewResponse(&runnerv1.UpdateTaskResponse{ + State: &runnerv1.TaskState{ + Id: req.Msg.State.Id, + Result: task.Status.AsResult(), + }, + }), nil + } + + task, err = actions_model.UpdateTaskByState(ctx, req.Msg.State) + if err != nil { + return nil, status.Errorf(codes.Internal, "update task: %v", err) + } + + if err := task.LoadJob(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "load job: %v", err) + } + + if err := actions_service.CreateCommitStatus(ctx, task.Job); err != nil { + log.Error("Update commit status failed: %v", err) + // go on + } + + if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED { + if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil { + log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err) + } + } + + return connect.NewResponse(&runnerv1.UpdateTaskResponse{ + State: &runnerv1.TaskState{ + Id: req.Msg.State.Id, + Result: task.Status.AsResult(), + }, + }), nil +} + +// UpdateLog uploads log of the task. +func (s *Service) UpdateLog( + ctx context.Context, + req *connect.Request[runnerv1.UpdateLogRequest], +) (*connect.Response[runnerv1.UpdateLogResponse], error) { + res := connect.NewResponse(&runnerv1.UpdateLogResponse{}) + + task, err := actions_model.GetTaskByID(ctx, req.Msg.TaskId) + if err != nil { + return nil, status.Errorf(codes.Internal, "get task: %v", err) + } + ack := task.LogLength + + if len(req.Msg.Rows) == 0 || req.Msg.Index > ack || int64(len(req.Msg.Rows))+req.Msg.Index <= ack { + res.Msg.AckIndex = ack + return res, nil + } + + if task.LogInStorage { + return nil, status.Errorf(codes.AlreadyExists, "log file has been archived") + } + + rows := req.Msg.Rows[ack-req.Msg.Index:] + ns, err := actions.WriteLogs(ctx, task.LogFilename, task.LogSize, rows) + if err != nil { + return nil, status.Errorf(codes.Internal, "write logs: %v", err) + } + task.LogLength += int64(len(rows)) + for _, n := range ns { + task.LogIndexes = append(task.LogIndexes, task.LogSize) + task.LogSize += int64(n) + } + + res.Msg.AckIndex = task.LogLength + + var remove func() + if req.Msg.NoMore { + task.LogInStorage = true + remove, err = actions.TransferLogs(ctx, task.LogFilename) + if err != nil { + return nil, status.Errorf(codes.Internal, "transfer logs: %v", err) + } + } + + if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_length", "log_size", "log_in_storage"); err != nil { + return nil, status.Errorf(codes.Internal, "update task: %v", err) + } + if remove != nil { + remove() + } + + return res, nil +} diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go new file mode 100644 index 0000000000000..80e71941f21c0 --- /dev/null +++ b/routers/api/actions/runner/utils.go @@ -0,0 +1,122 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "context" + "fmt" + + actions_model "code.gitea.io/gitea/models/actions" + secret_model "code.gitea.io/gitea/models/secret" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + secret_module "code.gitea.io/gitea/modules/secret" + "code.gitea.io/gitea/modules/setting" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "google.golang.org/protobuf/types/known/structpb" +) + +func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) { + t, ok, err := actions_model.CreateTaskForRunner(ctx, runner) + if err != nil { + return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err) + } + if !ok { + return nil, false, nil + } + + task := &runnerv1.Task{ + Id: t.ID, + WorkflowPayload: t.Job.WorkflowPayload, + Context: generateTaskContext(t), + Secrets: getSecretsOfTask(ctx, t), + } + return task, true, nil +} + +func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { + secrets := map[string]string{} + if task.Job.Run.IsForkPullRequest { + // ignore secrets for fork pull request + return secrets + } + + ownerSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID}) + if err != nil { + log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err) + // go on + } + repoSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID}) + if err != nil { + log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err) + // go on + } + + for _, secret := range append(ownerSecrets, repoSecrets...) { + if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil { + log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err) + // go on + } else { + secrets[secret.Name] = v + } + } + + if _, ok := secrets["GITHUB_TOKEN"]; !ok { + secrets["GITHUB_TOKEN"] = task.Token + } + if _, ok := secrets["GITEA_TOKEN"]; !ok { + secrets["GITEA_TOKEN"] = task.Token + } + + return secrets +} + +func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { + event := map[string]interface{}{} + _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) + + taskContext, _ := structpb.NewStruct(map[string]interface{}{ + // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. + "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. + "action_ref": "", // string, For a step executing an action, this is the ref of the action being executed. For example, v2. + "action_repository": "", // string, For a step executing an action, this is the owner and repository name of the action. For example, actions/checkout. + "action_status": "", // string, For a composite action, the current result of the composite action. + "actor": t.Job.Run.TriggerUser.Name, // string, The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. + "api_url": "", // string, The URL of the GitHub REST API. + "base_ref": "", // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. + "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." + "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. + "event_name": t.Job.Run.Event.Event(), // string, The name of the event that triggered the workflow run. + "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. + "graphql_url": "", // string, The URL of the GitHub GraphQL API. + "head_ref": "", // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. + "job": fmt.Sprint(t.JobID), // string, The job_id of the current job. + "ref": t.Job.Run.Ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. + "ref_name": t.Job.Run.Ref, // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. + "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. + "ref_type": "", // string, The type of ref that triggered the workflow run. Valid values are branch or tag. + "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." + "repository": t.Job.Run.Repo.OwnerName + "/" + t.Job.Run.Repo.Name, // string, The owner and repository name. For example, Codertocat/Hello-World. + "repository_owner": t.Job.Run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. + "repositoryUrl": t.Job.Run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. + "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. + "run_id": fmt.Sprint(t.Job.RunID), // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. + "run_number": fmt.Sprint(t.Job.Run.Index), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. + "run_attempt": fmt.Sprint(t.Job.Attempt), // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. + "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. + "server_url": setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. + "sha": t.Job.Run.CommitSHA, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. + "token": t.Token, // string, A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the GITHUB_TOKEN secret. For more information, see "Automatic token authentication." + "triggering_actor": "", // string, The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. + "workflow": t.Job.Run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. + "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. + + // additional contexts + "gitea_default_actions_url": setting.Actions.DefaultActionsURL, + }) + + return taskContext +} diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 78eb5e860be26..7a07fea815e87 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/packages/composer" "code.gitea.io/gitea/routers/api/packages/conan" + "code.gitea.io/gitea/routers/api/packages/conda" "code.gitea.io/gitea/routers/api/packages/container" "code.gitea.io/gitea/routers/api/packages/generic" "code.gitea.io/gitea/routers/api/packages/helm" @@ -167,6 +168,43 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/conda", func() { + var ( + downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`) + uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`) + ) + + r.Get("/*", func(ctx *context.Context) { + m := downloadPattern.FindStringSubmatch(ctx.Params("*")) + if len(m) == 0 { + ctx.Status(http.StatusNotFound) + return + } + + ctx.SetParams("channel", strings.TrimSuffix(m[1], "/")) + ctx.SetParams("architecture", m[2]) + ctx.SetParams("filename", m[3]) + + switch m[3] { + case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": + conda.EnumeratePackages(ctx) + default: + conda.DownloadPackageFile(ctx) + } + }) + r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) { + m := uploadPattern.FindStringSubmatch(ctx.Params("*")) + if len(m) == 0 { + ctx.Status(http.StatusNotFound) + return + } + + ctx.SetParams("channel", strings.TrimSuffix(m[1], "/")) + ctx.SetParams("filename", m[2]) + + conda.UploadPackageFile(ctx) + }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/generic", func() { r.Group("/{packagename}/{packageversion}", func() { r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage) diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go new file mode 100644 index 0000000000000..2ff619fed4e00 --- /dev/null +++ b/routers/api/packages/conda/conda.go @@ -0,0 +1,306 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package conda + +import ( + "errors" + "fmt" + "io" + "net/http" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + conda_model "code.gitea.io/gitea/models/packages/conda" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + packages_module "code.gitea.io/gitea/modules/packages" + conda_module "code.gitea.io/gitea/modules/packages/conda" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" + + "github.com/dsnet/compress/bzip2" +) + +func apiError(ctx *context.Context, status int, obj interface{}) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.JSON(status, struct { + Reason string `json:"reason"` + Message string `json:"message"` + }{ + Reason: http.StatusText(status), + Message: message, + }) + }) +} + +func EnumeratePackages(ctx *context.Context) { + type Info struct { + Subdir string `json:"subdir"` + } + + type PackageInfo struct { + Name string `json:"name"` + Version string `json:"version"` + NoArch string `json:"noarch"` + Subdir string `json:"subdir"` + Timestamp int64 `json:"timestamp"` + Build string `json:"build"` + BuildNumber int64 `json:"build_number"` + Dependencies []string `json:"depends"` + License string `json:"license"` + LicenseFamily string `json:"license_family"` + HashMD5 string `json:"md5"` + HashSHA256 string `json:"sha256"` + Size int64 `json:"size"` + } + + type RepoData struct { + Info Info `json:"info"` + Packages map[string]*PackageInfo `json:"packages"` + PackagesConda map[string]*PackageInfo `json:"packages.conda"` + Removed map[string]*PackageInfo `json:"removed"` + } + + repoData := &RepoData{ + Info: Info{ + Subdir: ctx.Params("architecture"), + }, + Packages: make(map[string]*PackageInfo), + PackagesConda: make(map[string]*PackageInfo), + Removed: make(map[string]*PackageInfo), + } + + pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Channel: ctx.Params("channel"), + Subdir: repoData.Info.Subdir, + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if len(pfs) == 0 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + pds := make(map[int64]*packages_model.PackageDescriptor) + + for _, pf := range pfs { + pd, exists := pds[pf.VersionID] + if !exists { + pv, err := packages_model.GetVersionByID(ctx, pf.VersionID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pd, err = packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pds[pf.VersionID] = pd + } + + var pfd *packages_model.PackageFileDescriptor + for _, d := range pd.Files { + if d.File.ID == pf.ID { + pfd = d + break + } + } + + var fileMetadata *conda_module.FileMetadata + if err := json.Unmarshal([]byte(pfd.Properties.GetByName(conda_module.PropertyMetadata)), &fileMetadata); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + versionMetadata := pd.Metadata.(*conda_module.VersionMetadata) + + pi := &PackageInfo{ + Name: pd.PackageProperties.GetByName(conda_module.PropertyName), + Version: pd.Version.Version, + NoArch: fileMetadata.NoArch, + Subdir: repoData.Info.Subdir, + Timestamp: fileMetadata.Timestamp, + Build: fileMetadata.Build, + BuildNumber: fileMetadata.BuildNumber, + Dependencies: fileMetadata.Dependencies, + License: versionMetadata.License, + LicenseFamily: versionMetadata.LicenseFamily, + HashMD5: pfd.Blob.HashMD5, + HashSHA256: pfd.Blob.HashSHA256, + Size: pfd.Blob.Size, + } + + if fileMetadata.IsCondaPackage { + repoData.PackagesConda[pfd.File.Name] = pi + } else { + repoData.Packages[pfd.File.Name] = pi + } + } + + resp := ctx.Resp + + var w io.Writer = resp + + if strings.HasSuffix(ctx.Params("filename"), ".json") { + resp.Header().Set("Content-Type", "application/json") + } else { + resp.Header().Set("Content-Type", "application/x-bzip2") + + zw, err := bzip2.NewWriter(w, nil) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer zw.Close() + + w = zw + } + + resp.WriteHeader(http.StatusOK) + + if err := json.NewEncoder(w).Encode(repoData); err != nil { + log.Error("JSON encode: %v", err) + } +} + +func UploadPackageFile(ctx *context.Context) { + upload, close, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if close { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + var pck *conda_module.Package + if strings.HasSuffix(strings.ToLower(ctx.Params("filename")), ".tar.bz2") { + pck, err = conda_module.ParsePackageBZ2(buf) + } else { + pck, err = conda_module.ParsePackageConda(buf, buf.Size()) + } + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + apiError(ctx, http.StatusBadRequest, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + fullName := pck.Name + + channel := ctx.Params("channel") + if channel != "" { + fullName = channel + "/" + pck.Name + } + + extension := ".tar.bz2" + if pck.FileMetadata.IsCondaPackage { + extension = ".conda" + } + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeConda, + Name: fullName, + Version: pck.Version, + }, + SemverCompatible: false, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + PackageProperties: map[string]string{ + conda_module.PropertyName: pck.Name, + conda_module.PropertyChannel: channel, + }, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s-%s%s", pck.Name, pck.Version, pck.FileMetadata.Build, extension), + CompositeKey: pck.Subdir, + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + conda_module.PropertySubdir: pck.Subdir, + conda_module.PropertyMetadata: string(fileMetadataRaw), + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageFile: + apiError(ctx, http.StatusConflict, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + ctx.Status(http.StatusCreated) +} + +func DownloadPackageFile(ctx *context.Context) { + pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Channel: ctx.Params("channel"), + Subdir: ctx.Params("architecture"), + Filename: ctx.Params("filename"), + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + pf := pfs[0] + + s, _, err := packages_service.GetPackageFileStream(ctx, pf) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer s.Close() + + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index eef2a64244083..5f57977c290bb 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -69,6 +69,7 @@ import ( "net/http" "strings" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" @@ -184,10 +185,39 @@ func repoAssignment() func(ctx *context.APIContext) { repo.Owner = owner ctx.Repo.Repository = repo - ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) - return + if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { + taskID := ctx.Data["ActionsTaskID"].(int64) + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err) + return + } + if task.RepoID != repo.ID { + ctx.NotFound() + return + } + + if task.IsForkPullRequest { + ctx.Repo.Permission.AccessMode = perm.AccessModeRead + } else { + ctx.Repo.Permission.AccessMode = perm.AccessModeWrite + } + + if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadUnits", err) + return + } + ctx.Repo.Permission.Units = ctx.Repo.Repository.Units + ctx.Repo.Permission.UnitsMode = make(map[unit.Type]perm.AccessMode) + for _, u := range ctx.Repo.Repository.Units { + ctx.Repo.Permission.UnitsMode[u.Type] = ctx.Repo.Permission.AccessMode + } + } else { + ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + return + } } if !ctx.Repo.HasAccess() { @@ -209,6 +239,11 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) // Contexter middleware already checks token for user sign in process. func reqToken(requiredScope auth_model.AccessTokenScope) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { + // If actions token is present + if true == ctx.Data["IsActionsToken"] { + return + } + // If OAuth2 token is present if _, ok := ctx.Data["ApiTokenScope"]; ctx.Data["IsApiToken"] == true && ok { // no scope required diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 6f9083ba327b5..5ffefc4862c1e 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) { // in: query // description: package type filter // type: string - // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant] + // enum: [composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant] // - name: q // in: query // description: name filter diff --git a/routers/init.go b/routers/init.go index 25f557d69569c..ab2e2a0bb7b1b 100644 --- a/routers/init.go +++ b/routers/init.go @@ -30,11 +30,13 @@ import ( "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + actions_router "code.gitea.io/gitea/routers/api/actions" packages_router "code.gitea.io/gitea/routers/api/packages" apiv1 "code.gitea.io/gitea/routers/api/v1" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/private" web_routers "code.gitea.io/gitea/routers/web" + actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/automerge" @@ -172,6 +174,8 @@ func GlobalInitInstalled(ctx context.Context) { auth.Init() svg.Init() + actions_service.Init() + // Finally start up the cron cron.NewContext(ctx) } @@ -197,5 +201,11 @@ func NormalRoutes(ctx context.Context) *web.Route { // This implements the OCI API (Note this is not preceded by /api but is instead /v2) r.Mount("/v2", packages_router.ContainerRoutes(ctx)) } + + if setting.Actions.Enabled { + prefix := "/api/actions" + r.Mount(prefix, actions_router.Routes(ctx, prefix)) + } + return r } diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 8468227077fdf..c711fc947714f 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -472,25 +472,41 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool { return true } - user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) - if err != nil { - log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err), - }) - return false - } - ctx.user = user - - userPerm, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, user) - if err != nil { - log.Error("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err), - }) - return false + if ctx.opts.UserID == user_model.ActionsUserID { + ctx.user = user_model.NewActionsUser() + ctx.userPerm.AccessMode = perm_model.AccessMode(ctx.opts.ActionPerm) + if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { + log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err), + }) + return false + } + ctx.userPerm.Units = ctx.Repo.Repository.Units + ctx.userPerm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) + for _, u := range ctx.Repo.Repository.Units { + ctx.userPerm.UnitsMode[u.Type] = ctx.userPerm.AccessMode + } + } else { + user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) + if err != nil { + log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err), + }) + return false + } + ctx.user = user + userPerm, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, user) + if err != nil { + log.Error("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err), + }) + return false + } + ctx.userPerm = userPerm } - ctx.userPerm = userPerm if ctx.opts.DeployKeyID != 0 { deployKey, err := asymkey_model.GetDeployKeyByID(ctx, ctx.opts.DeployKeyID) diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go index 57cf5f49e5b60..46dc734d252fa 100644 --- a/routers/web/admin/hooks.go +++ b/routers/web/admin/hooks.go @@ -22,6 +22,7 @@ const ( func DefaultOrSystemWebhooks(ctx *context.Context) { var err error + ctx.Data["Title"] = ctx.Tr("admin.hooks") ctx.Data["PageIsAdminSystemHooks"] = true ctx.Data["PageIsAdminDefaultHooks"] = true diff --git a/routers/web/admin/runners.go b/routers/web/admin/runners.go new file mode 100644 index 0000000000000..a70b53fdcb1cb --- /dev/null +++ b/routers/web/admin/runners.go @@ -0,0 +1,78 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "net/url" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + actions_shared "code.gitea.io/gitea/routers/web/shared/actions" +) + +const ( + tplRunners base.TplName = "admin/runners/base" + tplRunnerEdit base.TplName = "admin/runners/edit" +) + +// Runners show all the runners +func Runners(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.runners") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminRunners"] = true + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + opts := actions_model.FindRunnerOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: 100, + }, + Sort: ctx.Req.URL.Query().Get("sort"), + Filter: ctx.Req.URL.Query().Get("q"), + } + + actions_shared.RunnersList(ctx, tplRunners, opts) +} + +// EditRunner show editing runner page +func EditRunner(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminRunners"] = true + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + actions_shared.RunnerDetails(ctx, tplRunnerEdit, page, ctx.ParamsInt64(":runnerid"), 0, 0) +} + +// EditRunnerPost response for editing runner +func EditRunnerPost(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.runners.edit") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminRunners"] = true + actions_shared.RunnerDetailsEditPost(ctx, ctx.ParamsInt64(":runnerid"), 0, 0, + setting.AppSubURL+"/admin/runners/"+url.PathEscape(ctx.Params(":runnerid"))) +} + +// DeleteRunnerPost response for deleting a runner +func DeleteRunnerPost(ctx *context.Context) { + actions_shared.RunnerDeletePost(ctx, ctx.ParamsInt64(":runnerid"), + setting.AppSubURL+"/admin/runners/", + setting.AppSubURL+"/admin/runners/"+url.PathEscape(ctx.Params(":runnerid")), + ) +} + +func ResetRunnerRegistrationToken(ctx *context.Context) { + actions_shared.RunnerResetRegistrationToken(ctx, 0, 0, setting.AppSubURL+"/admin/runners/") +} diff --git a/routers/web/org/org_runners.go b/routers/web/org/org_runners.go new file mode 100644 index 0000000000000..f87a506504af9 --- /dev/null +++ b/routers/web/org/org_runners.go @@ -0,0 +1,78 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "net/url" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + actions_shared "code.gitea.io/gitea/routers/web/shared/actions" +) + +// Runners render runners page +func Runners(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("org.runners") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsOrgSettingsRunners"] = true + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + opts := actions_model.FindRunnerOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: 100, + }, + Sort: ctx.Req.URL.Query().Get("sort"), + Filter: ctx.Req.URL.Query().Get("q"), + OwnerID: ctx.Org.Organization.ID, + WithAvailable: true, + } + + actions_shared.RunnersList(ctx, tplSettingsRunners, opts) +} + +// ResetRunnerRegistrationToken reset runner registration token +func ResetRunnerRegistrationToken(ctx *context.Context) { + actions_shared.RunnerResetRegistrationToken(ctx, + ctx.Org.Organization.ID, 0, + ctx.Org.OrgLink+"/settings/runners") +} + +// RunnersEdit render runner edit page +func RunnersEdit(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("org.runners.edit") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsOrgSettingsRunners"] = true + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + actions_shared.RunnerDetails(ctx, tplSettingsRunnersEdit, page, + ctx.ParamsInt64(":runnerid"), ctx.Org.Organization.ID, 0, + ) +} + +// RunnersEditPost response for editing runner +func RunnersEditPost(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("org.runners.edit") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsOrgSettingsRunners"] = true + actions_shared.RunnerDetailsEditPost(ctx, ctx.ParamsInt64(":runnerid"), + ctx.Org.Organization.ID, 0, + ctx.Org.OrgLink+"/settings/runners/"+url.PathEscape(ctx.Params(":runnerid"))) +} + +// RunnerDeletePost response for deleting runner +func RunnerDeletePost(ctx *context.Context) { + actions_shared.RunnerDeletePost(ctx, + ctx.ParamsInt64(":runnerid"), + ctx.Org.OrgLink+"/settings/runners", + ctx.Org.OrgLink+"/settings/runners/"+url.PathEscape(ctx.Params(":runnerid"))) +} diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index e625962f7597a..5c9b7967c3dc2 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - secret_model "code.gitea.io/gitea/models/secret" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/base" @@ -38,8 +37,10 @@ const ( tplSettingsHooks base.TplName = "org/settings/hooks" // tplSettingsLabels template path for render labels settings tplSettingsLabels base.TplName = "org/settings/labels" - // tplSettingsSecrets template path for render secrets settings - tplSettingsSecrets base.TplName = "org/settings/secrets" + // tplSettingsRunners template path for render runners settings + tplSettingsRunners base.TplName = "org/settings/runners" + // tplSettingsRunnersEdit template path for render runners edit settings + tplSettingsRunnersEdit base.TplName = "org/settings/runners_edit" ) // Settings render the main settings page @@ -249,51 +250,3 @@ func Labels(ctx *context.Context) { ctx.Data["LabelTemplates"] = repo_module.LabelTemplates ctx.HTML(http.StatusOK, tplSettingsLabels) } - -// Secrets render organization secrets page -func Secrets(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.secrets") - ctx.Data["PageIsOrgSettings"] = true - ctx.Data["PageIsOrgSettingsSecrets"] = true - - secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: ctx.Org.Organization.ID}) - if err != nil { - ctx.ServerError("FindSecrets", err) - return - } - ctx.Data["Secrets"] = secrets - - ctx.HTML(http.StatusOK, tplSettingsSecrets) -} - -// SecretsPost add secrets -func SecretsPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.AddSecretForm) - - _, err := secret_model.InsertEncryptedSecret(ctx, ctx.Org.Organization.ID, 0, form.Title, form.Content) - if err != nil { - ctx.Flash.Error(ctx.Tr("secrets.creation.failed")) - log.Error("validate secret: %v", err) - ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets") - return - } - - log.Trace("Org %d: secret added", ctx.Org.Organization.ID) - ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title)) - ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets") -} - -// SecretsDelete delete secrets -func SecretsDelete(ctx *context.Context) { - id := ctx.FormInt64("id") - if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil { - ctx.Flash.Error(ctx.Tr("secrets.deletion.failed")) - log.Error("delete secret %d: %v", id, err) - } else { - ctx.Flash.Success(ctx.Tr("secrets.deletion.success")) - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Org.OrgLink + "/settings/secrets", - }) -} diff --git a/routers/web/org/setting_secrets.go b/routers/web/org/setting_secrets.go new file mode 100644 index 0000000000000..1cdbe35f32ade --- /dev/null +++ b/routers/web/org/setting_secrets.go @@ -0,0 +1,48 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + shared "code.gitea.io/gitea/routers/web/shared/secrets" +) + +const ( + tplSettingsSecrets base.TplName = "org/settings/secrets" +) + +// Secrets render organization secrets page +func Secrets(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("secrets.secrets") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsOrgSettingsSecrets"] = true + + shared.SetSecretsContext(ctx, ctx.ContextUser.ID, 0) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplSettingsSecrets) +} + +// SecretsPost add secrets +func SecretsPost(ctx *context.Context) { + shared.PerformSecretsPost( + ctx, + ctx.ContextUser.ID, + 0, + ctx.Org.OrgLink+"/settings/secrets", + ) +} + +// SecretsDelete delete secrets +func SecretsDelete(ctx *context.Context) { + shared.PerformSecretsDelete( + ctx, + ctx.Org.OrgLink+"/settings/secrets", + ) +} diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go new file mode 100644 index 0000000000000..146bf27dafd8f --- /dev/null +++ b/routers/web/repo/actions/actions.go @@ -0,0 +1,139 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/convert" +) + +const ( + tplListActions base.TplName = "repo/actions/list" + tplViewActions base.TplName = "repo/actions/view" +) + +// MustEnableActions check if actions are enabled in settings +func MustEnableActions(ctx *context.Context) { + if !setting.Actions.Enabled { + ctx.NotFound("MustEnableActions", nil) + return + } + + if unit.TypeActions.UnitGlobalDisabled() { + ctx.NotFound("MustEnableActions", nil) + return + } + + if ctx.Repo.Repository != nil { + if !ctx.Repo.CanRead(unit.TypeActions) { + ctx.NotFound("MustEnableActions", nil) + return + } + } +} + +func List(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.actions") + ctx.Data["PageIsActions"] = true + + var workflows git.Entries + if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } else if !empty { + defaultBranch, err := ctx.Repo.GitRepo.GetDefaultBranch() + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + commit, err := ctx.Repo.GitRepo.GetBranchCommit(defaultBranch) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + workflows, err = actions.ListWorkflows(commit) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + } + + ctx.Data["workflows"] = workflows + ctx.Data["RepoLink"] = ctx.Repo.Repository.HTMLURL() + + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + workflow := ctx.FormString("workflow") + ctx.Data["CurWorkflow"] = workflow + + opts := actions_model.FindRunOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + }, + RepoID: ctx.Repo.Repository.ID, + WorkflowFileName: workflow, + } + + // open counts + opts.IsClosed = util.OptionalBoolFalse + numOpenRuns, err := actions_model.CountRuns(ctx, opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + ctx.Data["NumOpenActionRuns"] = numOpenRuns + + // closed counts + opts.IsClosed = util.OptionalBoolTrue + numClosedRuns, err := actions_model.CountRuns(ctx, opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + ctx.Data["NumClosedActionRuns"] = numClosedRuns + + opts.IsClosed = util.OptionalBoolNone + if ctx.FormString("state") == "closed" { + opts.IsClosed = util.OptionalBoolTrue + ctx.Data["IsShowClosed"] = true + } else { + opts.IsClosed = util.OptionalBoolFalse + } + runs, total, err := actions_model.FindRuns(ctx, opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + for _, run := range runs { + run.Repo = ctx.Repo.Repository + } + + if err := runs.LoadTriggerUser(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + ctx.Data["Runs"] = runs + + pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplListActions) +} diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go new file mode 100644 index 0000000000000..5370310e8d9dc --- /dev/null +++ b/routers/web/repo/actions/view.go @@ -0,0 +1,297 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/actions" + context_module "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + actions_service "code.gitea.io/gitea/services/actions" + + "xorm.io/builder" +) + +func View(ctx *context_module.Context) { + ctx.Data["PageIsActions"] = true + runIndex := ctx.ParamsInt64("run") + jobIndex := ctx.ParamsInt64("job") + ctx.Data["RunIndex"] = runIndex + ctx.Data["JobIndex"] = jobIndex + ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions" + + if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplViewActions) +} + +type ViewRequest struct { + LogCursors []struct { + Step int `json:"step"` + Cursor int64 `json:"cursor"` + Expanded bool `json:"expanded"` + } `json:"logCursors"` +} + +type ViewResponse struct { + State struct { + Run struct { + Link string `json:"link"` + Title string `json:"title"` + CanCancel bool `json:"canCancel"` + Done bool `json:"done"` + Jobs []*ViewJob `json:"jobs"` + } `json:"run"` + CurrentJob struct { + Title string `json:"title"` + Detail string `json:"detail"` + Steps []*ViewJobStep `json:"steps"` + } `json:"currentJob"` + } `json:"state"` + Logs struct { + StepsLog []*ViewStepLog `json:"stepsLog"` + } `json:"logs"` +} + +type ViewJob struct { + ID int64 `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + CanRerun bool `json:"canRerun"` +} + +type ViewJobStep struct { + Summary string `json:"summary"` + Duration string `json:"duration"` + Status string `json:"status"` +} + +type ViewStepLog struct { + Step int `json:"step"` + Cursor int64 `json:"cursor"` + Lines []*ViewStepLogLine `json:"lines"` +} + +type ViewStepLogLine struct { + Index int64 `json:"index"` + Message string `json:"message"` + Timestamp float64 `json:"timestamp"` +} + +func ViewPost(ctx *context_module.Context) { + req := web.GetForm(ctx).(*ViewRequest) + runIndex := ctx.ParamsInt64("run") + jobIndex := ctx.ParamsInt64("job") + + current, jobs := getRunJobs(ctx, runIndex, jobIndex) + if ctx.Written() { + return + } + run := current.Run + + resp := &ViewResponse{} + + resp.State.Run.Title = run.Title + resp.State.Run.Link = run.Link() + resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) + resp.State.Run.Done = run.Status.IsDone() + resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json + for _, v := range jobs { + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{ + ID: v.ID, + Name: v.Name, + Status: v.Status.String(), + CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions), + }) + } + + var task *actions_model.ActionTask + if current.TaskID > 0 { + var err error + task, err = actions_model.GetTaskByID(ctx, current.TaskID) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + task.Job = current + if err := task.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + } + + resp.State.CurrentJob.Title = current.Name + resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale) + resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json + resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json + if task != nil { + steps := actions.FullSteps(task) + + for _, v := range steps { + resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{ + Summary: v.Name, + Duration: v.Duration().String(), + Status: v.Status.String(), + }) + } + + for _, cursor := range req.LogCursors { + if !cursor.Expanded { + continue + } + + step := steps[cursor.Step] + + logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json + if c := cursor.Cursor; c < step.LogLength && c >= 0 { + index := step.LogIndex + c + length := step.LogLength - cursor.Cursor + offset := task.LogIndexes[index] + var err error + logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + for i, row := range logRows { + logLines = append(logLines, &ViewStepLogLine{ + Index: cursor.Cursor + int64(i) + 1, // start at 1 + Message: row.Content, + Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second), + }) + } + } + + resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{ + Step: cursor.Step, + Cursor: cursor.Cursor + int64(len(logLines)), + Lines: logLines, + }) + } + } + + ctx.JSON(http.StatusOK, resp) +} + +func Rerun(ctx *context_module.Context) { + runIndex := ctx.ParamsInt64("run") + jobIndex := ctx.ParamsInt64("job") + + job, _ := getRunJobs(ctx, runIndex, jobIndex) + if ctx.Written() { + return + } + status := job.Status + if !status.IsDone() { + ctx.JSON(http.StatusOK, struct{}{}) + return + } + + job.TaskID = 0 + job.Status = actions_model.StatusWaiting + job.Started = 0 + job.Stopped = 0 + + if err := db.WithTx(ctx, func(ctx context.Context) error { + if _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped"); err != nil { + return err + } + return actions_service.CreateCommitStatus(ctx, job) + }); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + ctx.JSON(http.StatusOK, struct{}{}) +} + +func Cancel(ctx *context_module.Context) { + runIndex := ctx.ParamsInt64("run") + + _, jobs := getRunJobs(ctx, runIndex, -1) + if ctx.Written() { + return + } + + 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 fmt.Errorf("job has changed, try again") + } + continue + } + if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil { + return err + } + if err := actions_service.CreateCommitStatus(ctx, job); err != nil { + return err + } + } + return nil + }); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + ctx.JSON(http.StatusOK, struct{}{}) +} + +// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs. +// Any error will be written to the ctx. +// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0. +func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) { + run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, err.Error()) + return nil, nil + } + ctx.Error(http.StatusInternalServerError, err.Error()) + return nil, nil + } + run.Repo = ctx.Repo.Repository + + jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return nil, nil + } + if len(jobs) == 0 { + ctx.Error(http.StatusNotFound, err.Error()) + return nil, nil + } + + for _, v := range jobs { + v.Run = run + } + + if jobIndex >= 0 && jobIndex < int64(len(jobs)) { + return jobs[jobIndex], jobs + } + return jobs[0], jobs +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 8d45f8d674444..35f923d561ea5 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/upload" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/gitdiff" @@ -789,16 +790,29 @@ func CompareDiff(ctx *context.Context) { ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true) } - // If a template content is set, prepend the "content". In this case that's only - // applicable if you have one commit to compare and that commit has a message. - // In that case the commit message will be prepend to the template body. - if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" { - if content, ok := ctx.Data["content"].(string); ok && content != "" { + if content, ok := ctx.Data["content"].(string); ok && content != "" { + // If a template content is set, prepend the "content". In this case that's only + // applicable if you have one commit to compare and that commit has a message. + // In that case the commit message will be prepend to the template body. + if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" { // Re-use the same key as that's priortized over the "content" key. // Add two new lines between the content to ensure there's always at least // one empty line between them. ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent } + + // When using form fields, also add content to field with id "body". + if fields, ok := ctx.Data["Fields"].([]*api.IssueFormField); ok { + for _, field := range fields { + if field.ID == "body" { + if fieldValue, ok := field.Attributes["value"].(string); ok && fieldValue != "" { + field.Attributes["value"] = content + "\n\n" + fieldValue + } else { + field.Attributes["value"] = content + } + } + } + } } ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go index b2a49e3e3a103..2c91ed6178e05 100644 --- a/routers/web/repo/http.go +++ b/routers/web/repo/http.go @@ -18,6 +18,7 @@ import ( "sync" "time" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -163,7 +164,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) { return } - if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true { + if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true { _, err = auth.GetTwoFactorByUID(ctx.Doer.ID) if err == nil { // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented @@ -180,21 +181,56 @@ func httpBase(ctx *context.Context) (h *serviceHandler) { return } - if repoExist { - p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return - } + environ = []string{ + repo_module.EnvRepoUsername + "=" + username, + repo_module.EnvRepoName + "=" + reponame, + repo_module.EnvPusherName + "=" + ctx.Doer.Name, + repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID), + repo_module.EnvAppURL + "=" + setting.AppURL, + } + if repoExist { // Because of special ref "refs/for" .. , need delay write permission check if git.SupportProcReceive { accessMode = perm.AccessModeRead } - if !p.CanAccess(accessMode, unitType) { - ctx.PlainText(http.StatusForbidden, "User permission denied") - return + if ctx.Data["IsActionsToken"] == true { + taskID := ctx.Data["ActionsTaskID"].(int64) + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + ctx.ServerError("GetTaskByID", err) + return + } + if task.RepoID != repo.ID { + ctx.PlainText(http.StatusForbidden, "User permission denied") + return + } + + if task.IsForkPullRequest { + if accessMode > perm.AccessModeRead { + ctx.PlainText(http.StatusForbidden, "User permission denied") + return + } + environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead)) + } else { + if accessMode > perm.AccessModeWrite { + ctx.PlainText(http.StatusForbidden, "User permission denied") + return + } + environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite)) + } + } else { + p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + + if !p.CanAccess(accessMode, unitType) { + ctx.PlainText(http.StatusForbidden, "User permission denied") + return + } } if !isPull && repo.IsMirror { @@ -203,14 +239,6 @@ func httpBase(ctx *context.Context) (h *serviceHandler) { } } - environ = []string{ - repo_module.EnvRepoUsername + "=" + username, - repo_module.EnvRepoName + "=" + reponame, - repo_module.EnvPusherName + "=" + ctx.Doer.Name, - repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID), - repo_module.EnvAppURL + "=" + setting.AppURL, - } - if !ctx.Doer.KeepEmailPrivate { environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email) } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index ed7461bbc669c..5bff9e67f340a 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -784,6 +784,14 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles ctx.Data[ctxDataKey] = template.Content if template.Type() == api.IssueTemplateTypeYaml { + // Replace field default values by values from query + for _, field := range template.Fields { + fieldValue := ctx.FormString("field:" + field.ID) + if fieldValue != "" { + field.Attributes["value"] = fieldValue + } + } + ctx.Data["Fields"] = template.Fields ctx.Data["TemplateFile"] = template.FileName } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index c2208120fcf24..71bc98d13d07c 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1395,7 +1395,7 @@ func CleanUpPullRequest(ctx *context.Context) { } func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) { - fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch + fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch if err := repo_service.DeleteBranch(ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil { switch { case git.IsErrBranchNotExist(err): diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 54f503642bc90..5204b5fd0075f 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -328,6 +328,14 @@ func NewRelease(ctx *context.Context) { } } ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled + var err error + // Get assignees. + ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository) + if err != nil { + ctx.ServerError("GetAssignees", err) + return + } + upload.AddUploadContext(ctx, "release") ctx.HTML(http.StatusOK, tplReleaseNew) } @@ -484,6 +492,13 @@ func EditRelease(ctx *context.Context) { } ctx.Data["attachments"] = rel.Attachments + // Get assignees. + ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, rel.Repo) + if err != nil { + ctx.ServerError("GetAssignees", err) + return + } + ctx.HTML(http.StatusOK, tplReleaseNew) } diff --git a/routers/web/repo/runners.go b/routers/web/repo/runners.go new file mode 100644 index 0000000000000..d3dbff17fddba --- /dev/null +++ b/routers/web/repo/runners.go @@ -0,0 +1,76 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/url" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + actions_shared "code.gitea.io/gitea/routers/web/shared/actions" +) + +const ( + tplRunners = "repo/settings/runners" + tplRunnerEdit = "repo/settings/runner_edit" +) + +// Runners render runners page +func Runners(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.runners") + ctx.Data["PageIsSettingsRunners"] = true + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + opts := actions_model.FindRunnerOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: 100, + }, + Sort: ctx.Req.URL.Query().Get("sort"), + Filter: ctx.Req.URL.Query().Get("q"), + RepoID: ctx.Repo.Repository.ID, + WithAvailable: true, + } + + actions_shared.RunnersList(ctx, tplRunners, opts) +} + +func RunnersEdit(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.runners") + ctx.Data["PageIsSettingsRunners"] = true + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + actions_shared.RunnerDetails(ctx, tplRunnerEdit, page, + ctx.ParamsInt64(":runnerid"), 0, ctx.Repo.Repository.ID, + ) +} + +func RunnersEditPost(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.runners") + ctx.Data["PageIsSettingsRunners"] = true + actions_shared.RunnerDetailsEditPost(ctx, ctx.ParamsInt64(":runnerid"), + 0, ctx.Repo.Repository.ID, + ctx.Repo.RepoLink+"/settings/runners/"+url.PathEscape(ctx.Params(":runnerid"))) +} + +func ResetRunnerRegistrationToken(ctx *context.Context) { + actions_shared.RunnerResetRegistrationToken(ctx, + 0, ctx.Repo.Repository.ID, + ctx.Repo.RepoLink+"/settings/runners") +} + +// RunnerDeletePost response for deleting runner +func RunnerDeletePost(ctx *context.Context) { + actions_shared.RunnerDeletePost(ctx, ctx.ParamsInt64(":runnerid"), + ctx.Repo.RepoLink+"/settings/runners", + ctx.Repo.RepoLink+"/settings/runners/"+url.PathEscape(ctx.Params(":runnerid"))) +} diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 43a615abfe991..da52957548be8 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -19,7 +19,6 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" - secret_model "code.gitea.io/gitea/models/secret" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" @@ -61,7 +60,7 @@ const ( // SettingsCtxData is a middleware that sets all the general context data for the // settings template. func SettingsCtxData(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.options") ctx.Data["PageIsSettingsOptions"] = true ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled @@ -488,6 +487,15 @@ func SettingsPost(ctx *context.Context) { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) } + if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeReleases, + }) + } else if !unit_model.TypeReleases.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) + } + if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, @@ -497,6 +505,15 @@ func SettingsPost(ctx *context.Context) { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) } + if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeActions, + }) + } else if !unit_model.TypeActions.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) + } + if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, @@ -862,7 +879,7 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R // Collaboration render a repository's collaboration page func Collaboration(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration") ctx.Data["PageIsSettingsCollaboration"] = true users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) @@ -1102,7 +1119,7 @@ func GitHooksEditPost(ctx *context.Context) { // DeployKeys render the deploy keys list of a repository page func DeployKeys(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + " / " + ctx.Tr("secrets.secrets") ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled @@ -1113,37 +1130,12 @@ func DeployKeys(ctx *context.Context) { } ctx.Data["Deploykeys"] = keys - secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: ctx.Repo.Repository.ID}) - if err != nil { - ctx.ServerError("FindSecrets", err) - return - } - ctx.Data["Secrets"] = secrets - ctx.HTML(http.StatusOK, tplDeployKeys) } -// SecretsPost response for creating a new secret -func SecretsPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.AddSecretForm) - - _, err := secret_model.InsertEncryptedSecret(ctx, 0, ctx.Repo.Repository.ID, form.Title, form.Content) - if err != nil { - ctx.Flash.Error(ctx.Tr("secrets.creation.failed")) - log.Error("validate secret: %v", err) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") - return - } - - log.Trace("Secret added: %d", ctx.Repo.Repository.ID) - ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title)) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") -} - // DeployKeysPost response for adding a deploy key of a repository func DeployKeysPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.AddKeyForm) - ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled @@ -1202,20 +1194,6 @@ func DeployKeysPost(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") } -func DeleteSecret(ctx *context.Context) { - id := ctx.FormInt64("id") - if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil { - ctx.Flash.Error(ctx.Tr("secrets.deletion.failed")) - log.Error("delete secret %d: %v", id, err) - } else { - ctx.Flash.Success(ctx.Tr("secrets.deletion.success")) - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Repo.RepoLink + "/settings/keys", - }) -} - // DeleteDeployKey response for deleting a deploy key func DeleteDeployKey(ctx *context.Context) { if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil { diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go index 31abde1ef6801..a54565c1f1512 100644 --- a/routers/web/repo/setting_protected_branch.go +++ b/routers/web/repo/setting_protected_branch.go @@ -31,7 +31,7 @@ const ( // ProtectedBranchRules render the page to protect the repository func ProtectedBranchRules(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.branches") ctx.Data["PageIsSettingsBranches"] = true rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) @@ -46,7 +46,7 @@ func ProtectedBranchRules(ctx *context.Context) { // SetDefaultBranchPost set default branch func SetDefaultBranchPost(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.branches.update_default_branch") ctx.Data["PageIsSettingsBranches"] = true repo := ctx.Repo.Repository diff --git a/routers/web/repo/setting_secrets.go b/routers/web/repo/setting_secrets.go new file mode 100644 index 0000000000000..c42dee583b923 --- /dev/null +++ b/routers/web/repo/setting_secrets.go @@ -0,0 +1,46 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + shared "code.gitea.io/gitea/routers/web/shared/secrets" +) + +const ( + tplSecrets base.TplName = "repo/settings/secrets" +) + +func Secrets(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("secrets.secrets") + ctx.Data["PageIsSettingsSecrets"] = true + ctx.Data["DisableSSH"] = setting.SSH.Disabled + + shared.SetSecretsContext(ctx, 0, ctx.Repo.Repository.ID) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplSecrets) +} + +func SecretsPost(ctx *context.Context) { + shared.PerformSecretsPost( + ctx, + 0, + ctx.Repo.Repository.ID, + ctx.Repo.RepoLink+"/settings/secrets", + ) +} + +func DeleteSecret(ctx *context.Context) { + shared.PerformSecretsDelete( + ctx, + ctx.Repo.RepoLink+"/settings/secrets", + ) +} diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go index aa9edc73755d9..95bc6dfce7f80 100644 --- a/routers/web/repo/tag.go +++ b/routers/web/repo/tag.go @@ -133,7 +133,7 @@ func DeleteProtectedTagPost(ctx *context.Context) { } func setTagsContext(ctx *context.Context) error { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.tags") ctx.Data["PageIsSettingsTags"] = true protectedTags, err := git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID) diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go new file mode 100644 index 0000000000000..f63d37f165b8f --- /dev/null +++ b/routers/web/shared/actions/runners.go @@ -0,0 +1,190 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "errors" + "net/http" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" +) + +// RunnersList render common runners list page +func RunnersList(ctx *context.Context, tplName base.TplName, opts actions_model.FindRunnerOptions) { + count, err := actions_model.CountRunners(ctx, opts) + if err != nil { + ctx.ServerError("AdminRunners", err) + return + } + + runners, err := actions_model.FindRunners(ctx, opts) + if err != nil { + ctx.ServerError("AdminRunners", err) + return + } + if err := runners.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + + // ownid=0,repo_id=0,means this token is used for global + var token *actions_model.ActionRunnerToken + token, err = actions_model.GetUnactivatedRunnerToken(ctx, opts.OwnerID, opts.RepoID) + if errors.Is(err, util.ErrNotExist) { + token, err = actions_model.NewRunnerToken(ctx, opts.OwnerID, opts.RepoID) + if err != nil { + ctx.ServerError("CreateRunnerToken", err) + return + } + } else if err != nil { + ctx.ServerError("GetUnactivatedRunnerToken", err) + return + } + + ctx.Data["Keyword"] = opts.Filter + ctx.Data["Runners"] = runners + ctx.Data["Total"] = count + ctx.Data["RegistrationToken"] = token.Token + ctx.Data["RunnerOnwerID"] = opts.OwnerID + ctx.Data["RunnerRepoID"] = opts.RepoID + + pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplName) +} + +// RunnerDetails render runner details page +func RunnerDetails(ctx *context.Context, tplName base.TplName, page int, runnerID, ownerID, repoID int64) { + runner, err := actions_model.GetRunnerByID(ctx, runnerID) + if err != nil { + ctx.ServerError("GetRunnerByID", err) + return + } + if err := runner.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + if !runner.Editable(ownerID, repoID) { + err = errors.New("no permission to edit this runner") + ctx.NotFound("RunnerDetails", err) + return + } + + ctx.Data["Runner"] = runner + + opts := actions_model.FindTaskOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: 30, + }, + Status: actions_model.StatusUnknown, // Unknown means all + IDOrderDesc: true, + RunnerID: runner.ID, + } + + count, err := actions_model.CountTasks(ctx, opts) + if err != nil { + ctx.ServerError("CountTasks", err) + return + } + + tasks, err := actions_model.FindTasks(ctx, opts) + if err != nil { + ctx.ServerError("FindTasks", err) + return + } + if err = tasks.LoadAttributes(ctx); err != nil { + ctx.ServerError("TasksLoadAttributes", err) + return + } + + ctx.Data["Tasks"] = tasks + pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplName) +} + +// RunnerDetailsEditPost response for edit runner details +func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64, redirectTo string) { + runner, err := actions_model.GetRunnerByID(ctx, runnerID) + if err != nil { + log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL) + ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err) + return + } + if !runner.Editable(ownerID, repoID) { + ctx.NotFound("RunnerDetailsEditPost.Editable", util.NewPermissionDeniedErrorf("no permission to edit this runner")) + return + } + + form := web.GetForm(ctx).(*forms.EditRunnerForm) + runner.Description = form.Description + runner.CustomLabels = splitLabels(form.CustomLabels) + + err = actions_model.UpdateRunner(ctx, runner, "description", "custom_labels") + if err != nil { + log.Warn("RunnerDetailsEditPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL) + ctx.Flash.Warning(ctx.Tr("actions.runners.update_runner_failed")) + ctx.Redirect(redirectTo) + return + } + + log.Debug("RunnerDetailsEditPost success: %s", ctx.Req.URL) + + ctx.Flash.Success(ctx.Tr("actions.runners.update_runner_success")) + ctx.Redirect(redirectTo) +} + +// RunnerResetRegistrationToken reset registration token +func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) { + _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID) + if err != nil { + ctx.ServerError("ResetRunnerRegistrationToken", err) + return + } + + ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success")) + ctx.Redirect(redirectTo) +} + +// RunnerDeletePost response for deleting a runner +func RunnerDeletePost(ctx *context.Context, runnerID int64, + successRedirectTo, failedRedirectTo string, +) { + if err := actions_model.DeleteRunner(ctx, runnerID); err != nil { + log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL) + ctx.Flash.Warning(ctx.Tr("actions.runners.delete_runner_failed")) + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": failedRedirectTo, + }) + return + } + + log.Info("DeleteRunnerPost success: %s", ctx.Req.URL) + + ctx.Flash.Success(ctx.Tr("actions.runners.delete_runner_success")) + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": successRedirectTo, + }) +} + +func splitLabels(s string) []string { + labels := strings.Split(s, ",") + for i, v := range labels { + labels[i] = strings.TrimSpace(v) + } + return labels +} diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go new file mode 100644 index 0000000000000..e242c5e81611d --- /dev/null +++ b/routers/web/shared/secrets/secrets.go @@ -0,0 +1,54 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package secrets + +import ( + "net/http" + + "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" +) + +func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) { + secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: ownerID, RepoID: repoID}) + if err != nil { + ctx.ServerError("FindSecrets", err) + return + } + + ctx.Data["Secrets"] = secrets +} + +func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) { + form := web.GetForm(ctx).(*forms.AddSecretForm) + + s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Title, form.Content) + if err != nil { + log.Error("InsertEncryptedSecret: %v", err) + ctx.Flash.Error(ctx.Tr("secrets.creation.failed")) + } else { + ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name)) + } + + ctx.Redirect(redirectURL) +} + +func PerformSecretsDelete(ctx *context.Context, redirectURL string) { + id := ctx.FormInt64("id") + + if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil { + log.Error("Delete secret %d failed: %v", id, err) + ctx.Flash.Error(ctx.Tr("secrets.deletion.failed")) + } else { + ctx.Flash.Success(ctx.Tr("secrets.deletion.success")) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": redirectURL, + }) +} diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 95b3b4040d96e..dbaa8fd351b29 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -30,7 +30,7 @@ const ( // Account renders change user's password, user's email and user suicide page func Account(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.account") ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.Doer.Email ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go index 0aaf5920bc6d1..844d6fa166a1d 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -17,7 +17,7 @@ import ( // AdoptOrDeleteRepository adopts or deletes a repository func AdoptOrDeleteRepository(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.adopt") ctx.Data["PageIsSettingsRepos"] = true allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowAdopt"] = allowAdopt diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index b66806ff2de0b..ac935e51bbb38 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -21,7 +21,7 @@ const ( // Applications render manage access token page func Applications(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.applications") ctx.Data["PageIsSettingsApplications"] = true loadApplicationsData(ctx) diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index ec50eef9c18de..0ecc39ecd17ed 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -23,7 +23,7 @@ const ( // Keys render user's SSH/GPG public keys page func Keys(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys") ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index af161952501b5..e01f3cdeea94e 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -42,7 +42,7 @@ const ( // Profile render user's profile page func Profile(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.profile") ctx.Data["PageIsSettingsProfile"] = true ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() @@ -219,7 +219,7 @@ func DeleteAvatar(ctx *context.Context) { // Organization render all the organization of the user func Organization(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.organization") ctx.Data["PageIsSettingsOrganization"] = true opts := organization.FindOrgOptions{ @@ -254,7 +254,7 @@ func Organization(ctx *context.Context) { // Repos display a list of all repositories of the user func Repos(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.repos") ctx.Data["PageIsSettingsRepos"] = true ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories @@ -360,7 +360,7 @@ func Repos(ctx *context.Context) { // Appearance render user's appearance settings func Appearance(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.appearance") ctx.Data["PageIsSettingsAppearance"] = true var hiddenCommentTypes *big.Int diff --git a/routers/web/user/setting/secrets.go b/routers/web/user/setting/secrets.go new file mode 100644 index 0000000000000..3a57897d8f607 --- /dev/null +++ b/routers/web/user/setting/secrets.go @@ -0,0 +1,45 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + shared "code.gitea.io/gitea/routers/web/shared/secrets" +) + +const ( + tplSettingsSecrets base.TplName = "user/settings/secrets" +) + +func Secrets(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("secrets.secrets") + ctx.Data["PageIsSettingsSecrets"] = true + + shared.SetSecretsContext(ctx, ctx.Doer.ID, 0) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplSettingsSecrets) +} + +func SecretsPost(ctx *context.Context) { + shared.PerformSecretsPost( + ctx, + ctx.Doer.ID, + 0, + setting.AppSubURL+"/user/settings/secrets", + ) +} + +func SecretsDelete(ctx *context.Context) { + shared.PerformSecretsDelete( + ctx, + setting.AppSubURL+"/user/settings/secrets", + ) +} diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index db6faaed6e197..6e6e7efb0b2c1 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -22,7 +22,7 @@ const ( // Security render change user's password page and 2FA func Security(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.security") ctx.Data["PageIsSettingsSecurity"] = true if ctx.FormString("openid.return_to") != "" { diff --git a/routers/web/web.go b/routers/web/web.go index d37d82820de88..b7128fc3a9fcc 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -34,6 +34,7 @@ import ( "code.gitea.io/gitea/routers/web/misc" "code.gitea.io/gitea/routers/web/org" "code.gitea.io/gitea/routers/web/repo" + "code.gitea.io/gitea/routers/web/repo/actions" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/routers/web/user/setting/security" @@ -468,6 +469,11 @@ func RegisterRoutes(m *web.Route) { }) }) }, packagesEnabled) + m.Group("/secrets", func() { + m.Get("", user_setting.Secrets) + m.Post("", web.Bind(forms.AddSecretForm{}), user_setting.SecretsPost) + m.Post("/delete", user_setting.SecretsDelete) + }) m.Get("/organization", user_setting.Organization) m.Get("/repos", user_setting.Repos) m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository) @@ -620,6 +626,13 @@ func RegisterRoutes(m *web.Route) { return } }) + + m.Group("/runners", func() { + m.Get("", admin.Runners) + m.Get("/reset_registration_token", admin.ResetRunnerRegistrationToken) + m.Combo("/{runnerid}").Get(admin.EditRunner).Post(web.Bind(forms.EditRunnerForm{}), admin.EditRunnerPost) + m.Post("/{runnerid}/delete", admin.DeleteRunnerPost) + }, actions.MustEnableActions) }, func(ctx *context.Context) { ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable ctx.Data["EnablePackages"] = setting.Packages.Enabled @@ -661,6 +674,8 @@ func RegisterRoutes(m *web.Route) { reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(unit.TypeIssues, unit.TypePullRequests) reqRepoProjectsReader := context.RequireRepoReader(unit.TypeProjects) reqRepoProjectsWriter := context.RequireRepoWriter(unit.TypeProjects) + reqRepoActionsReader := context.RequireRepoReader(unit.TypeActions) + reqRepoActionsWriter := context.RequireRepoWriter(unit.TypeActions) reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { @@ -774,6 +789,14 @@ func RegisterRoutes(m *web.Route) { m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), org.InitializeLabels) }) + m.Group("/runners", func() { + m.Get("", org.Runners) + m.Combo("/{runnerid}").Get(org.RunnersEdit). + Post(web.Bind(forms.EditRunnerForm{}), org.RunnersEditPost) + m.Post("/{runnerid}/delete", org.RunnerDeletePost) + m.Get("/reset_registration_token", org.ResetRunnerRegistrationToken) + }, actions.MustEnableActions) + m.Group("/secrets", func() { m.Get("", org.Secrets) m.Post("", web.Bind(forms.AddSecretForm{}), org.SecretsPost) @@ -964,10 +987,12 @@ func RegisterRoutes(m *web.Route) { m.Combo("").Get(repo.DeployKeys). Post(web.Bind(forms.AddKeyForm{}), repo.DeployKeysPost) m.Post("/delete", repo.DeleteDeployKey) - m.Group("/secrets", func() { - m.Post("", web.Bind(forms.AddSecretForm{}), repo.SecretsPost) - m.Post("/delete", repo.DeleteSecret) - }) + }) + + m.Group("/secrets", func() { + m.Get("", repo.Secrets) + m.Post("", web.Bind(forms.AddSecretForm{}), repo.SecretsPost) + m.Post("/delete", repo.DeleteSecret) }) m.Group("/lfs", func() { @@ -983,6 +1008,14 @@ func RegisterRoutes(m *web.Route) { m.Post("/{lid}/unlock", repo.LFSUnlock) }) }) + + m.Group("/runners", func() { + m.Get("", repo.Runners) + m.Combo("/{runnerid}").Get(repo.RunnersEdit). + Post(web.Bind(forms.EditRunnerForm{}), repo.RunnersEditPost) + m.Post("/{runnerid}/delete", repo.RunnerDeletePost) + m.Get("/reset_registration_token", repo.ResetRunnerRegistrationToken) + }, actions.MustEnableActions) }, func(ctx *context.Context) { ctx.Data["PageIsSettings"] = true ctx.Data["LFSStartServer"] = setting.LFS.StartServer @@ -1230,6 +1263,23 @@ func RegisterRoutes(m *web.Route) { }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) }, reqRepoProjectsReader, repo.MustEnableProjects) + m.Group("/actions", func() { + m.Get("", actions.List) + + m.Group("/runs/{run}", func() { + m.Combo(""). + Get(actions.View). + Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) + m.Group("/jobs/{job}", func() { + m.Combo(""). + Get(actions.View). + Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) + m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) + }) + m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) + }) + }, reqRepoActionsReader, actions.MustEnableActions) + m.Group("/wiki", func() { m.Combo("/"). Get(repo.Wiki). diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go new file mode 100644 index 0000000000000..583e588de4054 --- /dev/null +++ b/services/actions/clear_tasks.go @@ -0,0 +1,94 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" +) + +const ( + zombieTaskTimeout = 10 * time.Minute + endlessTaskTimeout = 3 * time.Hour + abandonedJobTimeout = 24 * time.Hour +) + +// StopZombieTasks stops the task which have running status, but haven't been updated for a long time +func StopZombieTasks(ctx context.Context) error { + return stopTasks(ctx, actions_model.FindTaskOptions{ + Status: actions_model.StatusRunning, + UpdatedBefore: timeutil.TimeStamp(time.Now().Add(-zombieTaskTimeout).Unix()), + }) +} + +// StopEndlessTasks stops the tasks which have running status and continuous updates, but don't end for a long time +func StopEndlessTasks(ctx context.Context) error { + return stopTasks(ctx, actions_model.FindTaskOptions{ + Status: actions_model.StatusRunning, + StartedBefore: timeutil.TimeStamp(time.Now().Add(-endlessTaskTimeout).Unix()), + }) +} + +func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { + tasks, err := actions_model.FindTasks(ctx, opts) + if err != nil { + return fmt.Errorf("find tasks: %w", err) + } + + for _, task := range tasks { + if err := db.WithTx(ctx, func(ctx context.Context) error { + if err := actions_model.StopTask(ctx, task.ID, actions_model.StatusFailure); err != nil { + return err + } + if err := task.LoadJob(ctx); err != nil { + return err + } + return CreateCommitStatus(ctx, task.Job) + }); err != nil { + log.Warn("Cannot stop task %v: %v", task.ID, err) + // go on + } else if remove, err := actions.TransferLogs(ctx, task.LogFilename); err != nil { + log.Warn("Cannot transfer logs of task %v: %v", task.ID, err) + } else { + remove() + } + } + return nil +} + +// CancelAbandonedJobs cancels the jobs which have waiting status, but haven't been picked by a runner for a long time +func CancelAbandonedJobs(ctx context.Context) error { + jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{ + Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked}, + UpdatedBefore: timeutil.TimeStamp(time.Now().Add(-abandonedJobTimeout).Unix()), + }) + if err != nil { + log.Warn("find abandoned tasks: %v", err) + return err + } + + now := timeutil.TimeStampNow() + for _, job := range jobs { + job.Status = actions_model.StatusCancelled + job.Stopped = now + if err := db.WithTx(ctx, func(ctx context.Context) error { + if _, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped"); err != nil { + return err + } + return CreateCommitStatus(ctx, job) + }); err != nil { + log.Warn("cancel abandoned job %v: %v", job.ID, err) + // go on + } + } + + return nil +} diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go new file mode 100644 index 0000000000000..c17f8ef1534ac --- /dev/null +++ b/services/actions/commit_status.go @@ -0,0 +1,88 @@ +// Copyright 2023 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" + git_model "code.gitea.io/gitea/models/git" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" +) + +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 + if run.Event != webhook_module.HookEventPush { + return nil + } + + payload, err := run.GetPushEventPayload() + if err != nil { + return fmt.Errorf("GetPushEventPayload: %w", err) + } + + creator, err := user_model.GetUserByID(ctx, payload.Pusher.ID) + if err != nil { + return fmt.Errorf("GetUserByID: %w", err) + } + + repo := run.Repo + sha := payload.HeadCommit.ID + ctxname := job.Name + state := toCommitStatus(job.Status) + + if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{}); err == nil { + for _, v := range statuses { + if v.Context == ctxname { + if v.State == state { + return nil + } + break + } + } + } else { + return fmt.Errorf("GetLatestCommitStatus: %w", err) + } + + if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ + Repo: repo, + SHA: payload.HeadCommit.ID, + Creator: creator, + CommitStatus: &git_model.CommitStatus{ + SHA: sha, + TargetURL: run.HTMLURL(), + Description: "", + Context: ctxname, + CreatorID: payload.Pusher.ID, + State: state, + }, + }); err != nil { + return fmt.Errorf("NewCommitStatus: %w", err) + } + + return nil +} + +func toCommitStatus(status actions_model.Status) api.CommitStatusState { + switch status { + case actions_model.StatusSuccess: + return api.CommitStatusSuccess + case actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped: + return api.CommitStatusFailure + case actions_model.StatusWaiting, actions_model.StatusBlocked: + return api.CommitStatusPending + case actions_model.StatusRunning: + return api.CommitStatusRunning + default: + return api.CommitStatusError + } +} diff --git a/services/actions/init.go b/services/actions/init.go new file mode 100644 index 0000000000000..3fd03eeb6f26c --- /dev/null +++ b/services/actions/init.go @@ -0,0 +1,22 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" +) + +func Init() { + if !setting.Actions.Enabled { + return + } + + jobEmitterQueue = queue.CreateUniqueQueue("actions_ready_job", jobEmitterQueueHandle, new(jobUpdate)) + go graceful.GetManager().RunWithShutdownFns(jobEmitterQueue.Run) + + notification.RegisterNotifier(NewNotifier()) +} diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go new file mode 100644 index 0000000000000..cb2cc8d1ac064 --- /dev/null +++ b/services/actions/job_emitter.go @@ -0,0 +1,140 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "errors" + "fmt" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/queue" + + "xorm.io/builder" +) + +var jobEmitterQueue queue.UniqueQueue + +type jobUpdate struct { + RunID int64 +} + +func EmitJobsIfReady(runID int64) error { + err := jobEmitterQueue.Push(&jobUpdate{ + RunID: runID, + }) + if errors.Is(err, queue.ErrAlreadyInQueue) { + return nil + } + return err +} + +func jobEmitterQueueHandle(data ...queue.Data) []queue.Data { + ctx := graceful.GetManager().ShutdownContext() + var ret []queue.Data + for _, d := range data { + update := d.(*jobUpdate) + if err := checkJobsOfRun(ctx, update.RunID); err != nil { + ret = append(ret, d) + } + } + return ret +} + +func checkJobsOfRun(ctx context.Context, runID int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: runID}) + if err != nil { + return err + } + idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs)) + for _, job := range jobs { + idToJobs[job.JobID] = append(idToJobs[job.JobID], job) + } + + 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) + } + } + } + return nil + }) +} + +type jobStatusResolver struct { + statuses map[int64]actions_model.Status + needs map[int64][]int64 +} + +func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver { + idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs)) + for _, job := range jobs { + idToJobs[job.JobID] = append(idToJobs[job.JobID], job) + } + + statuses := make(map[int64]actions_model.Status, len(jobs)) + needs := make(map[int64][]int64, len(jobs)) + for _, job := range jobs { + statuses[job.ID] = job.Status + for _, need := range job.Needs { + for _, v := range idToJobs[need] { + needs[job.ID] = append(needs[job.ID], v.ID) + } + } + } + return &jobStatusResolver{ + statuses: statuses, + needs: needs, + } +} + +func (r *jobStatusResolver) Resolve() map[int64]actions_model.Status { + ret := map[int64]actions_model.Status{} + for i := 0; i < len(r.statuses); i++ { + updated := r.resolve() + if len(updated) == 0 { + return ret + } + for k, v := range updated { + ret[k] = v + r.statuses[k] = v + } + } + return ret +} + +func (r *jobStatusResolver) resolve() map[int64]actions_model.Status { + ret := map[int64]actions_model.Status{} + for id, status := range r.statuses { + 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 + } + } + if allDone { + if allSucceed { + ret[id] = actions_model.StatusWaiting + } else { + ret[id] = actions_model.StatusSkipped + } + } + } + return ret +} diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go new file mode 100644 index 0000000000000..e81aa61d80730 --- /dev/null +++ b/services/actions/job_emitter_test.go @@ -0,0 +1,80 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + + "github.com/stretchr/testify/assert" +) + +func Test_jobStatusResolver_Resolve(t *testing.T) { + tests := []struct { + name string + jobs actions_model.ActionJobList + want map[int64]actions_model.Status + }{ + { + name: "no blocked", + jobs: actions_model.ActionJobList{ + {ID: 1, JobID: "1", Status: actions_model.StatusWaiting, Needs: []string{}}, + {ID: 2, JobID: "2", Status: actions_model.StatusWaiting, Needs: []string{}}, + {ID: 3, JobID: "3", Status: actions_model.StatusWaiting, Needs: []string{}}, + }, + want: map[int64]actions_model.Status{}, + }, + { + name: "single blocked", + jobs: actions_model.ActionJobList{ + {ID: 1, JobID: "1", Status: actions_model.StatusSuccess, Needs: []string{}}, + {ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}}, + {ID: 3, JobID: "3", Status: actions_model.StatusWaiting, Needs: []string{}}, + }, + want: map[int64]actions_model.Status{ + 2: actions_model.StatusWaiting, + }, + }, + { + name: "multiple blocked", + jobs: actions_model.ActionJobList{ + {ID: 1, JobID: "1", Status: actions_model.StatusSuccess, Needs: []string{}}, + {ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}}, + {ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"1"}}, + }, + want: map[int64]actions_model.Status{ + 2: actions_model.StatusWaiting, + 3: actions_model.StatusWaiting, + }, + }, + { + name: "chain blocked", + jobs: actions_model.ActionJobList{ + {ID: 1, JobID: "1", Status: actions_model.StatusFailure, Needs: []string{}}, + {ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}}, + {ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"2"}}, + }, + want: map[int64]actions_model.Status{ + 2: actions_model.StatusSkipped, + 3: actions_model.StatusSkipped, + }, + }, + { + name: "loop need", + jobs: actions_model.ActionJobList{ + {ID: 1, JobID: "1", Status: actions_model.StatusBlocked, Needs: []string{"3"}}, + {ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}}, + {ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"2"}}, + }, + want: map[int64]actions_model.Status{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := newJobStatusResolver(tt.jobs) + assert.Equal(t, tt.want, r.Resolve()) + }) + } +} diff --git a/services/actions/notifier.go b/services/actions/notifier.go new file mode 100644 index 0000000000000..0ed69097dce9f --- /dev/null +++ b/services/actions/notifier.go @@ -0,0 +1,528 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + packages_model "code.gitea.io/gitea/models/packages" + perm_model "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + 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/log" + "code.gitea.io/gitea/modules/notification/base" + "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/convert" +) + +type actionsNotifier struct { + base.NullNotifier +} + +var _ base.Notifier = &actionsNotifier{} + +// NewNotifier create a new actionsNotifier notifier +func NewNotifier() base.Notifier { + return &actionsNotifier{} +} + +// NotifyNewIssue notifies issue created event +func (n *actionsNotifier) NotifyNewIssue(ctx context.Context, issue *issues_model.Issue, _ []*user_model.User) { + ctx = withMethod(ctx, "NotifyNewIssue") + if err := issue.LoadRepo(ctx); err != nil { + log.Error("issue.LoadRepo: %v", err) + return + } + if err := issue.LoadPoster(ctx); err != nil { + log.Error("issue.LoadPoster: %v", err) + return + } + mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + + newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).WithPayload(&api.IssuePayload{ + Action: api.HookIssueOpened, + Index: issue.Index, + Issue: convert.ToAPIIssue(ctx, issue), + Repository: convert.ToRepo(ctx, issue.Repo, mode), + Sender: convert.ToUser(issue.Poster, nil), + }).Notify(withMethod(ctx, "NotifyNewIssue")) +} + +// NotifyIssueChangeStatus notifies close or reopen issue to notifiers +func (n *actionsNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) { + ctx = withMethod(ctx, "NotifyIssueChangeStatus") + mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + if issue.IsPull { + if err := issue.LoadPullRequest(ctx); err != nil { + log.Error("LoadPullRequest: %v", err) + return + } + // Merge pull request calls issue.changeStatus so we need to handle separately. + apiPullRequest := &api.PullRequestPayload{ + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, issue.PullRequest, nil), + Repository: convert.ToRepo(ctx, issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + CommitID: commitID, + } + if isClosed { + apiPullRequest.Action = api.HookIssueClosed + } else { + apiPullRequest.Action = api.HookIssueReOpened + } + newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequest). + WithDoer(doer). + WithPayload(apiPullRequest). + Notify(ctx) + return + } + apiIssue := &api.IssuePayload{ + Index: issue.Index, + Issue: convert.ToAPIIssue(ctx, issue), + Repository: convert.ToRepo(ctx, issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + } + if isClosed { + apiIssue.Action = api.HookIssueClosed + } else { + apiIssue.Action = api.HookIssueReOpened + } + newNotifyInputFromIssue(issue, webhook_module.HookEventIssues). + WithDoer(doer). + WithPayload(apiIssue). + Notify(ctx) +} + +func (n *actionsNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, + _, _ []*issues_model.Label, +) { + ctx = withMethod(ctx, "NotifyIssueChangeLabels") + + var err error + if err = issue.LoadRepo(ctx); err != nil { + log.Error("LoadRepo: %v", err) + return + } + + if err = issue.LoadPoster(ctx); err != nil { + log.Error("LoadPoster: %v", err) + return + } + + mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + if issue.IsPull { + if err = issue.LoadPullRequest(ctx); err != nil { + log.Error("loadPullRequest: %v", err) + return + } + if err = issue.PullRequest.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return + } + newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestLabel). + WithDoer(doer). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueLabelUpdated, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + Repository: convert.ToRepo(ctx, issue.Repo, perm_model.AccessModeNone), + Sender: convert.ToUser(doer, nil), + }). + Notify(ctx) + return + } + newNotifyInputFromIssue(issue, webhook_module.HookEventIssueLabel). + WithDoer(doer). + WithPayload(&api.IssuePayload{ + Action: api.HookIssueLabelUpdated, + Index: issue.Index, + Issue: convert.ToAPIIssue(ctx, issue), + Repository: convert.ToRepo(ctx, issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + }). + Notify(ctx) +} + +// NotifyCreateIssueComment notifies comment on an issue to notifiers +func (n *actionsNotifier) NotifyCreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, + issue *issues_model.Issue, comment *issues_model.Comment, _ []*user_model.User, +) { + ctx = withMethod(ctx, "NotifyCreateIssueComment") + + mode, _ := access_model.AccessLevel(ctx, doer, repo) + + if issue.IsPull { + newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestComment). + WithDoer(doer). + WithPayload(&api.IssueCommentPayload{ + Action: api.HookIssueCommentCreated, + Issue: convert.ToAPIIssue(ctx, issue), + Comment: convert.ToComment(comment), + Repository: convert.ToRepo(ctx, repo, mode), + Sender: convert.ToUser(doer, nil), + IsPull: true, + }). + Notify(ctx) + return + } + newNotifyInputFromIssue(issue, webhook_module.HookEventIssueComment). + WithDoer(doer). + WithPayload(&api.IssueCommentPayload{ + Action: api.HookIssueCommentCreated, + Issue: convert.ToAPIIssue(ctx, issue), + Comment: convert.ToComment(comment), + Repository: convert.ToRepo(ctx, repo, mode), + Sender: convert.ToUser(doer, nil), + IsPull: false, + }). + Notify(ctx) +} + +func (n *actionsNotifier) NotifyNewPullRequest(ctx context.Context, pull *issues_model.PullRequest, _ []*user_model.User) { + ctx = withMethod(ctx, "NotifyNewPullRequest") + + if err := pull.LoadIssue(ctx); err != nil { + log.Error("pull.LoadIssue: %v", err) + return + } + if err := pull.Issue.LoadRepo(ctx); err != nil { + log.Error("pull.Issue.LoadRepo: %v", err) + return + } + if err := pull.Issue.LoadPoster(ctx); err != nil { + log.Error("pull.Issue.LoadPoster: %v", err) + return + } + + mode, _ := access_model.AccessLevel(ctx, pull.Issue.Poster, pull.Issue.Repo) + + newNotifyInputFromIssue(pull.Issue, webhook_module.HookEventPullRequest). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueOpened, + Index: pull.Issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, pull, nil), + Repository: convert.ToRepo(ctx, pull.Issue.Repo, mode), + Sender: convert.ToUser(pull.Issue.Poster, nil), + }). + WithPullRequest(pull). + Notify(ctx) +} + +func (n *actionsNotifier) NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { + ctx = withMethod(ctx, "NotifyCreateRepository") + + newNotifyInput(repo, doer, webhook_module.HookEventRepository).WithPayload(&api.RepositoryPayload{ + Action: api.HookRepoCreated, + Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Organization: convert.ToUser(u, nil), + Sender: convert.ToUser(doer, nil), + }).Notify(ctx) +} + +func (n *actionsNotifier) NotifyForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { + ctx = withMethod(ctx, "NotifyForkRepository") + + oldMode, _ := access_model.AccessLevel(ctx, doer, oldRepo) + mode, _ := access_model.AccessLevel(ctx, doer, repo) + + // forked webhook + newNotifyInput(oldRepo, doer, webhook_module.HookEventFork).WithPayload(&api.ForkPayload{ + Forkee: convert.ToRepo(ctx, oldRepo, oldMode), + Repo: convert.ToRepo(ctx, repo, mode), + Sender: convert.ToUser(doer, nil), + }).Notify(ctx) + + u := repo.MustOwner(ctx) + + // Add to hook queue for created repo after session commit. + if u.IsOrganization() { + newNotifyInput(repo, doer, webhook_module.HookEventRepository). + WithRef(oldRepo.DefaultBranch). + WithPayload(&api.RepositoryPayload{ + Action: api.HookRepoCreated, + Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Organization: convert.ToUser(u, nil), + Sender: convert.ToUser(doer, nil), + }).Notify(ctx) + } +} + +func (n *actionsNotifier) NotifyPullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, _ *issues_model.Comment, _ []*user_model.User) { + ctx = withMethod(ctx, "NotifyPullRequestReview") + + var reviewHookType webhook_module.HookEventType + + switch review.Type { + case issues_model.ReviewTypeApprove: + reviewHookType = webhook_module.HookEventPullRequestReviewApproved + case issues_model.ReviewTypeComment: + reviewHookType = webhook_module.HookEventPullRequestComment + case issues_model.ReviewTypeReject: + reviewHookType = webhook_module.HookEventPullRequestReviewRejected + default: + // unsupported review webhook type here + log.Error("Unsupported review webhook type") + return + } + + if err := pr.LoadIssue(ctx); err != nil { + log.Error("pr.LoadIssue: %v", err) + return + } + + mode, err := access_model.AccessLevel(ctx, review.Issue.Poster, review.Issue.Repo) + if err != nil { + log.Error("models.AccessLevel: %v", err) + return + } + + newNotifyInput(review.Issue.Repo, review.Reviewer, reviewHookType). + WithRef(review.CommitID). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueReviewed, + Index: review.Issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), + Repository: convert.ToRepo(ctx, review.Issue.Repo, mode), + Sender: convert.ToUser(review.Reviewer, nil), + Review: &api.ReviewPayload{ + Type: string(reviewHookType), + Content: review.Content, + }, + }).Notify(ctx) +} + +func (*actionsNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + ctx = withMethod(ctx, "NotifyMergePullRequest") + + // Reload pull request information. + if err := pr.LoadAttributes(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + if err := pr.LoadIssue(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + if err := pr.Issue.LoadRepo(db.DefaultContext); err != nil { + log.Error("pr.Issue.LoadRepo: %v", err) + return + } + + mode, err := access_model.AccessLevel(ctx, doer, pr.Issue.Repo) + if err != nil { + log.Error("models.AccessLevel: %v", err) + return + } + + // Merge pull request calls issue.changeStatus so we need to handle separately. + apiPullRequest := &api.PullRequestPayload{ + Index: pr.Issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + Action: api.HookIssueClosed, + } + + newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequest). + WithRef(pr.MergedCommitID). + WithPayload(apiPullRequest). + WithPullRequest(pr). + Notify(ctx) +} + +func (n *actionsNotifier) NotifyPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { + ctx = withMethod(ctx, "NotifyPushCommits") + + apiPusher := convert.ToUser(pusher, nil) + apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo.RepoPath(), repo.HTMLURL()) + if err != nil { + log.Error("commits.ToAPIPayloadCommits failed: %v", err) + return + } + + newNotifyInput(repo, pusher, webhook_module.HookEventPush). + WithRef(opts.RefFullName). + WithPayload(&api.PushPayload{ + Ref: opts.RefFullName, + Before: opts.OldCommitID, + After: opts.NewCommitID, + CompareURL: setting.AppURL + commits.CompareURL, + Commits: apiCommits, + HeadCommit: apiHeadCommit, + Repo: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Pusher: apiPusher, + Sender: apiPusher, + }). + Notify(ctx) +} + +func (n *actionsNotifier) NotifyCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) { + ctx = withMethod(ctx, "NotifyCreateRef") + + apiPusher := convert.ToUser(pusher, nil) + apiRepo := convert.ToRepo(ctx, repo, perm_model.AccessModeNone) + refName := git.RefEndName(refFullName) + + newNotifyInput(repo, pusher, webhook_module.HookEventCreate). + WithRef(refName). + WithPayload(&api.CreatePayload{ + Ref: refName, + Sha: refID, + RefType: refType, + Repo: apiRepo, + Sender: apiPusher, + }). + Notify(ctx) +} + +func (n *actionsNotifier) NotifyDeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName string) { + ctx = withMethod(ctx, "NotifyDeleteRef") + + apiPusher := convert.ToUser(pusher, nil) + apiRepo := convert.ToRepo(ctx, repo, perm_model.AccessModeNone) + refName := git.RefEndName(refFullName) + + newNotifyInput(repo, pusher, webhook_module.HookEventDelete). + WithRef(refName). + WithPayload(&api.DeletePayload{ + Ref: refName, + RefType: refType, + PusherType: api.PusherTypeUser, + Repo: apiRepo, + Sender: apiPusher, + }). + Notify(ctx) +} + +func (n *actionsNotifier) NotifySyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { + ctx = withMethod(ctx, "NotifySyncPushCommits") + + apiPusher := convert.ToUser(pusher, nil) + apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(db.DefaultContext, repo.RepoPath(), repo.HTMLURL()) + if err != nil { + log.Error("commits.ToAPIPayloadCommits failed: %v", err) + return + } + + newNotifyInput(repo, pusher, webhook_module.HookEventPush). + WithRef(opts.RefFullName). + WithPayload(&api.PushPayload{ + Ref: opts.RefFullName, + Before: opts.OldCommitID, + After: opts.NewCommitID, + CompareURL: setting.AppURL + commits.CompareURL, + Commits: apiCommits, + TotalCommits: commits.Len, + HeadCommit: apiHeadCommit, + Repo: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Pusher: apiPusher, + Sender: apiPusher, + }). + Notify(ctx) +} + +func (n *actionsNotifier) NotifySyncCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) { + ctx = withMethod(ctx, "NotifySyncCreateRef") + n.NotifyCreateRef(ctx, pusher, repo, refType, refFullName, refID) +} + +func (n *actionsNotifier) NotifySyncDeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName string) { + ctx = withMethod(ctx, "NotifySyncDeleteRef") + n.NotifyDeleteRef(ctx, pusher, repo, refType, refFullName) +} + +func (n *actionsNotifier) NotifyNewRelease(ctx context.Context, rel *repo_model.Release) { + ctx = withMethod(ctx, "NotifyNewRelease") + notifyRelease(ctx, rel.Publisher, rel, rel.Sha1, api.HookReleasePublished) +} + +func (n *actionsNotifier) NotifyUpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) { + ctx = withMethod(ctx, "NotifyUpdateRelease") + notifyRelease(ctx, doer, rel, rel.Sha1, api.HookReleaseUpdated) +} + +func (n *actionsNotifier) NotifyDeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) { + ctx = withMethod(ctx, "NotifyDeleteRelease") + notifyRelease(ctx, doer, rel, rel.Sha1, api.HookReleaseDeleted) +} + +func (n *actionsNotifier) NotifyPackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { + ctx = withMethod(ctx, "NotifyPackageCreate") + notifyPackage(ctx, doer, pd, api.HookPackageCreated) +} + +func (n *actionsNotifier) NotifyPackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { + ctx = withMethod(ctx, "NotifyPackageDelete") + notifyPackage(ctx, doer, pd, api.HookPackageDeleted) +} + +func (n *actionsNotifier) NotifyAutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + ctx = withMethod(ctx, "NotifyAutoMergePullRequest") + n.NotifyMergePullRequest(ctx, doer, pr) +} + +func (n *actionsNotifier) NotifyPullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + ctx = withMethod(ctx, "NotifyPullRequestSynchronized") + + if err := pr.LoadIssue(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + if err := pr.Issue.LoadRepo(db.DefaultContext); err != nil { + log.Error("pr.Issue.LoadRepo: %v", err) + return + } + + newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequestSync). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueSynchronized, + Index: pr.Issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, perm_model.AccessModeNone), + Sender: convert.ToUser(doer, nil), + }). + WithPullRequest(pr). + Notify(ctx) +} + +func (n *actionsNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) { + ctx = withMethod(ctx, "NotifyPullRequestChangeTargetBranch") + + if err := pr.LoadIssue(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + if err := pr.Issue.LoadRepo(db.DefaultContext); err != nil { + log.Error("pr.Issue.LoadRepo: %v", err) + return + } + + mode, _ := access_model.AccessLevel(ctx, pr.Issue.Poster, pr.Issue.Repo) + newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequest). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueEdited, + Index: pr.Issue.Index, + Changes: &api.ChangesPayload{ + Ref: &api.ChangesFromPayload{ + From: oldBranch, + }, + }, + PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + }). + WithPullRequest(pr). + Notify(ctx) +} diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go new file mode 100644 index 0000000000000..5b8f6bfdf6ef4 --- /dev/null +++ b/services/actions/notifier_helper.go @@ -0,0 +1,236 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + issues_model "code.gitea.io/gitea/models/issues" + packages_model "code.gitea.io/gitea/models/packages" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/convert" + + "github.com/nektos/act/pkg/jobparser" +) + +var methodCtxKey struct{} + +// withMethod sets the notification method that this context currently executes. +// Used for debugging/ troubleshooting purposes. +func withMethod(ctx context.Context, method string) context.Context { + // don't overwrite + if v := ctx.Value(methodCtxKey); v != nil { + if _, ok := v.(string); ok { + return ctx + } + } + return context.WithValue(ctx, methodCtxKey, method) +} + +// getMethod gets the notification method that this context currently executes. +// Default: "notify" +// Used for debugging/ troubleshooting purposes. +func getMethod(ctx context.Context) string { + if v := ctx.Value(methodCtxKey); v != nil { + if s, ok := v.(string); ok { + return s + } + } + return "notify" +} + +type notifyInput struct { + // required + Repo *repo_model.Repository + Doer *user_model.User + Event webhook_module.HookEventType + + // optional + Ref string + Payload api.Payloader + PullRequest *issues_model.PullRequest +} + +func newNotifyInput(repo *repo_model.Repository, doer *user_model.User, event webhook_module.HookEventType) *notifyInput { + return ¬ifyInput{ + Repo: repo, + Doer: doer, + Event: event, + } +} + +func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput { + input.Doer = doer + return input +} + +func (input *notifyInput) WithRef(ref string) *notifyInput { + input.Ref = ref + return input +} + +func (input *notifyInput) WithPayload(payload api.Payloader) *notifyInput { + input.Payload = payload + return input +} + +func (input *notifyInput) WithPullRequest(pr *issues_model.PullRequest) *notifyInput { + input.PullRequest = pr + if input.Ref == "" { + input.Ref = pr.GetGitRefName() + } + return input +} + +func (input *notifyInput) Notify(ctx context.Context) { + log.Trace("execute %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name) + + if err := notify(ctx, input); err != nil { + log.Error("an error occurred while executing the %s actions method: %v", getMethod(ctx), err) + } +} + +func notify(ctx context.Context, input *notifyInput) error { + if input.Doer.IsActions() { + // avoiding triggering cyclically, for example: + // a comment of an issue will trigger the runner to add a new comment as reply, + // and the new comment will trigger the runner again. + log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name) + return nil + } + if unit_model.TypeActions.UnitGlobalDisabled() { + return nil + } + if err := input.Repo.LoadUnits(ctx); err != nil { + return fmt.Errorf("repo.LoadUnits: %w", err) + } else if !input.Repo.UnitEnabled(ctx, unit_model.TypeActions) { + return nil + } + + gitRepo, err := git.OpenRepository(context.Background(), input.Repo.RepoPath()) + if err != nil { + return fmt.Errorf("git.OpenRepository: %w", err) + } + defer gitRepo.Close() + + ref := input.Ref + if ref == "" { + ref = input.Repo.DefaultBranch + } + + // Get the commit object for the ref + commit, err := gitRepo.GetCommit(ref) + if err != nil { + return fmt.Errorf("gitRepo.GetCommit: %w", err) + } + + workflows, err := actions_module.DetectWorkflows(commit, input.Event, input.Payload) + if err != nil { + return fmt.Errorf("DetectWorkflows: %w", err) + } + + if len(workflows) == 0 { + log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) + return nil + } + + p, err := json.Marshal(input.Payload) + if err != nil { + return fmt.Errorf("json.Marshal: %w", err) + } + + for id, content := range workflows { + run := actions_model.ActionRun{ + Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], + RepoID: input.Repo.ID, + OwnerID: input.Repo.OwnerID, + WorkflowID: id, + TriggerUserID: input.Doer.ID, + Ref: ref, + CommitSHA: commit.ID.String(), + IsForkPullRequest: input.PullRequest != nil && input.PullRequest.IsFromFork(), + Event: input.Event, + EventPayload: string(p), + Status: actions_model.StatusWaiting, + } + jobs, err := jobparser.Parse(content) + if err != nil { + log.Error("jobparser.Parse: %v", err) + continue + } + if err := actions_model.InsertRun(ctx, &run, jobs); err != nil { + log.Error("InsertRun: %v", err) + continue + } + if jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID}); err != nil { + log.Error("FindRunJobs: %v", err) + } else { + for _, job := range jobs { + if err := CreateCommitStatus(ctx, job); err != nil { + log.Error("CreateCommitStatus: %v", err) + } + } + } + + } + return nil +} + +func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput { + return newNotifyInput(issue.Repo, issue.Poster, event) +} + +func notifyRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release, ref string, action api.HookReleaseAction) { + if err := rel.LoadAttributes(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + mode, _ := access_model.AccessLevel(ctx, doer, rel.Repo) + + newNotifyInput(rel.Repo, doer, webhook_module.HookEventRelease). + WithRef(ref). + WithPayload(&api.ReleasePayload{ + Action: action, + Release: convert.ToRelease(rel), + Repository: convert.ToRepo(ctx, rel.Repo, mode), + Sender: convert.ToUser(doer, nil), + }). + Notify(ctx) +} + +func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) { + if pd.Repository == nil { + // When a package is uploaded to an organization, it could trigger an event to notify. + // So the repository could be nil, however, actions can't support that yet. + // See https://github.com/go-gitea/gitea/pull/17940 + return + } + + apiPackage, err := convert.ToPackage(ctx, pd, sender) + if err != nil { + log.Error("Error converting package: %v", err) + return + } + + newNotifyInput(pd.Repository, sender, webhook_module.HookEventPackage). + WithPayload(&api.PackagePayload{ + Action: action, + Package: apiPackage, + Sender: convert.ToUser(sender, nil), + }). + Notify(ctx) +} diff --git a/services/auth/basic.go b/services/auth/basic.go index 5fb80703ab5a4..dc03780905c96 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" @@ -70,6 +71,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore log.Trace("Basic Authorization: Attempting login with username as token") } + // check oauth2 token uid := CheckOAuthAccessToken(authToken) if uid != 0 { log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid) @@ -84,6 +86,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore return u, nil } + // check personal access token token, err := auth_model.GetAccessTokenBySHA(authToken) if err == nil { log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid) @@ -104,6 +107,17 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore log.Error("GetAccessTokenBySha: %v", err) } + // check task token + task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken) + if err == nil && task != nil { + log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) + + store.GetData()["IsActionsToken"] = true + store.GetData()["ActionsTaskID"] = task.ID + + return user_model.NewActionsUser(), nil + } + if !setting.Service.EnableBasicAuth { return nil, nil } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 1be78b85c5efa..b70f84da9b639 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -9,6 +9,7 @@ import ( "strings" "time" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" @@ -94,7 +95,18 @@ func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 { } t, err := auth_model.GetAccessTokenBySHA(tokenSHA) if err != nil { - if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { + if auth_model.IsErrAccessTokenNotExist(err) { + // check task token + task, err := actions_model.GetRunningTaskByToken(db.DefaultContext, tokenSHA) + if err == nil && task != nil { + log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) + + store.GetData()["IsActionsToken"] = true + store.GetData()["ActionsTaskID"] = task.ID + + return user_model.ActionsUserID + } + } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { log.Error("GetAccessTokenBySHA: %v", err) } return 0 @@ -118,12 +130,13 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor } id := o.userIDFromToken(req, store) - if id <= 0 { + + if id <= 0 && id != -2 { // -2 means actions, so we need to allow it. return nil, nil } log.Trace("OAuth2 Authorization: Found token for user[%d]", id) - user, err := user_model.GetUserByID(req.Context(), id) + user, err := user_model.GetPossibleUserByID(req.Context(), id) if err != nil { if !user_model.IsErrUserNotExist(err) { log.Error("GetUserByName: %v", err) diff --git a/services/cron/cron.go b/services/cron/cron.go index bda8f12f122e5..72deb94cebe14 100644 --- a/services/cron/cron.go +++ b/services/cron/cron.go @@ -30,6 +30,7 @@ func NewContext(original context.Context) { _, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().ShutdownContext(), "Service: Cron", process.SystemProcessType, true) initBasicTasks() initExtendedTasks() + initActionsTasks() lock.Lock() for _, task := range tasks { diff --git a/services/cron/tasks_actions.go b/services/cron/tasks_actions.go new file mode 100644 index 0000000000000..30e8749a5e189 --- /dev/null +++ b/services/cron/tasks_actions.go @@ -0,0 +1,51 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cron + +import ( + "context" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + actions_service "code.gitea.io/gitea/services/actions" +) + +func initActionsTasks() { + if !setting.Actions.Enabled { + return + } + registerStopZombieTasks() + registerStopEndlessTasks() + registerCancelAbandonedJobs() +} + +func registerStopZombieTasks() { + RegisterTaskFatal("stop_zombie_tasks", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 5m", + }, func(ctx context.Context, _ *user_model.User, cfg Config) error { + return actions_service.StopZombieTasks(ctx) + }) +} + +func registerStopEndlessTasks() { + RegisterTaskFatal("stop_endless_tasks", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 30m", + }, func(ctx context.Context, _ *user_model.User, cfg Config) error { + return actions_service.StopEndlessTasks(ctx) + }) +} + +func registerCancelAbandonedJobs() { + RegisterTaskFatal("cancel_abandoned_jobs", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 6h", + }, func(ctx context.Context, _ *user_model.User, cfg Config) error { + return actions_service.CancelAbandonedJobs(ctx) + }) +} diff --git a/services/forms/package_form.go b/services/forms/package_form.go index 734bb05dc69f7..e78e64ef7ed18 100644 --- a/services/forms/package_form.go +++ b/services/forms/package_form.go @@ -15,7 +15,7 @@ import ( type PackageCleanupRuleForm struct { ID int64 Enabled bool - Type string `binding:"Required;In(composer,conan,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"` + Type string `binding:"Required;In(composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"` KeepCount int `binding:"In(0,1,5,10,25,50,100)"` KeepPattern string `binding:"RegexPattern"` RemoveDays int `binding:"In(0,7,14,30,60,90,180)"` diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index b7687af2b5af4..436d79df68cb6 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -146,8 +146,10 @@ type RepoSettingForm struct { ExternalTrackerRegexpPattern string EnableCloseIssuesViaCommitInAnyBranch bool EnableProjects bool + EnableReleases bool EnablePackages bool EnablePulls bool + EnableActions bool PullsIgnoreWhitespace bool PullsAllowMerge bool PullsAllowRebase bool diff --git a/services/forms/runner.go b/services/forms/runner.go new file mode 100644 index 0000000000000..906306034628f --- /dev/null +++ b/services/forms/runner.go @@ -0,0 +1,25 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forms + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web/middleware" + + "gitea.com/go-chi/binding" +) + +// EditRunnerForm form for admin to create runner +type EditRunnerForm struct { + Description string + CustomLabels string // comma-separated +} + +// Validate validates form fields +func (f *EditRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} diff --git a/services/packages/packages.go b/services/packages/packages.go index 754dfa7110235..9e52cb1450962 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -339,6 +339,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p typeSpecificSize = setting.Packages.LimitSizeComposer case packages_model.TypeConan: typeSpecificSize = setting.Packages.LimitSizeConan + case packages_model.TypeConda: + typeSpecificSize = setting.Packages.LimitSizeConda case packages_model.TypeContainer: typeSpecificSize = setting.Packages.LimitSizeContainer case packages_model.TypeGeneric: diff --git a/services/pull/pull.go b/services/pull/pull.go index c983c4f3e78ec..317875d2112d1 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -298,7 +298,6 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, } } - pr.Issue.PullRequest = pr notification.NotifyPullRequestSynchronized(ctx, doer, pr) } } diff --git a/services/repository/push.go b/services/repository/push.go index 01352433886b9..ef6460cef4231 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -110,9 +110,13 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } if opts.IsTag() { // If is tag reference if pusher == nil || pusher.ID != opts.PusherID { - var err error - if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil { - return err + if opts.PusherID == user_model.ActionsUserID { + pusher = user_model.NewActionsUser() + } else { + var err error + if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil { + return err + } } } tagName := opts.TagName() @@ -150,9 +154,13 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } } else if opts.IsBranch() { // If is branch reference if pusher == nil || pusher.ID != opts.PusherID { - var err error - if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil { - return err + if opts.PusherID == user_model.ActionsUserID { + pusher = user_model.NewActionsUser() + } else { + var err error + if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil { + return err + } } } diff --git a/templates/admin/applications/list.tmpl b/templates/admin/applications/list.tmpl index 6d627129df0e9..4da6cb044659c 100644 --- a/templates/admin/applications/list.tmpl +++ b/templates/admin/applications/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
diff --git a/templates/admin/applications/oauth2_edit.tmpl b/templates/admin/applications/oauth2_edit.tmpl index 84d821eccacea..20231c4b1ca94 100644 --- a/templates/admin/applications/oauth2_edit.tmpl +++ b/templates/admin/applications/oauth2_edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}} {{template "user/settings/applications_oauth2_edit_form" .}} diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index bf9d53152c2e7..91e4b1df52513 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl index c43287ee1ae0d..afe814cc6cb2f 100644 --- a/templates/admin/auth/list.tmpl +++ b/templates/admin/auth/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 213c621b42f21..ab84dfccaf981 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 982cfb2800815..8f572c8396128 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} @@ -303,14 +303,14 @@
{{.locale.Tr "admin.config.disable_gravatar"}}
- +
{{.locale.Tr "admin.config.enable_federated_avatar"}}
- +
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 80eea91210cc2..40f28068d7d63 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index f5f6a86dc8ae6..091f5011f9530 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index 407eae9646cba..0c018ff2939e2 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/hooks.tmpl b/templates/admin/hooks.tmpl index a23cff4342697..26f92c7064101 100644 --- a/templates/admin/hooks.tmpl +++ b/templates/admin/hooks.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/monitor.tmpl b/templates/admin/monitor.tmpl index f11d071ea433c..d53e9e18dcc8c 100644 --- a/templates/admin/monitor.tmpl +++ b/templates/admin/monitor.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index cfd32b6484036..97ee9171671d9 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -33,6 +33,11 @@ {{.locale.Tr "settings.applications"}} {{end}} + {{if .EnableActions}} + + {{.locale.Tr "actions.runners"}} + + {{end}} {{.locale.Tr "admin.config"}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index 36752d47b26e4..a2c7ca2f6ae83 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl index 11dc23c60eb90..9bf7a6268e5b7 100644 --- a/templates/admin/org/list.tmpl +++ b/templates/admin/org/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 4c96d2bf1053f..bb36ca1ae358e 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/queue.tmpl b/templates/admin/queue.tmpl index 675ec417e40ee..19dd70da12146 100644 --- a/templates/admin/queue.tmpl +++ b/templates/admin/queue.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 837802f0d0ce7..11216b8c86399 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index 0c27c80e930be..ca0b4c3bb9a02 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/runners/base.tmpl b/templates/admin/runners/base.tmpl new file mode 100644 index 0000000000000..a00a4dbc2615d --- /dev/null +++ b/templates/admin/runners/base.tmpl @@ -0,0 +1,8 @@ +{{template "base/head" .}} +
+ {{template "admin/navbar" .}} +
+ {{template "shared/actions/runner_list" .}} +
+
+{{template "base/footer" .}} diff --git a/templates/admin/runners/edit.tmpl b/templates/admin/runners/edit.tmpl new file mode 100644 index 0000000000000..0963e97a5b6cc --- /dev/null +++ b/templates/admin/runners/edit.tmpl @@ -0,0 +1,8 @@ +{{template "base/head" .}} +
+ {{template "admin/navbar" .}} +
+ {{template "shared/actions/runner_edit" .}} +
+
+{{template "base/footer" .}} diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index 91929deaa8884..4e16036ae3a3c 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 9e0f1d89fd15e..ef436c718a6de 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 67a726eb3f6d1..88af2172b7612 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 9fdf0dce93dd2..e5ca864cb77d7 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/explore/code.tmpl b/templates/explore/code.tmpl index f4e46d119876e..924a3e627482a 100644 --- a/templates/explore/code.tmpl +++ b/templates/explore/code.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "code/searchform" .}} diff --git a/templates/explore/organizations.tmpl b/templates/explore/organizations.tmpl index 36267baf33954..56c6ee56a076f 100644 --- a/templates/explore/organizations.tmpl +++ b/templates/explore/organizations.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "explore/search" .}} diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl index 457ad74115e44..dfede2ffcc065 100644 --- a/templates/explore/repos.tmpl +++ b/templates/explore/repos.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "explore/repo_search" .}} diff --git a/templates/explore/users.tmpl b/templates/explore/users.tmpl index 5fb79c0dd027a..336daea7c8a2c 100644 --- a/templates/explore/users.tmpl +++ b/templates/explore/users.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "explore/search" .}} diff --git a/templates/home.tmpl b/templates/home.tmpl index 87192fa55b6e2..52ad1b12dfc1e 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
diff --git a/templates/install.tmpl b/templates/install.tmpl index 0625f43cc4e62..3d33dcbcb766b 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+

diff --git a/templates/org/create.tmpl b/templates/org/create.tmpl index 26c432303ce00..8da46fb325b24 100644 --- a/templates/org/create.tmpl +++ b/templates/org/create.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 64742044936e0..b3724e7a63f13 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{avatar .Org 140 "org-avatar"}}
@@ -13,7 +13,7 @@ {{if .Org.Visibility.IsPrivate}}
{{.locale.Tr "org.settings.visibility.private_shortname"}}
{{end}}
- {{if $.RenderedDescription}}

{{$.RenderedDescription|Str2html}}

{{end}} + {{if $.RenderedDescription}}
{{$.RenderedDescription|Str2html}}
{{end}}
{{if .Org.Location}}
{{svg "octicon-location"}} {{.Org.Location}}
{{end}} {{if .Org.Website}}
{{svg "octicon-link"}} {{.Org.Website}}
{{end}} diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index fceb0801341bd..b4f788e52308f 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl index 544ed387423c5..1f113b28c84c9 100644 --- a/templates/org/projects/list.tmpl +++ b/templates/org/projects/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}} {{template "projects/list" .}}
diff --git a/templates/org/projects/new.tmpl b/templates/org/projects/new.tmpl index b3d6c6001e376..f550cdc828d22 100644 --- a/templates/org/projects/new.tmpl +++ b/templates/org/projects/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}} {{template "projects/new" .}}
diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl index 03327e253001e..c2d8f015f105e 100644 --- a/templates/org/projects/view.tmpl +++ b/templates/org/projects/view.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}} {{template "projects/view" .}}
diff --git a/templates/org/settings/applications.tmpl b/templates/org/settings/applications.tmpl index 8bdd99deb8621..35736df591eb3 100644 --- a/templates/org/settings/applications.tmpl +++ b/templates/org/settings/applications.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/applications_oauth2_edit.tmpl b/templates/org/settings/applications_oauth2_edit.tmpl index 2c7fa842b3b3a..861651a15e7d7 100644 --- a/templates/org/settings/applications_oauth2_edit.tmpl +++ b/templates/org/settings/applications_oauth2_edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}} {{template "user/settings/applications_oauth2_edit_form" .}} diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl index 8a38dcafe3562..669e393e1decc 100644 --- a/templates/org/settings/delete.tmpl +++ b/templates/org/settings/delete.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index 081f5a2839487..4685225f4c863 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/hooks.tmpl b/templates/org/settings/hooks.tmpl index cb0243bb1eb4d..3abbc62ecf877 100644 --- a/templates/org/settings/hooks.tmpl +++ b/templates/org/settings/hooks.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl index a6b5405a42841..5436bcba05ca6 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index 765bb6aaaef29..7380deff779be 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -25,6 +25,11 @@ {{.locale.Tr "packages.title"}} {{end}} + {{if .EnableActions}} + + {{.locale.Tr "actions.runners"}} + + {{end}} {{.locale.Tr "org.settings.delete"}} diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 1b12c1221b97b..b52639fb19b98 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/packages.tmpl b/templates/org/settings/packages.tmpl index bb5d95e1072a2..412a9813c21b4 100644 --- a/templates/org/settings/packages.tmpl +++ b/templates/org/settings/packages.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/packages_cleanup_rules_edit.tmpl b/templates/org/settings/packages_cleanup_rules_edit.tmpl index 8c3725f4d7e9e..195c21da0c541 100644 --- a/templates/org/settings/packages_cleanup_rules_edit.tmpl +++ b/templates/org/settings/packages_cleanup_rules_edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/packages_cleanup_rules_preview.tmpl b/templates/org/settings/packages_cleanup_rules_preview.tmpl index e0e4652c367eb..771e6cb8f3dcf 100644 --- a/templates/org/settings/packages_cleanup_rules_preview.tmpl +++ b/templates/org/settings/packages_cleanup_rules_preview.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/runners.tmpl b/templates/org/settings/runners.tmpl new file mode 100644 index 0000000000000..7c70f6d956b40 --- /dev/null +++ b/templates/org/settings/runners.tmpl @@ -0,0 +1,13 @@ +{{template "base/head" .}} +
+ {{template "org/header" .}} +
+
+ {{template "org/settings/navbar" .}} +
+ {{template "shared/actions/runner_list" .}} +
+
+
+
+{{template "base/footer" .}} diff --git a/templates/org/settings/runners_edit.tmpl b/templates/org/settings/runners_edit.tmpl new file mode 100644 index 0000000000000..931ca3701ab19 --- /dev/null +++ b/templates/org/settings/runners_edit.tmpl @@ -0,0 +1,13 @@ +{{template "base/head" .}} +
+ {{template "org/header" .}} +
+
+ {{template "org/settings/navbar" .}} +
+ {{template "shared/actions/runner_edit" .}} +
+
+
+
+{{template "base/footer" .}} diff --git a/templates/org/settings/secrets.tmpl b/templates/org/settings/secrets.tmpl index dd2a437b75253..909c16f4484e2 100644 --- a/templates/org/settings/secrets.tmpl +++ b/templates/org/settings/secrets.tmpl @@ -1,83 +1,15 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "org/settings/navbar" .}}
{{template "base/alert" .}} -

- {{.locale.Tr "secrets.secrets"}} -
-
{{.locale.Tr "secrets.creation"}}
-
-

-
-
- - {{.CsrfTokenHtml}} -
- {{.locale.Tr "secrets.description"}} -
-
- - -
-
- - -
- - - -
- {{if .Secrets}} -
- {{range .Secrets}} -
-
- -
-
- {{svg "octicon-key" 32}} -
-
- {{.Name}} -
******
-
- - {{$.locale.Tr "settings.add_on"}} - {{.CreatedUnix.FormatShort}} - -
-
-
- {{end}} -
- {{else}} - {{.locale.Tr "secrets.none"}} - {{end}} -
+ {{template "shared/secrets/add_list" .}}
- - {{template "base/footer" .}} diff --git a/templates/org/team/invite.tmpl b/templates/org/team/invite.tmpl index a696d99498007..ef365dee3a0d0 100644 --- a/templates/org/team/invite.tmpl +++ b/templates/org/team/invite.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "base/alert" .}}
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index 1a58dc5339ec8..13bd6c5b5db22 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index 10b5abda31457..d9d3902fe1a37 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
@@ -78,7 +78,7 @@ {{.locale.Tr "units.unit"}} {{.locale.Tr "org.teams.none_access"}} - {{svg "octicon-question" 16 "ml-2"}} + {{svg "octicon-question" 16 "ml-2"}} {{.locale.Tr "org.teams.read_access"}} {{svg "octicon-question" 16 "ml-2"}} {{.locale.Tr "org.teams.write_access"}} @@ -99,17 +99,17 @@
- +
- +
- +
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 80bdf7b3dbfc0..0391a762faf22 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 5399f3a594081..66ac4a4211961 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/package/content/conda.tmpl b/templates/package/content/conda.tmpl new file mode 100644 index 0000000000000..ecc26bce98e64 --- /dev/null +++ b/templates/package/content/conda.tmpl @@ -0,0 +1,30 @@ +{{if eq .PackageDescriptor.Package.Type "conda"}} +

{{.locale.Tr "packages.installation"}}

+
+
+
+ +
channel_alias: {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
+channels:
+  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
+default_channels:
+  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
+
+
+ + {{$channel := .PackageDescriptor.PackageProperties.GetByName "conda.channel"}} +
conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}
+
+
+ +
+
+
+ + {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Summary}} +

{{.locale.Tr "packages.about"}}

+
+ {{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{else}}{{.PackageDescriptor.Metadata.Summary}}{{end}} +
+ {{end}} +{{end}} diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl new file mode 100644 index 0000000000000..2201c803562bb --- /dev/null +++ b/templates/package/metadata/conda.tmpl @@ -0,0 +1,6 @@ +{{if eq .PackageDescriptor.Package.Type "conda"}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.conda.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.conda.details.documentation_site"}}
{{end}} +{{end}} diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl index d1475be593c6c..a2c4bd4c28293 100644 --- a/templates/package/settings.tmpl +++ b/templates/package/settings.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}}
{{template "base/alert" .}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index a5b2a2ef68a39..4611cdb8d840f 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}}
@@ -21,6 +21,7 @@
{{template "package/content/composer" .}} {{template "package/content/conan" .}} + {{template "package/content/conda" .}} {{template "package/content/container" .}} {{template "package/content/generic" .}} {{template "package/content/helm" .}} @@ -44,6 +45,7 @@
{{svg "octicon-download" 16 "mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}
{{template "package/metadata/composer" .}} {{template "package/metadata/conan" .}} + {{template "package/metadata/conda" .}} {{template "package/metadata/container" .}} {{template "package/metadata/generic" .}} {{template "package/metadata/helm" .}} diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl index 4abfe171dddd9..e098f43fda82e 100644 --- a/templates/post-install.tmpl +++ b/templates/post-install.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index ae2eaec6eaa7a..21a3350a75db1 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -1,4 +1,4 @@ -
+
{{if .CanWriteProjects}}