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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Agent Zero is an open-source autonomous engineer that finds, fixes, and verifies
| `packages/source-control` | `@agent-zero/source-control` | Provider-neutral contracts plus GitHub, GitLab, Bitbucket, Gitea |
| `packages/config` | `@agent-zero/config` | Configuration parsing and policy |
| `packages/shared` | `@agent-zero/shared` | Stable cross-package contracts |
| `packages/build-env` | `@agent-zero/build-env` | Build metadata resolution and the Nuxt module that publishes it |
| `packages/cli` | `@agent-zero/cli` | Argument parsing and terminal presentation |
| `packages/database` | `@agent-zero/database` | Schema, Drizzle client, and checked-in migrations |
| `packages/auth` | `@agent-zero/auth` | Authentication policy and the Better Auth options factory |
Expand Down Expand Up @@ -130,6 +131,7 @@ Use the smallest relevant check while iterating, then run the complete set befor
- `packages/source-control`: provider-neutral source-control contracts, with GitHub, GitLab, Bitbucket, and Gitea adapters underneath.
- `packages/config`: configuration parsing and policy.
- `packages/shared`: stable cross-package contracts.
- `packages/build-env`: build metadata (version, commit, branch, deploy channel) and the Nuxt module that publishes it under `runtimeConfig.public.buildInfo`. Build-time resolution reads the hosting provider's variables first and the checkout second; the deployed server completes only what the build could not resolve. Not a runtime package: nothing in `packages/agent`, `packages/runner`, or their adapters may import it.
- `packages/cli`: argument parsing and terminal presentation.
- `packages/database`: the schema, the Drizzle client, and the checked-in migrations. The only package that talks to Postgres. No policy, no HTTP, no runtime imports.
- `packages/auth`: authentication policy and the Better Auth options factory. Reads the store through `packages/database`. No HTTP server, no runtime imports.
Expand Down
17 changes: 17 additions & 0 deletions apps/dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,20 @@ VITEHUB_HOSTING=
# has no writable filesystem to keep task history on. Unused by the self-hosted `node` preset.
KV_REST_API_URL=
KV_REST_API_TOKEN=

# Build metadata (packages/build-env), published under runtimeConfig.public.buildInfo and read
# with useBuildInfo(). On Vercel every field is resolved from the platform's own VERCEL_GIT_*
# variables while the build runs, and none of these need to be set. Everywhere else — a container
# image built in CI, a self-hosted `node .output/server/index.mjs` — the build resolves what it
# can from the checkout and the server completes the rest from these at run time.
#
# Setting any of them takes precedence over every auto-detected provider, so a deployment can
# always state what it is rather than have it inferred.
AGENT_ZERO_BUILD_COMMIT=
AGENT_ZERO_BUILD_BRANCH=
AGENT_ZERO_BUILD_PR_NUMBER=
AGENT_ZERO_BUILD_URL=
AGENT_ZERO_BUILD_PRODUCTION_URL=
# One of dev, preview, canary, release. The one field no detection can work out for a self-hosted
# staging deployment, which is a preview in every way that matters to the people looking at it.
AGENT_ZERO_BUILD_ENV=
72 changes: 72 additions & 0 deletions apps/dashboard/modules/shared/components/BuildEnvironment.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div class="mt-3 flex flex-wrap items-center gap-x-1.5 gap-y-1 mono text-muted">
<NuxtTime
class="whitespace-nowrap"
:datetime="buildTime"
year="numeric"
month="short"
day="numeric"
/>

<span aria-hidden="true">&middot;</span>

<!-- A release is the one build whose version names something a visitor can go and read; every
other channel is identified by the channel itself, because two previews share a version. -->
<NuxtLink
v-if="buildInfo.env === 'release'"
class="text-ink transition hover:text-accent"
:to="`${repositoryUrl}/releases/tag/v${buildInfo.version}`"
external
target="_blank"
rel="noreferrer"
:aria-label="t('common.build.releaseAria', { version: buildInfo.version })"
>
v{{ buildInfo.version }}
</NuxtLink>
<span v-else class="text-ink tracking-wider">{{ buildInfo.env }}</span>

