Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 98 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,112 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
name: test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.25.x'
go-version: stable
cache: true
- run: gofmt -w . && git diff --exit-code
- run: go vet ./...
- run: go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
- run: go test ./...

native-tests:
name: native-${{ matrix.os }}-go-${{ matrix.go }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
go: ['1.25.x', stable]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ matrix.go }}
cache: true
- run: go test ./...

race:
name: race
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache: true
- run: go test -race ./...
- run: go build ./cmd/envguard

vet:
name: vet
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache: true
- run: test -z "$(gofmt -l .)"
- run: go vet ./...

staticcheck:
name: staticcheck
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache: true
- run: go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...

vulnerability:
name: vulnerability
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache: true
- run: go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./...

release-validation:
name: release-validation
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache: true
- uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
version: v2.17.1
args: release --snapshot --clean --skip=publish
- run: go run ./cmd/releasecheck -dist dist
- name: Prove corruption blocks validation
shell: bash
run: |
test_dir="$(mktemp -d)"
cp dist/*.tar.gz dist/*.zip dist/checksums.txt "$test_dir/"
corrupt="$(find "$test_dir" -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) | sort | sed -n '2p')"
test -n "$corrupt"
printf 'corrupt' >> "$corrupt"
if go run ./cmd/releasecheck -dist "$test_dir" -smoke=false; then
echo "release validation accepted a corrupted non-first archive" >&2
exit 1
fi
28 changes: 28 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CodeQL

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '17 3 * * 4'

permissions:
contents: read

jobs:
analyze:
name: analyze (go)
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
with:
languages: go
- uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
- uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
126 changes: 95 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,115 @@ name: Release

on:
push:
tags: ["v*"]

permissions:
contents: write
id-token: write
attestations: write
tags: ['v*']

jobs:
release:
build:
name: build-package
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.25.x'
go-version: stable
cache: true
- name: Test release source
run: go test ./...
- name: Build and publish release
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
run: |
test -z "$(gofmt -l .)"
go test ./...
go test -race ./...
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./...
- name: Build without publishing
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
version: v2.17.0
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify packaged archive
version: v2.17.1
args: release --clean --skip=publish
- name: Preserve candidate artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-candidate
path: |
dist/*.tar.gz
dist/*.zip
dist/checksums.txt
if-no-files-found: error
retention-days: 7

verify:
name: verify-every-artifact
needs: build
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache: true
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate
path: dist
- name: Validate contents, checksums, matrix, and native smoke test
run: go run ./cmd/releasecheck -dist dist
- name: Prove a corrupted non-first archive cannot pass
shell: bash
run: |
verify_dir="$(mktemp -d)"
archive="$(find dist -maxdepth 1 -name 'envguard_*_linux_amd64.tar.gz' -print -quit)"
test -n "$archive"
tar -xzf "$archive" -C "$verify_dir"
version_output="$("$verify_dir/envguard" version)"
test -n "$version_output"
test "$version_output" != "dev"
test -f "$verify_dir/LICENSE"
test -f "$verify_dir/README.md"
- name: Attest archives and checksums
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
test_dir="$(mktemp -d)"
cp dist/*.tar.gz dist/*.zip dist/checksums.txt "$test_dir/"
corrupt="$(find "$test_dir" -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) | sort | sed -n '2p')"
test -n "$corrupt"
printf 'corrupt' >> "$corrupt"
if go run ./cmd/releasecheck -dist "$test_dir" -smoke=false; then
echo "release validation accepted a corrupted non-first archive" >&2
exit 1
fi

attest:
name: attest-verified-digests
needs: verify
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate
path: dist
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
dist/*.tar.gz
dist/*.zip
dist/checksums.txt

publish:
name: publish-verified-artifacts
needs: attest
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate
path: dist
- name: Create public GitHub release from verified artifacts
env:
GH_TOKEN: ${{ github.token }}
run: >-
gh release create "$GITHUB_REF_NAME"
dist/*.tar.gz dist/*.zip dist/checksums.txt
--repo "$GITHUB_REPOSITORY"
--verify-tag
--generate-notes
5 changes: 4 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ builds:
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
archives:
- formats: [tar.gz]
- name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
formats: [tar.gz]
files:
- LICENSE
- README.md
Expand All @@ -19,3 +20,5 @@ checksum:
name_template: checksums.txt
changelog:
sort: asc
release:
disable: true
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ A runnable contract and development environment are available under [`examples/`
envguard check --template examples/example.env --schema examples/envguard.json examples/development.env
```

Without a schema, every template key is required and extra keys produce warnings. `--strict-extra` promotes extras to errors. When `envguard.json` exists it is loaded automatically; pass `--no-schema` to disable that behavior.
Without a schema, every template key is required and extra keys produce warnings. `--strict-extra` promotes extras to errors. When `envguard.json` exists it is loaded automatically; its absence is allowed only during this implicit discovery. A path supplied with `--schema` is mandatory and any missing, unreadable, non-regular, or invalid file returns operational exit code 3. Pass `--no-schema` to disable discovery; it cannot be combined with `--schema`.

## Schema

Expand All @@ -43,7 +43,7 @@ Without a schema, every template key is required and extra keys produce warnings
}
```

Supported types are `string`, `integer`, `number`, `boolean`, `url`, `json`, and `enum`. Constraints include `min`, `max`, `minLength`, `maxLength`, `pattern`, `values`, and URL `schemes`. Unknown and duplicate JSON keys are rejected.
Supported types are `string`, `integer`, `number`, `boolean`, `url`, `json`, and `enum`. Constraints are type-specific: strings accept length bounds and a pattern, integer and number accept numeric bounds, enum requires values, and URL accepts schemes. Unknown fields, duplicate JSON keys, duplicate enum values, duplicate schemes, and irrelevant constraints are rejected.

## Security behavior

Expand Down
11 changes: 10 additions & 1 deletion cmd/envguard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ package main

import (
"os"
"runtime/debug"

"github.com/1337lean/envguard/internal/app"
)

var version = "dev"

func main() {
os.Exit(app.Run(os.Args[1:], os.Stdout, os.Stderr, version))
build, _ := debug.ReadBuildInfo()
os.Exit(app.Run(os.Args[1:], os.Stdout, os.Stderr, resolvedVersion(version, build)))
}

func resolvedVersion(injected string, build *debug.BuildInfo) string {
if injected != "dev" || build == nil || build.Main.Version == "" || build.Main.Version == "(devel)" {
return injected
}
return build.Main.Version
}
26 changes: 26 additions & 0 deletions cmd/envguard/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"runtime/debug"
"testing"
)

func TestResolvedVersion(t *testing.T) {
for _, tc := range []struct {
name string
injected string
build *debug.BuildInfo
want string
}{
{name: "release injection wins", injected: "1.2.3", build: &debug.BuildInfo{Main: debug.Module{Version: "v9.9.9"}}, want: "1.2.3"},
{name: "module version", injected: "dev", build: &debug.BuildInfo{Main: debug.Module{Version: "v0.1.0"}}, want: "v0.1.0"},
{name: "development build", injected: "dev", build: &debug.BuildInfo{Main: debug.Module{Version: "(devel)"}}, want: "dev"},
{name: "missing build info", injected: "dev", want: "dev"},
} {
t.Run(tc.name, func(t *testing.T) {
if got := resolvedVersion(tc.injected, tc.build); got != tc.want {
t.Fatalf("resolvedVersion() = %q, want %q", got, tc.want)
}
})
}
}
Loading