Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0986623
feat(mint): cross-org authorization and e2e WIF auth (#2155)
ifireball Jun 14, 2026
5ecaf94
Merge branch 'main' into cursor/b69a09e5
ifireball Jun 18, 2026
0c8c454
Merge branch 'main' into cursor/b69a09e5
ifireball Jun 22, 2026
6e4a2e4
docs: renumber cross-org mint ADR from 0046 to 0051
ifireball Jun 22, 2026
dbb1e03
fix(cli): use org-wide visibility for FOREIGN allowlist variables
ifireball Jun 22, 2026
d658530
refactor(e2e): reuse mintclient for cross-org tokens and fix mint rep…
ifireball Jun 22, 2026
6e0bfed
feat(mint): grant e2e app org variables permission for FOREIGN reads
ifireball Jun 22, 2026
ea97fef
feat(mint): allow installation-wide mint tokens when repos are omitted
ifireball Jun 22, 2026
9d7d415
chore: fix gofmt alignment in AppPermissions struct
ifireball Jun 22, 2026
4b1123a
fix(cli): detect installation tokens before OAuth scope preflight
ifireball Jun 22, 2026
a4afead
Merge upstream/main into cursor/b69a09e5.
ifireball Jun 22, 2026
40edec6
fix(forge): resolve installation token identity via GraphQL viewer
ifireball Jun 22, 2026
a10b6e6
feat(mint): grant e2e role repository variables write permission
ifireball Jun 23, 2026
93e5c4d
feat(mint): grant e2e role organization variables write permission
ifireball Jun 23, 2026
8472786
Merge upstream/main and address PR #2277 review feedback
ifireball Jun 23, 2026
1704870
fix(mint): allow installation-wide tokens on same-org path
ifireball Jun 23, 2026
8bd58ff
test: improve patch coverage for foreign mint and CLI paths
ifireball Jun 23, 2026
c5e38d6
chore: gofmt foreign_test.go
ifireball Jun 23, 2026
cfa6ec8
test: raise patch coverage for foreign mint paths
ifireball Jun 23, 2026
270af39
Merge upstream/main and address post-approval review feedback
ifireball Jun 24, 2026
dc589e6
docs: renumber cross-org mint ADR to 0055
ifireball Jun 24, 2026
838e2bf
fix(e2e): open triage test issues with user token for ADR 0054
ifireball Jun 24, 2026
d92663a
fix(e2e): fall back to E2E_GITHUB_PASSWORD for issue author in CI
ifireball Jun 24, 2026
33aac55
docs(e2e): keep E2E_GITHUB_PASSWORD as interim issue-author secret
ifireball Jun 25, 2026
aa01014
refactor(e2e): use FULLSEND_MINT_URL / hosted default instead of E2E_…
ifireball Jun 25, 2026
f4ad9cc
docs(make): update e2e-test help for hosted mint default
ifireball Jun 25, 2026
b324850
docs: document interim E2E_GITHUB_PASSWORD PAT in AGENTS.md
ifireball Jun 25, 2026
cf9e747
Merge branch 'main' of https://github.com/fullsend-ai/fullsend into c…
ifireball Jun 30, 2026
d2f03fc
fix(e2e): trigger triage via ready-for-triage label dispatch
ifireball Jun 30, 2026
eef4612
fix(mint,cli): address review feedback and renumber cross-org ADR to …
ifireball Jun 30, 2026
8f18dae
fix(ci,e2e): gofmt token test and clean stale .fullsend forks
ifireball Jun 30, 2026
7e411e6
fix(e2e): use --direct scaffold delivery for mint tokens in CI
ifireball Jun 30, 2026
3e1fd5f
Merge branch 'main' of https://github.com/fullsend-ai/fullsend
ifireball Jun 30, 2026
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
35 changes: 3 additions & 32 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,51 +134,22 @@ jobs:
with:
go-version-file: go.mod

- name: Install Playwright system dependencies
if: steps.changes.outputs.relevant != 'false'
run: npx playwright install-deps chromium

- name: Check for secrets
if: steps.changes.outputs.relevant != 'false'
id: secrets-check
run: |
if [ -z "$E2E_GITHUB_SESSION_B64" ]; then
echo "::warning::E2E secrets are not configured. Skipping e2e tests."
echo "available=false" >> "$GITHUB_OUTPUT"
else
echo "available=true" >> "$GITHUB_OUTPUT"
fi
env:
E2E_GITHUB_SESSION_B64: ${{ secrets.E2E_GITHUB_SESSION }}

- name: Decode session
if: steps.changes.outputs.relevant != 'false' && steps.secrets-check.outputs.available == 'true'
run: |
SESSION_FILE="${RUNNER_TEMP}/github-session.json"
printf '%s' "$E2E_GITHUB_SESSION_B64" | base64 -d > "$SESSION_FILE"
echo "E2E_GITHUB_SESSION_FILE=${SESSION_FILE}" >> "$GITHUB_ENV"
env:
E2E_GITHUB_SESSION_B64: ${{ secrets.E2E_GITHUB_SESSION }}

- name: Authenticate to GCP
if: steps.changes.outputs.relevant != 'false' && steps.secrets-check.outputs.available == 'true'
if: steps.changes.outputs.relevant != 'false'
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ secrets.E2E_GCP_WIF_PROVIDER }}
service_account: ${{ secrets.E2E_GCP_SERVICE_ACCOUNT }}