<template v-if="shortCommit">
<span aria-hidden="true">&middot;</span>
<NuxtLink
class="text-ink transition hover:text-accent"
:to="`${repositoryUrl}/commit/${buildInfo.commit}`"
external
target="_blank"
rel="noreferrer"
:aria-label="t('common.build.commitAria', { commit: shortCommit })"
>
{{ shortCommit }}
</NuxtLink>
</template>
</div>
</template>

<script setup lang="ts">
import { type BuildInfo, shortenCommit } from '@agent-zero/build-env';

/**
* The build to render. Defaults to the one this bundle is, and is only ever passed to render a
* build the running server is not — which is what lets the release branch below be tested at all,
* since a test run is by definition never a release.
*/
const { buildInfo: override } = defineProps<{ buildInfo?: BuildInfo }>();

const { t } = useI18n();

/**
* What this build is, resolved by `packages/build-env`.
*
* On Vercel every field was resolved while the build ran. Anywhere else the server completed
* whatever the build could not discover at start-up, so a self-hosted deployment still links the
* commit it is actually serving rather than showing a placeholder.
*/
const resolved = useBuildInfo();
const buildInfo = computed(() => override ?? resolved);
const buildTime = computed(() => new Date(buildInfo.value.time));

// A build that resolved no commit — a source tarball, a container image whose host was told
// nothing — hides the link rather than pointing it at `/commit/null`.
const shortCommit = computed(() => shortenCommit(buildInfo.value.commit));

const repositoryUrl = 'https://github.com/wolfstar-project/agent-zero';
</script>
12 changes: 6 additions & 6 deletions apps/dashboard/modules/shared/components/app/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@
<span class="h-1.5 w-1.5 rounded-full bg-accent shadow-[0_0_8px_var(--az-accent-glow)]" />
{{ $t('common.system.healthy') }}
</p>
<div class="mt-3 flex items-center justify-between mono">
<span class="text-muted">{{ $t('common.version.label') }}</span>
<span class="text-ink">v{{ version }}</span>
</div>
<p class="m-0 mt-3 label-upper">{{ $t('common.build.label') }}</p>
<!-- Sits directly under the signed-in user, where an operator reading a bug report looks
for what the deployment in front of them actually is. -->
<BuildEnvironment />
</template>
<div
v-else
Expand Down Expand Up @@ -100,8 +100,8 @@
</template>

<script setup lang="ts">
import { version } from '~~/package.json';

// Used as a value by the `<component :is>` above, not as a tag, so auto-import does not
// cover it: the nav renders a link where an item has a route and a plain button otherwise.
import { NuxtLink } from '#components';

