Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ jobs:
E2E_GITHUB_TOTP_SECRET: ${{ secrets.E2E_GITHUB_TOTP_SECRET }}
E2E_MINT_URL: ${{ secrets.E2E_MINT_URL }}
E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }}
E2E_UPSTREAM_REF: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Upload debug screenshots
if: always() && steps.changes.outputs.relevant != 'false' && steps.secrets-check.outputs.available == 'true'
Expand Down
3 changes: 3 additions & 0 deletions e2e/admin/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ func TestAdminInstallUninstall(t *testing.T) {
if env.cfg.gcpProjectID != "" {
installArgs = append(installArgs, "--inference-project", env.cfg.gcpProjectID)
}
if ref := os.Getenv("E2E_UPSTREAM_REF"); ref != "" {
installArgs = append(installArgs, "--upstream-ref", ref)
}
runCLI(t, env.binary, env.token, installArgs...)

// Verify install artifacts.
Expand Down
22 changes: 20 additions & 2 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ type perRepoInstallConfig struct {
AppSet string
VendorBinary bool
FullsendBinary string
UpstreamRef string
}

// wifProviderPattern validates the full WIF provider resource name format
Expand Down Expand Up @@ -228,6 +229,7 @@ func newInstallCmd() *cobra.Command {
var skipAppSetup bool
var vendorBinary bool
var fullsendBinary string
var upstreamRef string
var enrollAllFlag bool
var enrollNoneFlag bool
var inferenceProject string
Expand Down Expand Up @@ -310,6 +312,7 @@ Inference authentication:
AppSet: appSet,
VendorBinary: vendorBinary,
FullsendBinary: fullsendBinary,
UpstreamRef: upstreamRef,
})
}

Expand Down Expand Up @@ -539,7 +542,7 @@ Inference authentication:
agentCreds = creds
}

return runInstall(ctx, client, printer, org, repos, roles, agentCreds, inferenceProvider, inferenceProviderName, vendorBinary, fullsendBinary, mintProvider, mintProject, mintRegion, mintSourceDir, mintSkipDeploy, mintURL, skipMintCheck, allRepos)
return runInstall(ctx, client, printer, org, repos, roles, agentCreds, inferenceProvider, inferenceProviderName, vendorBinary, fullsendBinary, upstreamRef, mintProvider, mintProject, mintRegion, mintSourceDir, mintSkipDeploy, mintURL, skipMintCheck, allRepos)
},
}

Expand All @@ -548,6 +551,7 @@ Inference authentication:
cmd.Flags().BoolVar(&skipAppSetup, "skip-app-setup", false, "skip GitHub App creation/setup")
cmd.Flags().BoolVar(&vendorBinary, "vendor-fullsend-binary", false, "resolve and upload a linux/amd64 fullsend binary for CI")
cmd.Flags().StringVar(&fullsendBinary, "fullsend-binary", "", "path to a Linux fullsend binary to upload when vendoring (default: auto-resolve)")
cmd.Flags().StringVar(&upstreamRef, "upstream-ref", "", "pin workflow references to this git ref instead of v0 (e.g. v0.15.0 or a commit SHA)")
cmd.Flags().BoolVar(&enrollAllFlag, "enroll-all", false, "enroll all repositories without prompting")
cmd.Flags().BoolVar(&enrollNoneFlag, "enroll-none", false, "skip repository enrollment without prompting")
cmd.Flags().StringVar(&inferenceProject, "inference-project", "", "GCP project ID for inference (Agent Platform)")
Expand Down Expand Up @@ -1035,6 +1039,13 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
}
}

if c.UpstreamRef != "" {
printer.Blank()
if err := pinUpstreamRef(ctx, client, printer, owner, repo, c.UpstreamRef, perRepoWorkflowPaths); err != nil {
return err
}
}