- name: Run e2e tests
if: steps.changes.outputs.relevant != 'false' && steps.secrets-check.outputs.available == 'true'
if: steps.changes.outputs.relevant != 'false'
run: make e2e-test
env:
E2E_SCREENSHOT_DIR: ${{ runner.temp }}/e2e-screenshots
E2E_GITHUB_PASSWORD: ${{ secrets.E2E_GITHUB_PASSWORD }}
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 }}

- name: Upload debug screenshots
if: always() && steps.changes.outputs.relevant != 'false' && steps.secrets-check.outputs.available == 'true'
if: always() && steps.changes.outputs.relevant != 'false'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-screenshots-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.run_id }}
Expand Down
45 changes: 0 additions & 45 deletions .github/workflows/pat-cleanup.yml

This file was deleted.

12 changes: 5 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,16 @@ When making changes to Go code under `cmd/` or `internal/`:
1. **Unit tests:** Run `make go-test` (or `go test ./...`) and fix any failures before committing.
2. **Coverage:** CI enforces thresholds via [Codecov](https://about.codecov.io/) (see [`.codecov.yml`](.codecov.yml)). **Patch coverage** on changed lines must meet **80%** (with a 5% tolerance). **Project coverage** must not drop more than **1%** below the base branch. `make go-test` runs tests with `-cover` locally but does not enforce these thresholds — a PR can still fail the Codecov status check if new or changed code lacks tests. Add or extend `_test.go` files for logic you introduce or modify.
3. **Vet:** Run `make go-vet` to catch common issues.
4. **E2E tests:** Run `make e2e-test` if your changes touch `internal/appsetup/`, `internal/forge/`, `internal/cli/`, or `internal/layers/`. These tests exercise the full admin install/uninstall flow against a live GitHub org using Playwright browser automation.
4. **E2E tests:** Run `make e2e-test` if your changes touch `internal/appsetup/`, `internal/forge/`, `internal/cli/`, or `internal/layers/`. These tests exercise the full admin install/uninstall flow against live GitHub pool orgs using mint/OIDC authentication.

### Running e2e tests

The e2e tests require GitHub credentials. There are three ways to provide them:
The e2e tests mint short-lived GitHub App installation tokens via the central token mint. Pool-org admin operations use mint/OIDC in CI and do not require a dedicated mint URL secret.

- **`E2E_GITHUB_PASSWORD` env var:** Set directly with the password.
- **`E2E_GITHUB_PASSWORD_FILE` env var:** Set to a file path containing the password (used in devaipod environments where secrets are mounted as files).
- **`E2E_GITHUB_SESSION_FILE` env var:** Set to a pre-exported Playwright session file (skips login).
- **`E2E_GITHUB_TOTP_SECRET` env var:** Optional. The TOTP secret (base32) for the test account's 2FA. Required only when the test account has 2FA enabled — used during session export and sudo confirmation.
- **CI (mint):** Uses the hosted public mint (same default as `fullsend admin --mint-url`) with the workflow's OIDC identity. The e2e workflow exchanges the OIDC JWT for an `e2e`-role installation token on the pool org. Override with `FULLSEND_MINT_URL` if needed.
- **Local:** Run `gh auth login` (or set `GH_TOKEN` / `GITHUB_TOKEN` with pool-org admin access). Mint uses `FULLSEND_MINT_URL` or the hosted default.

If only `E2E_GITHUB_USERNAME` and a password source are available, `make e2e-test` will automatically generate a session file before running tests. See `make help` for all available targets.
See `docs/guides/dev/e2e-testing.md` and `make help` for pool org setup and troubleshooting.

## Shell scripting

Expand Down
38 changes: 4 additions & 34 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
.PHONY: help bootstrap lint lint-all check fmt \
mindmap go-build go-test go-lint go-fmt go-vet go-tidy \
lint-md-links script-test test \
e2e-test e2e-playwright e2e-export-session e2e-upload-session \
lint-eval-cases functional-tests
e2e-test lint-eval-cases functional-tests

# Let Go automatically download the toolchain version required by go.mod.
# This ensures local builds use the right version without manual intervention.
Expand All @@ -27,10 +26,8 @@ help:
@echo " go-tidy - Run go mod tidy"
@echo " lint-md-links - Check markdown files for broken in-repo links and anchors"
@echo " script-test - Run shell script tests (post-triage, post-code, post-review, pre-fetch-prior-review, reconcile-repos, validate-output-schema)"
@echo " test - Run all checks: lint-all, go-test, script-test"
@echo " e2e-test - Run admin e2e tests (requires E2E_GITHUB_SESSION_FILE or E2E_GITHUB_USERNAME + E2E_GITHUB_PASSWORD)"
@echo " e2e-export-session - Login to GitHub and export a Playwright session file"
@echo " e2e-upload-session - Export session and upload it as a GitHub repo secret"
@echo " test - Run all checks: lint-all, go-test, script-test, lint-eval-cases"
@echo " e2e-test - Run admin e2e tests (CI: OIDC mint; local: gh auth login or GH_TOKEN)"
@echo " lint-eval-cases - Lint eval case definitions (annotations.yaml completeness)"
@echo " functional-tests - Run functional agent tests (requires EVAL_ORG, FULLSEND_DIR, GH_TOKEN, GCP creds)"

Expand Down Expand Up @@ -135,36 +132,9 @@ script-test:

test: lint-all go-test script-test lint-eval-cases

E2E_SESSION_FILE ?= $(CURDIR)/.playwright/session.json

e2e-test: e2e-playwright
@if [ -n "$$E2E_GITHUB_PASSWORD_FILE" ] && [ -z "$$E2E_GITHUB_PASSWORD" ]; then \
export E2E_GITHUB_PASSWORD="$$(cat "$$E2E_GITHUB_PASSWORD_FILE")"; \
fi; \
if [ -z "$$E2E_GITHUB_SESSION_FILE" ] && [ -n "$$E2E_GITHUB_USERNAME" ] && [ -n "$$E2E_GITHUB_PASSWORD" ]; then \
echo "==> No session file set, generating one from credentials..."; \
$(MAKE) e2e-export-session; \
export E2E_GITHUB_SESSION_FILE="$(E2E_SESSION_FILE)"; \
fi; \
e2e-test:
go test -tags e2e -v -count=1 -timeout 30m ./e2e/admin/

e2e-export-session: e2e-playwright
@if [ -n "$$E2E_GITHUB_PASSWORD_FILE" ] && [ -z "$$E2E_GITHUB_PASSWORD" ]; then \
export E2E_GITHUB_PASSWORD="$$(cat "$$E2E_GITHUB_PASSWORD_FILE")"; \
fi; \
E2E_GITHUB_SESSION_FILE="$(E2E_SESSION_FILE)" go run ./e2e/cmd/export-session/

e2e-upload-session: e2e-export-session
@echo "==> Uploading session to GitHub repo secret..."
base64 -w0 "$(E2E_SESSION_FILE)" | gh secret set E2E_GITHUB_SESSION
@echo "==> Done. Session uploaded as E2E_GITHUB_SESSION."

e2e-playwright:
@if [ -z "$$(ls -d $(HOME)/.cache/ms-playwright/chromium-* 2>/dev/null)" ]; then \
echo "==> Installing Playwright Chromium..."; \
go run github.com/playwright-community/playwright-go/cmd/playwright install chromium; \
fi

# Functional agent evals — run agents against ephemeral GitHub repos and judge results.
# Required env: EVAL_ORG (GitHub org for ephemeral repos), plus GCP creds for Vertex AI.
# GH_TOKEN defaults to `gh auth token` if not set.
Expand Down
6 changes: 3 additions & 3 deletions docs/ADRs/0010-stored-session-for-e2e-browser-auth.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "10. Stored browser session for e2e authentication in CI"
status: Accepted
status: Superseded
relates_to:
- testing-agents
topics:
Expand All @@ -16,9 +16,9 @@ Date: 2026-04-03

## Status

Accepted
Superseded by [ADR 0060](0060-cross-org-mint-authorization-via-org-variables.md) and the e2e mint/OIDC refactor ([#2155](https://github.com/fullsend-ai/fullsend/issues/2155)). Playwright session export is no longer used for CI authentication.

Extended by [ADR 0039](0039-totp-automation-for-e2e-2fa.md).
Extended by [ADR 0039](0039-totp-automation-for-e2e-2fa.md) (also superseded).

## Context

Expand Down
6 changes: 3 additions & 3 deletions docs/ADRs/0039-totp-automation-for-e2e-2fa.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "39. TOTP automation for e2e 2FA"
status: Accepted
status: Superseded
relates_to:
- testing-agents
topics:
Expand All @@ -16,9 +16,9 @@ Date: 2026-05-19

## Status

Accepted
Superseded by [ADR 0060](0060-cross-org-mint-authorization-via-org-variables.md) and the e2e mint/OIDC refactor ([#2155](https://github.com/fullsend-ai/fullsend/issues/2155)). TOTP automation for Playwright login is no longer used.

Extends [ADR 0010](0010-stored-session-for-e2e-browser-auth.md).
Extends [ADR 0010](0010-stored-session-for-e2e-browser-auth.md) (also superseded).

## Context

Expand Down
7 changes: 5 additions & 2 deletions docs/ADRs/0040-org-pool-for-parallel-e2e-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ slice in the test code. No architectural changes are needed.
`test-repo` for enrollment testing.
- A crashed run leaves a stale lock that self-heals via the age-based
staleness check.
- The single `botsend` test account and its stored browser session are shared
across all orgs; session export and PAT creation remain per-run.
- Each pool org must install the `fullsend-ai-e2e` app and authorize CI via
`FULLSEND_FOREIGN_E2E_REPOS` on the target org — see
[ADR 0060](0060-cross-org-mint-authorization-via-org-variables.md).
- CI acquires per-org tokens via cross-org mint ([#2155](https://github.com/fullsend-ai/fullsend/issues/2155));
local runs use a user token with pool-org admin access (`gh auth login`).
Comment thread
ifireball marked this conversation as resolved.
- Pool expansion is an operational task (provision org, update one slice
literal), not an architectural change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
Comment thread
ifireball marked this conversation as resolved.
title: "60. Cross-org mint authorization via org variables"
status: Accepted
relates_to:
- agent-infrastructure
- security-threat-model
topics:
- identity
- oidc
- github-apps
- cross-org
---

# 60. Cross-org mint authorization via org variables

Date: 2026-06-07

## Status

Accepted

## Context

The central token mint ([ADR 0029](0029-central-token-mint-secretless-fullsend.md)) issues
short-lived GitHub App installation tokens to OIDC-authenticated workflows. Today the mint
scopes tokens to the caller's `repository_owner`: the App installation lookup and PEM
lookup both use the org from the OIDC `repository` claim.

Some workloads need to act on a **different** org than the workflow's owner. The e2e
test pool ([ADR 0040](0040-org-pool-for-parallel-e2e-tests.md)) runs CI from
`fullsend-ai/fullsend` but mutates dedicated pool orgs (`halfsend-01`, …). Future
cross-org agent flows ([#672](https://github.com/fullsend-ai/fullsend/issues/672),
[#1916](https://github.com/fullsend-ai/fullsend/issues/1916)) have the same shape.

The target org must explicitly authorize which foreign repos or orgs may request tokens
for a given role. A mint-operator central allowlist does not scale and does not give target
orgs control over their own policy.

## Decision

1. **Optional `target_org` on mint requests.** When omitted, or when equal to the caller's
`repository_owner` (case-insensitive), behavior uses the same `mintToken` path with no
FOREIGN check. When `repos` is omitted, the mint issues an installation-wide token via
org-level installation lookup (same as the cross-org path). Callers are authenticated via
WIF/OIDC; only enrolled workflows that pass mint enrollment checks can reach the handler.

2. **Cross-org path** applies only when `target_org` is set and differs from the caller org:
- Resolve the requested role's App installation on `target_org` via org-level installation lookup.
- Read `FULLSEND_FOREIGN_<role>_REPOS` on the target org using that role's App installation
token (`organization_actions_variables: read`).
- Deny if installation lookup fails, the variable is missing/empty, or the OIDC caller
(`repository` or bare `repository_owner`) is not on the allowlist.
Comment thread
ifireball marked this conversation as resolved.
- Mint an installation token for the requested repos on the target org, or installation-wide
when `repos` is omitted. The `e2e` role acting on pool orgs from CI is the first consumer
([#2155](https://github.com/fullsend-ai/fullsend/issues/2155)).

3. **Variable format.** Org-level GitHub Actions variable on the **target** org:
- Name: `FULLSEND_FOREIGN_<ROLE>_REPOS` (uppercase role suffix, per [ADR 0014](0014-admin-install-github-apps-secrets-v1.md))
- Value: comma-separated list of `org/repo` (exact `repository` match) and/or bare `org`
(`repository_owner` match)

4. **Role-agnostic mechanism.** Any allowed role may use the cross-org path when the target
org has installed that role's App and configured the FOREIGN variable. The `e2e` role
([#2155](https://github.com/fullsend-ai/fullsend/issues/2155)) is the first consumer.

5. **CLI.** `fullsend admin foreign allow|list|revoke` manages FOREIGN variables on a target org.

## Consequences

- Cross-org mint requests add GitHub API calls. FOREIGN allowlist lookups are cached in-memory
per mint instance (key: `target_org/role`, TTL 60s). Cache entries include empty/missing
allowlists (negative cache) so revoked or unset variables may take up to one TTL window to
take effect. Cardinality is bounded by enrolled orgs × roles; no explicit eviction beyond TTL.
- Roles used on the cross-org path need `organization_actions_variables: read` on their App permissions.
- The `e2e` role additionally needs `actions_variables: write` and
`organization_actions_variables: write` so pool tests can set repo/org variables during
install flows; these writes are scoped to pool orgs that explicitly authorize CI via
`FULLSEND_FOREIGN_E2E_REPOS`.
- Installation-wide tokens (empty `repos`) are permitted on both same-org and cross-org
paths. Cross-org requests additionally require FOREIGN authorization on the target org.
Same-org elevation relies on WIF/OIDC enrollment: only trusted workflows can call the mint.
- Target orgs opt in by installing the role App and setting the FOREIGN allowlist (cross-org).
- Same-org mint for enrolled orgs adds zero FOREIGN API calls; optional `repos` omission uses
org-level installation lookup when callers need installation-wide scope.
- Pool org provisioning must install the e2e App and set `FULLSEND_FOREIGN_E2E_REPOS` for CI callers.
Loading
Loading