Skip to content
Merged
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
15 changes: 14 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,21 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Trigger post-publish verification
verify-publish:
name: Verify Published Package
needs: [build, publish-main]
if: |
always() &&
github.event.inputs.dry_run != 'true' &&
needs.publish-main.result == 'success'
uses: ./.github/workflows/verify-publish.yml
with:
version: ${{ needs.build.outputs.new_version }}

summary:
name: Summary
needs: [build, publish-packages, publish-main]
needs: [build, publish-packages, publish-main, verify-publish]
runs-on: ubuntu-latest
if: always()

Expand All @@ -464,3 +476,4 @@ jobs:
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "- Packages: ${{ needs.publish-packages.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Main: ${{ needs.publish-main.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Verification: ${{ needs.verify-publish.result }}" >> $GITHUB_STEP_SUMMARY
239 changes: 239 additions & 0 deletions .github/workflows/verify-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
name: Verify Published Package

# This workflow verifies that the published npm package works correctly
# across multiple Node.js versions using both global install and npx.
#
# Triggered:
# - Automatically after publish workflow completes
# - Manually via workflow_dispatch
# - On PR (for testing the workflow itself)

on:
pull_request:
paths:
- ".github/workflows/verify-publish.yml"
- "scripts/post-publish-verify/**"
workflow_dispatch:
inputs:
version:
description: "Package version to verify (default: latest)"
required: false
type: string
default: "latest"
workflow_call:
inputs:
version:
description: "Package version to verify"
required: false
type: string
default: "latest"

