Repository: getsops/sops (language: python)

Issue:
prep v3.7.2
and fix the CHANGELOG's formatting for the last few releases.


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4f84e2dc7..b55a04f7a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,10 +4,10 @@ workflows:
   build-and-deploy:
     jobs:
       - build
-      - push:     
+      - push:
           filters:
             tags:
-              only: /^v.*/   
+              only: /^v.*/
             branches:
               ignore: /.*/
 jobs:
@@ -30,7 +30,7 @@ jobs:
     resource_class: large
     steps:
       - checkout
-      - run: 
+      - run:
           name: semver check
           command: |
             MAJOR=$(echo ${CIRCLE_TAG#v} | cut -d"." -f1)
@@ -46,8 +46,8 @@ jobs:
 
             This job uses the semver from the git TAG as the public version to publish.
 
-            - This should only run on workflows triggered by a tag. 
-            - The tag name should be a semver like 'v1.2.3' 
+            - This should only run on workflows triggered by a tag.
+            - The tag name should be a semver like 'v1.2.3'
             - The version should follow conventions documented at https://github.com/fsaintjacques/semver-tool
             EOF
             exit 1
diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index 3f493bb4c..90b64f87f 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -12,11 +12,15 @@ on:
 
 jobs:
   build:
-    name: Build and test ${{ matrix.os }}
+    name: Build and test ${{ matrix.os }} ${{ matrix.arch }}
     runs-on: ubuntu-latest
     strategy:
       matrix:
         os: [linux, darwin, windows]
+        arch: [amd64, arm64]
+        exclude:
+          - os: windows
+            arch: arm64
     env:
       VAULT_VERSION: "1.1.3"
       VAULT_TOKEN: "root"
@@ -24,10 +28,10 @@ jobs:
     steps:
       - name: Install dependencies
         run: sudo apt-get update && sudo apt-get install git -y
-      - name: Set up Go 1.13
+      - name: Set up Go 1.17
         uses: actions/setup-go@v2
         with:
-          go-version: 1.13
+          go-version: 1.17
         id: go
       - name: Check out code into the Go module directory
         uses: actions/checkout@v2
@@ -37,13 +41,24 @@ jobs:
           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-
-      - name: Build
+      - name: Build Linux and Darwin
+        if: matrix.os != 'windows'
+        run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o sops-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} -v ./cmd/sops
+      - name: Build Windows
+        if: matrix.os == 'windows'
         run: GOOS=${{ matrix.os }} go build -o sops-${{ matrix.os }}-${{ github.sha }} -v ./cmd/sops
       - name: Import test GPG keys
         run: for i in 1 2 3 4 5; do gpg --import pgp/sops_functional_tests_key.asc && break || sleep 15; done
       - name: Test
         run: make test
-      - name: Upload artifact
+      - name: Upload artifact for Linux and Darwin
+        if: matrix.os != 'windows'
+        uses: actions/upload-artifact@v2
+        with:
+          name: sops-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }}
+          path: sops-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }}
+      - name: Upload artifact for Windows
+        if: matrix.os == 'windows'
         uses: actions/upload-artifact@v2
         with:
           name: sops-${{ matrix.os }}-${{ github.sha }}
@@ -63,9 +78,9 @@ jobs:
         uses: actions/checkout@v2
       - uses: actions/download-artifact@v2
         with:
-          name: sops-linux-${{ github.sha }}
+          name: sops-linux-amd64-${{ github.sha }}
       - name: Move SOPS binary
-        run: mv sops-linux-${{ github.sha }} ./functional-tests/sops
+        run: mv sops-linux-amd64-${{ github.sha }} ./functional-tests/sops
       - name: Make SOPS binary executable
         run: chmod +x ./functional-tests/sops
       - name: Download Vault
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 962edf62b..4a64fe7ba 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,13 +15,15 @@ jobs:
         run: sudo apt-get update && sudo apt-get install git ruby rpm -y
       - name: Install fpm
         run: gem install fpm || sudo gem install fpm
-      - name: Set up Go 1.15
+      - name: Set up Go 1.17
         uses: actions/setup-go@v2
         with:
-          go-version: 1.15
+          go-version: 1.17
         id: go
       - name: Check out code into the Go module directory
         uses: actions/checkout@v2
+      - name: Go vendor
+        run: go mod vendor
       - name: Make release directory
         run: mkdir dist
       - name: Build deb and rpm
@@ -32,12 +34,18 @@ jobs:
         run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
       - name: Set RELEASE_NUMBER
         run: echo "RELEASE_NUMBER=$(echo $RELEASE_VERSION | cut -c2-)" >> $GITHUB_ENV
-      - name: Build darwin binary
-        run: GOOS=darwin CGO_ENABLED=0 go build -mod vendor -o dist/sops-${{ env.RELEASE_VERSION }}.darwin go.mozilla.org/sops/v3/cmd/sops
+      - name: Build linux arm64 binary
+        run: GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -mod vendor -o dist/sops-${{ env.RELEASE_VERSION }}.linux.arm64 go.mozilla.org/sops/v3/cmd/sops
+      - name: Build darwin amd64 binary
+        run: GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -mod vendor -o dist/sops-${{ env.RELEASE_VERSION }}.darwin.amd64 go.mozilla.org/sops/v3/cmd/sops
+      - name: Copy darwin amd64 to have a no-architecture labeled version
+        run: cp dist/sops-${{ env.RELEASE_VERSION }}.darwin.amd64 dist/sops-${{ env.RELEASE_VERSION }}.darwin
+      - name: Build darwin arm64 binary
+        run: GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -mod vendor -o dist/sops-${{ env.RELEASE_VERSION }}.darwin.arm64 go.mozilla.org/sops/v3/cmd/sops
       - name: Build windows binary
         run: GOOS=windows CGO_ENABLED=0 go build -mod vendor -o dist/sops-${{ env.RELEASE_VERSION }}.exe go.mozilla.org/sops/v3/cmd/sops
       - name: Copy already built linux binary
-        run: cp tmppkg/usr/local/bin/sops dist/sops-${{ env.RELEASE_VERSION }}.linux
+        run: cp tmppkg/usr/local/bin/sops dist/sops-${{ env.RELEASE_VERSION }}.linux && cp tmppkg/usr/local/bin/sops dist/sops-${{ env.RELEASE_VERSION }}.linux.amd64
       - name: Create release
         uses: "mozilla/action-automatic-releases@latest"
         with:
@@ -45,7 +53,13 @@ jobs:
           prerelease: true
           files: |
             dist/sops-${{ env.RELEASE_VERSION }}.exe
