Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ce1a479
feat: reusable workflows (ADR 31), centralized routing (ADR 34), laye…
waynesun09 May 11, 2026
a8a3479
fix: scope release workflow trigger to semver tags only
waynesun09 May 11, 2026
78ae828
fix: update e2e tests for NewWorkflowsLayer signature change
waynesun09 May 11, 2026
8fcc932
fix: review corrections for ADR 31/35, security hardening, mint test fix
waynesun09 May 12, 2026
b9a5d90
fix: use v0 ref for upstream defaults checkout in reusable workflows
waynesun09 May 12, 2026
74717cb
fix: add env/ to layered dirs for runtime resolution
waynesun09 May 12, 2026
4afb7da
fix: add OPENSHELL_VERSION env var to upstream action files
waynesun09 May 12, 2026
4f0e6a1
refactor: consolidate fullsend action to root action.yml
waynesun09 May 12, 2026
64e0988
fix: align reusable workflows and setup-gcp with direct-WIF (PR 856)
waynesun09 May 13, 2026
b682141
fix: update stale v1 refs to v0 in prioritize, repo-maintenance, and …
waynesun09 May 13, 2026
264dfa5
fix: remove extra arg from NewWorkflowsLayer call in e2e test
waynesun09 May 13, 2026
c9cc35c
fix: replace sed with bash parameter expansion to satisfy shellcheck …
waynesun09 May 13, 2026
2a336a9
fix: harden fork detection and correct ADR 35 layered-dir references
waynesun09 May 13, 2026
b1dbc1d
docs: fix ADR 31 dispatch.yml claim and remove stale normative specs
waynesun09 May 13, 2026
af0bf5b
fix: remove broken links to deleted normative specs in ADRs
waynesun09 May 13, 2026
1cabeda
fix: address review feedback — fail-closed allowlist, retro role, ADR…
waynesun09 May 13, 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
64 changes: 64 additions & 0 deletions .github/actions/mint-token/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Mint Token
description: >-
Exchange a GitHub OIDC token for a role-scoped installation token via the
fullsend token mint. Requires id-token: write permission on the calling job.

inputs:
role:
description: Agent role name (e.g. triage, coder, review, fullsend)
required: true
repos:
description: Comma-separated repo names to scope the token to
required: true
mint_url:
description: URL of the token mint (typically from vars.FULLSEND_MINT_URL)
required: true

outputs:
token:
description: Scoped GitHub installation token
value: ${{ steps.mint.outputs.token }}

runs:
using: composite
steps:
- name: Mint token via OIDC
id: mint
shell: bash
env:
MINT_URL: ${{ inputs.mint_url }}
ROLE: ${{ inputs.role }}
REPOS: ${{ inputs.repos }}
run: |
set -euo pipefail
if [[ -z "$MINT_URL" ]]; then
echo "::error::FULLSEND_MINT_URL is not set"
exit 1
fi
echo "::add-mask::$MINT_URL"
if [[ -z "$REPOS" ]]; then
echo "::error::repos input is required"
exit 1
fi
OIDC_TOKEN=$(curl -sSf --retry 3 --retry-delay 2 --retry-all-errors \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=fullsend-mint" | jq -r '.value')
if [[ -z "$OIDC_TOKEN" || "$OIDC_TOKEN" == "null" ]]; then
echo "::error::Failed to obtain OIDC token"
exit 1
fi
echo "::add-mask::$OIDC_TOKEN"
REPOS_JSON=$(echo "$REPOS" | jq -Rc 'split(",") | map(select(length > 0))')
BODY=$(jq -nc --arg role "$ROLE" --argjson repos "$REPOS_JSON" \
'{role: $role, repos: $repos}')
TOKEN=$(curl -sSf --retry 5 --retry-delay 5 --retry-all-errors \
-H "Authorization: Bearer $OIDC_TOKEN" \
-H "Content-Type: application/json" \
-d "$BODY" \
"${MINT_URL}/v1/token" | jq -r '.token')
if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
echo "::error::Token mint returned no token for role=$ROLE"
exit 1
fi
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
37 changes: 37 additions & 0 deletions .github/actions/setup-gcp/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Setup GCP
description: Authenticate to Google Cloud via Workload Identity Federation, mask credentials, and prepare sandbox credentials

inputs:
gcp_wif_provider:
description: 'Workload Identity Federation provider resource name'
required: true
gcp_project_id:
description: 'GCP project ID — passed to google-github-actions/auth so the runner env has GOOGLE_CLOUD_PROJECT'
required: false

runs:
using: composite
steps:
- name: Pre-mask GCP credential file path
shell: bash
run: echo "::add-mask::${GITHUB_WORKSPACE}/gha-creds-"

- name: Authenticate to Google Cloud (WIF)
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ inputs.gcp_wif_provider }}
project_id: ${{ inputs.gcp_project_id }}

