├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── check ├── git-in ├── in └── out └── test ├── all.sh ├── check.sh ├── helpers.sh ├── in.sh └── out.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # Workflow trigger Event 4 | on: 5 | push: 6 | # Publish semver tags as releases. 7 | tags: ['v*.*.*'] 8 | pull_request: 9 | branches: [master] 10 | 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository }} 14 | 15 | jobs: 16 | build: 17 | name: Push Docker image to GitHub Container Registry 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | packages: write 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | 27 | - name: Setup Docker buildx 28 | uses: docker/setup-buildx-action@v2 29 | 30 | # Login against a Docker registry except on PR 31 | # https://github.com/docker/login-action 32 | - name: Log into registry ${{ env.REGISTRY }} 33 | if: github.event_name != 'pull_request' 34 | uses: docker/login-action@v2 35 | with: 36 | registry: ${{ env.REGISTRY }} 37 | username: ${{ github.actor }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | # Extract metadata (tags, labels) for Docker 41 | # https://github.com/docker/metadata-action 42 | - name: Extract Docker metadata 43 | id: meta 44 | uses: docker/metadata-action@v4 45 | with: 46 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 47 | 48 | # Build and push Docker image with Buildx (don't push on PR) 49 | # https://github.com/docker/build-push-action 50 | - name: Build and push Docker image 51 | id: build-and-push 52 | uses: docker/build-push-action@v4 53 | with: 54 | context: . 55 | push: ${{ github.event_name != 'pull_request' }} 56 | tags: ${{ steps.meta.outputs.tags }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | test/gpg/* 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | ## 2.0.0 3 | 4 | - no changes on the code itself. We now publish the container on ghcr instead of docker hub. 5 | - We move out of beta, we've been running 2.0 without changes in production 6 | 7 | ## 2.0.0-beta1 8 | 9 | - Use a custom `in` script instead of the script provided by the embedded [git-resource](https://github.com/concourse/git-resource). 10 | This allows us to efficiently fetch shallow clones at exactly a specified 11 | revision without incurring needless `git fetch --deepen` roundtrips. 12 | 13 | Note: this release can break configuration for the `get` step in your pipelines as 14 | it removes configuration options inherited from `git-resource` that don't make 15 | sense in the context of gate-resource anymore. This helps to achieve better `get` performance and is critical for large gate repository. 16 | 17 | Please review the [README](./README.md) for updated configuration options. 18 | 19 | ## 1.1.1 20 | 21 | - Patch an issue with shallow-clones not fetching at the correct depth in git-resource. 22 | See https://github.com/concourse/git-resource/pull/316 for more details. 23 | 24 | ## 1.1.0 25 | 26 | - Updates git-resource to 1.7.0 27 | - Use shallow clones for cloning the gate repository. This should increase performance. 28 | 29 | ## 1.0.0 30 | 31 | Initial Release. 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM concourse/git-resource:1.7.0 as resource 2 | 3 | RUN mv /opt/resource /opt/git-resource 4 | 5 | ADD assets/ /opt/resource/ 6 | RUN chmod +x /opt/resource/* 7 | 8 | FROM resource AS tests 9 | ADD test/ /tests 10 | RUN /tests/all.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | user=meshcloud 2 | name=gate-resource 3 | image=$(user)/$(name) 4 | tag=$(shell git log --format="%h" -n 1) 5 | 6 | docker=docker 7 | dockerfile = Dockerfile 8 | 9 | build: 10 | $(docker) build -t $(image):$(tag) -f $(dockerfile) . 11 | 12 | push: build 13 | $(docker) push $(image):$(tag) 14 | $(docker) tag $(image):$(tag) $(image):latest 15 | $(docker) push $(image):latest 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gate-resource 2 | 3 | [![Build](https://github.com/meshcloud/gate-resource/actions/workflows/release.yml/badge.svg)](https://github.com/meshcloud/gate-resource/actions/workflows/release.yml) 4 | 5 | A generic gate resource for Concourse CI. 6 | 7 | Allows you to model quality gates and pipeline control flow. 8 | 9 | This resource is backed by a Git repository and wraps [git-resource](https://github.com/concourse/git-resource). 10 | 11 | A public container build of this repo is available at ghcr.io [meshcloud/gate-resource](https://github.com/meshcloud/gate-resource/pkgs/container/gate-resource). 12 | 13 | > Contributors Welcome: This resource is new and hot off the press. We welcome your feedback and contributions! 14 | 15 | ## Example 16 | 17 | To see an example pipeline using gate-resource, head over to [gate-resource-example](https://github.com/Meshcloud/gate-resource-example). 18 | 19 | ## Git Repository Structure 20 | 21 | ```text 22 | . 23 | ├── my-gate 24 | │ ├── 1 25 | │ └── 2 26 | └── my-autogate 27 | ├── a 28 | └── b.autogate 29 | ``` 30 | 31 | > note: The gate repository is currently append-only. Files are not expected to be deleted or cleaned. 32 | 33 | The gate-resource supports two types of gates: simple-gates and auto-gates. 34 | 35 | ### Simple-gates 36 | 37 | Each folder in the repository represents a gate. Files in each gate-folder represent items that successfully passed the gate. The files may be empty or contain any metadata that you whish to track. In the example above, `my-gate` is a simple-gate 38 | 39 | ### Auto-gates 40 | 41 | Auto-gates are gates that automatically close depending on items passing through other gates. An auto-gate contains `.autoclose` items, which is a simple text file that contains dependant items, one on each line ("autoclose spec"). For example, `b.autoclose` depends on these two items passing through `my-gate`: 42 | 43 | ```b.autoclose 44 | my-gate/2 45 | my-gate/3 46 | # metadata after this line 47 | arbitrary content goes here 48 | ``` 49 | 50 | When all dependant items passed, the autoclose item closes and drops the `.autoclose` extension from its filename. Autoclose files can also contain additional metadata. To separate the autoclose spec from additional metadata, gate-resource looks for a line starting with `#` and stops interpreting any lines after this. 51 | 52 | #### The "none" version 53 | 54 | Unfortunately Concourse does not support emitting an empty set of versions from `out`. However, this is necessary for gate-resource as it may not find any autoclose items to close in a particular update. As a workaround, gate-resource emits the `none` version. `in` will no-op on encountering this version. 55 | 56 | Since concourse detects that the `none` version already exists after the first time it's generated, it will only trigger a single build when an auto-gate is used with `trigger: true`. You can detect that the `none` version was fetched when no `passed` and `metadata` files were created by the `get` step (see below). 57 | 58 | ## Source Configuration 59 | 60 | * `git`: *Required.* Configuration of the repository. Supports the following subset of [git-resource](https://github.com/concourse/git-resource) configuration options (review the link for descriptions) 61 | * *Required*: `uri`, `branch`, `private_key` 62 | * *Optional*: `git_config`, `short_ref_format`, 63 | * `gate`: *Optional.* The gate to track. 64 | 65 | ## Behavior 66 | 67 | ### `check`: Check for changes to the gate 68 | 69 | The repository is cloned (or pulled if already present), and any commits made to the specified `gate` from the given version on are checked for items passing through the gate. 70 | 71 | > note: If you want to ensure the resource triggers for every item that passed the gate, use the resource with `version: every` 72 | 73 | ### `in`: Fetch an item that passed a gate 74 | 75 | Outputs 2 files: 76 | 77 | * `passed`: Contains the name of the item that passed the gate 78 | * `metadata`: Contains the contents of whatever was in your gate item. This is 79 | useful for environment configuration settings or documenting approval workflows. 80 | 81 | > note: the git repository cloned during `in` is a shallow clone and does not track an upstream branch. 82 | > This is not a problem when using the `out` step to create or update gates. 83 | 84 | ### `out`: Pass an item through a gate 85 | 86 | Performs one of the following actions to change the state of a gate. 87 | 88 | #### Parameters 89 | 90 | One of the following is required. 91 | 92 | * `item_file`: Path to a file containing an item to pass through the gate. Wildcards are allowed, but should match only a single item. This file may also be a `.autogate` file. 93 | * `update_autoclose`: Process pending autoclose items in the repository 94 | 95 | > Note: Gate puts are idempotent. When putting a new gate item or `.autoclose` spec, the resource checks if a matching item already passed the gate. When this is the case, `out` will emit the version that previously produced this item. 96 | 97 | #### Creating .autoclose items 98 | 99 | `.autoclose` items can be put to the gate just like regular items. However, the version that gate-resource will emmit for this item may vary: 100 | 101 | * when a matching target item (i.e. the filename without `.autoclose` extension) already 102 | exists, emits the version that produced this target item. 103 | * when the `.autoclose` item is immediately closable (all dependent items alreday passed 104 | their gates), close the item and emit the version 105 | * otherwise emit a `none` version (see above) 106 | 107 | ## Development 108 | 109 | ### Prerequisites 110 | 111 | * docker is *required* - version 17.06.x is tested; earlier versions may also 112 | work. 113 | 114 | ### Running the tests 115 | 116 | The tests have been embedded with the `Dockerfile`; ensuring that the testing 117 | environment is consistent across any `docker` enabled platform. When the docker 118 | image builds, the test are run inside the docker container, on failure they 119 | will stop the build. 120 | 121 | Run the tests with the following command: 122 | 123 | ```sh 124 | docker build -t gate-resource . 125 | ``` 126 | -------------------------------------------------------------------------------- /assets/check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | payload=$TMPDIR/gate-resource-request 10 | cat > $payload <&0 11 | 12 | gate=$(jq -r '.source.gate // ""' < $payload) 13 | version=$(jq -r '.version // {}' < $payload) 14 | 15 | # extract git configuration and pass it to git resource 16 | git_source=$(cat "$payload" | jq -r .source.git) 17 | git_payload=$(echo '{ "source": '$git_source', "version": '$version' }' | jq -r) 18 | 19 | # only watch the gate path 20 | git_payload=$(echo "$git_payload" | jq -r '.source.paths = ["'$gate'"]') 21 | git_payload=$(echo "$git_payload" | jq -r '.source.ignore_paths = ["'**.autoclose'"]') 22 | 23 | # forward to git-resource to let it do its checking 24 | echo "$git_payload" | /opt/git-resource/check "$@" >&3 -------------------------------------------------------------------------------- /assets/git-in: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | source /opt/git-resource/common.sh 10 | 11 | destination=$1 12 | 13 | if [ -z "$destination" ]; then 14 | echo "usage: $0 " >&2 15 | exit 1 16 | fi 17 | 18 | # for jq 19 | PATH=/usr/local/bin:$PATH 20 | 21 | bin_dir="${0%/*}" 22 | if [ "${bin_dir#/}" == "$bin_dir" ]; then 23 | bin_dir="$PWD/$bin_dir" 24 | fi 25 | 26 | payload=$(mktemp $TMPDIR/git-resource-request.XXXXXX) 27 | 28 | cat > $payload <&0 29 | 30 | load_pubkey $payload 31 | configure_https_tunnel $payload 32 | configure_git_ssl_verification $payload 33 | configure_credentials $payload 34 | 35 | uri=$(jq -r '.source.uri // ""' < $payload) 36 | git_config_payload=$(jq -r '.source.git_config // []' < $payload) 37 | ref=$(jq -r '.version.ref // "HEAD"' < $payload) 38 | depth=$(jq -r '(.params.depth // 0)' < $payload) 39 | short_ref_format=$(jq -r '(.params.short_ref_format // "%s")' < $payload) 40 | 41 | configure_git_global "${git_config_payload}" 42 | 43 | if [ -z "$uri" ]; then 44 | echo "invalid payload (missing uri):" >&2 45 | cat $payload >&2 46 | exit 1 47 | fi 48 | 49 | depthflag="" 50 | if test "$depth" -gt 0 2> /dev/null; then 51 | depthflag="--depth $depth" 52 | fi 53 | 54 | git init $destination 55 | cd $destination 56 | 57 | git remote add origin $uri 58 | git fetch origin "$ref" $depthflag 59 | 60 | # this will set master to the current commit 61 | git reset --hard FETCH_HEAD 62 | 63 | git log -1 --oneline 64 | git clean --force --force -d 65 | 66 | if [ "$ref" == "HEAD" ]; then 67 | return_ref=$(git rev-parse HEAD) 68 | else 69 | return_ref=$ref 70 | fi 71 | 72 | # Store committer email in .git/committer. Can be used to send email to last committer on failed build 73 | # Using https://github.com/mdomke/concourse-email-resource for example 74 | git --no-pager log -1 --pretty=format:"%ae" > .git/committer 75 | 76 | # Store git-resource returned version ref .git/ref. Useful to know concourse 77 | # pulled ref in following tasks and resources. 78 | echo "${return_ref}" > .git/ref 79 | 80 | # Store short ref with templating. Useful to build Docker images with 81 | # a custom tag 82 | echo "${return_ref}" | cut -c1-7 | awk "{ printf \"${short_ref_format}\", \$1 }" > .git/short_ref 83 | 84 | # Store commit message in .git/commit_message. Can be used to inform about 85 | # the content of a successfull build. 86 | # Using https://github.com/cloudfoundry-community/slack-notification-resource 87 | # for example 88 | git log -1 --format=format:%B > .git/commit_message 89 | 90 | metadata=$(git_metadata) 91 | 92 | jq -n "{ 93 | version: {ref: $(echo $return_ref | jq -R .)}, 94 | metadata: $metadata 95 | }" >&3 96 | -------------------------------------------------------------------------------- /assets/in: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | destination=$1 10 | 11 | if [ -z "$destination" ]; then 12 | echo "usage: $0 " >&2 13 | exit 1 14 | fi 15 | 16 | # for jq 17 | PATH=/usr/local/bin:$PATH 18 | 19 | payload=$(mktemp $TMPDIR/gate-resource-request.XXXXXX) 20 | cat > $payload <&0 21 | 22 | # parse parameters 23 | gate=$(jq -r '.source.gate // ""' < $payload) 24 | version=$(jq -r '.version' < $payload) 25 | version_ref=$(jq -r '.version.ref' < $payload) 26 | 27 | if [ "$version_ref" == "none" ]; then 28 | echo "version.ref is none, nothing to do" 29 | passed_item="none" 30 | else 31 | # extract git configuration and pass it to git resource 32 | # we need to fetch with --depth=2 so we can use git diff-tree to diff to the previous commit 33 | git_source=$(cat "$payload" | jq -r .source.git) 34 | git_payload=$(echo '{ 35 | "source": '$git_source', 36 | "version": '$version', 37 | "params": { 38 | "depth": 2 39 | } 40 | }' | jq -r) 41 | 42 | # forward to git-resource to let it fetch the repository 43 | echo "$git_payload" | /opt/resource/git-in $destination 44 | 45 | cd $destination 46 | 47 | # ref is available, identify the value that passsed the gate 48 | # emit the item that passed gate based on the file changed in the commit fetched 49 | 50 | echo "finding passed file" 51 | 52 | # note: if no file can be found that changed at this revision in the gate, assume it's "none" 53 | # this is a workaround for emitting "empty" versions, e.g. when `out` did put a .autoclose file 54 | # or an `out` invocation did not update any autoclose gates when it was invoked with `update_autoclose: true` 55 | changed_files=$(git diff-tree --no-commit-id --diff-filter=d --name-only -r "$version_ref" | grep "$gate/" | grep -v ".autoclose" || true) 56 | 57 | changed_files_count=$(echo $changed_files | wc -l) 58 | if [ ! "$changed_files_count" == "1" ]; then 59 | printf "could not determine exactly 1 file in version %s $version_ref. Changed files were:\\n%s" "$version_ref" "$changed_files" 60 | exit 1 61 | fi 62 | 63 | passed_file=$changed_files 64 | if [ -z "$passed_file" ]; then 65 | echo "could not determine passed file in version $version_ref" 66 | exit 1 67 | fi 68 | passed_item=$(basename $passed_file) 69 | 70 | # write two files, passed (item name) and metadata (item file) 71 | if [ ! "$passed_file" == "none" ]; then 72 | echo "$passed_item" > passed 73 | cp "$passed_file" metadata 74 | fi 75 | fi 76 | 77 | gate_meta=$(jq -n "[ 78 | { name: \"gate\", value: \"$gate\" }, 79 | { name: \"passed\", value: \"$passed_item\" } 80 | ]") 81 | 82 | jq -n "{ 83 | version: $version, 84 | metadata: $gate_meta 85 | }" >&3 86 | -------------------------------------------------------------------------------- /assets/out: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | shopt -s nullglob 10 | source /opt/git-resource/common.sh 11 | 12 | source=$1 13 | 14 | if [ -z "$source" ]; then 15 | echo "usage: $0 " 16 | exit 1 17 | fi 18 | 19 | # for jq 20 | PATH=/usr/local/bin:$PATH 21 | 22 | payload=$(mktemp $TMPDIR/gate-resource-request.XXXXXX) 23 | git_source_payload=$(mktemp $TMPDIR/gate-resource-request.git.XXXXXX) 24 | 25 | cat > $payload <&0 26 | 27 | # configure git access using git-resource functions 28 | jq -n "{ 29 | source: $(jq -r '.source.git' < $payload) 30 | }" > $git_source_payload 31 | 32 | load_pubkey $git_source_payload 33 | configure_https_tunnel $git_source_payload 34 | configure_git_ssl_verification $git_source_payload 35 | configure_credentials $git_source_payload 36 | 37 | # parse parameters 38 | gate=$(jq -r '.source.gate // ""' < $payload) 39 | item_file=$(jq -r '.params.item_file // ""' < $payload) 40 | update_autoclose=$(jq -r '.params.update_autoclose // ""' < $payload) 41 | simulate_rebase=$(jq -r '.params.for_test_only_simulate_rebase // ""' < $payload) 42 | 43 | branch=$(jq -r '.source.git.branch // ""' < $payload) 44 | uri=$(jq -r '.source.git.uri // ""' < $payload) 45 | git_config_payload=$(jq -r '.source.git.git_config // []' < $payload) 46 | 47 | configure_git_global "${git_config_payload}" 48 | 49 | dashed_line="----------------------" 50 | 51 | if [ -z "$uri" ]; then 52 | echo "invalid payload (missing uri):" >&2 53 | cat $payload >&2 54 | exit 1 55 | fi 56 | 57 | if [[ -z "$item_file" && -z "$update_autoclose" ]]; then 58 | echo "invalid payload (missing item_file or update_autoclose):" >&2 59 | cat $payload >&2 60 | exit 1 61 | fi 62 | 63 | commit() { 64 | local repository="$1" 65 | local msg="$2" 66 | git -C "$repository" add . 67 | if [ -z "$(git -C "$repository" status --porcelain)" ]; then 68 | echo "nothing to commit - skipping" 69 | else 70 | git -C "$repository" commit --quiet -m "$msg" 71 | fi 72 | } 73 | 74 | rev() { 75 | local repository="$1" 76 | local files="$2" 77 | 78 | git -C "$repository" log --format='%H' --first-parent --reverse HEAD -- "$files" | tail -n 1 79 | } 80 | 81 | try_autoclose() { 82 | local repository="$1" 83 | local gate="$2" 84 | local item="$3" 85 | 86 | local item_path="$repository/$gate/$item" 87 | # we process all item_paths as this helps debuggability 88 | local passed=true 89 | echo "" 90 | echo "try autoclose: $gate/$item" 91 | 92 | echo "$dashed_line" 93 | while IFS="" read -r p || [ -n "$p" ] 94 | do 95 | # check if this is the beginning of metadata 96 | if [[ "$p" == "#"* ]]; then 97 | break 98 | fi; 99 | 100 | echo -n "- testing $p: " 101 | if [[ ! -e "$repository/$p" ]]; then 102 | echo "fail" 103 | passed=false 104 | else 105 | echo "ok" 106 | fi; 107 | done < "$item_path" 108 | 109 | if [ $passed = true ]; then 110 | echo "=> autoclosing $gate/$item" 111 | mv "$item_path" "${item_path//\.autoclose/}" 112 | 113 | commit "$repository" "put: $gate/$item (via autoclose)" 114 | fi 115 | echo "$dashed_line" 116 | } 117 | 118 | # clone the repository at its current latest commit - we want to make more commits on top. 119 | # by default we only need to fetch a single branch and the latest commit 120 | repository=$(mktemp -d "$TMPDIR/gate-resource-repo.XXXXXX") 121 | git clone $uri --branch $branch $repository --depth=1 122 | 123 | # FOR TEST ONLY: simulate out of date repository 124 | if [ ! -z $simulate_rebase ]; then 125 | git -C $repository fetch --deepen=1 # fetch the previous commit so we can go back to it 126 | echo $(git -C $repository rev-parse HEAD~1) > $repository/.git/refs/remotes/origin/$branch 127 | git -C $repository reset --hard HEAD~1 128 | fi 129 | 130 | cd "$source" 131 | 132 | result_item="" 133 | result_ref="" 134 | 135 | # retry loop for commiting changes and pushing to the source 136 | while true; do 137 | 138 | # rollback any commits that we may have made in a failed previous iteration of the loop 139 | git -C $repository reset --hard origin/$branch 140 | git -C $repository clean --force --force -d # make sure we start with a clean slate 141 | git -C $repository pull origin $branch # this should only pull new commits 142 | 143 | # reset state 144 | result_item="" 145 | result_ref="" 146 | 147 | if [ ! -z "$item_file" ]; then 148 | # note that item_file may contain wildcards so it must be unquoted here 149 | echo "processing item_file:" $item_file 150 | item=$(basename $item_file) 151 | 152 | # the item target is where the item will end up when it's passed 153 | # for .autoclose items, this is the path without the .autoclose extension 154 | item_target="$gate/${item%.*}" 155 | 156 | # check if the item already exists in the gate 157 | if [ -f "$repository/$item_target" ]; then 158 | echo "$item_target already exists, not committing a new item" 159 | 160 | #we need to unshallow the repository to find the existing commit, otherwise rev() can't find the correct commit 161 | git -C $repository fetch --unshallow 162 | 163 | result_item=$(basename "$item_target") 164 | result_ref="$(rev "$repository" "$item_target")" # find last revision that file changed 165 | else 166 | # write and commit gate gate 167 | mkdir -p "$repository/$gate" # ensure gate-directory exists 168 | cp $item_file "$repository/$gate/$item" # item_file should be interpolated, so we don't quote it 169 | 170 | echo "" 171 | echo "wrote $gate/$item with contents:" 172 | echo "$dashed_line" 173 | cat "$repository/$gate/$item" 174 | echo "$dashed_line" 175 | 176 | commit "$repository" "put: $gate/$item" 177 | 178 | if [[ ! $item == *.autoclose ]]; then 179 | # it's not an autoclose item, emit the current revision 180 | result_item=$(basename "$item_target") 181 | result_ref=$(git -C "$repository" rev-parse HEAD) 182 | else 183 | # it's an autoclose item, try to close it immediately 184 | try_autoclose "$repository" "$gate" "$item" 185 | 186 | # check if the autoclose was successful, i.e. the item_target now exists 187 | if [ -f "$repository/$item_target" ]; then 188 | # it was closed, emit the closed revision 189 | result_item=$(basename "$item_target") 190 | result_ref=$(git -C "$repository" rev-parse HEAD) 191 | else 192 | # it was not closed, emit dummy revision 193 | result_item="none" 194 | result_ref="none" 195 | fi 196 | 197 | fi 198 | 199 | # push all commits that were made 200 | if ! git -C "$repository" push; then 201 | echo "pushing failed, retrying" 202 | continue 203 | fi 204 | 205 | fi 206 | 207 | elif [ ! -z "$update_autoclose" ]; then 208 | echo "processing update_autoclose..." 209 | 210 | for item_path in $repository/$gate/*.autoclose; do 211 | item=$(basename "$item_path") 212 | try_autoclose "$repository" "$gate" "$item" 213 | done 214 | 215 | # we may have closed multiple gates at once. However, it's important 216 | # that we only emit the _first_ gate that we closed so that the resource can 217 | # still be checked using `version: every` for all following gates that were closed 218 | 219 | # find first autoclose commit, this is the first commit after origin/master 220 | first_gate_ref=$(git -C $repository rev-list --ancestry-path origin/$branch..HEAD | tail -n 1) 221 | if [ -z $first_gate_ref ]; then 222 | # we did not close any gates, return a dummy version 223 | # concourse will detect this resource version as already existing and won't trigger anything 224 | # this could be better handled with https://github.com/concourse/concourse/issues/2660 225 | echo "no autoclose items were closed" 226 | result_item="none" 227 | result_ref="none" 228 | else 229 | echo "some autoclose items were closed, emitting the first one (remaining will be found by next resource check)" 230 | passed_file=$(git -C "$repository" diff-tree --no-commit-id --name-only -r "$first_gate_ref" | grep "$gate") 231 | 232 | result_item=$(basename $passed_file) 233 | result_ref=$first_gate_ref 234 | 235 | if ! git -C $repository push; then 236 | echo "pushing failed, retrying" 237 | continue 238 | fi 239 | fi 240 | else 241 | echo "invalid out parameters, neither update_autoclose or item_file specified" 242 | exit 1 243 | fi 244 | 245 | break 246 | done 247 | 248 | # return result 249 | result_meta=$(jq -n "[ 250 | { name: \"gate\", value: \"$gate\" }, 251 | { name: \"passed\", value: \"$result_item\" } 252 | ]") 253 | 254 | jq -n "{ 255 | version: { ref: $(echo $result_ref | jq -R .) }, 256 | metadata: $result_meta 257 | }" >&3 -------------------------------------------------------------------------------- /test/all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | $(dirname $0)/in.sh 6 | $(dirname $0)/check.sh 7 | $(dirname $0)/out.sh 8 | 9 | echo -e '\e[32mall tests passed!\e[0m' 10 | -------------------------------------------------------------------------------- /test/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source $(dirname $0)/helpers.sh 6 | 7 | it_can_check_from_head() { 8 | local repo=$(init_repo) 9 | local ref=$(make_commit $repo) 10 | 11 | local gate_ref=$(make_commit_to_file $repo "my-gate/1234") 12 | 13 | check_gate $repo "my-gate" | jq -e " 14 | . == [{ref: $(echo $gate_ref | jq -R .)}] 15 | " 16 | } 17 | 18 | it_can_check_empty_repo() { 19 | local repo=$(init_repo) 20 | 21 | check_gate $repo "my-gate" | jq -e ' 22 | . == [] 23 | ' 24 | } 25 | 26 | it_can_check_empty_gate() { 27 | local repo=$(init_repo) 28 | 29 | make_commit $repo 30 | make_commit_to_file $repo "other-gate/1234" 31 | 32 | check_gate $repo "my-gate" | jq -e ' 33 | . == [] 34 | ' 35 | } 36 | 37 | it_can_check_ignores_autoclose_items() { 38 | local repo=$(init_repo) 39 | 40 | make_commit $repo 41 | make_commit_to_file $repo "my-gate/1234.autoclose" 42 | 43 | check_gate $repo "my-gate" | jq -e ' 44 | . == [] 45 | ' 46 | } 47 | 48 | it_can_check_from_a_ref() { 49 | local repo=$(init_repo) 50 | 51 | local gate_ref1=$(make_commit_to_file $repo "my-gate/1") 52 | local gate_ref2=$(make_commit_to_file $repo "my-gate/2") 53 | local non_gate_ref2=$(make_commit_to_file $repo "other-gate/1") 54 | local gate_ref3=$(make_commit_to_file $repo "my-gate/3") 55 | 56 | check_gate_at_ref $repo "my-gate" "$gate_ref2" | jq -e " 57 | . == [ 58 | {ref: $(echo $gate_ref2 | jq -R .)}, 59 | {ref: $(echo $gate_ref3 | jq -R .)} 60 | ] 61 | " 62 | } 63 | 64 | run it_can_check_from_head 65 | run it_can_check_empty_repo 66 | run it_can_check_empty_gate 67 | run it_can_check_ignores_autoclose_items 68 | run it_can_check_from_a_ref -------------------------------------------------------------------------------- /test/helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u 4 | 5 | set -o pipefail 6 | 7 | export TMPDIR_ROOT=$(mktemp -d /tmp/git-tests.XXXXXX) 8 | trap "rm -rf $TMPDIR_ROOT" EXIT 9 | 10 | if [ -d /opt/resource ]; then 11 | resource_dir=/opt/resource 12 | else 13 | resource_dir=$(cd $(dirname $0)/../assets && pwd) 14 | fi 15 | test_dir=$(cd $(dirname $0) && pwd) 16 | keygrip=276D99F5B65388AF85DF54B16B08EF0A44C617AC 17 | fingerprint=A3E20CD6371D49E244B0730D1CDD25AEB0F5F8EF 18 | 19 | run() { 20 | export TMPDIR=$(mktemp -d ${TMPDIR_ROOT}/git-tests.XXXXXX) 21 | 22 | echo -e 'running \e[33m'"$@"$'\e[0m...' 23 | eval "$@" 2>&1 | sed -e 's/^/ /g' 24 | echo "" 25 | } 26 | 27 | init_repo() { 28 | ( 29 | set -e 30 | 31 | cd $(mktemp -d $TMPDIR/repo.XXXXXX) 32 | 33 | git init -q 34 | 35 | # start with an initial commit 36 | git \ 37 | -c user.name='test' \ 38 | -c user.email='test@example.com' \ 39 | commit -q --allow-empty -m "init" 40 | 41 | # create some bogus branch 42 | git checkout -b bogus 43 | 44 | git \ 45 | -c user.name='test' \ 46 | -c user.email='test@example.com' \ 47 | commit -q --allow-empty -m "commit on other branch" 48 | 49 | # back to master 50 | git checkout master 51 | 52 | # print resulting repo 53 | pwd 54 | ) 55 | } 56 | 57 | check_gate() { 58 | local repo=$1 59 | local gate=$2 60 | 61 | jq -n "{ 62 | source: { 63 | git: { 64 | uri: $(echo file://$repo | jq -R .) 65 | }, 66 | gate: $(echo $gate | jq -R .) 67 | } 68 | }" | ${resource_dir}/check | tee /dev/stderr 69 | } 70 | 71 | check_gate_at_ref() { 72 | local repo=$1 73 | local gate=$2 74 | local ref=$3 75 | 76 | jq -n "{ 77 | source: { 78 | git: { 79 | uri: $(echo $repo | jq -R .) 80 | }, 81 | gate: $(echo $gate | jq -R .) 82 | }, 83 | version: { 84 | ref: $(echo $ref | jq -R .) 85 | } 86 | }" | ${resource_dir}/check | tee /dev/stderr 87 | } 88 | 89 | make_commit_to_file_on_branch() { 90 | local repo=$1 91 | local file=$2 92 | local branch=$3 93 | local msg=${4-} 94 | 95 | # ensure branch exists 96 | if ! git -C $repo rev-parse --verify $branch >/dev/null; then 97 | git -C $repo branch $branch master 98 | fi 99 | 100 | # switch to branch 101 | git -C $repo checkout -q $branch 102 | 103 | # ensure dir exists 104 | mkdir -p "$(dirname $repo/$file)" 105 | # modify file and commit 106 | echo x >> $repo/$file 107 | git -C $repo add $file 108 | git -C $repo \ 109 | -c user.name='test' \ 110 | -c user.email='test@example.com' \ 111 | commit -q -m "commit $(wc -l $repo/$file) $msg" 112 | 113 | # output resulting sha 114 | git -C $repo rev-parse HEAD 115 | } 116 | 117 | make_commit_to_file() { 118 | make_commit_to_file_on_branch $1 $2 master "${3-}" 119 | } 120 | 121 | make_commit_to_branch() { 122 | make_commit_to_file_on_branch $1 some-file $2 123 | } 124 | 125 | make_commit() { 126 | make_commit_to_file $1 some-file "${2:-}" 127 | } 128 | 129 | make_commit_with_all_changes() { 130 | local repo="$1" 131 | 132 | git -C $repo add . 133 | git -C $repo \ 134 | -c user.name='test' \ 135 | -c user.email='test@example.com' \ 136 | commit -q -m "commit" 137 | 138 | # output resulting sha 139 | git -C $repo rev-parse HEAD 140 | } 141 | 142 | get_gate_at_ref() { 143 | local uriPath="$1" 144 | local ref="$2" 145 | local gate="$3" 146 | local destination="$4" 147 | 148 | jq -n "{ 149 | source: { 150 | git: { 151 | uri: $(echo file://$uriPath | jq -R .), 152 | branch: \"master\" 153 | }, 154 | gate: $(echo $gate | jq -R .) 155 | }, 156 | version: { 157 | ref: $(echo $ref | jq -R .) 158 | } 159 | }" | ${resource_dir}/in "$destination" | tee /dev/stderr 160 | } 161 | 162 | put_gate_item_file() { 163 | local uriPath="$1" 164 | local source="$2" 165 | local gate="$3" 166 | local item_file="$4" 167 | 168 | jq -n "{ 169 | source: { 170 | git: { 171 | uri: $(echo file://$uriPath | jq -R .), 172 | branch: \"master\" 173 | }, 174 | gate: $(echo $gate | jq -R .) 175 | }, 176 | params: { 177 | item_file: $(echo $item_file | jq -R .) 178 | } 179 | }" | ${resource_dir}/out "$source" | tee /dev/stderr 180 | } 181 | 182 | put_gate_update_autoclose() { 183 | local uriPath="$1" 184 | local source="$2" 185 | local gate="$3" 186 | 187 | jq -n "{ 188 | source: { 189 | git: { 190 | uri: $(echo file://$uriPath | jq -R .), 191 | branch: \"master\" 192 | }, 193 | gate: $(echo $gate | jq -R .) 194 | }, 195 | params: { 196 | update_autoclose: true 197 | } 198 | }" | ${resource_dir}/out "$source" | tee /dev/stderr 199 | } 200 | 201 | put_gate_update_autoclose_rebase() { 202 | local uriPath="$1" 203 | local source="$2" 204 | local gate="$3" 205 | 206 | jq -n "{ 207 | source: { 208 | git: { 209 | uri: $(echo file://$uriPath | jq -R .), 210 | branch: \"master\" 211 | }, 212 | gate: $(echo $gate | jq -R .) 213 | }, 214 | params: { 215 | update_autoclose: true, 216 | for_test_only_simulate_rebase: true 217 | } 218 | }" | ${resource_dir}/out "$source" | tee /dev/stderr 219 | } 220 | 221 | upstream_repo_allow_push() { 222 | # cannot push to repo while it's checked out to a branch, so we switch to a different branch 223 | git -C $1 checkout --quiet refs/heads/master 224 | } 225 | 226 | upstream_repo_allow_asserts() { 227 | # cannot push to repo while it's checked out to a branch, so we switch to a different branch 228 | git -C $1 checkout --quiet master 229 | } -------------------------------------------------------------------------------- /test/in.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source $(dirname $0)/helpers.sh 6 | 7 | it_can_get_from_regular_version() { 8 | local repo=$(init_repo) 9 | local dest=$TMPDIR/destination 10 | local gate="my-gate" 11 | local item="1234" 12 | 13 | local gate_ref=$(make_commit_to_file $repo "$gate/$item") 14 | 15 | echo "xxxx" 16 | git -C "$repo" branch -a 17 | echo "xxxx" 18 | 19 | result=$(get_gate_at_ref "$repo" "$gate_ref" "$gate" "$dest") 20 | echo "$result" | jq -e ' 21 | .version == { "ref": "'$gate_ref'" } 22 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 23 | and (.metadata | .[] | select(.name == "passed") | .value == "'$item'") 24 | ' 25 | 26 | test $(cat $dest/passed) = "$item" 27 | test $(cat $dest/metadata) = "x" 28 | } 29 | 30 | it_can_get_from_none_version_and_skip_output() { 31 | local repo=$(init_repo) 32 | local dest=$TMPDIR/destination 33 | local gate="my-gate" 34 | local item="1234" 35 | 36 | local gate_ref="none" 37 | result=$(get_gate_at_ref "$repo" "$gate_ref" "$gate" "$dest") 38 | echo "$result" | jq -e ' 39 | .version == { "ref": "none" } 40 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 41 | and (.metadata | .[] | select(.name == "passed") | .value == "none") 42 | ' 43 | 44 | test ! -f $dest/passed 45 | test ! -f $dest/metadata 46 | } 47 | 48 | 49 | it_can_get_from_autoclose_version() { 50 | local repo=$(init_repo) 51 | local dest=$TMPDIR/destination 52 | local gate="autogate" 53 | local item="1234" 54 | 55 | mkdir -p $repo/$gate 56 | echo "x" >> "$repo/$gate/$item.autoclose" 57 | local intial_ref=$(make_commit_with_all_changes $repo) 58 | 59 | mv "$repo/$gate/$item.autoclose" "$repo/$gate/$item" 60 | local autoclose_ref=$(make_commit_with_all_changes $repo) 61 | 62 | result=$(get_gate_at_ref "$repo" "$autoclose_ref" "$gate" "$dest") 63 | echo "$result" | jq -e ' 64 | .version == { "ref": "'$autoclose_ref'" } 65 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 66 | and (.metadata | .[] | select(.name == "passed") | .value == "'$item'") 67 | ' 68 | 69 | test $(cat $dest/passed) = "$item" 70 | test $(cat $dest/metadata) = "x" 71 | } 72 | 73 | run it_can_get_from_regular_version 74 | run it_can_get_from_none_version_and_skip_output 75 | run it_can_get_from_autoclose_version -------------------------------------------------------------------------------- /test/out.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source $(dirname $0)/helpers.sh 6 | 7 | it_can_put_item_to_simple_gate() { 8 | local upstreamRepo=$(init_repo) 9 | local initial_ref=$(make_commit $upstreamRepo) 10 | upstream_repo_allow_push $upstreamRepo 11 | 12 | local src=$(mktemp -d $TMPDIR/put-src.XXXXXX) 13 | local localRepo=$src/gate-repo 14 | git clone $upstreamRepo $localRepo 15 | 16 | local gate="my-gate" 17 | local item_file="gating-task/*" 18 | local item="1234" 19 | mkdir -p $src/gating-task 20 | echo "arbitrary contents" > $src/gating-task/$item 21 | 22 | result=$(put_gate_item_file $upstreamRepo $src $gate $item_file) 23 | 24 | git -C $upstreamRepo checkout master 25 | local pushed_ref="$(git -C $upstreamRepo rev-parse HEAD)" 26 | 27 | # check a new commit was created 28 | test ! $pushed_ref == $initial_ref 29 | # check output 30 | echo "$result" | jq -e ' 31 | .version == { "ref": "'$pushed_ref'" } 32 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 33 | and (.metadata | .[] | select(.name == "passed") | .value == "'$item'") 34 | ' 35 | # check that the gate file was written 36 | test -e "$upstreamRepo/$gate/$item" 37 | } 38 | 39 | it_can_put_existing_item_to_simple_gate_returns_existing_ref() { 40 | local repo=$(init_repo) 41 | local src=$TMPDIR/src 42 | local gate="my-gate" 43 | local item="1234" 44 | local other_item="abcd" 45 | 46 | local existing_ref=$(make_commit_to_file $repo "$gate/$item") 47 | local other_ref=$(make_commit_to_file $repo "$gate/$other_item") 48 | 49 | local item_file="gating-task/*" 50 | mkdir -p "$src/gating-task" 51 | echo "arbitrary contents" > "$src/gating-task/$item" 52 | 53 | result=$(put_gate_item_file $repo $src $gate $item_file) 54 | 55 | test ! $existing_ref == $other_ref 56 | # check output points to existing ref 57 | echo "$result" | jq -e ' 58 | .version == { "ref": "'$existing_ref'" } 59 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 60 | and (.metadata | .[] | select(.name == "passed") | .value == "'$item'") 61 | ' 62 | } 63 | 64 | it_can_put_autoclose_item_to_autoclose_gate() { 65 | local upstreamRepo=$(init_repo) 66 | local initial_ref=$(make_commit $upstreamRepo) 67 | upstream_repo_allow_push $upstreamRepo 68 | 69 | local src=$(mktemp -d $TMPDIR/put-src.XXXXXX) 70 | local localRepo=$src/gate-repo 71 | git clone $upstreamRepo $localRepo 72 | 73 | local gate="auto-gate" 74 | local item_file="gating-task/*.autoclose" 75 | local item="1234.autoclose" 76 | mkdir -p $src/gating-task 77 | echo "simple-gate/1" >> $src/gating-task/$item 78 | 79 | result=$(put_gate_item_file $upstreamRepo $src $gate $item_file) 80 | 81 | git -C $upstreamRepo checkout master 82 | local pushed_ref="$(git -C $upstreamRepo rev-parse HEAD)" 83 | 84 | # check a new commit was created 85 | test ! $pushed_ref == $initial_ref 86 | # check output 87 | echo "$result" | jq -e ' 88 | .version == { "ref": "none" } 89 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 90 | and (.metadata | .[] | select(.name == "passed") | .value == "none") 91 | ' 92 | # check that the gate file was written 93 | test -e "$upstreamRepo/$gate/$item" 94 | } 95 | 96 | it_can_put_autoclose_item_that_is_closable_to_autoclose_gate() { 97 | local upstreamRepo=$(init_repo) 98 | local simple_gate="simple-gate" 99 | local simple_item="1" 100 | local simple_ref=$(make_commit_to_file "$upstreamRepo" "$simple_gate/$simple_item") 101 | 102 | upstream_repo_allow_push $upstreamRepo 103 | 104 | local src=$(mktemp -d "$TMPDIR/put-src.XXXXXX") 105 | local localRepo=$src/gate-repo 106 | git clone "$upstreamRepo" "$localRepo" 107 | 108 | local gate="auto-gate" 109 | local item_file="gating-task/*.autoclose" 110 | local closed_item="1234" 111 | local closable_item="$closed_item.autoclose" 112 | mkdir -p "$src/gating-task" 113 | echo "$simple_gate/$simple_item" >> "$src/gating-task/$closable_item" 114 | 115 | result=$(put_gate_item_file $upstreamRepo $src $gate $item_file) 116 | 117 | git -C $upstreamRepo checkout master 118 | local closable_ref="$(git -C $upstreamRepo rev-parse HEAD~1)" 119 | local closed_ref="$(git -C $upstreamRepo rev-parse HEAD)" 120 | 121 | # checknew commits were created 122 | test ! "$closable_ref" == "$simple_ref" 123 | test ! "$closed_ref" == "$closable_ref" 124 | 125 | # check output 126 | echo "$result" | jq -e ' 127 | .version == { "ref": "'$closed_ref'" } 128 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 129 | and (.metadata | .[] | select(.name == "passed") | .value == "'$closed_item'") 130 | ' 131 | # check that the gate file was written 132 | test -e "$upstreamRepo/$gate/$closed_item" 133 | } 134 | 135 | it_can_put_existing_item_to_autoclose_gate_returns_existing_ref() { 136 | local repo=$(init_repo) 137 | local src=$TMPDIR/src 138 | local gate="my-gate" 139 | local item="1234" 140 | local other_item="abcd" 141 | 142 | # there already exists a passed item for which we want to emit an autoclose spec 143 | local existing_ref=$(make_commit_to_file $repo "$gate/$item") 144 | local other_ref=$(make_commit_to_file $repo "$gate/$other_item") 145 | 146 | local item_file="gating-task/*" 147 | mkdir -p "$src/gating-task" 148 | echo "arbitrary contents" > "$src/gating-task/$item.autoclose" 149 | 150 | result=$(put_gate_item_file $repo $src $gate $item_file) 151 | 152 | test ! "$existing_ref" == "$other_ref" 153 | # check output points to existing ref 154 | echo "$result" | jq -e ' 155 | .version == { "ref": "'$existing_ref'" } 156 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 157 | and (.metadata | .[] | select(.name == "passed") | .value == "'$item'") 158 | ' 159 | } 160 | 161 | update_autoclose_gate_with_no_closable_items_returns_none() { 162 | local gate="auto-gate" 163 | 164 | local upstreamRepo=$(init_repo) 165 | mkdir -p "$upstreamRepo/$gate" 166 | echo "simple-gate/1" >> "$upstreamRepo/$gate/1.autoclose" 167 | local head_ref=$(make_commit_with_all_changes $upstreamRepo) 168 | 169 | local src=$(mktemp -d $TMPDIR/put-src.XXXXXX) 170 | local localRepo=$src/gate-repo 171 | git clone $upstreamRepo $localRepo 172 | 173 | upstream_repo_allow_push $upstreamRepo 174 | result=$(put_gate_update_autoclose $upstreamRepo $src $gate) 175 | 176 | upstream_repo_allow_asserts $upstreamRepo 177 | local pushed_ref="$(git -C $upstreamRepo rev-parse HEAD)" 178 | 179 | # check no new commit was created 180 | test $pushed_ref == $head_ref 181 | # check output 182 | echo "$result" | jq -e ' 183 | .version == { "ref": "none" } 184 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 185 | and (.metadata | .[] | select(.name == "passed") | .value == "none") 186 | ' 187 | } 188 | 189 | update_autoclose_gate_with_empty_gate_returns_none() { 190 | local gate="auto-gate" 191 | 192 | local upstreamRepo=$(init_repo) 193 | local head_ref=$(make_commit_to_file $upstreamRepo "my-gate/1") 194 | 195 | local src=$(mktemp -d $TMPDIR/put-src.XXXXXX) 196 | local localRepo=$src/gate-repo 197 | git clone $upstreamRepo $localRepo 198 | 199 | upstream_repo_allow_push $upstreamRepo 200 | result=$(put_gate_update_autoclose $upstreamRepo $src $gate) 201 | 202 | upstream_repo_allow_asserts $upstreamRepo 203 | local pushed_ref="$(git -C $upstreamRepo rev-parse HEAD)" 204 | 205 | # check no new commit was created 206 | test $pushed_ref == $head_ref 207 | # check output 208 | echo "$result" | jq -e ' 209 | .version == { "ref": "none" } 210 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 211 | and (.metadata | .[] | select(.name == "passed") | .value == "none") 212 | ' 213 | } 214 | 215 | update_autoclose_gate_ignores_metadata_after_hash() { 216 | local gate="auto-gate" 217 | 218 | local upstreamRepo=$(init_repo) 219 | mkdir -p "$upstreamRepo/$gate" 220 | echo "" >> "$upstreamRepo/$gate/1.autoclose" 221 | echo "#" >> "$upstreamRepo/$gate/1.autoclose" 222 | echo "some metadata" >> "$upstreamRepo/$gate/1.autoclose" 223 | local head_ref=$(make_commit_with_all_changes $upstreamRepo) 224 | 225 | local src=$(mktemp -d $TMPDIR/put-src.XXXXXX) 226 | local localRepo=$src/gate-repo 227 | git clone $upstreamRepo $localRepo 228 | 229 | upstream_repo_allow_push $upstreamRepo 230 | result=$(put_gate_update_autoclose $upstreamRepo $src $gate) 231 | 232 | upstream_repo_allow_asserts $upstreamRepo 233 | local pushed_ref="$(git -C $upstreamRepo rev-parse HEAD)" 234 | 235 | # check no new commit was created 236 | test ! $pushed_ref == $head_ref 237 | # check output 238 | echo "$result" | jq -e ' 239 | .version == { "ref": "'$pushed_ref'" } 240 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 241 | and (.metadata | .[] | select(.name == "passed") | .value == "1") 242 | ' 243 | } 244 | 245 | update_autoclose_gate_with_closable_items_returns_first_closed() { 246 | local gate="auto-gate" 247 | 248 | local upstreamRepo=$(init_repo) 249 | mkdir -p "$upstreamRepo/$gate" 250 | mkdir -p "$upstreamRepo/simple-gate" 251 | echo "simple-gate/a" >> "$upstreamRepo/$gate/1.autoclose" 252 | echo "simple-gate/a" >> "$upstreamRepo/$gate/2.autoclose" 253 | echo "x" >> "$upstreamRepo/simple-gate/a" 254 | local head_ref=$(make_commit_with_all_changes $upstreamRepo) 255 | 256 | local src=$(mktemp -d $TMPDIR/put-src.XXXXXX) 257 | local localRepo=$src/gate-repo 258 | git clone $upstreamRepo $localRepo 259 | 260 | upstream_repo_allow_push $upstreamRepo 261 | result=$(put_gate_update_autoclose $upstreamRepo $src $gate) 262 | 263 | upstream_repo_allow_asserts $upstreamRepo 264 | local pushed_ref="$(git -C $upstreamRepo rev-parse HEAD~1)" # 2 items were closed 265 | 266 | # check new commit was created 267 | test ! $pushed_ref == $head_ref 268 | # check output 269 | echo "$result" | jq -e ' 270 | .version == { "ref": "'$pushed_ref'" } 271 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 272 | and (.metadata | .[] | select(.name == "passed") | .value == "1") 273 | ' 274 | } 275 | 276 | update_autoclose_gate_with_closable_items_retries_using_rebase_on_conflicts() { 277 | local gate="auto-gate" 278 | 279 | local upstreamRepo=$(init_repo) 280 | mkdir -p "$upstreamRepo/$gate" 281 | mkdir -p "$upstreamRepo/simple-gate" 282 | echo "simple-gate/a" >> "$upstreamRepo/$gate/1.autoclose" 283 | echo "simple-gate/b" >> "$upstreamRepo/$gate/2.autoclose" 284 | echo "x" >> "$upstreamRepo/simple-gate/a" 285 | local upstream_initial_ref=$(make_commit_with_all_changes $upstreamRepo) 286 | 287 | local src=$(mktemp -d $TMPDIR/put-src.XXXXXX) 288 | local localRepo=$src/gate-repo 289 | git clone $upstreamRepo $localRepo 290 | 291 | # make a change to upstream, local has to rebase before pushing 292 | # now two gates are autoclosable 293 | echo "x" >> "$upstreamRepo/simple-gate/b" 294 | local upstream_rebase_ref=$(make_commit_with_all_changes $upstreamRepo) 295 | 296 | echo "" 297 | echo "bumped upstream $upstream_initial_ref -> $upstream_rebase_ref" 298 | echo "" 299 | 300 | upstream_repo_allow_push $upstreamRepo 301 | result=$(put_gate_update_autoclose_rebase $upstreamRepo $src $gate) 302 | 303 | upstream_repo_allow_asserts $upstreamRepo 304 | local rebased_from_ref="$(git -C $upstreamRepo rev-parse HEAD~2)" 305 | local first_closed_ref="$(git -C $upstreamRepo rev-parse HEAD~1)" 306 | 307 | # check new commit was created 308 | test ! $upstream_initial_ref == $upstream_rebase_ref 309 | test $rebased_from_ref == $upstream_rebase_ref 310 | # check output 311 | echo "$result" | jq -e ' 312 | .version == { "ref": "'$first_closed_ref'" } 313 | and (.metadata | .[] | select(.name == "gate") | .value == "'$gate'") 314 | and (.metadata | .[] | select(.name == "passed") | .value == "1") 315 | ' 316 | } 317 | 318 | run it_can_put_item_to_simple_gate 319 | run it_can_put_existing_item_to_simple_gate_returns_existing_ref 320 | run it_can_put_autoclose_item_to_autoclose_gate 321 | run it_can_put_autoclose_item_that_is_closable_to_autoclose_gate 322 | run it_can_put_existing_item_to_autoclose_gate_returns_existing_ref 323 | run update_autoclose_gate_with_no_closable_items_returns_none 324 | run update_autoclose_gate_ignores_metadata_after_hash 325 | run update_autoclose_gate_with_empty_gate_returns_none 326 | run update_autoclose_gate_with_closable_items_returns_first_closed 327 | run update_autoclose_gate_with_closable_items_retries_using_rebase_on_conflicts --------------------------------------------------------------------------------