const collapsed = useSidebarCollapsed();
Expand Down
5 changes: 5 additions & 0 deletions apps/dashboard/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export default defineNuxtConfig({
// Upstash on Vercel — otherwise): ViteHub picks the driver from the deployment preset, and a
// serverless function has no writable filesystem to keep task history on.
['vite-hub/nuxt', { preset: viteHubPreset, kv: true }],
// Publishes what this build is (version, commit, branch, deploy channel) under
// `runtimeConfig.public.buildInfo`. On Vercel the values are resolved while the build runs;
// every other target resolves what it can and the server completes the rest at boot, so a
// self-hosted bundle still reports the commit it was built from. See packages/build-env.
'@agent-zero/build-env/nuxt',
'@unocss/nuxt',
'@nuxt/icon',
'@nuxtjs/i18n',
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@agent-zero/api": "workspace:*",
"@agent-zero/auth": "workspace:*",
"@agent-zero/build-env": "workspace:*",
"@agent-zero/i18n": "workspace:*",
"@agent-zero/mail": "workspace:*",
"@agent-zero/shared": "workspace:*",
Expand Down
30 changes: 30 additions & 0 deletions apps/dashboard/test/nuxt/build-env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';

import { useBuildInfo } from '#imports';

/**
* The dashboard's half of `packages/build-env`: that the module is registered, that it publishes
* its metadata where `useBuildInfo()` reads it, and that a test run gets the fixed values rather
* than whatever branch and commit the checkout happens to be on.
*
* The resolution itself is covered by the package's own suites; what cannot be covered there is
* the wiring — the auto-import, and the runtime-config key it reads.
*/
describe('useBuildInfo', () => {
it('publishes deterministic metadata under test, so no assertion depends on the checkout', () => {
expect(useBuildInfo()).toStrictEqual({
version: '0.0.0',
commit: '0000000000000000000000000000000000000000',
branch: 'test',
env: 'dev',
time: 0,
prNumber: null,
previewUrl: null,
productionUrl: null,
});
});

it('reads the same values the build published to the public runtime config', () => {
expect(useBuildInfo()).toStrictEqual(useRuntimeConfig().public.buildInfo);
});
});
67 changes: 67 additions & 0 deletions apps/dashboard/test/nuxt/components/BuildEnvironment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { mountSuspended } from '@nuxt/test-utils/runtime';
import { describe, expect, it } from 'vitest';
import BuildEnvironment from '~~/modules/shared/components/BuildEnvironment.vue';

/**
* Under test the module publishes its fixed metadata (`dev`, commit `0000…`), so every assertion
* here is about what the component does with a build, not about the checkout it runs in.
*/
describe('BuildEnvironment', () => {
it('names the deploy channel rather than a version outside a release', async () => {
const wrapper = await mountSuspended(BuildEnvironment);

expect(wrapper.text()).toContain('dev');
expect(wrapper.text()).not.toContain('v0.0.0');
});

it('links the commit it is serving, so a bug report can be pinned to one build', async () => {
const wrapper = await mountSuspended(BuildEnvironment);
const commit = wrapper.get('a[href*="/commit/"]');

expect(commit.text()).toBe('0000000');
expect(commit.attributes('href')).toBe(
'https://github.com/wolfstar-project/agent-zero/commit/0000000000000000000000000000000000000000',
);
});

it('opens GitHub in a new tab without handing it a window reference', async () => {
const wrapper = await mountSuspended(BuildEnvironment);
const commit = wrapper.get('a[href*="/commit/"]');

expect(commit.attributes('target')).toBe('_blank');
expect(commit.attributes('rel')).toContain('noreferrer');
expect(commit.attributes('aria-label')).toContain('0000000');
});
});

describe('BuildEnvironment on a release', () => {
/** A release, which a test run can never be: the module publishes `dev` under test. */
const release = {
version: '1.2.3',
commit: '1234567890abcdef1234567890abcdef12345678',
branch: 'main',
env: 'release',
time: 0,
prNumber: null,
previewUrl: null,
productionUrl: 'https://agent-zero.dev',
} as const;

it('names the version and links its tag, which is the one channel that has one', async () => {
const wrapper = await mountSuspended(BuildEnvironment, { props: { buildInfo: release } });
const version = wrapper.get('a[href*="/releases/tag/"]');

expect(version.text()).toBe('v1.2.3');
expect(version.attributes('href')).toBe(
'https://github.com/wolfstar-project/agent-zero/releases/tag/v1.2.3',
);
});

it('hides the commit link for a build that resolved no commit', async () => {
const wrapper = await mountSuspended(BuildEnvironment, {
props: { buildInfo: { ...release, commit: null } },
});

expect(wrapper.find('a[href*="/commit/"]').exists()).toBe(false);
});
});
46 changes: 46 additions & 0 deletions apps/docs/content/1.guide/4.environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,52 @@ rather than resolved to the neighbouring target.

See [Deployment](/guide/deployment).

## Build metadata

Every Nuxt app resolves what build it is — version, commit, branch, deploy channel, deploy URL —
and publishes it under `runtimeConfig.public.buildInfo`, read with `useBuildInfo()`. The resolution
lives in `packages/build-env` and runs in two passes.

The first pass runs during the build. It asks the hosting provider first and the checkout second:
a provider knows the branch a detached CI checkout cannot name, and knows whether the deploy is
production. `VERCEL_GIT_COMMIT_SHA` and friends on Vercel, `COMMIT_REF` and `CONTEXT` on Netlify,
`CF_PAGES_COMMIT_SHA` on Cloudflare Pages, `GITHUB_SHA` on a GitHub Actions runner. None of these
are set by you; they are set by the platform, and are listed in `turbo.jsonc` so a build never
restores a cached bundle that reports a different commit.

The second pass runs in the deployed server, and is what every target that is not Vercel needs. A
container image built in CI has none of the platform variables in scope while it is being built, so
its first pass resolves what git can tell it and leaves the rest as `unknown`. Those fields — and
only those — are completed when the server starts, from the environment the host actually runs it
in. A field the build resolved is never overwritten: the commit a bundle was compiled from is a
property of the bundle, not of the machine serving the request.

For a deployment on no recognised platform, or one that wants to state outright what it is:

| Variable | Purpose |
| --------------------------------- | -------------------------------------------------------- |
| `AGENT_ZERO_BUILD_COMMIT` | Full commit SHA the bundle was built from |
| `AGENT_ZERO_BUILD_BRANCH` | Git branch |
| `AGENT_ZERO_BUILD_PR_NUMBER` | Pull request number, for a pull-request deploy |
| `AGENT_ZERO_BUILD_URL` | URL of this deploy |
| `AGENT_ZERO_BUILD_PRODUCTION_URL` | URL of the production domain |
| `AGENT_ZERO_BUILD_ENV` | Deploy channel: `dev`, `preview`, `canary`, or `release` |

Setting any of them takes precedence over every auto-detected platform, so an operator's answer
always wins over an inferred one. `AGENT_ZERO_BUILD_ENV` is the field detection can never work out
on its own: a self-hosted staging deployment is a `preview` in every way that matters to the people
looking at it, and nothing about a plain `node .output/server/index.mjs` says so.

Individual fields can also be overridden through Nuxt's own public runtime config channel —
`NUXT_PUBLIC_BUILD_INFO_COMMIT`, `NUXT_PUBLIC_BUILD_INFO_BRANCH`, and so on — which applies before
the server's own pass and needs no rebuild. There is no `_SHORT_COMMIT` variant: the dashboard
derives the abbreviated form from `commit` wherever it is displayed, rather than declaring it as
its own field that could drift from the commit it abbreviates.

A prerendered route has no server left to ask, so on `apps/marketing` the second pass runs while
the page is prerendered rather than while it is served. On Vercel that changes nothing: the build
already resolved every field.

## Documentation

| Variable | Purpose |
Expand Down
16 changes: 16 additions & 0 deletions apps/marketing/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,19 @@
MARKETING_SITE_URL=http://localhost:3001
# Where the marketing site's "Sign in" link and primary calls to action point.
MARKETING_DASHBOARD_URL=http://localhost:3000

# Build metadata (packages/build-env), published under runtimeConfig.public.buildInfo and read
# with useBuildInfo(). On Vercel every field is resolved from the platform's own VERCEL_GIT_*
# variables while the build runs, and none of these need to be set. Everywhere else the build
# resolves what it can from the checkout and the server completes the rest from these at run time
# — for the routes this site prerenders, that means at build time, since a prerendered page has no
# server left to ask.
#
# Setting any of them takes precedence over every auto-detected provider.
AGENT_ZERO_BUILD_COMMIT=
AGENT_ZERO_BUILD_BRANCH=
AGENT_ZERO_BUILD_PR_NUMBER=
AGENT_ZERO_BUILD_URL=
AGENT_ZERO_BUILD_PRODUCTION_URL=
# One of dev, preview, canary, release.
AGENT_ZERO_BUILD_ENV=
5 changes: 5 additions & 0 deletions apps/marketing/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export default defineNuxtConfig({
// `modules/` is scanned by Nuxt itself, so the local feature modules (shared, home, contact,
// blog, i18n-strip-empty) register themselves without being listed here.
modules: [
// Publishes what this build is (version, commit, branch, deploy channel) under
// `runtimeConfig.public.buildInfo`. On Vercel the values are resolved while the build runs;
// every other target resolves what it can and the server completes the rest at boot, so a
// self-hosted bundle still reports the commit it was built from. See packages/build-env.
'@agent-zero/build-env/nuxt',
'@unocss/nuxt',
'@nuxt/icon',
'@nuxt/content',
Expand Down
1 change: 1 addition & 0 deletions apps/marketing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@agent-zero/build-env": "workspace:*",
"@agent-zero/i18n": "workspace:*",
"nuxt": "^4.5.2",
"vue": "^3.5.40",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"i18n:report": "turbo run i18n:report",
"i18n:schema": "turbo run i18n:schema",
"i18n:status": "turbo run i18n:status",
"knip": "turbo run build --filter=@agent-zero/i18n --filter=@agent-zero/auth && knip",
"knip": "turbo run build --filter=@agent-zero/i18n --filter=@agent-zero/auth --filter=@agent-zero/build-env && knip",
"lint": "turbo run lint",
"lint:ci": "aube run format:check && aube run lint && aube run knip",
"lint:fix": "turbo run lint:fix",
Expand Down
Loading
Loading