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
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- name: Integration tests (root)
# Root integration packages share the host account databases and must
# not run useradd/userdel concurrently across package processes.
run: sudo -E env "PATH=$PATH" go test -race -p 1 -tags integration ./...
run: sudo -E env "PATH=$PATH" go test -count=1 -race -p 1 -tags integration ./...

static-cross-build:
name: Static cross-build (${{ matrix.goarch }})
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ All notable changes to this project are documented here.
and remains only as an audit record; no published v2.8.1 artifacts exist.
- Record that the current v1 ed25519 release key has historical network-host
exposure instead of claiming an air-gapped custody history it does not have.
- Keep documented root integration runs uncached and package-serialized, and
scan every workflow plus the contributor commands for unsafe parallel forms.
- Adopt the single-maintainer release model: protected `main` requires no
independent approval, CODEOWNERS remains metadata, last-push approval is not
required, and neither protected release environment has required reviewers.

## v2.8.1 - 2026-07-27

Expand Down
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ go vet -tags integration -printf.funcs=printf,errorf,warnf ./...
test -z "$(gofmt -l .)" # gofmt must be clean
go test -count=1 ./...
go test -count=1 -race ./...
sudo -E env "PATH=$PATH" go test -count=1 -tags integration ./...
sudo -E env "PATH=$PATH" go test -count=1 -race -tags integration ./...
sudo -E env "PATH=$PATH" go test -count=1 -p 1 -tags integration ./...
sudo -E env "PATH=$PATH" go test -count=1 -race -p 1 -tags integration ./...
staticcheck ./...
staticcheck -tags integration ./...
govulncheck ./...
```

The integration suites use fixed disposable account names. Run the two integration commands **serially**, never concurrently, and only on a disposable host.
The integration suites use fixed disposable account names and shared host account
databases. Each command serializes package processes with `-p 1`; also run the
two commands themselves serially, never concurrently, and only on a disposable
host.

**Release/install scripts**, if you touch `scripts/`:

Expand Down
168 changes: 152 additions & 16 deletions internal/selfmanage/release_pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3339,25 +3339,161 @@ func TestVulnerabilityScannerIsPinnedAndRunsInReleaseGate(t *testing.T) {
}

func TestRootIntegrationPackagesRunSerially(t *testing.T) {
workflows := map[string]struct {
path string
want string
}{
"Go": {
path: "../../.github/workflows/go.yml",
want: `go test -race -p 1 -tags integration ./...`,
},
"Release": {
path: "../../.github/workflows/release.yml",
want: `go test -mod=readonly -count=1 -race -p 1 -tags integration ./...`,
},
var paths []string
for _, pattern := range []string{"../../.github/workflows/*.yml", "../../.github/workflows/*.yaml"} {
matches, err := filepath.Glob(pattern)
if err != nil {
t.Fatal(err)
}
paths = append(paths, matches...)
}
paths = append(paths, "../../CONTRIBUTING.md")
commandCount := 0
for _, path := range paths {
content := readReleaseFile(t, path)
count, unserialized := rootIntegrationPackageCommands(content)
commandCount += count
for _, line := range unserialized {
t.Errorf("%s:%d runs root integration packages without -p 1", path, line)
}
}
if commandCount != 4 {
t.Fatalf("found %d root integration package commands, want 4", commandCount)
}

goWorkflow := readReleaseFile(t, "../../.github/workflows/go.yml")
if !strings.Contains(goWorkflow, `go test -count=1 -race -p 1 -tags integration ./...`) {
t.Error("Go workflow does not force an uncached serialized root integration run")
}
for name, workflow := range workflows {
content := readReleaseFile(t, workflow.path)
if !strings.Contains(content, workflow.want) {
t.Errorf("%s workflow does not serialize root integration packages", name)
releaseWorkflow := readReleaseFile(t, "../../.github/workflows/release.yml")
if !strings.Contains(releaseWorkflow, `go test -mod=readonly -count=1 -race -p 1 -tags integration ./...`) {
t.Error("Release workflow does not force an uncached serialized root integration run")
}

bait := "# go test -p 1 -tags integration ./...\n" +
"go test -tags integration ./... # -p 1\n" +
"go test -p 1 -tags integration ./... && go test -tags integration ./...\n" +
"go test -p 1 -tags integration ./...; go test -tags integration ./...\n"
count, unserialized := rootIntegrationPackageCommands(bait)
if count != 5 || len(unserialized) != 3 || unserialized[0] != 2 || unserialized[1] != 3 || unserialized[2] != 4 {
t.Fatalf("root command scan accepted a comment bait or missed a chained unserialized command: count=%d lines=%v", count, unserialized)
}
}

func rootIntegrationPackageCommands(content string) (int, []int) {
content = strings.ReplaceAll(content, "\\\n", " ")
commandCount := 0
var unserialized []int
for lineNumber, line := range strings.Split(content, "\n") {
for _, fields := range shellCommandFields(line) {
goTest := false
integration := false
allPackages := false
serialized := false
for i, field := range fields {
if field == "go" && i+1 < len(fields) && fields[i+1] == "test" {
goTest = true
}
if field == "-tags" && i+1 < len(fields) {
integration = integration || commaListContains(fields[i+1], "integration")
} else if strings.HasPrefix(field, "-tags=") {
integration = integration || commaListContains(strings.TrimPrefix(field, "-tags="), "integration")
}
if field == "./..." {
allPackages = true
}
if field == "-p=1" || (field == "-p" && i+1 < len(fields) && fields[i+1] == "1") {
serialized = true
}
}
if !goTest || !integration || !allPackages {
continue
}
commandCount++
if !serialized {
unserialized = append(unserialized, lineNumber+1)
}
}
}
return commandCount, unserialized
}

func shellCommandFields(line string) [][]string {
var commands [][]string
var fields []string
var field strings.Builder
var quote byte
escaped := false

flushField := func() {
if field.Len() == 0 {
return
}
fields = append(fields, field.String())
field.Reset()
}
flushCommand := func() {
flushField()
if len(fields) == 0 {
return
}
commands = append(commands, fields)
fields = nil
}

for i := 0; i < len(line); i++ {
ch := line[i]
if escaped {
field.WriteByte(ch)
escaped = false
continue
}
if quote != '\'' && ch == '\\' {
escaped = true
continue
}
if quote != 0 {
if ch == quote {
quote = 0
} else {
field.WriteByte(ch)
}
continue
}
if ch == '\'' || ch == '"' {
quote = ch
continue
}
if ch == '#' && field.Len() == 0 {
break
}
if ch == ';' || ch == '|' || (ch == '&' && i+1 < len(line) && line[i+1] == '&') {
flushCommand()
if i+1 < len(line) && line[i+1] == ch {
i++
}
continue
}
if ch == ' ' || ch == '\t' || ch == '\r' {
flushField()
continue
}
field.WriteByte(ch)
}
if escaped {
field.WriteByte('\\')
}
flushCommand()
return commands
}

func commaListContains(list, want string) bool {
for _, item := range strings.Split(list, ",") {
if item == want {
return true
}
}
return false
}

func TestReleaseKeyringValidationIsPortableAcrossAwkImplementations(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions internal/selfmanage/release_pubkey.hex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
#
# CONFIGURED: signed self-upgrade is ENABLED. `upgrade` verifies each downloaded
# release binary against every key before installing it, failing closed on any
# mismatch. Private keys exist only on the air-gapped signing machine. Candidate
# source and online preparation/publication never receive a private-key path.
# mismatch. The current v1 key has historical network-host exposure; new keys
# must follow the air-gapped custody model in docs/releasing.md. Candidate source
# and online preparation/publication never receive a private-key path.
#
982471fb0c9f82f76143c89d045f8a931180f8cca27dad5286bb2000614eaf1c