jobs:
verify:
name: Verify Node ${{ matrix.node }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ["18", "20", "22"]

steps:
- name: Checkout (for scripts)
uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

- name: Get package spec
id: pkg
run: |
VERSION="${{ inputs.version || 'latest' }}"
if [ "$VERSION" = "latest" ]; then
SPEC="agent-relay"
else
SPEC="agent-relay@${VERSION}"
fi
echo "spec=$SPEC" >> $GITHUB_OUTPUT
echo "Testing: $SPEC"

# Wait for npm to propagate the package
- name: Wait for npm propagation
if: inputs.version != 'latest'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Wait step runs with empty version on pull_request trigger causing invalid npm command

The condition if: inputs.version != 'latest' evaluates to true when inputs.version is empty (which happens on pull_request trigger), causing the npm view command to run with an empty version.

Click to expand

How it happens

On pull_request trigger (lines 12-15), no inputs are provided, so inputs.version is empty/null. The condition at line 63:

if: inputs.version != 'latest'

evaluates to true because empty string != 'latest'.

This causes line 67 to execute:

if npm view agent-relay@${{ inputs.version }} version

Which becomes npm view agent-relay@ version - an invalid command.

Impact

The workflow will fail with a confusing npm error when triggered on PRs that modify the workflow files.

Recommendation: Change the condition to also check for empty: if: inputs.version && inputs.version != 'latest' or use the resolved VERSION variable from the previous step.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

run: |
echo "Waiting for npm to propagate version ${{ inputs.version }}..."
for i in {1..30}; do
if npm view agent-relay@${{ inputs.version }} version 2>/dev/null; then
echo "Package found on npm registry"
break
fi
echo "Attempt $i: Package not yet available, waiting 10s..."
sleep 10
done
Comment on lines +66 to +73
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 npm propagation wait loop silently continues if package is never found

The wait loop attempts 30 times to find the package, but if all attempts fail, it silently continues to the next step instead of failing the workflow.

Click to expand

How it happens

Lines 66-73 loop but never fail:

for i in {1..30}; do
    if npm view agent-relay@${{ inputs.version }} version 2>/dev/null; then
        echo "Package found on npm registry"
        break
    fi
    echo "Attempt $i: Package not yet available, waiting 10s..."
    sleep 10
done

After 30 failed attempts (5 minutes), execution continues to the install step which will then fail with a confusing error.

Impact

The verification will proceed against a non-existent package version, causing cryptic npm install failures instead of a clear "package not found after timeout" error.

Recommendation: Add a flag to track if the package was found, and exit 1 after the loop if not found: FOUND=false; for i in ...; do if npm view ...; then FOUND=true; break; fi; done; if [ "$FOUND" = false ]; then echo "Package not found after 30 attempts"; exit 1; fi

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


# Test 1: Global npm install
- name: "Test: Global npm install"
run: |
echo "Installing ${{ steps.pkg.outputs.spec }} globally..."
npm install -g ${{ steps.pkg.outputs.spec }}
# Add npm global bin to PATH
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH

- name: "Test: Global --version"
run: |
# Ensure npm global bin is in PATH
export PATH="$(npm config get prefix)/bin:$PATH"
VERSION=$(agent-relay --version)
echo "Version output: $VERSION"
if echo "$VERSION" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+'; then
echo "Version check passed"
else
echo "Version check failed - no version number found"
exit 1
fi

- name: "Test: Global -V flag"
run: |
export PATH="$(npm config get prefix)/bin:$PATH"
agent-relay -V

- name: "Test: Global version command"
run: |
export PATH="$(npm config get prefix)/bin:$PATH"
OUTPUT=$(agent-relay version)
echo "$OUTPUT"
if echo "$OUTPUT" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+'; then
echo "Version command check passed"
else
exit 1
fi

- name: "Test: Global --help"
run: |
export PATH="$(npm config get prefix)/bin:$PATH"
agent-relay --help | head -20

- name: Cleanup global install
run: |
export PATH="$(npm config get prefix)/bin:$PATH"
npm uninstall -g agent-relay

# Test 2: npx execution
- name: "Test: npx --version"
run: |
# npx with @ syntax - downloads and runs in one command
VERSION=$(npx ${{ steps.pkg.outputs.spec }} -- --version 2>&1)
echo "npx version output: $VERSION"
if echo "$VERSION" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+'; then
echo "npx version check passed"
else
exit 1
fi

- name: "Test: npx --help"
run: npx ${{ steps.pkg.outputs.spec }} -- --help | head -20

- name: "Test: npx version command"
run: |
OUTPUT=$(npx ${{ steps.pkg.outputs.spec }} -- version 2>&1)
echo "$OUTPUT"
if echo "$OUTPUT" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+'; then
echo "npx version command check passed"
else
exit 1
fi

# Test 3: Local project install
- name: "Test: Local project install"
run: |
mkdir -p /tmp/test-project
cd /tmp/test-project
npm init -y
npm install ${{ steps.pkg.outputs.spec }}

- name: "Test: Local npx execution"
run: |
cd /tmp/test-project
VERSION=$(npx agent-relay --version)
echo "Local npx version: $VERSION"
if echo "$VERSION" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+'; then
echo "Local npx check passed"
else
exit 1
fi

- name: "Test: Local bin executable"
run: |
cd /tmp/test-project
if [ -x "./node_modules/.bin/agent-relay" ]; then
VERSION=$(./node_modules/.bin/agent-relay --version)
echo "Local bin version: $VERSION"
else
echo "Local bin not found or not executable"
exit 1
fi

- name: Cleanup
run: rm -rf /tmp/test-project

# Docker-based verification (more isolated)
verify-docker:
name: Verify Docker (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ["18", "20", "22"]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build verification image
run: |
VERSION="${{ inputs.version || 'latest' }}"
docker build \
--build-arg NODE_VERSION=${{ matrix.node }} \
--build-arg PACKAGE_VERSION=$VERSION \
-t agent-relay-verify:node${{ matrix.node }} \
-f scripts/post-publish-verify/Dockerfile \
scripts/post-publish-verify/

- name: Run verification
run: |
docker run --rm agent-relay-verify:node${{ matrix.node }}

summary:
name: Verification Summary
needs: [verify, verify-docker]
runs-on: ubuntu-latest
if: always()

steps:
- name: Summary
run: |
echo "## Post-Publish Verification Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Package**: \`agent-relay@${{ inputs.version || 'latest' }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Native Tests" >> $GITHUB_STEP_SUMMARY
echo "| Node Version | Status |" >> $GITHUB_STEP_SUMMARY
echo "|--------------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Node 18 | ${{ needs.verify.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Node 20 | ${{ needs.verify.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Node 22 | ${{ needs.verify.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
Comment on lines +223 to +225
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Workflow summary displays same status for all Node versions instead of individual results

The verification summary in verify-publish.yml displays misleading per-Node-version status information.

Click to expand

Issue

The summary section (lines 212-214 and 219-221) shows three separate rows for Node 18, 20, and 22, but all rows display the same value from needs.verify.result:

echo "| Node 18 | ${{ needs.verify.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Node 20 | ${{ needs.verify.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Node 22 | ${{ needs.verify.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY

In GitHub Actions, needs.job.result for a matrix job returns the aggregate result, not individual matrix instance results. This means if Node 18 passes but Node 20 fails, all three rows will show ❌ (because the aggregate is failure).

Actual vs Expected

  • Actual: All three Node version rows show identical status (the aggregate matrix result)
  • Expected: Each row should show the individual result for that specific Node version, or the summary should clarify it's showing aggregate status

Impact

This is misleading to users reviewing the workflow summary - they may think all versions failed when only one did, or vice versa. The overall pass/fail logic is correct, but the granular display is inaccurate.

Recommendation: Either (1) remove the per-version table and just show aggregate pass/fail, (2) add a note clarifying this is aggregate status, or (3) use job outputs to capture individual matrix results for accurate per-version reporting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Tests" >> $GITHUB_STEP_SUMMARY
echo "| Node Version | Status |" >> $GITHUB_STEP_SUMMARY
echo "|--------------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Node 18 | ${{ needs.verify-docker.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Node 20 | ${{ needs.verify-docker.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Node 22 | ${{ needs.verify-docker.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Overall" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.verify.result }}" = "success" ] && [ "${{ needs.verify-docker.result }}" = "success" ]; then
echo "✅ All verification tests passed!" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Some verification tests failed" >> $GITHUB_STEP_SUMMARY
fi
3 changes: 1 addition & 2 deletions packages/daemon/src/consensus-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/

