Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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<<EOF"
echo "$NOTES"
echo "EOF"
} >> "$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 }}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
52 changes: 51 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>"
git tag v<version>
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

Expand Down
3 changes: 3 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional']
};
3 changes: 3 additions & 0 deletions commitlint.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}
38 changes: 24 additions & 14 deletions frontend/app/apps/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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");
Expand Down Expand Up @@ -72,9 +74,9 @@ function ProtocolDetailInner() {
if (!protocol) {
return (
<div style={{ textAlign: "center", padding: "4rem 0" }}>
<p className="muted">Protocol not found.</p>
<p className="muted">{t("protocolNotFound")}</p>
<Link href="/apps" className="btn btn-secondary btn-sm" style={{ marginTop: "1rem" }}>
<IconArrowLeft size={14} /> Back to Apps
<IconArrowLeft size={14} /> {t("backToApps")}
</Link>
</div>
);
Expand All @@ -91,7 +93,7 @@ function ProtocolDetailInner() {
className="row faint"
style={{ fontSize: "0.8125rem", gap: "0.35rem", marginBottom: "0.5rem", textDecoration: "none" }}
>
<IconArrowLeft size={13} /> Apps
<IconArrowLeft size={13} /> {t("backToApps")}
</Link>
<div className="row" style={{ gap: "0.6rem", alignItems: "center" }}>
<span style={{ color: "var(--accent)" }}>{protocol.icon}</span>
Expand Down Expand Up @@ -122,8 +124,8 @@ function ProtocolDetailInner() {
>
<IconCheck size={18} color="var(--accent)" stroke={2.5} />
<span>
<strong>Verification complete.</strong>{" "}
<span className="muted">You were returned here from StellarCred automatically.</span>
<strong>{t("verificationComplete")}</strong>{" "}
<span className="muted">{t("returnedFrom")}</span>
</span>
</div>
)}
Expand Down Expand Up @@ -156,8 +158,7 @@ function ProtocolDetailInner() {
<div className="mono faint" style={{ fontSize: "0.7rem" }}>{protocol.stat.sub}</div>
</div>

{/* Requirements */}
<span className="eyebrow" style={{ marginBottom: "0.4rem", display: "block" }}>Requirements</span>
<span className="eyebrow" style={{ marginBottom: "0.4rem", display: "block" }}>{t("requirements")}</span>
<div className="stack" style={{ marginBottom: "1.25rem" }}>
{protocol.requirements.map((r, i) => (
<div className="line" key={r.label}>
Expand All @@ -172,14 +173,21 @@ function ProtocolDetailInner() {
</span>
</span>
{statuses[i] ? (
<Badge variant="verified">Proved</Badge>
<Badge variant="verified">{t("proved")}</Badge>
) : (
<Badge variant="pending">Needed</Badge>
<Badge variant="pending">{t("needed")}</Badge>
)}
</div>
))}
</div>

{checked && !eligible && (
<Link href={protocol.verifyUrl} className="btn btn-secondary" style={{ width: "100%" }}>
{t("getVerified")}
<IconArrowRight size={14} />
</Link>
)}
{!activeWallet && (
{checked && !eligible && !isPreview && (
<div className="row" style={{ gap: "0.5rem" }}>
<Link
Expand Down Expand Up @@ -211,7 +219,7 @@ function ProtocolDetailInner() {

{!activeWallet && !isPreview && (
<p className="faint" style={{ marginTop: "0.75rem", fontSize: "0.8rem" }}>
Connect your wallet to check eligibility.
{t("connectToCheck")}
</p>
)}
</div>
Expand All @@ -228,8 +236,8 @@ function ProtocolDetailInner() {
<span className="eyebrow">{protocol.actionLabel}</span>
{checked && (
eligible
? <Badge variant="verified">Access granted</Badge>
: <Badge variant="denied">Access denied</Badge>
? <Badge variant="verified">{t("accessGranted")}</Badge>
: <Badge variant="denied">{t("accessDenied")}</Badge>
)}
</div>

Expand All @@ -252,15 +260,17 @@ function ProtocolDetailInner() {
}}
disabled={!eligible || isPreview}
>
{eligible ? protocol.actionLabel : (
<><IconLock size={14} /> {t("proveFirst")}</>
{isPreview ? "Connect wallet to check access" : eligible ? protocol.actionLabel : (
<><IconLock size={14} /> Prove eligibility first</>
)}
</button>

<p className="faint" style={{ marginTop: "1.25rem", fontSize: "0.8125rem", lineHeight: 1.6 }}>
{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 })}
</p>
</div>
</div>
Expand Down
Loading