Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
107 changes: 105 additions & 2 deletions .github/workflows/build.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

166 changes: 166 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: deploy
on:
# zizmor: ignore[dangerous-triggers] — intentional; workflow_run is required
# for OIDC id-token on PR builds. Mitigations: env-var-only untrusted input,
# least-privilege permissions per job, deploy environment approval gate.
workflow_run:
workflows: [build]
types: [completed]
permissions: {}
jobs:
resolve-targets:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
permissions:
actions: read
pull-requests: read
outputs:
matrix: ${{ steps.targets.outputs.matrix }}
has_targets: ${{ steps.targets.outputs.has_targets }}
run_id: ${{ github.event.workflow_run.id }}
steps:
- name: Download deploy intent
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: deploy-intent
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Resolve deploy targets
id: targets
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER_FROM_EVENT: ${{ github.event.workflow_run.pull_requests[0].number }}
ALLOWED_COMPUTE_TYPES: "agentcore"
run: |
ALL_TYPES='["agentcore"]'

validate_compute_type() {
local type="$1"
for allowed in $ALLOWED_COMPUTE_TYPES; do
[[ "$type" == "$allowed" ]] && return 0
done
echo "::error::Invalid compute_type: '$type'. Allowed: $ALLOWED_COMPUTE_TYPES"
return 1
}

filter_valid_types() {
local input_json="$1"
local valid_json="[]"
for type in $(echo "$input_json" | jq -r '.[]'); do
if validate_compute_type "$type" 2>/dev/null; then
valid_json=$(echo "$valid_json" | jq --arg t "$type" '. + [$t]')
else
echo "::warning::Ignoring invalid compute_type from label: '$type'"
fi
done
echo "$valid_json"
}

INTENT=$(jq -r '.deploy' deploy-intent.json)
echo "Deploy intent from build: $INTENT"

case "$INTENT" in
-)
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
;;
labels)
if [[ -z "$PR_NUMBER_FROM_EVENT" ]]; then
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
exit 0
fi

LABELS=$(gh api "repos/$REPO/pulls/$PR_NUMBER_FROM_EVENT" --jq '[.labels[].name]')

if echo "$LABELS" | jq -e 'index("deploy:*")' > /dev/null 2>&1; then
echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT"
echo "has_targets=true" >> "$GITHUB_OUTPUT"
elif echo "$LABELS" | jq -e '[.[] | select(startswith("deploy:"))] | length > 0' > /dev/null 2>&1; then
RAW_TYPES=$(echo "$LABELS" | jq -c '[.[] | select(startswith("deploy:")) | ltrimstr("deploy:")]')
VALIDATED=$(filter_valid_types "$RAW_TYPES")
COUNT=$(echo "$VALIDATED" | jq 'length')
if [[ "$COUNT" -gt 0 ]]; then
echo "matrix=$VALIDATED" >> "$GITHUB_OUTPUT"
echo "has_targets=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::All deploy:<type> labels were invalid"
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
fi
elif echo "$LABELS" | jq -e 'index("deploy")' > /dev/null 2>&1; then
echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT"
echo "has_targets=true" >> "$GITHUB_OUTPUT"
else
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
fi
;;
*)
if ! validate_compute_type "$INTENT"; then
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
exit 1
fi
echo "matrix=[\"$INTENT\"]" >> "$GITHUB_OUTPUT"
echo "has_targets=true" >> "$GITHUB_OUTPUT"
;;
esac

deploy:
needs: resolve-targets
if: needs.resolve-targets.outputs.has_targets == 'true'
runs-on: ubuntu-latest
environment: deploy
concurrency:
group: deploy-${{ matrix.compute_type }}
cancel-in-progress: false
strategy:
matrix:
compute_type: ${{ fromJson(needs.resolve-targets.outputs.matrix) }}
max-parallel: 3
permissions:
id-token: write
contents: read
actions: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Download CDK artifact (${{ matrix.compute_type }})
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: cdk-${{ matrix.compute_type }}-out
path: cdk/
run-id: ${{ needs.resolve-targets.outputs.run_id }}
github-token: ${{ github.token }}

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ vars.AWS_REGION }}

- name: Install mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.x

- name: Install dependencies
run: yarn install --immutable

