diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcb6f99d..b19d5e9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,21 @@ on: branches: [main] jobs: + commitlint: + name: Lint commit messages + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm install --save-dev @commitlint/cli @commitlint/config-conventional + - name: Lint commits in PR + run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + contracts: name: Contract tests & build runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..367cbb43 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + name: Changelog, GitHub Release & npm publish + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history for changelog generation + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Extract the version from the tag (strips leading 'v') + - name: Set version env + run: echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + + # Generate / update CHANGELOG.md at the repo root using conventional-changelog + - name: Update CHANGELOG.md + working-directory: . + run: | + npx --yes conventional-changelog-cli \ + -p angular \ + -i CHANGELOG.md \ + -s \ + -r 0 + + # Extract only the section for this release to use as the GitHub Release body + - name: Extract release notes + id: notes + working-directory: . + run: | + NOTES=$(npx --yes conventional-changelog-cli -p angular -r 1 --outfile /dev/stdout 2>/dev/null) + # Store as multiline env var + { + echo "RELEASE_NOTES<> "$GITHUB_ENV" + + # Commit the updated CHANGELOG.md back to the default branch + - name: Commit CHANGELOG.md + working-directory: . + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md + # Only commit if there are staged changes + git diff --cached --quiet || git commit -m "chore: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]" + git push origin HEAD:main + + # Create the GitHub Release + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + body: ${{ env.RELEASE_NOTES }} + + # Build and publish @stellarcred/sdk to npm + - name: Build SDK + working-directory: frontend/packages/sdk + run: pnpm build + + - name: Publish to npm + working-directory: frontend/packages/sdk + run: pnpm publish --no-git-checks --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..996dce58 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This file is generated automatically from conventional commit messages. +See [Conventional Commits](https://www.conventionalcommits.org/) for the commit message format. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85346a3a..b42501d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,7 +101,57 @@ Please do **not** open a public issue for security vulnerabilities. See [SECURIT ## Commit messages -Use the imperative mood and be specific: `add income_proof circuit`, `fix check_claim threshold comparison`, `remove SmileID dependency`. +StellarCred uses [Conventional Commits](https://www.conventionalcommits.org/). Every commit message must have a structured prefix so the changelog and release notes are generated automatically. + +| Prefix | When to use | +|--------|-------------| +| `feat:` | A new feature visible to users or integrators | +| `fix:` | A bug fix | +| `docs:` | Documentation only | +| `chore:` | Build process, tooling, dependency updates | +| `refactor:` | Code change that isn't a fix or feature | +| `test:` | Adding or updating tests | +| `ci:` | CI/CD pipeline changes | + +Examples: + +``` +feat: add income_proof circuit +fix: correct check_claim threshold comparison off-by-one +docs: document NPM_TOKEN secret setup +chore: bump soroban-sdk to 26.0.1 +``` + +Breaking changes: add `BREAKING CHANGE:` in the commit footer, or append `!` after the type (`feat!:`). + +Commit messages are linted automatically on pull requests via `commitlint`. + +## Releasing + +Releases are tag-driven. The GitHub Actions release workflow fires on any tag matching `v*` and: + +1. Regenerates `CHANGELOG.md` from the full conventional commit history. +2. Commits the updated changelog back to `main`. +3. Creates a GitHub Release with the changelog section for that version as the body. +4. Builds and publishes `@stellarcred/sdk` to npm. + +### Cutting a release + +```bash +# Bump the version in frontend/packages/sdk/package.json, then: +git add frontend/packages/sdk/package.json +git commit -m "chore: release v" +git tag v +git push origin main --tags +``` + +The workflow handles everything else. + +### Required secret + +The repository must have an `NPM_TOKEN` secret set under **Settings → Secrets and variables → Actions**. +Generate the token at [npmjs.com](https://www.npmjs.com) with **Automation** type and **Read and write** scope for the `@stellarcred` scope (or the package name). +Never commit the token value. ## License diff --git a/commitlint.config.json b/commitlint.config.json new file mode 100644 index 00000000..c30e5a97 --- /dev/null +++ b/commitlint.config.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/frontend/app/apps/[id]/page.tsx b/frontend/app/apps/[id]/page.tsx index e7429eeb..f5ec59c1 100644 --- a/frontend/app/apps/[id]/page.tsx +++ b/frontend/app/apps/[id]/page.tsx @@ -3,6 +3,7 @@ import { Suspense, useEffect, useState } from "react"; import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { IconLock, IconCheck, @@ -24,6 +25,7 @@ function ProtocolDetailInner() { const { id } = useParams<{ id: string }>(); const { address } = useWallet(); const searchParams = useSearchParams(); + const t = useTranslations("apps"); const scVerified = searchParams.get("sc_verified") === "true"; const scWallet = searchParams.get("sc_wallet"); @@ -72,9 +74,9 @@ function ProtocolDetailInner() { if (!protocol) { return (
-