import { generateId } from '@agent-relay/wrapper';
import type { VoteValue, ConsensusType } from '@agent-relay/protocol';
import {
ConsensusEngine,
createConsensusEngine,
Expand All @@ -40,8 +41,6 @@ import {
type Proposal,
type ConsensusResult,
type ConsensusConfig,
type VoteValue,
type ConsensusType,
type ParsedProposalCommand,
} from './consensus.js';
import type { Router } from './router.js';
Expand Down
27 changes: 8 additions & 19 deletions packages/daemon/src/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,14 @@
import { randomUUID } from 'node:crypto';
import { EventEmitter } from 'node:events';

// =============================================================================
// Types
// =============================================================================

export type ConsensusType =
| 'majority' // >50% agree
| 'supermajority' // >=threshold agree (default 2/3)
| 'unanimous' // 100% agree
| 'weighted' // Weighted by role
| 'quorum'; // Minimum participation + majority

export type VoteValue = 'approve' | 'reject' | 'abstain';

export type ProposalStatus =
| 'pending' // Awaiting votes
| 'approved' // Consensus reached (approved)
| 'rejected' // Consensus reached (rejected)
| 'expired' // Timeout without consensus
| 'cancelled'; // Proposer cancelled
// Import shared types from protocol (canonical source)
// NOTE: These types are NOT re-exported to avoid duplicate export errors
// in the main agent-relay package. Import from @agent-relay/protocol instead.
import type {
ConsensusType,
VoteValue,
ProposalStatus,
} from '@agent-relay/protocol';

export interface AgentWeight {
/** Agent name */
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/enhanced-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
type CompactionResult,
} from '@agent-relay/memory';

import type { VoteValue } from '@agent-relay/protocol';
import {
ConsensusEngine,
createConsensusEngine,
Expand All @@ -65,7 +66,6 @@ import {
type Proposal,
type ConsensusResult,
type ConsensusConfig,
type VoteValue,
} from './consensus.js';

// =============================================================================
Expand Down
7 changes: 4 additions & 3 deletions packages/daemon/src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface OrchestratorConfig {

/**
* Determine the default host binding.
* - In cloud environments (Fly.io, Docker with WORKSPACE_ID), bind to 0.0.0.0 for external access
* - In cloud environments, bind to '::' for IPv6+IPv4 dual-stack (required for Fly.io 6PN)
* - Locally, bind to localhost for security
* - Can be overridden with AGENT_RELAY_API_HOST env var
*/
Expand All @@ -62,13 +62,14 @@ function getDefaultHost(): string {
if (process.env.AGENT_RELAY_API_HOST) {
return process.env.AGENT_RELAY_API_HOST;
}
// Cloud environment detection - bind to all interfaces for load balancer access
// Cloud environment detection - bind to :: for IPv6 + IPv4 dual-stack
// Fly.io internal network uses IPv6 (fdaa:...), so 0.0.0.0 won't work
const isCloudEnvironment =
process.env.FLY_APP_NAME || // Fly.io
process.env.WORKSPACE_ID || // Agent Relay workspace
process.env.RELAY_WORKSPACE_ID || // Alternative workspace ID
process.env.RUNNING_IN_DOCKER === 'true'; // Docker container
return isCloudEnvironment ? '0.0.0.0' : 'localhost';
return isCloudEnvironment ? '::' : 'localhost';
}

const DEFAULT_CONFIG: OrchestratorConfig = {
Expand Down
Loading
Loading