Skip to content

feat: Go support — artifactory-oidc ecosystem + go-lockfile-hygiene - #18

Merged
rdelpret merged 1 commit into
mainfrom
go-publishing
Aug 24, 2026
Merged

feat: Go support — artifactory-oidc ecosystem + go-lockfile-hygiene#18
rdelpret merged 1 commit into
mainfrom
go-publishing

Conversation

@rdelpret

Copy link
Copy Markdown
Contributor

Adds the Go lane to the shared actions so public Go SDK repos (twilio/twilio-go first) can resolve through curated Artifactory and gate what they publish.

The Artifactory side is already in place — virtual-go-thirdparty and the twilio/twilio-go OIDC trust both exist in artifactory-cloud-twilio-config. This is the missing consumer half.

artifactory-oidc: ecosystem: go

  • GOPROXYhttps://<host>/artifactory/api/go/virtual-go-thirdparty, with no ,direct fallback so curation is fail-closed. With direct, any module Artifactory refuses (curation block, CVE, version-age cooldown) would be silently fetched straight from its VCS host — the control would fail open with no error. Same reasoning as disabling packagist.org in the PHP branch.
  • The token goes in ~/.netrc (0600), not in GOPROXY. go env prints GOPROXY verbatim, so a token embedded in the URL leaks into any log that dumps the Go environment.
  • Clears GOPRIVATE / GONOPROXY / GONOSUMDB. A self-hosted runner with GOPRIVATE=github.com/twilio/* makes the go command skip both the proxy and the checksum database for exactly the modules this lane exists to control.
  • GOSUMDB is deliberately left alone. The committed go.sum verifies every module already required; the checksum DB is only consulted for modules not yet in it, and the go command falls back to sum.golang.org directly if the proxy doesn't serve /sumdb/.

go-lockfile-hygiene (new)

Check Why
Internal module paths in go.mod / go.sum Denylist, not allowlist — in Go the module path is the fetch location, and the long tail of legitimate public vanity domains (golang.org/x, k8s.io, go.uber.org, rsc.io, gopkg.in) makes an allowlist pure noise. The clean-room resolve is the real fail-closed proof.
replace directives rejected The go command honours replace only in the main module. A library with one resolves a different graph in CI than at every consumer — the Go analogue of a committed Composer repositories block.
Module path under github.com/$GITHUB_REPOSITORY go get fetches by module path. A mismatched path publishes to a name nothing serves.
/vN suffix vs. the tag's major (on tag refs) v2+ without the suffix produces a tag the proxy refuses to serve.
Clean room Hermetic GOMODCACHE/GOPATH, public proxy + sumdb, ambient Go env cleared, then go mod download all + go mod verify + go build ./....

Parsing uses go mod edit -json rather than regex, so single-line and block require/replace/exclude forms, comments, and quoting all work. A parse failure is a hard error — the gate never reports a pass it couldn't actually verify.

Tests

  • Four scan fixtures (clean, internal-host, replace-directive, wrong-module-path) each asserting an expected outcome, so a gate that stops failing is caught as loudly as one that starts failing.
  • A clean-room job against a real module generated in CI from the public proxy.
  • A table test for the /vN rule, which only fires on a tag ref and so can never be reached by a PR run.

The thing that does not transfer from the other guides

In Go, the tag push is the publish. There is no upload, no registry account, no credential — proxy.golang.org fetches the tag on first request and sum.golang.org pins its h1: hash permanently.

So an environment: gate on an on: push: tags: workflow gates nothing — by the time the job starts the version is already public. Packagist at least waits for a GitHub Release; Go waits for nothing. The README documents the two controls that do work: a tag ruleset on refs/tags/v*, and a workflow_dispatch release workflow whose tagging job carries environment: production, so the tag only comes into existence after approval.

Companion IPD guide (setup-go-publishing.md) coming in a separate PR.

Adds the Go lane to the shared actions so public Go SDK repos (twilio-go
first) can resolve through curated Artifactory and gate what they publish.

artifactory-oidc: `ecosystem: go`
  - GOPROXY -> https://<host>/artifactory/api/go/virtual-go-thirdparty, with
    NO `,direct` fallback so curation is fail-closed. With `direct`, any
    module Artifactory refuses would be fetched straight from its VCS host.
  - Token goes in ~/.netrc (0600), not in GOPROXY: `go env` prints GOPROXY
    verbatim, so a token in the URL leaks into any log dumping the Go env.
  - Clears GOPRIVATE / GONOPROXY / GONOSUMDB. On a self-hosted runner
    GOPRIVATE=github.com/twilio/* silently disables both the proxy and the
    checksum database for exactly the modules that matter.
  - GOSUMDB is left alone: the committed go.sum verifies everything already
    required, and the sumdb is only consulted for modules not yet in it.

go-lockfile-hygiene (new)
  - Scan: internal module paths in go.mod / go.sum, using a denylist rather
    than an allowlist — in Go the module path IS the fetch location, and the
    long tail of legitimate public vanity domains makes an allowlist noise.
  - Rejects `replace` directives: the go command honours them only in the
    main module, so a library with one resolves a different graph in CI than
    at every consumer.
  - Checks the module path sits under github.com/$GITHUB_REPOSITORY, and on
    a tag ref that the /vN suffix matches the tag's major version.
  - Clean room: hermetic GOMODCACHE/GOPATH, public proxy + sumdb, ambient
    Go env cleared, then `go mod download all` + `go mod verify` + `go build`.

Self-tests: four scan fixtures asserting expected pass/fail, a clean-room job
against a real public module, and a table test for the /vN rule (which only
fires on a tag ref, so a PR run can never reach it).

README documents the thing that does not transfer from the other guides: in
Go the tag push IS the publish, so an `environment:` gate on a tag-push
workflow gates nothing. The gate has to sit in front of tag creation — a tag
ruleset plus a workflow_dispatch release job carrying environment: production.
@rdelpret
rdelpret requested review from a team and ryanrishi as code owners August 19, 2026 00:25
@ryanrishi
ryanrishi requested a lite review from Copilot August 20, 2026 17:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Go support to this repo’s shared GitHub Actions by (a) enabling Artifactory OIDC configuration for Go module resolution and (b) introducing a new go-lockfile-hygiene supply-chain gate with CI coverage and documentation updates.

Changes:

  • Extend artifactory-oidc with ecosystem: go to configure GOPROXY and authenticate via ~/.netrc, while clearing private-module env vars that can bypass proxy/sumdb behavior.
  • Add go-lockfile-hygiene composite action to scan Go module metadata for internal/unsafe patterns and optionally run a hermetic “clean-room” resolve/build.
  • Add fixture-based workflow tests plus README guidance for composing Go CI + release flows.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
artifactory-oidc/action.yml Adds Go ecosystem support (GOPROXY + netrc auth) and updates ecosystem defaults/docs.
go-lockfile-hygiene/action.yml New composite action implementing Go module scanning and clean-room resolution/build.
.github/workflows/test.yml Adds end-to-end fixture tests and a semantic-import-versioning table test for the Go hygiene action.
README.md Documents Go usage patterns and release mechanics where tags are the publish event.
test/fixtures/go/clean/go.mod Fixture for a passing Go module graph.
test/fixtures/go/internal-host/go.mod Fixture for failing internal module host detection.
test/fixtures/go/replace-directive/go.mod Fixture for failing replace directive detection.
test/fixtures/go/wrong-module-path/go.mod Fixture for failing module-path-vs-repo validation.
Suppressed comments (1)

go-lockfile-hygiene/action.yml:58

  • This step name claims go.work is scanned for internal module hosts, but the step only scans go.mod and go.sum (go.work is handled later with a warning only). Consider renaming the step (or actually parsing go.work) so logs aren’t misleading.
    - name: Scan go.mod / go.sum / go.work for internal module hosts
      if: ${{ inputs.scan == 'true' }}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +2 to +6
description: >-
Supply-chain gate for public Go modules. (1) Scan tracked go.mod / go.sum /
go.work for internal module hosts, `replace` directives, and a module path
that doesn't match the public repo; (2) optionally prove the committed
go.mod + go.sum resolve and build from the PUBLIC module proxy only.
# In a published library it is dead weight to consumers — so CI resolves
# one graph and every consumer resolves a different one. This is the Go
# analogue of a committed Composer `repositories` block.
REPL=$(go mod edit -json "$f" | jq -r '(.Replace // [])[] | " \(.Old.Path) => \(.New.Path // .New.Version)"')
Comment thread README.md
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \
GOPRIVATE= GONOPROXY= GONOSUMDB= GOFLAGS=-mod=mod \
go get "${MOD}@${VERSION}"
grep -E "^${MOD} ${VERSION} h1:" go.sum
@rdelpret
rdelpret merged commit 4ffc0c6 into main Aug 24, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants