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
4 changes: 2 additions & 2 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ builds:
binary: tq
id: tq
ldflags:
- -s -w -X github.com/version-1/tasq/internal/cli/tq.buildVersion={{.Version}} -X github.com/version-1/tasq/internal/cli/tq.buildCommit={{.ShortCommit}} -X github.com/version-1/tasq/internal/config.defaultHomeProfile={{.Env.TQ_BUILD_PROFILE}}
- -s -w -X github.com/version-1/tasq/internal/buildinfo.version={{.Version}} -X github.com/version-1/tasq/internal/buildinfo.commit={{.ShortCommit}} -X github.com/version-1/tasq/internal/config.defaultHomeProfile={{.Env.TQ_BUILD_PROFILE}}
env:
- CGO_ENABLED=0
goos:
Expand Down Expand Up @@ -44,7 +44,7 @@ builds:
binary: web
id: web
ldflags:
- -s -w -X github.com/version-1/tasq/internal/config.defaultHomeProfile={{.Env.TQ_BUILD_PROFILE}}
- -s -w -X github.com/version-1/tasq/internal/buildinfo.version={{.Version}} -X github.com/version-1/tasq/internal/buildinfo.commit={{.ShortCommit}} -X github.com/version-1/tasq/internal/config.defaultHomeProfile={{.Env.TQ_BUILD_PROFILE}}
env:
- CGO_ENABLED=0
goos:
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ TQ_INSTALL_DIR ?= $(HOME)/.local/bin
TQ_INSTALL_NAME ?= tq
TQ_BUILD_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || printf unknown)
TQ_BUILD_PROFILE ?=
TQ_BUILD_LDFLAGS ?= -X github.com/version-1/tasq/internal/cli/tq.buildCommit=$(TQ_BUILD_COMMIT) -X github.com/version-1/tasq/internal/config.defaultHomeProfile=$(TQ_BUILD_PROFILE)
TQ_BUILD_LDFLAGS ?= -X github.com/version-1/tasq/internal/buildinfo.commit=$(TQ_BUILD_COMMIT) -X github.com/version-1/tasq/internal/config.defaultHomeProfile=$(TQ_BUILD_PROFILE)
TQ_INSTALL_HOME = $(if $(filter file,$(origin TQ_HOME)),,$(TQ_HOME))

export TQ_HOME
Expand Down Expand Up @@ -69,10 +69,10 @@ build-tq: ## Build tq for the host into ./bin/tq.
build-tq-dev: ## Build tq and service binaries with the dev profile.
cd cmd/web/frontend && npm ci && npm run build
mkdir -p "$(HOME)/.tasq-dev/system/bin"
go build -ldflags "-X github.com/version-1/tasq/internal/cli/tq.buildCommit=$(TQ_BUILD_COMMIT) -X github.com/version-1/tasq/internal/config.defaultHomeProfile=dev" -o ./tqdev ./cmd/tq
go build -ldflags "-X github.com/version-1/tasq/internal/buildinfo.commit=$(TQ_BUILD_COMMIT) -X github.com/version-1/tasq/internal/config.defaultHomeProfile=dev" -o ./tqdev ./cmd/tq
go build -o "$(HOME)/.tasq-dev/system/bin/issue-tracker" ./cmd/issue-tracker
go build -o "$(HOME)/.tasq-dev/system/bin/orchestrator" ./cmd/orchestrator
go build -o "$(HOME)/.tasq-dev/system/bin/web" ./cmd/web
go build -ldflags "-X github.com/version-1/tasq/internal/buildinfo.commit=$(TQ_BUILD_COMMIT)" -o "$(HOME)/.tasq-dev/system/bin/web" ./cmd/web

.PHONY: dev-check
dev-check:
Expand Down
5 changes: 3 additions & 2 deletions cmd/web/frontend/docs/ui-design-system.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,9 @@ documented token になるまでは literal のまま扱います。
- nav 行: 最小高さ `44px`、icon gap `12px`、`--radius-sm`、hover で
`--surface-hover`。
- アクティブな route は `--surface-hover` 背景を使用 (border アクセントはなし)。
- sidebar 下部: settings リンクが `border-top` と theme switch 行で
仕切られた同じ separator を持ちます。
- sidebar 下部: settings リンクが `border-top`、同じ separator を持つ theme
switch 行、そして小さく muted な現在の release version を表示します。
commit が分かる場合のみ hover text で確認できます。

### Header

Expand Down
5 changes: 3 additions & 2 deletions cmd/web/frontend/docs/ui-design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,9 @@ The application shell is a two-column grid declared in
- Nav rows: `44px` min height, `12px` icon gap, `--radius-sm`, and
`--surface-hover` hover.
- The active route uses `--surface-hover` background (no border accent).
- Bottom of sidebar: settings link separated by `border-top` and a theme switch
row with the same separator.
- Bottom of sidebar: settings link separated by `border-top`, a theme switch
row with the same separator, and the current release version in small muted
text. The commit is available only as hover text when known.

### Header

Expand Down
2 changes: 2 additions & 0 deletions cmd/web/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/logo/BlackLogo-2.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="tasq-version" content="__TASQ_VERSION__" />
<meta name="tasq-commit" content="__TASQ_COMMIT__" />
<script>
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@
font-weight: 700;
}

.version {
align-self: flex-end;
color: var(--medium-gray);
font-size: 11px;
line-height: 1;
padding-inline: var(--space-1-5);
}

@media (max-width: 900px) {
.sidebar {
border-bottom: 1px solid var(--light-gray);
Expand Down
9 changes: 9 additions & 0 deletions cmd/web/frontend/src/components/layout/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { IconProxy } from "@/components/ui/icon-proxy";
import { Switch } from "@/components/ui/switch";
import { getBuildInfo } from "@/lib/build-info";
import type { Project } from "@/lib/types";
import styles from "./index.module.css";

Expand All @@ -23,6 +24,7 @@ export function Sidebar({
projects,
}: SidebarProps) {
const { t } = useTranslation();
const buildInfo = getBuildInfo();

return (
<aside className={styles.sidebar}>
Expand Down Expand Up @@ -97,6 +99,13 @@ export function Sidebar({
onChange={onDarkModeChange}
/>
</label>

<small
className={styles.version}
title={buildInfo.commit ? `commit: ${buildInfo.commit}` : undefined}
>
{buildInfo.version}
</small>
</aside>
);
}
22 changes: 22 additions & 0 deletions cmd/web/frontend/src/lib/build-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { getBuildInfo } from "./build-info";

describe("getBuildInfo", () => {
it("reads release metadata from the document", () => {
document.head.innerHTML = `
<meta name="tasq-version" content="0.1.0" />
<meta name="tasq-commit" content="abc1234" />
`;

expect(getBuildInfo()).toEqual({ version: "v0.1.0", commit: "abc1234" });
});

it("falls back to dev and omits an unknown commit", () => {
document.head.innerHTML = `
<meta name="tasq-version" content="__TASQ_VERSION__" />
<meta name="tasq-commit" content="unknown" />
`;

expect(getBuildInfo()).toEqual({ version: "dev", commit: null });
});
});
29 changes: 29 additions & 0 deletions cmd/web/frontend/src/lib/build-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type BuildInfo = {
version: string;
commit: string | null;
};

const PLACEHOLDER_PREFIX = "__TASQ_";

function readMetadata(name: string): string | null {
const value = document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`)?.content;
if (!value || value.startsWith(PLACEHOLDER_PREFIX)) {
return null;
}
return value;
}

function formatVersion(version: string): string {
if (version === "dev" || version.startsWith("v")) {
return version;
}
return `v${version}`;
}

export function getBuildInfo(): BuildInfo {
const commit = readMetadata("tasq-commit");
return {
version: formatVersion(readMetadata("tasq-version") ?? "dev"),
commit: commit === "unknown" ? null : commit,
};
}
11 changes: 8 additions & 3 deletions cmd/web/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"flag"
"fmt"
"html"
"io/fs"
"log"
"net"
Expand All @@ -18,6 +19,7 @@ import (
"syscall"
"time"

"github.com/version-1/tasq/internal/buildinfo"
"github.com/version-1/tasq/internal/config"
)

Expand Down Expand Up @@ -145,8 +147,8 @@ func (handler spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path = "index.html"
}

info, err := fs.Stat(handler.assets, path)
if err == nil && !info.IsDir() {
fileInfo, err := fs.Stat(handler.assets, path)
if path != "index.html" && err == nil && !fileInfo.IsDir() {
http.FileServer(http.FS(handler.assets)).ServeHTTP(w, r)
return
}
Expand All @@ -157,5 +159,8 @@ func (handler spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(index)
info := buildinfo.Current()
page := strings.ReplaceAll(string(index), "__TASQ_VERSION__", html.EscapeString(info.Version))
page = strings.ReplaceAll(page, "__TASQ_COMMIT__", html.EscapeString(info.Commit))
_, _ = w.Write([]byte(page))
}
26 changes: 26 additions & 0 deletions cmd/web/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)

Expand All @@ -20,3 +21,28 @@ func TestHealth(t *testing.T) {
t.Fatalf("status=%d", response.Code)
}
}

func TestIndexIncludesBuildMetadata(t *testing.T) {
handler, err := newMux("http://127.0.0.1:1", "http://127.0.0.1:2")
if err != nil {
t.Fatalf("new mux: %v", err)
}

request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)

body := response.Body.String()
if response.Code != http.StatusOK {
t.Fatalf("status=%d", response.Code)
}
if strings.Contains(body, "__TASQ_") {
t.Fatalf("body contains unresolved build metadata placeholder")
}
if !strings.Contains(body, `meta name="tasq-version" content="dev"`) {
t.Fatalf("body does not contain development version metadata")
}
if !strings.Contains(body, `meta name="tasq-commit" content="`) {
t.Fatalf("body does not contain commit metadata")
}
}
2 changes: 1 addition & 1 deletion docs/design/operations.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ CLI コマンド:

`tq tui` には対話型端末と text 出力が必要です。Issue Tracker の URL は通常の CLI 解決順に従います。`--orchestrator-url` はサービス状態に保存されたオーケストレーターのアドレスより優先されます。オーケストレーターが未設定または利用できない場合は Run タブだけを縮退表示し、Issue Tracker の障害時は再試行画面を表示します。

`make build-tq` はホスト用の `tq` binary を `./bin/tq` に build し、release build と同じ `buildCommit` ldflags variable を通じて現在の short commit hash を `tq version` に注入します。
`make build-tq` はホスト用の `tq` binary を `./bin/tq` に build し、release build と同じ共有 build-info ldflags variable を通じて現在の short commit hash を `tq version` に注入します。

`make dev-up` は OpenAPI UI を起動し、`dev` container 内で issue-tracker、orchestrator、web-ui を起動します。実行時状態は `$TQ_HOME` 配下に保存され、container 内の既定値は `/workspace/.tasq` です。`run-all` step はサービス起動前に migration を明示的に適用します。`make dev-codex-login` は device auth を使い、Codex の認証情報を `codex-home` Docker volume に永続化します。

Expand Down
2 changes: 1 addition & 1 deletion docs/design/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ CLI commands:

`tq tui` requires an interactive terminal and text output. The issue-tracker URL follows the normal CLI resolution order. `--orchestrator-url` overrides the orchestrator address from service state. A missing or unavailable orchestrator degrades only the Run tab; tracker failures show a retry screen.

`make build-tq` builds the host `tq` binary at `./bin/tq` and injects the current short commit hash into `tq version` through the same `buildCommit` ldflags variable used by release builds.
`make build-tq` builds the host `tq` binary at `./bin/tq` and injects the current short commit hash into `tq version` through the shared build-info ldflags variable used by release builds.

`make dev-up` starts the OpenAPI UI and launches the issue-tracker, orchestrator, and web-ui inside the `dev` container. Runtime state is stored under `$TQ_HOME`, which defaults to `/workspace/.tasq` inside the container. The `run-all` step applies migrations explicitly before starting services. `make dev-codex-login` uses device auth and persists Codex authentication in the `codex-home` Docker volume.

Expand Down
2 changes: 1 addition & 1 deletion docs/references/makefile.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
| `TQ_INSTALL_NAME` | `tq` | リリース版 `tq` バイナリとしてインストールするコマンド名。管理対象サービスの実行ファイルは、固定名で同じ場所にインストールされます。 |
| `TQ_BUILD_COMMIT` | current short Git commit | ホスト用 `tq` ビルドに ldflags 経由で注入するコミット値。 |
| `TQ_BUILD_PROFILE` | empty | local `tq` binary に注入し、既定の `TQ_HOME` を選ぶ build profile。 |
| `TQ_BUILD_LDFLAGS` | `buildCommit` ldflags assignment | `make build-tq` が使う Go linker flags。 |
| `TQ_BUILD_LDFLAGS` | 共有 build-info commit ldflags assignment | `make build-tq` が使う Go linker flags。 |
| `AIR_VERSION` | `v1.52.3` | Go サービスを watch mode で動かすために使う Air のバージョン。 |

固定ポートを使う例:
Expand Down
2 changes: 1 addition & 1 deletion docs/references/makefile.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Use `make help` to print the prefix guide and sectioned target list generated fr
| `TQ_INSTALL_NAME` | `tq` | Installed command name for the release `tq` binary. Managed service executables are installed next to it with fixed names. |
| `TQ_BUILD_COMMIT` | current short Git commit | Commit value injected into host `tq` builds through ldflags. |
| `TQ_BUILD_PROFILE` | empty | Build profile injected into the local `tq` binary to select its default `TQ_HOME`. |
| `TQ_BUILD_LDFLAGS` | `buildCommit` ldflags assignment | Go linker flags used by `make build-tq`. |
| `TQ_BUILD_LDFLAGS` | shared build-info commit ldflags assignment | Go linker flags used by `make build-tq`. |
| `AIR_VERSION` | `v1.52.3` | Air version used to run Go services in watch mode. |

Example with fixed ports:
Expand Down
83 changes: 83 additions & 0 deletions internal/buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package buildinfo

import (
"runtime/debug"
"strings"
)

var (
version = "dev"
commit = "unknown"
)

type Info struct {
Version string
Commit string
}

func Current() Info {
info := Info{Version: version, Commit: commit}
build, ok := debug.ReadBuildInfo()
if !ok {
return info
}
if info.Version == "dev" && build.Main.Version != "" && build.Main.Version != "(devel)" && !isPseudoVersion(build.Main.Version) {
info.Version = build.Main.Version
}

modified := false
for _, setting := range build.Settings {
switch setting.Key {
case "vcs.revision":
if info.Commit != "unknown" {
continue
}
if len(setting.Value) > 7 {
info.Commit = setting.Value[:7]
} else if setting.Value != "" {
info.Commit = setting.Value
}
case "vcs.modified":
modified = strings.EqualFold(setting.Value, "true")
}
}
if modified && info.Commit != "unknown" {
info.Commit += "-dirty"
}
return info
}

func isPseudoVersion(value string) bool {
value = strings.TrimSuffix(value, "+dirty")
parts := strings.Split(value, "-")
if len(parts) < 3 {
return false
}
timestamp := parts[len(parts)-2]
revision := parts[len(parts)-1]
return isDigits(timestamp, 14) && isHex(revision, 12)
}

func isDigits(value string, length int) bool {
if len(value) != length {
return false
}
for _, char := range value {
if char < '0' || char > '9' {
return false
}
}
return true
}

func isHex(value string, length int) bool {
if len(value) != length {
return false
}
for _, char := range value {
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
return false
}
}
return true
}
Loading
Loading