Skip to content

Commit 924ab6f

Browse files
authored
Merge pull request #13 from corespan/license-review
License review for ai-studio-cli
2 parents 0b36773 + 3ec15b3 commit 924ab6f

30 files changed

Lines changed: 3267 additions & 30 deletions

‎.gitattributes‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Line-ending rules.
2+
#
3+
# Scoped deliberately to the file types this change introduces, rather than a
4+
# blanket `* text=auto eol=lf`. Nine of the 33 tracked files (LICENSE, several
5+
# .go files, both existing workflows) currently carry CRLF; a blanket rule would
6+
# renormalise all of them the next time anyone touched them, producing a
7+
# whole-file diff at a moment unrelated to whatever they were actually changing.
8+
# That mixed state is worth cleaning up, but as its own commit, not as a side
9+
# effect of this one.
10+
11+
# Shell scripts must be LF. CRLF gives `bad interpreter: /usr/bin/env bash^M`,
12+
# which is a genuinely confusing way to fail.
13+
*.sh text eol=lf
14+
15+
# Generated compliance artifacts are written with LF. Without this, a
16+
# contributor with core.autocrlf=true checks them out as CRLF and the freshness
17+
# check reports them stale with a diff that looks empty.
18+
NOTICE text eol=lf
19+
THIRD-PARTY-*.md text eol=lf
20+
*.css text eol=lf
21+
22+
# Vendored assets — never normalise. These are embedded into the binary
23+
# verbatim and any rewriting corrupts them.
24+
*.woff2 binary
25+
*.woff binary
26+
*.png binary
27+
ai-studio-cli/internal/benchui/ui/vendor/js/*.js -text

‎.github/workflows/claude-code-review.yml‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,18 @@ jobs:
2929
- name: Checkout repository
3030
uses: actions/checkout@v4
3131
with:
32-
fetch-depth: 1
32+
# Full history, not a shallow clone.
33+
#
34+
# With fetch-depth: 1 the runner has only the PR head commit and no
35+
# base branch, so the action's own `git fetch origin master --depth=1`
36+
# had nothing to graft onto and failed with
37+
# "Command failed: git fetch origin master --depth=1"
38+
# before the review ever started. A review action has to diff against
39+
# the base branch, which means the base branch has to be present.
40+
#
41+
# 0 = full history. On a repo this size the extra clone cost is
42+
# negligible compared to a job that cannot run at all.
43+
fetch-depth: 0
3344

3445
- name: Run Claude Code Review
3546
id: claude-review

‎.github/workflows/claude.yml‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,18 @@ jobs:
2828
- name: Checkout repository
2929
uses: actions/checkout@v4
3030
with:
31-
fetch-depth: 1
31+
# Full history, not a shallow clone.
32+
#
33+
# With fetch-depth: 1 the runner has only the PR head commit and no
34+
# base branch, so the action's own `git fetch origin master --depth=1`
35+
# had nothing to graft onto and failed with
36+
# "Command failed: git fetch origin master --depth=1"
37+
# before the review ever started. A review action has to diff against
38+
# the base branch, which means the base branch has to be present.
39+
#
40+
# 0 = full history. On a repo this size the extra clone cost is
41+
# negligible compared to a job that cannot run at all.
42+
fetch-depth: 0
3243

3344
- name: Run Claude Code
3445
id: claude

‎.github/workflows/compliance.yml‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
name: Licence compliance
2+
3+
# Makes the licence position self-enforcing.
4+
#
5+
# The distribution model here is the strictest of the three AI Studio repos.
6+
# aistudio-server conveys container images; aistudio-app serves a JS bundle;
7+
# this ships a compiled Go binary that STATICALLY LINKS every dependency. There
8+
# is no node_modules or site-packages beside the artifact for notices to live
9+
# in — the binary is the whole distribution, so the notices have to be inside
10+
# it. These checks confirm they are.
11+
12+
on:
13+
push:
14+
branches: [master, main]
15+
pull_request:
16+
schedule:
17+
# Weekly. Upstream modules change licences occasionally and silently.
18+
- cron: "0 6 * * 1"
19+
workflow_dispatch:
20+
21+
permissions:
22+
contents: read
23+
24+
env:
25+
GO_VERSION: "1.25"
26+
27+
jobs:
28+
files:
29+
name: Licence files and bench UI assets
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Static compliance checks
35+
# Same target as `make compliance` locally, so the feedback loop does
36+
# not require a push. Covers LICENSE completeness and filled-in
37+
# copyright, required files, no CDN references in the bench UI, and that
38+
# the vendored fonts and Chart.js are present with their licences.
39+
#
40+
# Needs no Go toolchain and no network, deliberately — these are the
41+
# checks anyone should be able to run on any checkout. The placeholder
42+
# state is reported here, not failed on; that is the `build` job's job.
43+
run: make compliance
44+
45+
- name: Vendored UI assets match their declared versions
46+
run: |
47+
# These binaries are committed (go:embed needs them at compile time
48+
# and `go build` cannot run npm), so nothing stops someone editing
49+
# them by hand. Regenerate into a scratch dir and compare.
50+
VENDOR_DEST=/tmp/vendor-check ./scripts/vendor-ui-assets.sh
51+
if ! diff -r /tmp/vendor-check ai-studio-cli/internal/benchui/ui/vendor; then
52+
echo "::error::vendored UI assets differ from a clean regeneration."
53+
echo "Run ./scripts/vendor-ui-assets.sh and commit the result."
54+
exit 1
55+
fi
56+
echo "Vendored assets reproduce exactly."
57+
58+
- name: Every font referenced by fonts.css exists
59+
run: |
60+
# Guards against a weight being added to the script's list and
61+
# shipping as a CSS rule with no file behind it — which go:embed
62+
# would happily compile into the binary.
63+
cd ai-studio-cli/internal/benchui/ui/vendor
64+
missing=0
65+
for url in $(grep -oE 'url\("\./[^"]+"\)' fonts.css | sed -E 's|url\("\./||; s|"\)||'); do
66+
[ -f "$url" ] || { echo "::error::fonts.css references missing $url"; missing=1; }
67+
done
68+
exit $missing
69+
70+
build:
71+
name: Build with generated notices and verify the binary carries them
72+
runs-on: ubuntu-latest
73+
steps:
74+
- uses: actions/checkout@v4
75+
76+
- uses: actions/setup-go@v5
77+
with:
78+
go-version: ${{ env.GO_VERSION }}
79+
cache-dependency-path: ai-studio-cli/go.sum
80+
81+
- name: Generate notices and build
82+
# Generation happens HERE rather than being compared against a committed
83+
# copy. An earlier version of this workflow diffed the generated output
84+
# against a file in the repo, which meant a reviewable branch could not
85+
# exist without a Go toolchain and network access, and every dependency
86+
# bump carried a regenerate-and-commit step whose only failure mode was
87+
# a red build with a misleading message.
88+
#
89+
# `go list -deps` answers the only question that matters: which modules
90+
# are actually linked into this binary — not everything in go.sum, which
91+
# also lists test and tooling modules that never reach a user.
92+
run: make build-release
93+
94+
- name: "`licenses` prints real notices"
95+
# The check that actually matters. Everything else verifies files exist
96+
# in a repository; this verifies the notices are inside the artifact a
97+
# user receives, which is what the obligation is.
98+
run: |
99+
out=$(./bin/ai-studio-cli licenses 2>&1) || {
100+
echo "::error::\`bin/ai-studio-cli licenses\` failed:"
101+
echo "$out"
102+
exit 1
103+
}
104+
105+
if grep -q "NOTICES-NOT-GENERATED" <<<"$out"; then
106+
echo "::error::the binary embeds the placeholder — make build-release did not generate them."
107+
exit 1
108+
fi
109+
110+
copyrights=$(grep -ci "copyright" <<<"$out" || true)
111+
if [ "$copyrights" -lt 10 ]; then
112+
echo "::error::only $copyrights copyright notices in the binary output — expected 10+."
113+
exit 1
114+
fi
115+
echo "$copyrights copyright notices embedded."
116+
117+
# The go:embed'd UI assets are not Go modules, so the module walk
118+
# cannot see them. generate-notices.sh appends them; confirm it did.
119+
grep -qi "SIL Open Font License" <<<"$out" || {
120+
echo "::error::OFL-1.1 text missing, but Inter is embedded (OFL s2)."
121+
exit 1
122+
}
123+
grep -qi "chart.js" <<<"$out" || {
124+
echo "::error::Chart.js attribution missing, but it is embedded (MIT)."
125+
exit 1
126+
}
127+
echo "Embedded UI asset notices present."
128+
129+
- name: Bench UI serves no external assets
130+
run: |
131+
# Belt and braces against the Makefile check: assert the reference is
132+
# absent from the compiled binary, not just from the source tree.
133+
#
134+
# Matches a LOADING position (`="https://host`), not any mention of a
135+
# hostname. index.html carries a comment explaining why the Google
136+
# Fonts links were removed, and that comment names the hostname — it is
137+
# embedded verbatim by go:embed, so a bare hostname grep matches our own
138+
# documentation and fails the build. It did exactly that on the first
139+
# run of this check.
140+
#
141+
# The `="` prefix is what separates markup from prose: a real
142+
# regression looks like `<link href="https://fonts.googleapis.com/...`.
143+
if strings ./bin/ai-studio-cli \
144+
| grep -nE '=["'"'"']https?://(fonts\.(googleapis|gstatic)\.com|cdn\.|unpkg\.com|cdnjs\.|.*jsdelivr)'; then
145+
echo "::error::the compiled binary loads an asset from a third-party origin (above)."
146+
exit 1
147+
fi
148+
echo "No third-party asset loads in the binary."
149+
150+
- uses: actions/upload-artifact@v4
151+
if: always()
152+
with:
153+
name: third-party-notices
154+
path: ai-studio-cli/internal/notices/THIRD-PARTY-NOTICES.txt
155+
if-no-files-found: warn
156+
157+
vet:
158+
name: go vet
159+
runs-on: ubuntu-latest
160+
steps:
161+
- uses: actions/checkout@v4
162+
- uses: actions/setup-go@v5
163+
with:
164+
go-version: ${{ env.GO_VERSION }}
165+
cache-dependency-path: ai-studio-cli/go.sum
166+
- run: make vet

‎.github/workflows/release.yml‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
name: Release
2+
3+
# Publishes a release the installer can actually verify.
4+
#
5+
# The previous flow produced a tarball with no checksum, and install.sh
6+
# downloaded whatever "latest" resolved to and `sudo mv`d it into
7+
# /usr/local/bin unverified. This publishes SHA256SUMS alongside the binary,
8+
# and attaches the third-party notices as their own asset so they can be read
9+
# without running the binary.
10+
11+
on:
12+
push:
13+
tags:
14+
- "ai-studio-cli-*-opensource" # e.g. ai-studio-cli-1.2.0-1-opensource
15+
workflow_dispatch:
16+
inputs:
17+
tag:
18+
description: "Annotated tag to build and release (must already exist)"
19+
required: true
20+
21+
permissions:
22+
contents: write
23+
24+
env:
25+
GO_VERSION: "1.25"
26+
BINARY: ai-studio-cli
27+
28+
jobs:
29+
release:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
with:
34+
ref: ${{ github.event.inputs.tag || github.ref }}
35+
# Annotated tag metadata is needed to verify the tag is not
36+
# lightweight — see the check below.
37+
fetch-depth: 0
38+
39+
- uses: actions/setup-go@v5
40+
with:
41+
go-version: ${{ env.GO_VERSION }}
42+
cache-dependency-path: ai-studio-cli/go.sum
43+
44+
- name: Tag must be annotated
45+
run: |
46+
TAG="${{ github.event.inputs.tag }}"
47+
TAG="${TAG:-$GITHUB_REF_NAME}"
48+
type=$(git cat-file -t "$TAG" 2>/dev/null || echo missing)
49+
if [ "$type" != "tag" ]; then
50+
echo "::error::'$TAG' is a lightweight tag (type=$type)."
51+
echo "Lightweight tags carry no author, date or signature, and can be"
52+
echo "silently repointed. Re-cut it with: git tag -a -s $TAG -m ..."
53+
exit 1
54+
fi
55+
echo "$TAG is annotated."
56+
57+
- name: Compliance gate
58+
run: make compliance
59+
60+
- name: Build with generated notices
61+
# This is where the placeholder is a hard stop rather than a note.
62+
# `build-release` generates the notices, builds, and fails if the
63+
# resulting binary cannot print them. A released binary must never ship
64+
# without its attribution.
65+
run: make build-release
66+
67+
- name: Verify the binary carries its notices
68+
run: |
69+
out=$(./bin/${BINARY} licenses)
70+
# if/then, not `grep -q ... && { exit 1; }`. The AND-list returns
71+
# grep's status, so in the good case (marker absent) it evaluates to 1
72+
# — and if it were ever the last command in the step, `bash -e` would
73+
# fail the step precisely when nothing was wrong.
74+
if grep -q "NOTICES-NOT-GENERATED" <<<"$out"; then
75+
echo "::error::refusing to release a binary embedding the placeholder."
76+
exit 1
77+
fi
78+
echo "Notices embedded: $(grep -ci copyright <<<"$out") copyright lines."
79+
80+
- name: Package
81+
run: |
82+
TAG="${{ github.event.inputs.tag }}"
83+
TAG="${TAG:-$GITHUB_REF_NAME}"
84+
mkdir -p dist
85+
# The tarball carries the notices and licence beside the binary, so
86+
# they are present even for someone who never runs `licenses`.
87+
cp LICENSE NOTICE ai-studio-cli/internal/notices/THIRD-PARTY-NOTICES.txt dist/
88+
cp "bin/${BINARY}" dist/
89+
tar -czf "${TAG}.tar.gz" -C dist .
90+
cp ai-studio-cli/internal/notices/THIRD-PARTY-NOTICES.txt .
91+
92+
- name: Checksums
93+
run: |
94+
TAG="${{ github.event.inputs.tag }}"
95+
TAG="${TAG:-$GITHUB_REF_NAME}"
96+
sha256sum "${TAG}.tar.gz" THIRD-PARTY-NOTICES.txt > SHA256SUMS
97+
cat SHA256SUMS
98+
99+
- name: Publish
100+
env:
101+
GH_TOKEN: ${{ github.token }}
102+
run: |
103+
TAG="${{ github.event.inputs.tag }}"
104+
TAG="${TAG:-$GITHUB_REF_NAME}"
105+
gh release create "$TAG" \
106+
--title "ai-studio-cli $TAG" \
107+
--generate-notes \
108+
--verify-tag \
109+
"${TAG}.tar.gz" \
110+
THIRD-PARTY-NOTICES.txt \
111+
SHA256SUMS \
112+
|| gh release upload "$TAG" \
113+
"${TAG}.tar.gz" THIRD-PARTY-NOTICES.txt SHA256SUMS --clobber

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
# Build output — the binary is written to bin/, deliberately not the repo
3+
# root, where its name would collide with the ai-studio-cli/ module directory.
4+
bin/

‎LICENSE‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright [yyyy] [name of copyright owner]
189+
Copyright 2026 CoreSpan AI
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)