- name: Mask GCP credential file paths
shell: bash
run: |
for var in GOOGLE_GHA_CREDS_PATH GOOGLE_APPLICATION_CREDENTIALS CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE; do
val="${!var:-}"
if [[ -n "${val}" ]]; then
echo "::add-mask::${val}"
fi
done

- name: Prepare sandbox credentials
shell: bash
run: bash scripts/prepare-sandbox-credentials.sh
56 changes: 56 additions & 0 deletions .github/actions/validate-enrollment/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Validate Enrollment
description: Validate that source repository is enrolled and extract repository metadata

inputs:
source_repo:
description: 'Source repository in owner/repo format'
required: true

outputs:
name:
description: 'Repository name (without owner)'
value: ${{ steps.extract.outputs.name }}

runs:
using: composite
steps:
- name: Validate source repo is enrolled
shell: bash
env:
SOURCE_REPO: ${{ inputs.source_repo }}
run: |
set -euo pipefail
: "${SOURCE_REPO:?SOURCE_REPO is required}"
: "${GITHUB_REPOSITORY_OWNER:?GITHUB_REPOSITORY_OWNER is required}"
if [[ ! "$SOURCE_REPO" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
echo "::error::Invalid source_repo format: must be owner/repo"
exit 1
fi
REPO_OWNER="${SOURCE_REPO%%/*}"
if [[ "$REPO_OWNER" != "$GITHUB_REPOSITORY_OWNER" ]]; then
echo "::error::source_repo owner does not match org"
exit 1
fi
REPO_NAME="${SOURCE_REPO#*/}"
if [[ ! -f config.yaml ]]; then
echo "::error::config.yaml not found"
exit 1
fi
if ! command -v yq &> /dev/null; then
echo "::error::yq command not found"
exit 1
fi
ENABLED=$(yq ".repos.\"$REPO_NAME\".enabled" config.yaml)
if [[ "$ENABLED" != "true" ]]; then
echo "::error::repo is not enabled in config.yaml"
exit 1
fi
echo "Validation passed for ${SOURCE_REPO}"

- name: Extract repo parts
id: extract
shell: bash
env:
SOURCE_REPO: ${{ inputs.source_repo }}
run: |
echo "name=${SOURCE_REPO##*/}" >> "${GITHUB_OUTPUT}"
6 changes: 4 additions & 2 deletions .github/workflows/notify-adr-slack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ jobs:
id: changed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
all_files=$(gh pr diff "${{ github.event.pull_request.number }}" \
--repo "${{ github.repository }}" --name-only)
all_files=$(gh pr diff "$PR_NUMBER" \
--repo "$REPO" --name-only)
files=$(echo "$all_files" | grep '^docs/ADRs/' || true)

if [ -z "$files" ]; then
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Release
on:
push:
tags:
- "v*"
- "v[0-9]+.[0-9]+.[0-9]+*"

permissions:
contents: write
Expand Down
140 changes: 140 additions & 0 deletions .github/workflows/reusable-code.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Reusable code agent workflow. Called by thin callers in .fullsend repos
# via workflow_call. Runs in the caller's repo context (secrets, checkout).
name: Code Agent

on:
workflow_call:
inputs:
event_type:
required: true
type: string
source_repo:
required: true
type: string
event_payload:
required: true
type: string
mint_url:
required: true
type: string
gcp_region:
required: true
type: string
fullsend_version:
required: false
type: string
default: 'latest'
secrets:
FULLSEND_GCP_WIF_PROVIDER:
required: true
FULLSEND_GCP_PROJECT_ID:
required: true

jobs:
code:
name: Code
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
id-token: write
issues: write
packages: read
pull-requests: write

steps:
- name: Checkout config repository
uses: actions/checkout@v6

- name: Checkout upstream defaults
uses: actions/checkout@v6
with:
repository: fullsend-ai/fullsend
ref: v0
path: .defaults
sparse-checkout: |
internal/scaffold/fullsend-repo/

- name: Prepare workspace (upstream defaults + org overrides)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[moderate] (noted, deferred) This "Prepare workspace" step is byte-for-byte identical across all 5 reusable workflows (reusable-code, reusable-fix, reusable-review, reusable-triage, reusable-retro), with a slightly different variant in prioritize.yml. If the layering logic or LAYERED_DIRS changes, all 6 must be updated in lockstep. Consider extracting to a composite action (.github/actions/prepare-workspace/action.yml) consistent with the existing mint-token, setup-gcp, and validate-enrollment actions.

run: |
set -euo pipefail
SRC=".defaults/internal/scaffold/fullsend-repo"
LAYERED_DIRS="agents skills schemas harness policies scripts env"
for dir in ${LAYERED_DIRS}; do
if [[ -d "${SRC}/${dir}" ]]; then
mkdir -p "${dir}"
cp -r "${SRC}/${dir}/." "${dir}/"
fi
done
for dir in ${LAYERED_DIRS}; do
if [[ -d "customized/${dir}" ]]; then
find "customized/${dir}" -type f ! -name '.gitkeep' -print0 \
| while IFS= read -r -d '' f; do
rel="${f#customized/}"
mkdir -p "$(dirname "${rel}")"
cp "${f}" "${rel}"
done
fi
done
mkdir -p .github/scripts
cp "${SRC}/.github/scripts/setup-agent-env.sh" .github/scripts/setup-agent-env.sh
rm -rf .defaults

- name: Validate enrollment and extract repo metadata
id: repo-parts
uses: fullsend-ai/fullsend/.github/actions/validate-enrollment@v0
with:
source_repo: ${{ inputs.source_repo }}

- name: Mint coder token
id: app-token
uses: fullsend-ai/fullsend/.github/actions/mint-token@v0
with:
role: coder
repos: ${{ steps.repo-parts.outputs.name }}
mint_url: ${{ inputs.mint_url }}

- name: Checkout target repository
uses: actions/checkout@v6
with:
repository: ${{ inputs.source_repo }}
token: ${{ steps.app-token.outputs.token }}
path: target-repo
fetch-depth: 0
persist-credentials: false

- name: Validate inputs
env:
ISSUE_NUMBER: ${{ fromJSON(inputs.event_payload).issue.number }}
REPO_FULL_NAME: ${{ inputs.source_repo }}
GITHUB_ISSUE_URL: ${{ fromJSON(inputs.event_payload).issue.html_url }}
run: bash scripts/pre-code.sh

- name: Setup GCP and prepare credentials
uses: fullsend-ai/fullsend/.github/actions/setup-gcp@v0
with:
gcp_wif_provider: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}
gcp_project_id: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}

- name: Setup agent environment
env:
AGENT_PREFIX: CODE_
CODE_GH_TOKEN: ${{ steps.app-token.outputs.token }}
CODE_TARGET_REPO_DIR: target-repo
CODE_ANTHROPIC_VERTEX_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}
CODE_CLOUD_ML_REGION: ${{ inputs.gcp_region }}
CODE_ISSUE_NUMBER: ${{ fromJSON(inputs.event_payload).issue.number }}
run: bash .github/scripts/setup-agent-env.sh

- name: Run code agent
uses: fullsend-ai/fullsend@v0
env:
GITHUB_ISSUE_URL: ${{ fromJSON(inputs.event_payload).issue.html_url }}
ISSUE_NUMBER: ${{ fromJSON(inputs.event_payload).issue.number }}
REPO_FULL_NAME: ${{ inputs.source_repo }}
PUSH_TOKEN: ${{ steps.app-token.outputs.token }}
PUSH_TOKEN_SOURCE: github-app
TARGET_BRANCH: main
with:
agent: code
version: ${{ inputs.fullsend_version }}
Loading
Loading