Protocol not found.

+

{t("protocolNotFound")}

- Back to Apps + {t("backToApps")}
); @@ -91,7 +93,7 @@ function ProtocolDetailInner() { className="row faint" style={{ fontSize: "0.8125rem", gap: "0.35rem", marginBottom: "0.5rem", textDecoration: "none" }} > - Apps + {t("backToApps")}
{protocol.icon} @@ -122,8 +124,8 @@ function ProtocolDetailInner() { > - Verification complete.{" "} - You were returned here from StellarCred automatically. + {t("verificationComplete")}{" "} + {t("returnedFrom")}
)} @@ -156,8 +158,7 @@ function ProtocolDetailInner() {
{protocol.stat.sub}
- {/* Requirements */} - Requirements + {t("requirements")}
{protocol.requirements.map((r, i) => (
@@ -172,14 +173,21 @@ function ProtocolDetailInner() { {statuses[i] ? ( - Proved + {t("proved")} ) : ( - Needed + {t("needed")} )}
))}
+ {checked && !eligible && ( + + {t("getVerified")} + + + )} + {!activeWallet && ( {checked && !eligible && !isPreview && (
- Connect your wallet to check eligibility. + {t("connectToCheck")}

)}
@@ -228,8 +236,8 @@ function ProtocolDetailInner() { {protocol.actionLabel} {checked && ( eligible - ? Access granted - : Access denied + ? {t("accessGranted")} + : {t("accessDenied")} )} @@ -252,6 +260,8 @@ function ProtocolDetailInner() { }} disabled={!eligible || isPreview} > + {eligible ? protocol.actionLabel : ( + <> {t("proveFirst")} {isPreview ? "Connect wallet to check access" : eligible ? protocol.actionLabel : ( <> Prove eligibility first )} @@ -259,8 +269,8 @@ function ProtocolDetailInner() {

{eligible - ? `${protocol.name} read ProofRegistry.check_claim and found valid proofs for your address. No personal data was shared.` - : `${protocol.name} only reads ProofRegistry.check_claim for your address — it never sees the credential data behind your proofs.`} + ? t("noPersonalData", { name: protocol.name }) + : t("noPersonalDataDenied", { name: protocol.name })}

diff --git a/frontend/app/apps/page.tsx b/frontend/app/apps/page.tsx index a3e06b72..046584d7 100644 --- a/frontend/app/apps/page.tsx +++ b/frontend/app/apps/page.tsx @@ -3,6 +3,7 @@ import { Suspense, useEffect, useState, useMemo } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { IconCheck, IconCircle, IconSearch, IconFilter } from "@tabler/icons-react"; import { WalletButton } from "@/components/WalletButton"; import { useWallet } from "@/lib/wallet-context"; @@ -29,6 +30,7 @@ function ProtocolCard({ activeWallet: string | null; }) { const router = useRouter(); + const t = useTranslations("apps"); const [statuses, setStatuses] = useState(protocol.requirements.map(() => false)); const [checked, setChecked] = useState(false); const eligible = statuses.every(Boolean); @@ -85,46 +87,21 @@ function ProtocolCard({ {protocol.requirements.map((r, i) => { const isClaimMet = checked && statuses[i]; return ( - + {CLAIM_LABELS[r.type] || r.type} ); })} -

- {protocol.tagline} -

-

- {protocol.description} -

-
+

{protocol.tagline}

+

{protocol.description}

+
{protocol.stat.label}
{protocol.stat.value}
{protocol.stat.sub}
-
Requirements
+
{t("requirements")}
{protocol.requirements.map((r, i) => (
@@ -137,8 +114,8 @@ function ProtocolCard({ {statuses[i] - ? Proved - : Needed} + ? {t("proved")} + : {t("needed")}}
))}
@@ -149,6 +126,7 @@ function ProtocolCard({ function AppsInner() { const { address, connect } = useWallet(); const searchParams = useSearchParams(); + const t = useTranslations("apps"); const scVerified = searchParams.get("sc_verified") === "true"; const scWallet = searchParams.get("sc_wallet"); const activeWallet = address ?? scWallet ?? null; @@ -181,12 +159,14 @@ function AppsInner() { <>
- Demo protocols -

Apps

+ {t("eyebrow")} +

{t("title")}

+
+ {t("subtitle")}{" "}
{scVerified && ( -
+
- Verification complete.{" "} - You were returned here from StellarCred automatically. + {t("verificationComplete")}{" "} + {t("returnedFrom")}
)} - {/* Search and filter */}
- + setSearch(e.target.value)} - style={{ - width: "100%", - padding: "0.65rem 0.75rem 0.65rem 2.25rem", - borderRadius: "var(--radius)", - border: "1px solid var(--border)", - background: "var(--bg-raised)", - color: "var(--text)", - fontSize: "0.875rem", - }} + style={{ width: "100%", padding: "0.65rem 0.75rem 0.65rem 2.25rem", borderRadius: "var(--radius)", border: "1px solid var(--border)", background: "var(--bg-raised)", color: "var(--text)", fontSize: "0.875rem" }} />
@@ -262,40 +215,13 @@ function AppsInner() { {CREDENTIAL_TYPES.map((claim) => { const isActive = selectedClaims.has(claim); return ( - ); })} {selectedClaims.size > 0 && ( - )} @@ -303,15 +229,10 @@ function AppsInner() {
{filtered.length === 0 ? ( -
+
-

No apps match

-

- Try adjusting your search or removing claim filters. -

+

{t("noResults")}

+

{t("noResultsHint")}

) : (
@@ -335,4 +256,3 @@ export default function AppsPage() { ); } - diff --git a/frontend/app/holder/page.tsx b/frontend/app/holder/page.tsx index 800f7b7d..405c2f90 100644 --- a/frontend/app/holder/page.tsx +++ b/frontend/app/holder/page.tsx @@ -1,5 +1,7 @@ "use client"; +import { useEffect, useRef, useState } from "react"; +import { useTranslations } from "next-intl"; import { Suspense, useEffect, useRef, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { @@ -103,6 +105,7 @@ function CredCard({ }; }) { const status = proofStatus(c); + const t = useTranslations("holder"); return (
@@ -113,6 +116,8 @@ function CredCard({ {c.title} {c.claim}
+
+ {c.issuer} · {truncateHash(c.commitment)}
{c.issuer} · {truncateHash(c.commitment)} + {onTransfer && (
@@ -407,7 +482,7 @@ function HolderInner() { {/* ── Credentials to prove ── */} {unproved.length > 0 && (
- Ready to prove + {t("sectionReady")} {unproved.map((c) => ( 0 && (
- On-chain · active proofs + {t("sectionOnChain")} {proved.map((c) => ( 0 && (

- Connect a wallet to generate and submit proofs. + {t("connectWalletToProve")}

)} @@ -516,6 +591,14 @@ function HolderInner() { onCancel={() => setImporting(false)} /> ) : ( +