- name: Deploy
env:
COMPUTE_TYPE: ${{ matrix.compute_type }}
run: npx cdk deploy --app cdk/cdk.out --all --require-approval never
5 changes: 5 additions & 0 deletions .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/design/INTERACTIVE_AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ Opt-in per task: 4 KB previews + full trajectory to S3 with TTL.

### Deferred

- **LLM-synthesized status summary** — `bgagent ask` without targeting the agent; Lambda calls an LLM to narrate state. Cost + hallucination trade-offs; revisit if v1 feedback warrants.
- **LLM-synthesized status summary** — `bgagent ask` without targeting the agent; Lambda calls an LLM to narrate state. Cost + hallucination trade-offs; revisit if v1 feedback warrants. Tracked on the product roadmap as **LLM-synthesized status summary (optional)** under **Smart progress updates** ([ROADMAP.md](../guides/ROADMAP.md)).
- **Cedar `effect: "advise"` tier** — non-blocking FYI policy tier for post-v1. Design sketch in [`CEDAR_HITL_GATES.md`](./CEDAR_HITL_GATES.md).
- **Outbound WebSocket from agent** — only if a concrete sub-200 ms latency requirement surfaces. Agent-initiated egress avoids dual-auth problems and works on any compute.
- **Multi-user watch** — multiple users attached to the same task's live event stream (teams).
Expand Down
40 changes: 40 additions & 0 deletions docs/guides/DEPLOYMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,46 @@ For the full cost model including per-task costs, see [COST_MODEL.md](../design/

## Reference

## CI/CD pipeline (`deploy.yml`)

The repository includes a two-stage CI/CD pipeline:

### Stage 1: Build (`build.yml`)

Triggers on every PR and push to main. Runs `mise run build` (compile, test, lint, synth) and uploads the synthesized `cdk.out/` as a `deploy-intent` artifact. The intent file declares whether a deploy should happen and for which compute types.

### Stage 2: Deploy (`deploy.yml`)

Triggers via `workflow_run` when `build.yml` completes successfully. The pipeline:

1. **Skips fork PRs** — `head_repository.full_name == github.repository` prevents forks from entering the deploy flow. This is a security measure: an untrusted fork could modify `build.yml` to produce a deploy-intent artifact, which would otherwise prompt maintainers for approval unnecessarily.
2. **Downloads `deploy-intent.json`** from the triggering build run.
3. **Resolves targets** — Determines which compute types to deploy:
- `intent: "-"` → no-op (most PRs)
- `intent: "labels"` → reads PR labels against an allowlist
- `intent: "<type>"` → deploys the specified type (e.g., `agentcore`)
4. **Requires approval** — The `deploy` job uses a GitHub Environment with required reviewers. Approvals are logged and the self-review rule prevents unilateral deploys.
5. **Deploys via OIDC** — Assumes an IAM role via GitHub OIDC federation (no long-lived credentials). The role is scoped to the `cdk deploy` action with least-privilege policies per [DEPLOYMENT_ROLES.md](../design/DEPLOYMENT_ROLES.md).

### Security controls

| Control | Purpose |
|---------|---------|
| Fork exclusion (`head_repository` check) | Prevents fork PRs from triggering deploy approval prompts |
| Environment approval | Human gate before any deploy reaches AWS |
| OIDC federation | No stored AWS credentials; tokens are request-scoped |
| Compute type allowlist | Only pre-approved types can be deployed |
| Non-cancellable concurrency | Deploy can't be interrupted mid-flight |

### For administrators

- **Enable deploys**: Set the `deploy` Environment in repo settings with required reviewers.
- **Configure OIDC**: Set `AWS_ROLE_TO_ASSUME` secret and `AWS_REGION` variable.
- **Allowlist compute types**: Edit `ALLOWED_COMPUTE_TYPES` in `deploy.yml`.
- **Deploy via PR label**: Add the `deploy:<type>` label to a PR (e.g., `deploy:agentcore`).

## Related docs

- [Quick start](./QUICK_START.md) -- Zero-to-first-PR in 6 steps.
- [Developer guide](./DEVELOPER_GUIDE.md) -- Local development, testing, repository onboarding.
- [User guide](./USER_GUIDE.md) -- API reference, CLI usage, task management.
Expand Down
Loading
Loading