printer.Blank()
printer.StepDone(fmt.Sprintf("Per-repo installation complete for %s/%s", owner, repo))
return nil
Expand Down Expand Up @@ -1455,7 +1466,7 @@ func validateEnabledRepos(enabledRepos, discoveredNames []string) error {

// runInstall performs the full installation.
// If discoveredRepos is non-nil, it will be used instead of calling ListOrgRepos.
func runInstall(ctx context.Context, client forge.Client, printer *ui.Printer, org string, enabledRepos, roles []string, agentCreds []layers.AgentCredentials, inferenceProvider inference.Provider, inferenceProviderName string, vendorBinary bool, fullsendBinary, mintProvider, mintProject, mintRegion, mintSourceDir string, mintSkipDeploy bool, mintURL string, skipMintCheck bool, discoveredRepos []forge.Repository) error {
func runInstall(ctx context.Context, client forge.Client, printer *ui.Printer, org string, enabledRepos, roles []string, agentCreds []layers.AgentCredentials, inferenceProvider inference.Provider, inferenceProviderName string, vendorBinary bool, fullsendBinary, upstreamRef, mintProvider, mintProject, mintRegion, mintSourceDir string, mintSkipDeploy bool, mintURL string, skipMintCheck bool, discoveredRepos []forge.Repository) error {
var allRepos []forge.Repository
var err error

Expand Down Expand Up @@ -1561,6 +1572,13 @@ func runInstall(ctx context.Context, client forge.Client, printer *ui.Printer, o
return fmt.Errorf("installation failed: %w", err)
}

if upstreamRef != "" {
printer.Blank()
if err := pinUpstreamRef(ctx, client, printer, org, forge.ConfigRepoName, upstreamRef, perOrgWorkflowPaths); err != nil {
return err
}
}

printer.Blank()
printer.Summary("Installation complete", []string{
fmt.Sprintf("Organization: %s", org),
Expand Down
16 changes: 16 additions & 0 deletions internal/cli/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type githubSetupConfig struct {
enrollNone bool
vendorBinary bool
fullsendBinary string
upstreamRef string
dryRun bool
}

Expand Down Expand Up @@ -138,6 +139,7 @@ values (mint URL, WIF provider, project ID) are provided as flags.`,
cmd.Flags().BoolVar(&cfg.enrollNone, "enroll-none", false, "skip repository enrollment without prompting")
cmd.Flags().BoolVar(&cfg.vendorBinary, "vendor-fullsend-binary", false, "resolve and upload a linux/amd64 fullsend binary for CI")
cmd.Flags().StringVar(&cfg.fullsendBinary, "fullsend-binary", "", "path to a Linux fullsend binary to upload when vendoring (default: auto-resolve)")
cmd.Flags().StringVar(&cfg.upstreamRef, "upstream-ref", "", "pin workflow references to this git ref instead of v0 (e.g. v0.15.0 or a commit SHA)")
cmd.Flags().BoolVar(&cfg.dryRun, "dry-run", false, "preview changes without making them")

return cmd
Expand Down Expand Up @@ -327,6 +329,13 @@ func runGitHubSetupPerRepo(ctx context.Context, client forge.Client, printer *ui
}
}

if cfg.upstreamRef != "" {
printer.Blank()
if err := pinUpstreamRef(ctx, client, printer, owner, repo, cfg.upstreamRef, perRepoWorkflowPaths); err != nil {
return err
}
}

printer.Blank()
printer.StepDone(fmt.Sprintf("Per-repo setup complete for %s/%s", owner, repo))
return nil
Expand Down Expand Up @@ -528,6 +537,13 @@ func runGitHubSetupPerOrg(ctx context.Context, client forge.Client, printer *ui.
return fmt.Errorf("setup failed: %w", err)
}

if cfg.upstreamRef != "" {
printer.Blank()
if err := pinUpstreamRef(ctx, client, printer, org, forge.ConfigRepoName, cfg.upstreamRef, perOrgWorkflowPaths); err != nil {
return err
}
}

printer.Blank()
printer.Summary("Setup complete", []string{
fmt.Sprintf("Organization: %s", org),
Expand Down
77 changes: 77 additions & 0 deletions internal/cli/upstream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cli

import (
"bytes"
"context"
"fmt"

"github.com/fullsend-ai/fullsend/internal/forge"
"github.com/fullsend-ai/fullsend/internal/ui"
)

// pinUpstreamRef rewrites @v0 references in scaffolded workflow files to
// point to the given git ref. This is committed as a separate commit after
// install so the history shows the original v0 install followed by an
// explicit pin. Users can use this to pin to a release tag (e.g. v0.15.0)
// or a specific commit SHA for testing.
func pinUpstreamRef(ctx context.Context, client forge.Client, printer *ui.Printer, owner, repo, ref string, workflowPaths []string) error {
printer.StepStart(fmt.Sprintf("Pinning upstream ref to %s", ref))

var updated []forge.TreeFile
for _, path := range workflowPaths {
content, err := client.GetFileContent(ctx, owner, repo, path)
if err != nil {
if forge.IsNotFound(err) {
continue
}
return fmt.Errorf("reading %s: %w", path, err)
}
rewritten := bytes.ReplaceAll(content, []byte("@v0"), []byte("@"+ref))
rewritten = bytes.ReplaceAll(rewritten, []byte("fullsend_ai_ref: v0"), []byte("fullsend_ai_ref: "+ref))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] injection

pinUpstreamRef performs bytes.ReplaceAll substitution of a user-supplied ref value into workflow YAML content with no input validation. Attack surface limited to org admins; automated caller passes GitHub-provided commit SHA.

Suggested fix: Validate ref matches ^[a-zA-Z0-9._/-]+$ before substitution.

rewritten = bytes.ReplaceAll(rewritten, []byte("ref: v0"), []byte("ref: "+ref))
if !bytes.Equal(content, rewritten) {
updated = append(updated, forge.TreeFile{
Path: path,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

The ref: v0 replacement pattern is generic YAML and could match unrelated lines if a future workflow file in perOrgWorkflowPaths contains ref: v0 in a non-checkout context. Risk mitigated by hardcoded paths to scaffold-generated files.

Suggested fix: Consider using a more specific pattern such as indentation-scoped match.

Content: rewritten,
Mode: "100644",
})
}
}

if len(updated) == 0 {
printer.StepDone("No workflow files needed pinning")
return nil
}

committed, err := client.CommitFiles(ctx, owner, repo,
fmt.Sprintf("chore: pin upstream ref to %s", ref),
updated)
if err != nil {
printer.StepFail("Failed to pin upstream ref")
return fmt.Errorf("committing upstream ref pin: %w", err)
}
if committed {
printer.StepDone(fmt.Sprintf("Pinned %d workflow files to %s", len(updated), ref))
} else {
printer.StepDone("Workflow files already pinned")
}
return nil
}

// perOrgWorkflowPaths lists the scaffold workflow files installed in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] comment-style

Package-level variable comments for perOrgWorkflowPaths and perRepoWorkflowPaths use verb form instead of noun-phrase form.

// the per-org config repo (.fullsend).
var perOrgWorkflowPaths = []string{
".github/workflows/code.yml",
".github/workflows/fix.yml",
".github/workflows/prioritize-scheduler.yml",
".github/workflows/prioritize.yml",
".github/workflows/repo-maintenance.yml",
".github/workflows/retro.yml",
".github/workflows/review.yml",
".github/workflows/triage.yml",
}

// perRepoWorkflowPaths lists the workflow files installed in per-repo mode.
var perRepoWorkflowPaths = []string{
".github/workflows/fullsend.yaml",
}
68 changes: 68 additions & 0 deletions internal/cli/upstream_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cli

import (
"bytes"
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/fullsend-ai/fullsend/internal/forge"
"github.com/fullsend-ai/fullsend/internal/ui"
)

func TestPinUpstreamRef(t *testing.T) {
ctx := context.Background()

t.Run("rewrites all v0 patterns", func(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] test-adequacy

The test suite does not cover the per-repo workflow path (perRepoWorkflowPaths / .github/workflows/fullsend.yaml). Only per-org patterns are tested.

fc := forge.NewFakeClient()
fc.FileContents["org/repo/.github/workflows/triage.yml"] =
[]byte(" uses: fullsend-ai/fullsend/.github/workflows/reusable-triage.yml@v0\n" +
" fullsend_ai_ref: v0\n")
fc.FileContents["org/repo/.github/workflows/repo-maintenance.yml"] =
[]byte(" ref: v0\n" +
" uses: fullsend-ai/fullsend/.github/actions/mint-token@v0\n")

var buf bytes.Buffer
printer := ui.New(&buf)
err := pinUpstreamRef(ctx, fc, printer, "org", "repo", "abc123def456",
[]string{".github/workflows/triage.yml", ".github/workflows/repo-maintenance.yml"})
require.NoError(t, err)

triage, err := fc.GetFileContent(ctx, "org", "repo", ".github/workflows/triage.yml")
require.NoError(t, err)
assert.Contains(t, string(triage), "@abc123def456")
assert.Contains(t, string(triage), "fullsend_ai_ref: abc123def456")
assert.NotContains(t, string(triage), "@v0")

maint, err := fc.GetFileContent(ctx, "org", "repo", ".github/workflows/repo-maintenance.yml")
require.NoError(t, err)
assert.Contains(t, string(maint), "ref: abc123def456")
assert.Contains(t, string(maint), "@abc123def456")
assert.NotContains(t, string(maint), "@v0")
})

t.Run("skips missing files", func(t *testing.T) {
fc := forge.NewFakeClient()
var buf bytes.Buffer
printer := ui.New(&buf)
err := pinUpstreamRef(ctx, fc, printer, "org", "repo", "abc123def456",
[]string{".github/workflows/nonexistent.yml"})
require.NoError(t, err)
})

t.Run("no-op when files have no v0 refs", func(t *testing.T) {
fc := forge.NewFakeClient()
fc.FileContents["org/repo/.github/workflows/custom.yml"] =
[]byte("uses: some-other/action@v1\n")
var buf bytes.Buffer
printer := ui.New(&buf)
err := pinUpstreamRef(ctx, fc, printer, "org", "repo", "abc123def456",
[]string{".github/workflows/custom.yml"})
require.NoError(t, err)
// File should be unchanged
content, _ := fc.GetFileContent(ctx, "org", "repo", ".github/workflows/custom.yml")
assert.Equal(t, "uses: some-other/action@v1\n", string(content))
})
}
Loading