+            dist/sops-${{ env.RELEASE_VERSION }}.darwin.amd64
+            dist/sops-${{ env.RELEASE_VERSION }}.darwin.arm64
             dist/sops-${{ env.RELEASE_VERSION }}.darwin
+            dist/sops-${{ env.RELEASE_VERSION }}.linux.amd64
+            dist/sops-${{ env.RELEASE_VERSION }}.linux.arm64
             dist/sops-${{ env.RELEASE_VERSION }}.linux
             dist/sops_${{ env.RELEASE_NUMBER }}_amd64.deb
+            dist/sops_${{ env.RELEASE_NUMBER }}_arm64.deb
             dist/sops-${{ env.RELEASE_NUMBER }}-1.x86_64.rpm
+            dist/sops-${{ env.RELEASE_NUMBER }}-1.aarch64.rpm
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 23e8034c4..04b48bfbe 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,123 +1,138 @@
 Changelog
 =========
 
+3.7.2
+-----
+Changes:
+
+* README updates (#861, #860)
+* Various test fixes (#909, #906, #1008)
+* Added Linux and Darwin arm64 releases (#911, #891)
+* Upgrade to go v1.17 (#1012)
+* Support SOPS_AGE_KEY environment variable (#1006)
+
+Bug fixes:
+
+* Make sure comments in yaml files are not duplicated (#866)
+* Make sure configuration file paths work correctly relative to the config file in us (#853)
+
 3.7.1
 -----
 Changes:
 
-    * Security fix
-    * Add release workflow (#843)
-    * Fix issue where CI wouldn't run against master (#848)
-    * Trim extra whitespace around age keys (#846)
+* Security fix
+* Add release workflow (#843)
+* Fix issue where CI wouldn't run against master (#848)
+* Trim extra whitespace around age keys (#846)
 
 3.7.0
 -----
 Features:
 
-    * Add support for age (#688)
-    * Add filename to exec-file (#761)
+* Add support for age (#688)
+* Add filename to exec-file (#761)
 
 Changes:
 
-    * On failed decryption with GPG, return the error returned by GPG to the sops user (#762)
-    * Use yaml.v3 instead of modified yaml.v2 for handling YAML files (#791)
-    * Update aws-sdk-go to version v1.37.18 (#823)
+* On failed decryption with GPG, return the error returned by GPG to the sops user (#762)
+* Use yaml.v3 instead of modified yaml.v2 for handling YAML files (#791)
+* Update aws-sdk-go to version v1.37.18 (#823)
 
 Project Changes:
 
-    * Switch from TravisCI to Github Actions (#792)
+* Switch from TravisCI to Github Actions (#792)
 
 3.6.1
 -----
 Features:
 
-    * Add support for --unencrypted-regex (#715)
+* Add support for --unencrypted-regex (#715)
 
 Changes:
 
-    * Use keys.openpgp.org instead of gpg.mozilla.org (#732)
-    * Upgrade AWS SDK version (#714)
-    * Support --input-type for exec-file (#699)
+* Use keys.openpgp.org instead of gpg.mozilla.org (#732)
+* Upgrade AWS SDK version (#714)
+* Support --input-type for exec-file (#699)
 
 Bug fixes:
 
-    * Fixes broken Vault tests (#731)
-    * Revert "Add standard newline/quoting behavior to dotenv store" (#706)
+* Fixes broken Vault tests (#731)
+* Revert "Add standard newline/quoting behavior to dotenv store" (#706)
 
 
 3.6.0
 -----
 Features:
 
-    * Support for encrypting data through the use of Hashicorp Vault (#655)
-    * `sops publish` now supports `--recursive` flag for publishing all files in a directory (#602)
-    * `sops publish` now supports `--omit-extensions` flag for omitting the extension in the destination path (#602)
-    * sops now supports JSON arrays of arrays (#642)
+* Support for encrypting data through the use of Hashicorp Vault (#655)
+* `sops publish` now supports `--recursive` flag for publishing all files in a directory (#602)
+* `sops publish` now supports `--omit-extensions` flag for omitting the extension in the destination path (#602)
+* sops now supports JSON arrays of arrays (#642)
 
 Improvements:
 
-    * Updates and standardization for the dotenv store (#612, #622)
-    * Close temp files after using them for edit command (#685)
+* Updates and standardization for the dotenv store (#612, #622)
+* Close temp files after using them for edit command (#685)
 
 Bug fixes:
 
-    * AWS SDK usage now correctly resolves the `~/.aws/config` file (#680)
-    * `sops updatekeys` now correctly matches config rules (#682)
-    * `sops updatekeys` now correctly uses the config path cli flag (#672)
-    * Partially empty sops config files don't break the use of sops anymore (#662)
-    * Fix possible infinite loop in PGP's passphrase prompt call (#690)
+* AWS SDK usage now correctly resolves the `~/.aws/config` file (#680)
+* `sops updatekeys` now correctly matches config rules (#682)
+* `sops updatekeys` now correctly uses the config path cli flag (#672)
+* Partially empty sops config files don't break the use of sops anymore (#662)
+* Fix possible infinite loop in PGP's passphrase prompt call (#690)
 
 Project changes:
 
-    * Dockerfile now based off of golang version 1.14 (#649)
-    * Push alpine version of docker image to Dockerhub (#609)
-    * Push major, major.minor, and major.minor.patch tagged docker images to Dockerhub (#607)
-    * Removed out of date contact information (#668)
-    * Update authors in the cli help text (#645)
+* Dockerfile now based off of golang version 1.14 (#649)
+* Push alpine version of docker image to Dockerhub (#609)
+* Push major, major.minor, and major.minor.patch tagged docker images to Dockerhub (#607)
+* Removed out of date contact information (#668)
+* Update authors in the cli help text (#645)
 
 
 3.5.0
 -----
 Features:
 
-    * `sops exec-env` and `sops exec-file`, two new commands for utilizing sops secrets within a temporary file or env vars
+* `sops exec-env` and `sops exec-file`, two new commands for utilizing sops secrets within a temporary file or env vars
 
 Bug fixes:
 
-    * Sanitize AWS STS session name, as sops creates it based off of the machines hostname
-    * Fix for `decrypt.Data` to support `.ini` files
-    * Various package fixes related to switching to Go Modules
-    * Fixes for Vault-related tests running locally and in CI.
+* Sanitize AWS STS session name, as sops creates it based off of the machines hostname
+* Fix for `decrypt.Data` to support `.ini` files
+* Various package fixes related to switching to Go Modules
+* Fixes for Vault-related tests running locally and in CI.
 
 Project changes:
 
-    * Change to proper use of go modules, changing to primary module name to `go.mozilla.org/sops/v3`
-    * Change tags to requiring a `v` prefix.
-    * Add documentation for `sops updatekeys` command
+* Change to proper use of go modules, changing to primary module name to `go.mozilla.org/sops/v3`
+* Change tags to requiring a `v` prefix.
+* Add documentation for `sops updatekeys` command
 
 3.4.0
 -----
 Features:
 
-    * `sops publish`, a new command for publishing sops encrypted secrets to S3, GCS, or Hashicorp Vault
-    * Support for multiple Azure authentication mechanisms
-    * Azure Keyvault support to the sops config file
-    * `encrypted_regex` option to the sops config file
+* `sops publish`, a new command for publishing sops encrypted secrets to S3, GCS, or Hashicorp Vault
+* Support for multiple Azure authentication mechanisms
+* Azure Keyvault support to the sops config file
+* `encrypted_regex` option to the sops config file
 
 Bug fixes:
 
-    * Return non-zero exit code for invalid CLI flags
-    * Broken path handling for sops editing on Windows
-    * `go lint/fmt` violations
-    * Check for pgp fingerprint before slicing it
+* Return non-zero exit code for invalid CLI flags
+* Broken path handling for sops editing on Windows
+* `go lint/fmt` violations
+* Check for pgp fingerprint before slicing it
 
 Project changes:
 
-    * Build container using golang 1.12
-    * Switch to using go modules
-    * Hashicorp Vault server in Travis CI build
-    * Mozilla Publice License file to repo
-    * Replaced expiring test gpg keys
+* Build container using golang 1.12
+* Switch to using go modules
+* Hashicorp Vault server in Travis CI build
+* Mozilla Publice License file to repo
+* Replaced expiring test gpg keys
 
 3.3.1
 -----
diff --git a/Dockerfile b/Dockerfile
index 8585f16c7..00d47c285 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,10 @@
-FROM golang:1.15
+FROM golang:1.17
 
 COPY . /go/src/go.mozilla.org/sops
 WORKDIR /go/src/go.mozilla.org/sops
 
 RUN CGO_ENABLED=1 make install
 RUN apt-get update
-RUN apt-get install -y vim python-pip emacs
+RUN apt-get install -y vim python3-pip emacs
 RUN pip install awscli
 ENV EDITOR vim
diff --git a/Dockerfile.alpine b/Dockerfile.alpine
index 2be5d6b5c..1a782facf 100644
--- a/Dockerfile.alpine
+++ b/Dockerfile.alpine
@@ -1,4 +1,5 @@
 FROM golang:1.12-alpine3.10 AS builder
+FROM golang:1.17-alpine3.15 AS builder
 
 RUN apk --no-cache add make
 
@@ -8,7 +9,7 @@ WORKDIR /go/src/go.mozilla.org/sops
 RUN CGO_ENABLED=1 make install
 
 
-FROM alpine:3.10
+FROM alpine:3.15
 
 RUN apk --no-cache add \
   vim ca-certificates
diff --git a/Makefile b/Makefile
index b20bda234..0e9838117 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 PROJECT		:= go.mozilla.org/sops/v3
-GO 		:= GO15VENDOREXPERIMENT=1 GO111MODULE=on GOPROXY=https://proxy.golang.org go
+GO 		:= GOPROXY=https://proxy.golang.org go
 GOLINT 		:= golint
 
 all: test vet generate install functional-tests
@@ -48,31 +48,47 @@ functional-tests-all:
 	$(GO) build -o functional-tests/sops go.mozilla.org/sops/v3/cmd/sops
 	cd functional-tests && cargo test && cargo test -- --ignored
 
-deb-pkg: vendor
+# Creates variables during target re-definition. Basically this block allows the particular variables to be used in the final target
+build-deb-%: OS = $(word 1,$(subst -, ,$*))
+build-deb-%: ARCH = $(word 2,$(subst -, ,$*))
+build-deb-%: FPM_ARCH = $(word 3,$(subst -, ,$*))
+# Poor-mans function with parameters being split out from the variable part of it's name
+build-deb-%:
 	rm -rf tmppkg
 	mkdir -p tmppkg/usr/local/bin
-	GOOS=linux CGO_ENABLED=0 go build -mod vendor -o tmppkg/usr/local/bin/sops go.mozilla.org/sops/v3/cmd/sops
+	GOOS=$(OS) GOARCH="$(ARCH)" CGO_ENABLED=0 go build -mod vendor -o tmppkg/usr/local/bin/sops go.mozilla.org/sops/v3/cmd/sops
 	fpm -C tmppkg -n sops --license MPL2.0 --vendor mozilla \
 		--description "Sops is an editor of encrypted files that supports YAML, JSON and BINARY formats and encrypts with AWS KMS and PGP." \
 		-m "AJ Bahnken <ajvb+sops@mozilla.com>" \
 		--url https://go.mozilla.org/sops \
-		--architecture x86_64 \
+		--architecture $(FPM_ARCH) \
 		-v "$$(grep '^const Version' version/version.go |cut -d \" -f 2)" \
 		-s dir -t deb .
 
-rpm-pkg: vendor
+# Create .deb packages for multiple architectures
+deb-pkg: vendor build-deb-linux-amd64-x86_64 build-deb-linux-arm64-arm64
+
+# Creates variables during target re-definition. Basically this block allows the particular variables to be used in the final target
+build-rpm-%: OS = $(word 1,$(subst -, ,$*))
+build-rpm-%: ARCH = $(word 2,$(subst -, ,$*))
+build-rpm-%: FPM_ARCH = $(word 3,$(subst -, ,$*))
+# Poor-mans function with parameters being split out from the variable part of it's name
+build-rpm-%:
 	rm -rf tmppkg
 	mkdir -p tmppkg/usr/local/bin
-	GOOS=linux CGO_ENABLED=0 go build -mod vendor -o tmppkg/usr/local/bin/sops go.mozilla.org/sops/v3/cmd/sops
+	GOOS=$(OS) GOARCH="$(ARCH)" CGO_ENABLED=0 go build -mod vendor -o tmppkg/usr/local/bin/sops go.mozilla.org/sops/v3/cmd/sops
 	fpm -C tmppkg -n sops --license MPL2.0 --vendor mozilla \
 		--description "Sops is an editor of encrypted files that supports YAML, JSON and BINARY formats and encrypts with AWS KMS and PGP." \
 		-m "AJ Bahnken <ajvb+sops@mozilla.com>" \
 		--url https://go.mozilla.org/sops \
-		--architecture x86_64 \
-		--rpm-os linux \
+		--architecture $(FPM_ARCH) \
+		--rpm-os $(OS) \
 		-v "$$(grep '^const Version' version/version.go |cut -d \" -f 2)" \
 		-s dir -t rpm .
 
+# Create .rpm packages for multiple architectures
+rpm-pkg: vendor build-rpm-linux-amd64-x86_64 build-rpm-linux-arm64-arm64
+
 dmg-pkg: install
 ifneq ($(OS),darwin)
 		echo 'you must be on MacOS and set OS=darwin on the make command line to build an OSX package'
@@ -82,7 +98,7 @@ else
 	cp $$GOPATH/bin/sops tmppkg/usr/local/bin/
 	fpm -C tmppkg -n sops --license MPL2.0 --vendor mozilla \
 		--description "Sops is an editor of encrypted files that supports YAML, JSON and BINARY formats and encrypts with AWS KMS and PGP." \
-		-m "Julien Vehent <jvehent+sops@mozilla.com>" \
+		-m "Mozilla Security <security@mozilla.org>" \
 		--url https://go.mozilla.org/sops \
 		--architecture x86_64 \
 		-v "$$(grep '^const Version' version/version.go |cut -d \" -f 2)" \
diff --git a/README.rst b/README.rst
index b05fcf107..c3727a077 100644
--- a/README.rst
+++ b/README.rst
@@ -9,11 +9,8 @@ formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, age, and PGP.
 
 ------------
 
-.. image:: https://godoc.org/go.mozilla.org/sops?status.svg
-	:target: https://godoc.org/go.mozilla.org/sops
-
-.. image:: https://travis-ci.org/mozilla/sops.svg?branch=master
-	:target: https://travis-ci.org/mozilla/sops
+.. image:: https://pkg.go.dev/badge/go.mozilla.org/sops/v3.svg
+	:target: https://pkg.go.dev/go.mozilla.org/sops/v3
 
 Download
 --------
@@ -28,12 +25,13 @@ For the adventurous, unstable features are available in the `develop` branch, wh
 
 .. code:: bash
 
-	$ go get -u go.mozilla.org/sops/v3/cmd/sops
+	$ mkdir -p $GOPATH/src/go.mozilla.org/sops/
+        $ git clone https://github.com/mozilla/sops.git $GOPATH/src/go.mozilla.org/sops/
         $ cd $GOPATH/src/go.mozilla.org/sops/
         $ git checkout develop
         $ make install
 
-(requires Go >= 1.13)
+(requires Go >= 1.17)
 
 If you don't have Go installed, set it up with:
 
@@ -46,12 +44,7 @@ If you don't have Go installed, set it up with:
 
 Or whatever variation of the above fits your system and shell.
 
-To use **sops** as a library, take a look at the `decrypt package <https://godoc.org/go.mozilla.org/sops/decrypt>`_.
-
-**What happened to Python Sops?** We rewrote Sops in Go to solve a number of
-deployment issues, but the Python branch still exists under ``python-sops``. We
-will keep maintaining it for a while, and you can still ``pip install sops``,
-but we strongly recommend you use the Go version instead.
+To use **sops** as a library, take a look at the `decrypt package <https://pkg.go.dev/go.mozilla.org/sops/v3/decrypt>`_.
 
 .. sectnum::
 .. contents:: Table of Contents
@@ -189,14 +182,16 @@ the ``--age`` option or the **SOPS_AGE_RECIPIENTS** environment variable:
 
 .. code:: bash
 
-   $ sops --age age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw test.yaml > test.enc.yaml
+   $ sops --encrypt --age age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw test.yaml > test.enc.yaml
 
 When decrypting a file with the corresponding identity, sops will look for a
 text file name ``keys.txt`` located in a ``sops`` subdirectory of your user
-configuration directory. On Linux, this would be ``$XDG_CONFIG_HOME/sops/keys.txt``.
-On macOS, this would be ``$HOME/Library/Application Support/sops/keys.txt``. On
-Windows, this would be ``%AppData%\sops\keys.txt``. You can specify the location
+configuration directory. On Linux, this would be ``$XDG_CONFIG_HOME/sops/age/keys.txt``.
+On macOS, this would be ``$HOME/Library/Application Support/sops/age/keys.txt``. On
+Windows, this would be ``%AppData%\sops\age\keys.txt``. You can specify the location
 of this file manually by setting the environment variable **SOPS_AGE_KEY_FILE**.
+Alternatively you can provide the the key(s) directly by setting the **SOPS_AGE_KEY**
+environment variable.
 
 The contents of this key file should be a list of age X25519 identities, one
 per line. Lines beginning with ``#`` are considered comments and ignored. Each
@@ -650,7 +645,7 @@ and its KMS and PGP keys are used to encrypt the file. It should be noted that
 the looking up of ``.sops.yaml`` is from the working directory (CWD) instead of
 the directory of the encrypting file (see `Issue 242 <https://github.com/mozilla/sops/issues/242>`_).
 
-The path_regex checks the full path of the encrypting file. Here is another example:
+The path_regex checks the path of the encrypting file relative to the .sops.yaml config file. Here is another example:
 
 * files located under directory **development** should use one set of KMS A
 * files located under directory **production** should use another set of KMS B
@@ -704,12 +699,6 @@ By default, ``sops`` uses the key server ``keys.openpgp.org`` to retrieve the GP
 keys that are not present in the local keyring.
 This is no longer configurable. You can learn more about why from this write-up: `SKS Keyserver Network Under Attack <https://gist.github.com/rjhansen/67ab921ffb4084c865b3618d6955275f>`_.
 
-Example: place the following in your ``~/.bashrc``
-
-.. code:: bash
-
-	SOPS_GPG_KEYSERVER = 'gpg.example.com'
-
 
 Key groups
 ~~~~~~~~~~
@@ -1207,7 +1196,7 @@ This file will not work in sops:
 	  - array
 	  - elements
 
-But this one will because because the ``sops`` key can be added at the same level as the
+But this one will work because the ``sops`` key can be added at the same level as the
 ``data`` key.
 
 .. code:: yaml
@@ -1374,26 +1363,6 @@ The value must be formatted as json.
 
 	$ sops --set '["an_array"][1] {"uid1":null,"uid2":1000,"uid3":["bob"]}' ~/git/svc/sops/example.yaml
 
-Using sops as a library in a python script
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can import sops as a module and use it in your python program.
-
-.. code:: python
-
-	import sops
-
-	pathtype = sops.detect_filetype(path)
-	tree = sops.load_file_into_tree(path, pathtype)
-	sops_key, tree = sops.get_key(tree)
-	tree = sops.walk_and_decrypt(tree, sops_key)
-	sops.write_file(tree, path=path, filetype=pathtype)
-
-Note: this uses the previous implementation of `sops` written in python,
-
-and so doesn't support newer features such as GCP-KMS.
-To use the current version, call out to ``sops`` using ``subprocess.run``
-
 Showing diffs in cleartext in git
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1709,8 +1678,8 @@ file format introduced in **1.0**.
 Security
 --------
 
-Please report security issues to jvehent at mozilla dot com, or by using one
-of the contact method available on keybase: `https://keybase.io/jvehent <https://keybase.io/jvehent>`_
+Please report security issues to security at mozilla dot org, or by using one
+of the contact method available here: `https://www.mozilla.org/en-US/security/#For_Developers <https://www.mozilla.org/en-US/security/#For_Developers>`_
 
 License
 -------
@@ -1721,9 +1690,12 @@ Authors
 
 The core team is composed of:
 
+* AJ Banhken @ajvb
+
+The original authors were:
+
 * Adrian Utrilla @autrilla
 * Julien Vehent @jvehent
-* AJ Banhken @ajvb
 
 And a whole bunch of `contributors <https://github.com/mozilla/sops/graphs/contributors>`_
 
diff --git a/age/keysource.go b/age/keysource.go
index 941f422dc..e9e874ddf 100644
--- a/age/keysource.go
+++ b/age/keysource.go
@@ -20,7 +20,8 @@ func init() {
 	log = logging.NewLogger("AGE")
 }
 
-const privateKeySizeLimit = 1 << 24 // 16 MiB
+const SopsAgeKeyEnv = "SOPS_AGE_KEY"
+const SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE"
 
 // MasterKey is an age key used to encrypt and decrypt sops' data key.
 type MasterKey struct {
@@ -28,7 +29,6 @@ type MasterKey struct {
 	Recipient    string // a Bech32-encoded public key
 	EncryptedKey string // a sops data key encrypted with age
 
-	parsedIdentity  *age.X25519Identity  // a parsed age private key
 	parsedRecipient *age.X25519Recipient // a parsed age public key
 }
 
@@ -96,27 +96,46 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
 
 // Decrypt decrypts the EncryptedKey field with the age identity and returns the result.
 func (key *MasterKey) Decrypt() ([]byte, error) {
-	ageKeyFilePath, ok := os.LookupEnv("SOPS_AGE_KEY_FILE")
+	var ageKeyReader io.Reader
+	var ageKeyReaderName string
+
+	if ageKeyReader == nil {
+		ageKey, ok := os.LookupEnv(SopsAgeKeyEnv)
+		if ok {
+			ageKeyReader = strings.NewReader(ageKey)
+			ageKeyReaderName = "environment variable"
+		}
+	}
 
-	if !ok {
-		userConfigDir, err := os.UserConfigDir()
+	if ageKeyReader == nil {
+		ageKeyFilePath, ok := os.LookupEnv(SopsAgeKeyFileEnv)
+		if ok {
+			ageKeyFile, err := os.Open(ageKeyFilePath)
+			if err != nil {
+				return nil, fmt.Errorf("failed to open file: %w", err)
+			}
+			defer ageKeyFile.Close()
+			ageKeyReader = ageKeyFile
+			ageKeyReaderName = ageKeyFilePath
+		}
+	}
 
+	if ageKeyReader == nil {
+		userConfigDir, err := os.UserConfigDir()
 		if err != nil {
 			return nil, fmt.Errorf("user config directory could not be determined: %w", err)
 		}
-
-		ageKeyFilePath = filepath.Join(userConfigDir, "sops", "age", "keys.txt")
-	}
-
-	ageKeyFile, err := os.Open(ageKeyFilePath)
-
-	if err != nil {
-		return nil, fmt.Errorf("failed to open file: %w", err)
+		ageKeyFilePath := filepath.Join(userConfigDir, "sops", "age", "keys.txt")
+		ageKeyFile, err := os.Open(ageKeyFilePath)
+		if err != nil {
+			return nil, fmt.Errorf("failed to open file: %w", err)
+		}
+		defer ageKeyFile.Close()
+		ageKeyReader = ageKeyFile
+		ageKeyReaderName = ageKeyFilePath
 	}
 
-	defer ageKeyFile.Close()
-
-	identities, err := age.ParseIdentities(ageKeyFile)
+	identities, err := age.ParseIdentities(ageKeyReader)
 
 	if err != nil {
 		return nil, err
@@ -127,7 +146,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
 	r, err := age.Decrypt(ar, identities...)
 
 	if err != nil {
-		return nil, fmt.Errorf("no age identity found in %q that could decrypt the data", ageKeyFilePath)
+		return nil, fmt.Errorf("no age identity found in %q that could decrypt the data", ageKeyReaderName)
 	}
 
 	var b bytes.Buffer
diff --git a/config/config.go b/config/config.go
index e89336ddc..061d038e2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -8,7 +8,9 @@ import (
 	"io/ioutil"
 	"os"
 	"path"
+	"path/filepath"
 	"regexp"
+	"strings"
 
 	"github.com/sirupsen/logrus"
 	"go.mozilla.org/sops/v3"
@@ -313,12 +315,20 @@ func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptio
 	return config, nil
 }
 
-func parseCreationRuleForFile(conf *configFile, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
+func parseCreationRuleForFile(conf *configFile, confPath, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
 	// If config file doesn't contain CreationRules (it's empty or only contains DestionationRules), assume it does not exist
 	if conf.CreationRules == nil {
 		return nil, nil
 	}
 
+	configDir, err := filepath.Abs(filepath.Dir(confPath))
+	if err != nil {
+		return nil, err
+	}
+
+	// compare file path relative to path of config file
+	filePath = strings.TrimPrefix(filePath, configDir + string(filepath.Separator))
+
 	var rule *creationRule
 
 	for _, r := range conf.CreationRules {
@@ -356,7 +366,8 @@ func LoadCreationRuleForFile(confPath string, filePath string, kmsEncryptionCont
 	if err != nil {
 		return nil, err
 	}
-	return parseCreationRuleForFile(conf, filePath, kmsEncryptionContext)
+
+	return parseCreationRuleForFile(conf, confPath, filePath, kmsEncryptionContext)
 }
 
 // LoadDestinationRuleForFile works the same as LoadCreationRuleForFile, but gets the "creation_rule" from the matching destination_rule's
diff --git a/example.yaml b/example.yaml
index e79b06e03..a22d69e64 100644
--- a/example.yaml
+++ b/example.yaml
@@ -25,7 +25,7 @@ this:
 somelist_unencrypted:
 - all elements of this list
 - remain in clear text
-- because of the _encrypted suffix in the key
+- because of the _unencrypted suffix in the key
 nested_unencrypted:
     this:
         is:
diff --git a/go.mod b/go.mod
index 37784afcd..ef719487e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,42 +1,28 @@
 module go.mozilla.org/sops/v3
 
-go 1.13
+go 1.17
 
 require (
 	cloud.google.com/go v0.43.0
 	filippo.io/age v1.0.0-beta7
 	github.com/Azure/azure-sdk-for-go v31.2.0+incompatible
-	github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
 	github.com/Azure/go-autorest/autorest v0.9.0
 	github.com/Azure/go-autorest/autorest/azure/auth v0.1.0
-	github.com/Azure/go-autorest/autorest/to v0.3.0 // indirect
-	github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
-	github.com/Microsoft/go-winio v0.4.14 // indirect
-	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
 	github.com/aws/aws-sdk-go v1.37.18
 	github.com/blang/semver v3.5.1+incompatible
-	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
-	github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect
-	github.com/docker/go-connections v0.4.0 // indirect
-	github.com/docker/go-units v0.4.0 // indirect
 	github.com/fatih/color v1.7.0
 	github.com/golang/protobuf v1.4.1
 	github.com/google/go-cmp v0.5.0
 	github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf
-	github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
 	github.com/goware/prefixer v0.0.0-20160118172347-395022866408
 	github.com/hashicorp/vault/api v1.0.4
 	github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
 	github.com/lib/pq v1.2.0
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/go-wordwrap v1.0.0
-	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
-	github.com/opencontainers/image-spec v1.0.1 // indirect
-	github.com/opencontainers/runc v0.1.1 // indirect
 	github.com/ory/dockertest v3.3.4+incompatible
 	github.com/pkg/errors v0.9.1
 	github.com/sirupsen/logrus v1.4.2
-	github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect
 	github.com/stretchr/testify v1.5.1
 	go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a
 	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
@@ -49,5 +35,59 @@ require (
 	gopkg.in/ini.v1 v1.44.0
 	gopkg.in/urfave/cli.v1 v1.20.0
 	gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc
+)
+
+require (
+	github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
+	github.com/Azure/go-autorest/autorest/adal v0.5.0 // indirect
+	github.com/Azure/go-autorest/autorest/azure/cli v0.1.0 // indirect
+	github.com/Azure/go-autorest/autorest/date v0.1.0 // indirect
+	github.com/Azure/go-autorest/autorest/to v0.3.0 // indirect
+	github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
+	github.com/Azure/go-autorest/logger v0.1.0 // indirect
+	github.com/Azure/go-autorest/tracing v0.5.0 // indirect
+	github.com/Microsoft/go-winio v0.4.14 // indirect
+	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
+	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
+	github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
+	github.com/dimchansky/utfbom v1.1.0 // indirect
+	github.com/docker/go-connections v0.4.0 // indirect
+	github.com/docker/go-units v0.4.0 // indirect
+	github.com/golang/snappy v0.0.1 // indirect
+	github.com/googleapis/gax-go/v2 v2.0.5 // indirect
+	github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
+	github.com/hashicorp/go-multierror v1.0.0 // indirect
+	github.com/hashicorp/go-retryablehttp v0.5.4 // indirect
+	github.com/hashicorp/go-rootcerts v1.0.1 // indirect
+	github.com/hashicorp/go-sockaddr v1.0.2 // indirect
+	github.com/hashicorp/golang-lru v0.5.1 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/hashicorp/vault/sdk v0.1.13 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
+	github.com/mattn/go-colorable v0.0.9 // indirect
+	github.com/mattn/go-isatty v0.0.3 // indirect
+	github.com/mitchellh/mapstructure v1.1.2 // indirect
+	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
+	github.com/opencontainers/image-spec v1.0.1 // indirect
+	github.com/opencontainers/runc v0.1.1 // indirect
+	github.com/pierrec/lz4 v2.0.5+incompatible // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/ryanuber/go-glob v1.0.0 // indirect
+	github.com/sergi/go-diff v1.1.0 // indirect
+	github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect
+	github.com/stretchr/objx v0.1.1 // indirect
+	go.opencensus.io v0.22.0 // indirect
+	golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 // indirect
+	golang.org/x/text v0.3.3 // indirect
+	golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
+	google.golang.org/appengine v1.6.1 // indirect
+	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
+	gopkg.in/square/go-jose.v2 v2.3.1 // indirect
+	gopkg.in/yaml.v2 v2.2.8 // indirect
 	gotest.tools v2.2.0+incompatible // indirect
 )
diff --git a/stores/yaml/store.go b/stores/yaml/store.go
index a3ae2fb46..d9e78b528 100644
--- a/stores/yaml/store.go
+++ b/stores/yaml/store.go
@@ -70,7 +70,7 @@ func (store Store) nodeToTreeValue(node *yaml.Node, commentsWereHandled bool) (i
 		return result, nil
 	case yaml.MappingNode:
 		branch := make(sops.TreeBranch, 0)
-		return store.appendYamlNodeToTreeBranch(node, branch, false)
+		return store.appendYamlNodeToTreeBranch(node, branch, commentsWereHandled)
 	case yaml.ScalarNode:
 		var result interface{}
 		node.Decode(&result)
diff --git a/version/version.go b/version/version.go
index 4bd14cbb2..2791d9c54 100644
--- a/version/version.go
+++ b/version/version.go
@@ -11,7 +11,7 @@ import (
 )
 
 // Version represents the value of the current semantic version
-const Version = "3.7.1"
+const Version = "3.7.2"
 
 // PrintVersion handles the version command for sops
 func PrintVersion(c *cli.Context) {


Test patch:
diff --git a/age/keysource_test.go b/age/keysource_test.go
index 35891b20f..0b30b355d 100644
--- a/age/keysource_test.go
+++ b/age/keysource_test.go
@@ -1,6 +1,7 @@
 package age
 
 import (
+	"io/ioutil"
 	"os"
 	"path"
 	"runtime"
@@ -44,7 +45,7 @@ func TestAge(t *testing.T) {
 	assert.NoError(err)
 
 	_, filename, _, _ := runtime.Caller(0)
-	err = os.Setenv("SOPS_AGE_KEY_FILE", path.Join(path.Dir(filename), "keys.txt"))
+	err = os.Setenv(SopsAgeKeyFileEnv, path.Join(path.Dir(filename), "keys.txt"))
 	assert.NoError(err)
 
 	decryptedKey, err := key.Decrypt()
@@ -70,7 +71,33 @@ DOMAIN=files.127.0.0.1.nip.io`
 	assert.NoError(err)
 
 	_, filename, _, _ := runtime.Caller(0)
-	err = os.Setenv("SOPS_AGE_KEY_FILE", path.Join(path.Dir(filename), "keys.txt"))
+	err = os.Setenv(SopsAgeKeyFileEnv, path.Join(path.Dir(filename), "keys.txt"))
+	defer os.Unsetenv(SopsAgeKeyFileEnv)
+	assert.NoError(err)
+
+	decryptedKey, err := key.Decrypt()
+	assert.NoError(err)
+	assert.Equal(dataKey, decryptedKey)
+}
+
+func TestAgeEnv(t *testing.T) {
+	assert := assert.New(t)
+
+	key, err := MasterKeyFromRecipient("age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw")
+
+	assert.NoError(err)
+	assert.Equal("age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw", key.ToString())
+
+	dataKey := []byte("abcdefghijklmnopqrstuvwxyz123456")
+
+	err = key.Encrypt(dataKey)
+	assert.NoError(err)
+
+	_, filename, _, _ := runtime.Caller(0)
+	keysBytes, err := ioutil.ReadFile(path.Join(path.Dir(filename), "keys.txt"))
+	assert.NoError(err)
+	err = os.Setenv(SopsAgeKeyEnv, string(keysBytes))
+	defer os.Unsetenv(SopsAgeKeyEnv)
 	assert.NoError(err)
 
 	decryptedKey, err := key.Decrypt()
diff --git a/config/config_test.go b/config/config_test.go
index ac8aca6f3..a653fcb8e 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -75,6 +75,15 @@ creation_rules:
     hc_vault_uris: https://foz:443/v1/foz/keys/foz
 `)
 
+var sampleConfigWithAmbiguousPath = []byte(`
+creation_rules:
+  - path_regex: foo/*
+    kms: "1"
+    pgp: "2"
+    gcp_kms: "3"
+    hc_vault_uris: http://4:8200/v1/4/keys/4
+`)
+
 var sampleConfigWithGroups = []byte(`
 creation_rules:
   - path_regex: foobar*
@@ -299,12 +308,12 @@ func TestLoadConfigFileWithGroups(t *testing.T) {
 }
 
 func TestLoadConfigFileWithNoMatchingRules(t *testing.T) {
-	_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithNoMatchingRules, t), "foobar2000", nil)
+	_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithNoMatchingRules, t), "/conf/path", "foobar2000", nil)
 	assert.NotNil(t, err)
 }
 
 func TestLoadConfigFileWithInvalidComplicatedRegexp(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidComplicatedRegexp, t), "stage/prod/api.yml", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidComplicatedRegexp, t), "/conf/path", "stage/prod/api.yml", nil)
 	assert.Equal(t, "can not compile regexp: error parsing regexp: invalid escape sequence: `\\K`", err.Error())
 	assert.Nil(t, conf)
 }
@@ -315,58 +324,58 @@ func TestLoadConfigFileWithComplicatedRegexp(t *testing.T) {
 		"stage/dev/feature-foo.yml": "dev-feature",
 		"stage/dev/api.yml":         "dev",
 	} {
-		conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithComplicatedRegexp, t), filePath, nil)
+		conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithComplicatedRegexp, t), "/conf/path", filePath, nil)
 		assert.Nil(t, err)
 		assert.Equal(t, k, conf.KeyGroups[0][0].ToString())
 	}
 }
 
 func TestLoadEmptyConfigFile(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleEmptyConfig, t), "foobar2000", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleEmptyConfig, t), "/conf/path", "foobar2000", nil)
 	assert.Nil(t, conf)
 	assert.Nil(t, err)
 }
 
 func TestLoadConfigFileWithEmptyCreationRules(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithEmptyCreationRules, t), "foobar2000", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithEmptyCreationRules, t), "/conf/path", "foobar2000", nil)
 	assert.Nil(t, conf)
 	assert.Nil(t, err)
 }
 
 func TestLoadConfigFileWithOnlyDestinationRules(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithOnlyDestinationRules, t), "foobar2000", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithOnlyDestinationRules, t), "/conf/path", "foobar2000", nil)
 	assert.Nil(t, conf)
 	assert.Nil(t, err)
 }
 
 func TestKeyGroupsForFile(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfig, t), "foobar2000", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfig, t), "/conf/path", "foobar2000", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "2", conf.KeyGroups[0][0].ToString())
 	assert.Equal(t, "1", conf.KeyGroups[0][1].ToString())
-	conf, err = parseCreationRuleForFile(parseConfigFile(sampleConfig, t), "whatever", nil)
+	conf, err = parseCreationRuleForFile(parseConfigFile(sampleConfig, t), "/conf/path", "whatever", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "bar", conf.KeyGroups[0][0].ToString())
 	assert.Equal(t, "foo", conf.KeyGroups[0][1].ToString())
 }
 
 func TestKeyGroupsForFileWithPath(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithPath, t), "foo/bar2000", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithPath, t), "/conf/path", "foo/bar2000", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "2", conf.KeyGroups[0][0].ToString())
 	assert.Equal(t, "1", conf.KeyGroups[0][1].ToString())
-	conf, err = parseCreationRuleForFile(parseConfigFile(sampleConfigWithPath, t), "somefilename.yml", nil)
+	conf, err = parseCreationRuleForFile(parseConfigFile(sampleConfigWithPath, t), "/conf/path", "somefilename.yml", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "baggins", conf.KeyGroups[0][0].ToString())
 	assert.Equal(t, "bilbo", conf.KeyGroups[0][1].ToString())
-	conf, err = parseCreationRuleForFile(parseConfigFile(sampleConfig, t), "whatever", nil)
+	conf, err = parseCreationRuleForFile(parseConfigFile(sampleConfig, t), "/conf/path", "whatever", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "bar", conf.KeyGroups[0][0].ToString())
 	assert.Equal(t, "foo", conf.KeyGroups[0][1].ToString())
 }
 
 func TestKeyGroupsForFileWithGroups(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithGroups, t), "whatever", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithGroups, t), "/conf/path", "whatever", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "bar", conf.KeyGroups[0][0].ToString())
 	assert.Equal(t, "foo", conf.KeyGroups[0][1].ToString())
@@ -375,31 +384,39 @@ func TestKeyGroupsForFileWithGroups(t *testing.T) {
 }
 
 func TestLoadConfigFileWithUnencryptedSuffix(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithSuffixParameters, t), "foobar", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithSuffixParameters, t), "/conf/path", "foobar", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "_unencrypted", conf.UnencryptedSuffix)
 }
 
 func TestLoadConfigFileWithEncryptedSuffix(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithSuffixParameters, t), "barfoo", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithSuffixParameters, t), "/conf/path", "barfoo", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, "_enc", conf.EncryptedSuffix)
 }
 
 func TestLoadConfigFileWithUnencryptedRegex(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "barbar", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "/conf/path", "barbar", nil)
 	assert.Equal(t, nil, err)
 	assert.Equal(t, "^dec:", conf.UnencryptedRegex)
 }
 
 func TestLoadConfigFileWithEncryptedRegex(t *testing.T) {
-	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "barbar", nil)
+	conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "/conf/path", "barbar", nil)
 	assert.Equal(t, nil, err)
 	assert.Equal(t, "^enc:", conf.EncryptedRegex)
 }
 
 func TestLoadConfigFileWithInvalidParameters(t *testing.T) {
-	_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "foobar", nil)
+	_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "/conf/path", "foobar", nil)
+	assert.NotNil(t, err)
+}
+
+func TestLoadConfigFileWithAmbiguousPath(t *testing.T) {
+	config := parseConfigFile(sampleConfigWithAmbiguousPath, t)
+	_, err := parseCreationRuleForFile(config, "/foo/config", "/foo/foo/bar", nil)
+	assert.Nil(t, err)
+	_, err = parseCreationRuleForFile(config, "/foo/config", "/foo/fuu/bar", nil)
 	assert.NotNil(t, err)
 }
 
diff --git a/hcvault/keysource_test.go b/hcvault/keysource_test.go
index 9290acc1a..48a31dc83 100644
--- a/hcvault/keysource_test.go
+++ b/hcvault/keysource_test.go
@@ -25,7 +25,8 @@ func TestMain(m *testing.M) {
 		logger.Fatalf("Could not start resource: %s", err)
 	}
 
-	os.Setenv("VAULT_ADDR", fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("8200/tcp")))
+	vaultAddr := fmt.Sprintf("http://%s", resource.GetHostPort("8200/tcp"))
+	os.Setenv("VAULT_ADDR", vaultAddr)
 	os.Setenv("VAULT_TOKEN", "secret")
 	// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
 	if err := pool.Retry(func() error {
@@ -45,7 +46,7 @@ func TestMain(m *testing.M) {
 		logger.Fatalf("Could not connect to docker: %s", err)
 	}
 
-	key := NewMasterKey(fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("8200/tcp")), "sops", "main")
+	key := NewMasterKey(vaultAddr, "sops", "main")
 	err = key.createVaultTransitAndKey()
 	if err != nil {
 		logger.Fatal(err)
diff --git a/pgp/keysource_test.go b/pgp/keysource_test.go
index 0c10996d1..1e66056bc 100644
--- a/pgp/keysource_test.go
+++ b/pgp/keysource_test.go
@@ -44,7 +44,10 @@ func TestPGPKeySourceFromString(t *testing.T) {
 }
 
 func TestRetrievePGPKey(t *testing.T) {
-	fingerprint := "FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4"
+	// Requires a key available in https://keys.openpgp.org/ *with identity information* (that is, an email address).
+	// See https://keys.openpgp.org/about/faq#verify-multiple for details about identity information.
+	// We use the key of release@mozilla.com for here.
+	fingerprint := "14F26682D0916CDD81E37B6D61B7B526D98F0353"
 	_, err := getKeyFromKeyServer(fingerprint)
 	assert.NoError(t, err)
 }
diff --git a/stores/yaml/store_test.go b/stores/yaml/store_test.go
index be6b90eb2..05249f4a5 100644
--- a/stores/yaml/store_test.go
+++ b/stores/yaml/store_test.go
@@ -91,6 +91,26 @@ var COMMENT_5 = []byte(`# foo
 key: value
 `)
 
+// The following is a regression test for https://github.com/mozilla/sops/issues/865
+var COMMENT_6 = []byte(`a:
+    - a
+    # I no longer get duplicated
+    - {}
+`)
+
+var COMMENT_6_BRANCHES = sops.TreeBranches{
+	sops.TreeBranch{
+		sops.TreeItem{
+			Key:   "a",
+			Value: []interface{}{
+				"a",
+				sops.Comment{" I no longer get duplicated"},
+				sops.TreeBranch{},
+			},
+		},
+	},
+}
+
 func TestUnmarshalMetadataFromNonSOPSFile(t *testing.T) {
 	data := []byte(`hello: 2`)
 	_, err := (&Store{}).LoadEncryptedFile(data)
@@ -178,6 +198,16 @@ func TestEmpty2(t *testing.T) {
 }
 */
 
+func TestComment6(t *testing.T) {
+	branches, err := (&Store{}).LoadPlainFile(COMMENT_6)
+	assert.Nil(t, err)
+	assert.Equal(t, COMMENT_6_BRANCHES, branches)
+	bytes, err := (&Store{}).EmitPlainFile(branches)
+	assert.Nil(t, err)
+	assert.Equal(t, string(COMMENT_6), string(bytes))
+	assert.Equal(t, COMMENT_6, bytes)
+}
+
 func TestEmitValue(t *testing.T) {
 	// First iteration: load and store
 	bytes, err := (&Store{}).EmitValue(BRANCHES[0])
diff --git a/test.sh b/test.sh
index 34dbbfb31..d99b4e2f1 100755
--- a/test.sh
+++ b/test.sh
@@ -3,10 +3,18 @@
 set -e
 echo "" > coverage.txt
 
+failed=0
+
 for d in $(go list ./... | grep -v vendor); do
-    go test -race -coverprofile=profile.out -covermode=atomic $d
+    go test -race -coverprofile=profile.out -covermode=atomic $d && true
+    rc=$?
+    if [ $rc != 0 ]; then
+      failed=$rc
+    fi
     if [ -f profile.out ]; then
         cat profile.out >> coverage.txt
         rm profile.out
     fi
 done
+
+exit ${failed}