Merge branch 'main' into actors/francis-addr

This commit is contained in:
Alessandro (Ale) Segala
2026-09-20 10:14:39 -07:00
committed by GitHub
41 changed files with 1552 additions and 762 deletions
+187 -11
View File
@@ -1,27 +1,180 @@
name: Release
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
bump:
description: Version bump (auto uses conventional commits)
type: choice
default: auto
options:
- auto
- major
- minor
- patch
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: write
packages: write
id-token: write
attestations: write
artifact-metadata: write
contents: read
defaults:
run:
shell: bash
jobs:
release:
prepare:
name: Prepare release
runs-on: depot-ubuntu-latest
outputs:
tag: ${{ steps.version.outputs.tag }}
commit: ${{ steps.commit.outputs.commit }}
steps:
- name: Require the main branch
run: |
if [[ "$GITHUB_REF" != refs/heads/main ]]; then
echo "::error::Releases must be triggered from main."
exit 1
fi
- name: Checkout release source
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Setup git-cliff
uses: taiki-e/install-action@v2
with:
tool: git-cliff@2.14.2
- name: Calculate next version
id: version
env:
BUMP: ${{ inputs.bump }}
run: |
version=$(git cliff --bumped-version --unreleased --offline --bump "$BUMP")
version=${version#v}
if [[ "$version" == "$(cat .version)" ]]; then
echo "No commits requiring a version bump; no release created." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
echo "Preparing release v$version from $GITHUB_SHA." >> "$GITHUB_STEP_SUMMARY"
- name: Setup pnpm
if: steps.version.outputs.tag != ''
uses: pnpm/action-setup@v6
with:
run_install: false
- name: Setup Node.js
if: steps.version.outputs.tag != ''
uses: actions/setup-node@v6.5.0
with:
node-version: 24
cache: pnpm
- name: Install dependencies
if: steps.version.outputs.tag != ''
run: pnpm install --frozen-lockfile
- name: Update version and changelog
if: steps.version.outputs.tag != ''
env:
VERSION: ${{ steps.version.outputs.version }}
RELEASE_TAG: ${{ steps.version.outputs.tag }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
printf '%s\n' "$VERSION" > .version
jq --arg version "$VERSION" '.version = $version' frontend/package.json > "$RUNNER_TEMP/package.json"
mv "$RUNNER_TEMP/package.json" frontend/package.json
pnpm --dir frontend exec prettier --write package.json
git cliff --prepend CHANGELOG.md --tag "$RELEASE_TAG" --unreleased
- name: Create bot app token
if: steps.version.outputs.tag != ''
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
permission-contents: write
- name: Commit and tag release
if: steps.version.outputs.tag != ''
id: commit
env:
VERSION: ${{ steps.version.outputs.version }}
RELEASE_TAG: ${{ steps.version.outputs.tag }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
# Attribute the release commit to the app and authenticate the push with its installation token
bot_name="$APP_SLUG[bot]"
bot_id=$(gh api "users/$bot_name" --jq .id)
git config user.name "$bot_name"
git config user.email "$bot_id+$bot_name@users.noreply.github.com"
gh auth setup-git
git add .version frontend/package.json CHANGELOG.md
git commit -m "release: $VERSION"
git tag "$RELEASE_TAG"
# Publish both refs together so a concurrent main update cannot leave an orphaned release tag
git push --atomic origin HEAD:refs/heads/main "refs/tags/$RELEASE_TAG"
echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
build:
name: Build and attest
needs: prepare
if: needs.prepare.outputs.tag != ''
runs-on: depot-ubuntu-24.04-16
permissions:
contents: write
packages: write
id-token: write
attestations: write
artifact-metadata: write
env:
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.commit }}
fetch-depth: 0
- name: Create bot app token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
permission-contents: write
- name: Create draft release
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
# Reuse the draft when retrying a failed build
if is_draft=$(gh release view "$RELEASE_TAG" --json isDraft --jq .isDraft); then
if [[ "$is_draft" != true ]]; then
echo "::error::Release $RELEASE_TAG is already published."
exit 1
fi
else
awk '/^## v[0-9]/ { if (found) exit; found=1; next } found' CHANGELOG.md > "$RUNNER_TEMP/release-notes.md"
gh release create "$RELEASE_TAG" --verify-tag --title "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-notes.md" --draft
fi
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
@@ -39,6 +192,11 @@ jobs:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- name: Setup Syft
uses: anchore/sbom-action/download-syft@v0.24.2
with:
syft-version: v1.52.0
- name: Set up Depot CLI
uses: depot/setup-action@v1
@@ -85,7 +243,25 @@ jobs:
with:
subject-checksums: ./dist/digests.txt
publish:
name: Publish release
needs: [prepare, build]
runs-on: depot-ubuntu-latest
steps:
- name: Create bot app token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
permission-contents: write
- name: Publish release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release edit ${{ github.ref_name }} --draft=false
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
run: |
gh release edit "$RELEASE_TAG" --draft=false
echo "Published [$RELEASE_TAG](https://github.com/$GH_REPO/releases/tag/$RELEASE_TAG)." >> "$GITHUB_STEP_SUMMARY"
+26
View File
@@ -9,6 +9,9 @@ before:
hooks:
- pnpm install --frozen-lockfile
- pnpm -C frontend build
- mkdir -p .tmp
- mv backend/frontend/dist/cyclonedx/frontend.cdx.json .tmp/frontend.cdx.json
- rm -r backend/frontend/dist/cyclonedx
builds:
- id: pocket-id
@@ -91,6 +94,23 @@ archives:
checksum:
name_template: checksums.txt
sboms:
- id: binaries
artifacts: binary
disable: '{{ if index .Env "BUILD_NEXT" }}true{{ end }}'
documents:
- "pocket-id_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ targetVariant . }}.sbom.spdx.json"
cmd: sh
args:
- ../scripts/development/generate-binary-sbom.sh
- $artifact
- $document
- "pocket-id_{{ .Os }}_{{ .Arch }}{{ targetVariant . }}"
- "{{ .Version }}"
env:
- SYFT_CHECK_FOR_APP_UPDATE=false
- SYFT_CACHE_DIR=
docker_digest:
name_template: digests.txt
@@ -124,6 +144,7 @@ dockers_v2:
sbom: true
extra_files:
- scripts/docker
- .tmp/frontend.cdx.json
- id: pocket-id-distroless
ids:
@@ -152,6 +173,8 @@ dockers_v2:
"org.opencontainers.image.title": "Pocket ID"
disable: '{{ if index .Env "BUILD_NEXT" }}true{{ end }}'
sbom: true
extra_files:
- .tmp/frontend.cdx.json
- id: pocket-id-next
ids:
@@ -179,6 +202,7 @@ dockers_v2:
sbom: true
extra_files:
- scripts/docker
- .tmp/frontend.cdx.json
- id: pocket-id-next-distroless
ids:
@@ -204,6 +228,8 @@ dockers_v2:
"org.opencontainers.image.title": "Pocket ID"
disable: '{{ if not (index .Env "BUILD_NEXT") }}true{{ end }}'
sbom: true
extra_files:
- .tmp/frontend.cdx.json
notarize:
macos:
+1 -1
View File
@@ -1 +1 @@
2.15.0
2.16.0
+20
View File
@@ -1,3 +1,23 @@
## v2.16.0
### Features
- update the design of the email templates ([3249b0d](https://github.com/pocket-id/pocket-id/commit/3249b0dc8c00220b74e1008f94812cc02f6313c1) by @stonith404)
### Bug Fixes
- loading indicator not visible ([4ba2899](https://github.com/pocket-id/pocket-id/commit/4ba28992ff496caa20b6abe6f614eeb1fa777469) by @stonith404)
- login with Yubikey not working in some Safari browsers ([4858db3](https://github.com/pocket-id/pocket-id/commit/4858db3bb4cf6619cad8682da61f0e793f24874c) by @stonith404)
### Other
- upgrade git cliff version ([98f9e39](https://github.com/pocket-id/pocket-id/commit/98f9e39785ae913d65ab3ad3523feaf907d3686e) by @stonith404)
- update francis to rc7 ([\#1769](https://github.com/pocket-id/pocket-id/pull/1769) by @ItalyPaleAle)
- include frontend dependencies in SBOM ([5a1c6f0](https://github.com/pocket-id/pocket-id/commit/5a1c6f0547f779175107d31ea13d4f24000a1a65) by @stonith404)
- include SBOMs for binaries ([8fe53ed](https://github.com/pocket-id/pocket-id/commit/8fe53ed42c522717c8c69612cc700e195b8ab2ea) by @stonith404)
- replace local release script with action ([ba246e6](https://github.com/pocket-id/pocket-id/commit/ba246e61465378bc11571fcc77ca01c936fda174) by @stonith404)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.15.0...v2.16.0
## v2.15.0
### Bug Fixes
+2
View File
@@ -36,6 +36,7 @@ type template[V any] struct {
type templateData[V any] struct {
AppName string
LogoURL string
AppURL string
Data *V
}
@@ -138,6 +139,7 @@ func send[V any](ctx context.Context, module *Module, dbConfig *appconfig.AppCon
templateData := &templateData[V]{
AppName: dbConfig.AppName.String(),
LogoURL: common.EnvConfig.AppURL + "/api/application-images/email",
AppURL: common.EnvConfig.AppURL,
Data: data,
}
+1 -1
View File
@@ -67,7 +67,7 @@ func TestModuleSendsEveryEmailType(t *testing.T) {
{
name: "email verification",
subject: "Verify your Pocket ID Test email address",
bodyContains: []string{"EMAIL VERIFICATION", "Hello Test User", "https://id.example.test/verify-token"},
bodyContains: []string{"VERIFY YOUR EMAIL ADDRESS", "Hello Test User", "https://id.example.test/verify-token"},
send: func(ctx context.Context, config *appconfig.AppConfigModel) error {
return module.SendEmailVerification(ctx, config, user.FullName(), userEmail, "https://id.example.test/verify-token")
},
@@ -194,6 +194,22 @@ func writeErrorResponse(c *gin.Context, classified classifiedError, requestID st
func logRequestError(c *gin.Context, err error, classified classifiedError, requestID string) {
if classified.status < http.StatusInternalServerError {
cause := errors.Unwrap(err)
if cause == nil {
return
}
slog.DebugContext(c.Request.Context(), "Request rejected",
slog.String("error_code", string(classified.code)),
slog.String("error_type", errorTypeName(err)),
slog.String("cause_type", errorTypeName(cause)),
slog.Int("http_status", classified.status),
slog.String("request_id", requestID),
slog.String("http_method", c.Request.Method),
slog.String("http_path", c.Request.URL.Path),
slog.Any("error", err),
slog.Any("cause", cause),
)
return
}
+17
View File
@@ -281,6 +281,7 @@ func (s *Service) VerifyLogin(ctx context.Context, dbConfig *appconfig.AppConfig
Extensions: storedSession.Extensions,
CredParams: storedSession.CredentialParams,
}
discardUnrequestedFalseAppIDOutput(session.Extensions, credentialAssertionData)
var user *model.User
_, err := s.webAuthn.ValidateDiscoverableLogin(func(_, userHandle []byte) (gowebauthn.User, error) {
@@ -535,6 +536,7 @@ func (s *Service) CreateReauthenticationTokenWithWebauthn(ctx context.Context, s
Extensions: storedSession.Extensions,
CredParams: storedSession.CredentialParams,
}
discardUnrequestedFalseAppIDOutput(session.Extensions, credentialAssertionData)
// Validate the credential assertion
var user *model.User
@@ -589,6 +591,21 @@ func classifyPasskeyError(err error, fallback func(error) *apperror.Error) *appe
return fallback(err)
}
func discardUnrequestedFalseAppIDOutput(session protocol.SessionExtensions, credential *protocol.ParsedCredentialAssertionData) {
if credential == nil || credential.ClientExtensionResults.AppID == nil || *credential.ClientExtensionResults.AppID {
return
}
for _, requested := range session.Requested {
if requested == protocol.ExtensionAppID {
return
}
}
// Safari reports appid=false for security keys even when the relying party did not request the legacy extension
credential.ClientExtensionResults.AppID = nil
}
func (s *Service) ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error) {
hashedToken := utils.CreateSha256Hash(token)
var reauthToken ReauthenticationToken
+60
View File
@@ -263,6 +263,66 @@ func TestClassifyPasskeyErrorPreservesStructuredLookupFailure(t *testing.T) {
require.ErrorIs(t, err, cause)
}
func TestDiscardUnrequestedFalseAppIDOutput(t *testing.T) {
tests := []struct {
name string
requested []string
appID bool
extra map[string]any
wantAppID *bool
wantError string
}{
{
name: "unrequested false appid is discarded",
appID: false,
wantAppID: nil,
},
{
name: "requested false appid is preserved",
requested: []string{protocol.ExtensionAppID},
appID: false,
wantAppID: new(false),
},
{
name: "unrequested true appid is rejected",
appID: true,
wantAppID: new(true),
wantError: "appid",
},
{
name: "other unsolicited output is rejected",
appID: false,
extra: map[string]any{"example": true},
wantAppID: nil,
wantError: "example",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
credential := &protocol.ParsedCredentialAssertionData{
ParsedPublicKeyCredential: protocol.ParsedPublicKeyCredential{
ClientExtensionResults: protocol.AuthenticationExtensionsClientOutputs{
AppID: new(tc.appID),
Extra: tc.extra,
},
},
}
session := protocol.SessionExtensions{Requested: tc.requested}
discardUnrequestedFalseAppIDOutput(session, credential)
assert.Equal(t, tc.wantAppID, credential.ClientExtensionResults.AppID)
err := credential.ClientExtensionResults.Verify(session, protocol.AssertCeremony, protocol.UnsolicitedOutputPolicyReject)
if tc.wantError == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.wantError)
}
})
}
}
func TestWebAuthnManagementOperationsReturnSpecificNotFoundErrors(t *testing.T) {
service, err := newService(Dependencies{
DB: testutils.NewDatabaseForTest(t),
File diff suppressed because one or more lines are too long
@@ -1,11 +1,7 @@
{{define "root"}}{{.AppName}}
API KEY EXPIRING SOON
Warning
{{define "root"}}API KEY EXPIRING SOON
Hello {{.Data.Name}},
This is a reminder that your API key {{.Data.ApiKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}.
Please generate a new API key if you need continued access.{{end}}
Your API key {{.Data.ApiKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}. Anything that uses this key will stop working once it expires.
To keep access, create a new API key in your {{.AppName}} account settings before then.{{end}}
File diff suppressed because one or more lines are too long
@@ -1,10 +1,10 @@
{{define "root"}}{{.AppName}}
EMAIL VERIFICATION
{{define "root"}}VERIFY YOUR EMAIL ADDRESS
Hello {{.Data.UserFullName}},
Click the button below to verify your email address for {{.AppName}}. This link will expire in 24 hours.
Click the button below to confirm the email address for your {{.AppName}} account. This link expires in 24 hours.
Verify {{.Data.VerificationLink}}{{end}}
Verify email address {{.Data.VerificationLink}}
Or if you don't like clicking buttons, open this link:
{{.Data.VerificationLink}}{{end}}
File diff suppressed because one or more lines are too long
@@ -1,26 +1,17 @@
{{define "root"}}{{.AppName}}
{{define "root"}}NEW SIGN-IN DETECTED
Your {{.AppName}} account was recently accessed from a new IP address or browser. If this was you, no further action is needed.
NEW SIGN-IN DETECTED
Warning
Your {{.AppName}} account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.
DETAILS
Approximate Location
Approximate location
{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}
IP Address
IP address
{{.Data.IPAddress}}
Device
{{.Data.Device}}
Sign-In Time
Time
{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}
{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}{{end}}
If you don't recognize this activity, review the passkeys in your {{.AppName}} account settings and remove any you don't recognize.{{end}}
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
{{define "root"}}{{.AppName}}
{{define "root"}}YOUR LOGIN CODE
Use the code below to sign in to {{.AppName}}. It expires in {{.Data.ExpirationString}}.
YOUR LOGIN CODE
{{.Data.Code}}
Click the button below to sign in to {{.AppName}} with a login code.
Or visit {{.Data.LoginLink}} and enter the code {{.Data.Code}}.
Sign in {{.Data.LoginLinkWithCode}}
This code expires in {{.Data.ExpirationString}}.
Sign In {{.Data.LoginLinkWithCode}}{{end}}
Or open {{.Data.LoginLink}} and enter the code manually.{{end}}
File diff suppressed because one or more lines are too long
@@ -1,6 +1,3 @@
{{define "root"}}{{.AppName}}
TEST EMAIL
{{define "root"}}TEST EMAIL
Your email setup is working correctly!{{end}}
+1
View File
@@ -10,6 +10,7 @@ ARG TARGETVARIANT
WORKDIR /app
COPY linux/${TARGETARCH}/${TARGETVARIANT}/pocket-id /app/pocket-id
COPY ./.tmp/frontend.cdx.json /usr/share/doc/pocket-id/frontend.cdx.json
EXPOSE 1411
ENV APP_ENV=production
+1
View File
@@ -13,6 +13,7 @@ RUN apk add --no-cache su-exec
COPY linux/${TARGETARCH}/${TARGETVARIANT}/pocket-id /app/pocket-id
COPY ./scripts/docker /app/docker
COPY ./.tmp/frontend.cdx.json /usr/share/doc/pocket-id/frontend.cdx.json
EXPOSE 1411
ENV APP_ENV=production
+5 -4
View File
@@ -1,4 +1,4 @@
import { render } from "@react-email/components";
import { render } from "react-email";
import * as fs from "node:fs";
import * as path from "node:path";
@@ -17,9 +17,10 @@ async function buildTemplateFile(
templateName: string,
isPlainText: boolean
) {
const rendered = await render(Component(Component.TemplateProps), {
plainText: isPlainText,
});
const rendered = await render(
Component(Component.TemplateProps),
isPlainText ? { plainText: true } : {},
);
// Normalize quotes
const normalized = rendered.replace(/&quot;/g, '"');
+87 -49
View File
@@ -5,77 +5,115 @@ import {
Head,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from "@react-email/components";
} from "react-email";
import type { SharedProps } from "../props";
import { colors, fonts, radius } from "./theme";
interface BaseTemplateProps {
logoURL?: string;
appName: string;
interface BaseTemplateProps extends SharedProps {
preview?: string;
children: React.ReactNode;
}
export const BaseTemplate = ({
logoURL,
appName,
appURL,
preview,
children,
}: BaseTemplateProps) => {
return (
<Html>
<Head />
<Body style={mainStyle}>
<Container style={{ width: "500px", margin: "0 auto" }}>
<Section>
<Row
align="left"
style={{
marginBottom: "16px",
}}
>
<Column style={{ width: "50px" }}>
}: BaseTemplateProps) => (
<Html lang="en">
<Head>
<meta name="color-scheme" content="light" />
<meta name="supported-color-schemes" content="light" />
<style dangerouslySetInnerHTML={{ __html: fontFaceCss(appURL) }} />
</Head>
{preview && <Preview>{preview}</Preview>}
<Body style={bodyStyle}>
<Container style={containerStyle}>
<Section style={headerStyle} data-skip-in-text="true">
<Row>
<Column style={logoColumnStyle}>
<Link href={appURL}>
<Img
src={logoURL}
width="32"
height="32"
width="28"
height="28"
alt={appName}
style={logoStyle}
/>
</Column>
<Column>
<Text style={titleStyle}>{appName}</Text>
</Column>
</Row>
</Section>
<div style={content}>{children}</div>
</Container>
</Body>
</Html>
);
</Link>
</Column>
<Column>
<Link href={appURL} style={appNameStyle}>
{appName}
</Link>
</Column>
</Row>
</Section>
<Section>
<Row>
<Column style={cardStyle}>{children}</Column>
</Row>
</Section>
</Container>
</Body>
</Html>
);
// The heading font is self-hosted by the app, so it loads from the same origin as the logo and works without third-party requests
// Clients without web font support fall back to the serif stack declared on the headings
const fontFaceCss = (appURL: string) =>
`@font-face{font-family:'Gloock';font-style:normal;font-weight:400;mso-font-alt:'Georgia';src:url(${appURL}/fonts/Gloock-Regular.woff) format('woff');}`;
const bodyStyle = {
margin: 0,
padding: "32px 16px",
backgroundColor: colors.background,
fontFamily: fonts.sans,
color: colors.text,
};
const mainStyle = {
padding: "50px",
backgroundColor: "#FBFBFB",
fontFamily: "Arial, sans-serif",
const containerStyle = {
width: "100%",
maxWidth: "480px",
margin: "0 auto",
};
const logoStyle = {
width: "32px",
height: "32px",
const headerStyle = {
marginBottom: "20px",
};
const logoColumnStyle = {
width: "36px",
verticalAlign: "middle",
};
const titleStyle = {
fontSize: "23px",
fontWeight: "bold",
margin: "0",
padding: "0",
const logoStyle = {
display: "block",
width: "28px",
height: "28px",
borderRadius: "6px",
};
const content = {
backgroundColor: "white",
padding: "24px",
borderRadius: "10px",
boxShadow: "0 1px 4px 0px rgba(0, 0, 0, 0.1)",
const appNameStyle = {
fontFamily: fonts.serif,
fontSize: "20px",
lineHeight: "28px",
color: colors.foreground,
textDecoration: "none",
};
const cardStyle = {
backgroundColor: colors.card,
border: `1px solid ${colors.border}`,
borderRadius: radius.card,
padding: "32px",
textAlign: "left" as const,
};
+24 -24
View File
@@ -1,33 +1,33 @@
import { Button as EmailButton } from "@react-email/components";
import { Button as EmailButton, Section } from "react-email";
import { colors, fonts, radius } from "./theme";
interface ButtonProps {
href: string;
children: React.ReactNode;
style?: React.CSSProperties;
}
export const Button = ({ href, children, style = {} }: ButtonProps) => {
const buttonStyle = {
backgroundColor: "#000000",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "4px",
fontSize: "15px",
fontWeight: "500",
cursor: "pointer",
marginTop: "10px",
...style,
};
export const Button = ({ href, children }: ButtonProps) => (
<Section style={containerStyle}>
<EmailButton href={href} style={buttonStyle}>
{children}
</EmailButton>
</Section>
);
return (
<div style={buttonContainer}>
<EmailButton style={buttonStyle} href={href}>
{children}
</EmailButton>
</div>
);
};
const buttonContainer = {
const containerStyle = {
margin: "24px 0 8px 0",
textAlign: "center" as const,
};
const buttonStyle = {
display: "inline-block",
padding: "12px 28px",
backgroundColor: colors.primary,
color: colors.primaryForeground,
borderRadius: radius.pill,
fontFamily: fonts.sans,
fontSize: "14px",
lineHeight: "20px",
fontWeight: 500,
textDecoration: "none",
};
+16 -30
View File
@@ -1,38 +1,24 @@
import { Column, Heading, Row, Text } from "@react-email/components";
import { Heading } from "react-email";
import { colors, fonts } from "./theme";
export default function CardHeader({
title,
warning,
}: {
interface CardHeaderProps {
title: string;
warning?: boolean;
}) {
}
export default function CardHeader({ title }: CardHeaderProps) {
return (
<Row>
<Column>
<Heading as="h1" style={titleStyle}>
{title}
</Heading>
</Column>
<Column align="right">
{warning && <Text style={warningStyle}>Warning</Text>}
</Column>
</Row>
<Heading as="h1" style={titleStyle}>
{title}
</Heading>
);
}
const titleStyle = {
fontSize: "20px",
fontWeight: "bold" as const,
margin: 0,
};
const warningStyle = {
backgroundColor: "#ffd966",
color: "#7f6000",
padding: "1px 12px",
borderRadius: "50px",
fontSize: "12px",
display: "inline-block",
margin: 0,
margin: "0 0 16px 0",
fontFamily: fonts.serif,
fontSize: "26px",
lineHeight: "32px",
fontWeight: 400,
letterSpacing: "-0.01em",
color: colors.foreground,
};
+39
View File
@@ -0,0 +1,39 @@
import { Column, Row, Section, Text } from "react-email";
import { colors, fonts, radius } from "./theme";
interface CodeBoxProps {
code: string;
}
export const CodeBox = ({ code }: CodeBoxProps) => (
<Section style={boxStyle}>
<Row>
<Column style={cellStyle}>
<Text style={codeStyle}>{code}</Text>
</Column>
</Row>
</Section>
);
const boxStyle = {
margin: "8px 0 0 0",
backgroundColor: colors.muted,
border: `1px solid ${colors.border}`,
borderRadius: radius.box,
};
const cellStyle = {
padding: "20px 16px",
textAlign: "center" as const,
};
const codeStyle = {
margin: 0,
fontFamily: fonts.mono,
fontSize: "32px",
lineHeight: "40px",
fontWeight: 600,
letterSpacing: "8px",
paddingLeft: "8px",
color: colors.foreground,
};
@@ -0,0 +1,54 @@
import { Column, Row, Section } from "react-email";
import { colors, radius } from "./theme";
interface DetailsListProps {
items: { label: string; value: string }[];
}
// A key-value list inside a muted box, used for sign-in details and similar metadata
// Each value sits below its label so long values never fight the label for width on narrow screens
// Every item is its own Row so the plain-text build separates entries with a blank line
export const DetailsList = ({ items }: DetailsListProps) => (
<Section style={boxStyle}>
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<Row key={item.label}>
<Column style={isLast ? cellStyle : { ...cellStyle, ...dividerStyle }}>
<span style={labelStyle}>{item.label}</span>
<br />
<span style={valueStyle}>{item.value}</span>
</Column>
</Row>
);
})}
</Section>
);
const boxStyle = {
margin: "8px 0 0 0",
backgroundColor: colors.muted,
border: `1px solid ${colors.border}`,
borderRadius: radius.box,
};
const cellStyle = {
padding: "12px 16px",
};
const labelStyle = {
fontSize: "12px",
lineHeight: "18px",
color: colors.mutedForeground,
};
const valueStyle = {
fontSize: "14px",
lineHeight: "20px",
fontWeight: 500,
color: colors.foreground,
};
const dividerStyle = {
borderBottom: `1px solid ${colors.border}`,
};
+29
View File
@@ -0,0 +1,29 @@
import { Text } from "react-email";
import { colors } from "./theme";
interface TextProps {
children: React.ReactNode;
style?: React.CSSProperties;
}
export const Paragraph = ({ children, style }: TextProps) => (
<Text style={{ ...paragraphStyle, ...style }}>{children}</Text>
);
export const Muted = ({ children, style }: TextProps) => (
<Text style={{ ...mutedStyle, ...style }}>{children}</Text>
);
const paragraphStyle = {
margin: "0 0 16px 0",
fontSize: "15px",
lineHeight: "24px",
color: colors.text,
};
const mutedStyle = {
margin: "16px 0 0 0",
fontSize: "13px",
lineHeight: "20px",
color: colors.mutedForeground,
};
+25
View File
@@ -0,0 +1,25 @@
// Design tokens mirrored from the frontend's app.css (light theme) so emails match the app
// Email clients have no CSS variable or oklch support, so the values are the resolved hex equivalents
export const colors = {
background: "#f5f5f5",
card: "#ffffff",
border: "#e5e5e5",
muted: "#fafafa",
foreground: "#0a0a0a",
text: "#404040",
mutedForeground: "#737373",
primary: "#171717",
primaryForeground: "#ffffff",
};
export const fonts = {
sans: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
serif: "Gloock, Georgia, 'Times New Roman', serif",
mono: "'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace",
};
export const radius = {
card: "24px",
box: "16px",
pill: "9999px",
};
@@ -1,7 +1,11 @@
import { Text } from "@react-email/components";
import { BaseTemplate } from "../components/base-template";
import CardHeader from "../components/card-header";
import { sharedPreviewProps, sharedTemplateProps } from "../props";
import { Muted, Paragraph } from "../components/text";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
interface ApiKeyExpiringData {
name: string;
@@ -9,28 +13,30 @@ interface ApiKeyExpiringData {
expiresAt: string;
}
interface ApiKeyExpiringEmailProps {
logoURL: string;
appName: string;
interface ApiKeyExpiringEmailProps extends SharedProps {
data: ApiKeyExpiringData;
}
export const ApiKeyExpiringEmail = ({
logoURL,
appName,
data,
...props
}: ApiKeyExpiringEmailProps) => (
<BaseTemplate logoURL={logoURL} appName={appName}>
<CardHeader title="API Key Expiring Soon" warning />
<Text>
Hello {data.name}, <br />
This is a reminder that your API key <strong>
{data.apiKeyName}
</strong>{" "}
will expire on <strong>{data.expiresAt}</strong>.
</Text>
<BaseTemplate
{...props}
preview={`Your API key ${data.apiKeyName} expires on ${data.expiresAt}`}
>
<CardHeader title="API key expiring soon" />
<Paragraph>Hello {data.name},</Paragraph>
<Paragraph>
Your API key <strong>{data.apiKeyName}</strong> will expire on{" "}
<strong>{data.expiresAt}</strong>. Anything that uses this key will stop
working once it expires.
</Paragraph>
<Text>Please generate a new API key if you need continued access.</Text>
<Muted>
To keep access, create a new API key in your {props.appName} account
settings before then.
</Muted>
</BaseTemplate>
);
@@ -48,8 +54,8 @@ ApiKeyExpiringEmail.TemplateProps = {
ApiKeyExpiringEmail.PreviewProps = {
...sharedPreviewProps,
data: {
name: "Elias Schneider",
apiKeyName: "My API Key",
expiresAt: "September 30, 2024",
name: "Elias",
apiKeyName: "CI deploy key",
expiresAt: "2026-01-30 12:00:00 UTC",
},
};
+35 -17
View File
@@ -1,41 +1,59 @@
import { Text } from "@react-email/components";
import { Link } from "react-email";
import { BaseTemplate } from "../components/base-template";
import { Button } from "../components/button";
import CardHeader from "../components/card-header";
import { sharedPreviewProps, sharedTemplateProps } from "../props";
import { Muted, Paragraph } from "../components/text";
import { colors } from "../components/theme";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
interface EmailVerificationData {
userFullName: string;
verificationLink: string;
}
interface EmailVerificationProps {
logoURL: string;
appName: string;
interface EmailVerificationProps extends SharedProps {
data: EmailVerificationData;
}
export const EmailVerification = ({
logoURL,
appName,
data,
...props
}: EmailVerificationProps) => (
<BaseTemplate logoURL={logoURL} appName={appName}>
<CardHeader title="Email Verification" />
<BaseTemplate
{...props}
preview={`Confirm the email address for your ${props.appName} account`}
>
<CardHeader title="Verify your email address" />
<Paragraph>Hello {data.userFullName},</Paragraph>
<Paragraph>
Click the button below to confirm the email address for your{" "}
{props.appName} account. This link expires in 24 hours.
</Paragraph>
<Text>
Hello {data.userFullName}, <br />
Click the button below to verify your email address for {appName}. This
link will expire in 24 hours.
<Button href={data.verificationLink}>Verify email address</Button>
<Muted style={{ marginTop: "32px" }}>
Or if you don't like clicking buttons, open this link:
<br />
</Text>
<Button href={data.verificationLink}>Verify</Button>
<Link href={data.verificationLink} style={linkStyle}>
{data.verificationLink}
</Link>
</Muted>
</BaseTemplate>
);
export default EmailVerification;
const linkStyle = {
color: colors.mutedForeground,
textDecoration: "underline",
wordBreak: "break-all" as const,
};
EmailVerification.TemplateProps = {
...sharedTemplateProps,
data: {
@@ -49,6 +67,6 @@ EmailVerification.PreviewProps = {
data: {
userFullName: "Tim Cook",
verificationLink:
"https://localhost:1411/user/verify-email?code=abcdefg12345",
"https://id.example.com/user/verify-email?code=abcdefg12345",
},
};
@@ -1,7 +1,12 @@
import { Column, Heading, Row, Text } from "@react-email/components";
import { BaseTemplate } from "../components/base-template";
import CardHeader from "../components/card-header";
import { sharedPreviewProps, sharedTemplateProps } from "../props";
import { DetailsList } from "../components/details-list";
import { Muted, Paragraph } from "../components/text";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
interface SignInData {
location: string;
@@ -10,78 +15,44 @@ interface SignInData {
dateTime: string;
}
interface NewSignInEmailProps {
logoURL: string;
appName: string;
interface NewSignInEmailProps extends SharedProps {
data: SignInData;
}
export const NewSignInEmail = ({
logoURL,
appName,
data,
}: NewSignInEmailProps) => (
<BaseTemplate logoURL={logoURL} appName={appName}>
<CardHeader title="New Sign-In Detected" warning />
<Text>
Your {appName} account was recently accessed from a new IP address or
browser. If you recognize this activity, no further action is required.
</Text>
<Heading
style={{
fontSize: "1rem",
fontWeight: "bold",
margin: "30px 0 10px 0",
}}
as="h4"
>
Details
</Heading>
export const NewSignInEmail = ({ data, ...props }: NewSignInEmailProps) => (
<BaseTemplate
{...props}
preview={`A new sign-in to your ${props.appName} account was detected`}
>
<CardHeader title="New sign-in detected" />
<Paragraph>
Your {props.appName} account was recently accessed from a new IP address
or browser. If this was you, no further action is needed.
</Paragraph>
<Row>
<Column style={detailsBoxStyle}>
<Text style={detailsLabelStyle}>Approximate Location</Text>
<Text style={detailsBoxValueStyle}>{data.location}</Text>
</Column>
<Column style={detailsBoxStyle}>
<Text style={detailsLabelStyle}>IP Address</Text>
<Text style={detailsBoxValueStyle}>{data.ipAddress}</Text>
</Column>
</Row>
<DetailsList
items={[
{ label: "Approximate location", value: data.location },
{ label: "IP address", value: data.ipAddress },
{ label: "Device", value: data.device },
{ label: "Time", value: data.dateTime },
]}
/>
<Row style={{ marginTop: "10px" }}>
<Column style={detailsBoxStyle}>
<Text style={detailsLabelStyle}>Device</Text>
<Text style={detailsBoxValueStyle}>{data.device}</Text>
</Column>
<Column style={detailsBoxStyle}>
<Text style={detailsLabelStyle}>Sign-In Time</Text>
<Text style={detailsBoxValueStyle}>{data.dateTime}</Text>
</Column>
</Row>
<Muted>
If you don't recognize this activity, review the passkeys in your{" "}
{props.appName} account settings and remove any you don't recognize.
</Muted>
</BaseTemplate>
);
export default NewSignInEmail;
const detailsBoxStyle = {
width: "225px",
};
const detailsLabelStyle = {
margin: 0,
fontSize: "12px",
color: "gray",
};
const detailsBoxValueStyle = {
margin: 0,
};
NewSignInEmail.TemplateProps = {
...sharedTemplateProps,
data: {
location: "{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}",
location:
"{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}",
ipAddress: "{{.Data.IPAddress}}",
device: "{{.Data.Device}}",
dateTime: '{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}',
@@ -92,8 +63,8 @@ NewSignInEmail.PreviewProps = {
...sharedPreviewProps,
data: {
location: "San Francisco, USA",
ipAddress: "127.0.0.1",
ipAddress: "203.0.113.42",
device: "Chrome on macOS",
dateTime: "2024-01-01 12:00 PM UTC",
dateTime: "January 2, 2026 at 3:04 PM UTC",
},
};
+32 -24
View File
@@ -1,8 +1,15 @@
import { Link, Text } from "@react-email/components";
import { Link } from "react-email";
import { BaseTemplate } from "../components/base-template";
import { Button } from "../components/button";
import CardHeader from "../components/card-header";
import { sharedPreviewProps, sharedTemplateProps } from "../props";
import { CodeBox } from "../components/code-box";
import { Muted, Paragraph } from "../components/text";
import { colors } from "../components/theme";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
interface OneTimeAccessData {
code: string;
@@ -11,43 +18,44 @@ interface OneTimeAccessData {
expirationString: string;
}
interface OneTimeAccessEmailProps {
logoURL: string;
appName: string;
interface OneTimeAccessEmailProps extends SharedProps {
data: OneTimeAccessData;
}
export const OneTimeAccessEmail = ({
logoURL,
appName,
data,
...props
}: OneTimeAccessEmailProps) => (
<BaseTemplate logoURL={logoURL} appName={appName}>
<CardHeader title="Your Login Code" />
<BaseTemplate
{...props}
preview={`Your ${props.appName} login code is ${data.code}`}
>
<CardHeader title="Your login code" />
<Paragraph>
Use the code below to sign in to {props.appName}. It expires in{" "}
{data.expirationString}.
</Paragraph>
<Text>
Click the button below to sign in to {appName} with a login code.
<br />
Or visit{" "}
<CodeBox code={data.code} />
<Button href={data.buttonCodeLink}>Sign in</Button>
<Muted style={{ marginTop: "32px" }}>
Or open{" "}
<Link href={data.loginLink} style={linkStyle}>
{data.loginLink}
</Link>{" "}
and enter the code <strong>{data.code}</strong>.
<br />
<br />
This code expires in {data.expirationString}.
</Text>
<Button href={data.buttonCodeLink}>Sign In</Button>
and enter the code manually.
</Muted>
</BaseTemplate>
);
export default OneTimeAccessEmail;
const linkStyle = {
color: "#000",
color: colors.mutedForeground,
textDecoration: "underline",
fontFamily: "Arial, sans-serif",
wordBreak: "break-all" as const,
};
OneTimeAccessEmail.TemplateProps = {
@@ -64,8 +72,8 @@ OneTimeAccessEmail.PreviewProps = {
...sharedPreviewProps,
data: {
code: "123456",
loginLink: "https://example.com/login",
buttonCodeLink: "https://example.com/login?code=123456",
loginLink: "https://id.example.com/lc",
buttonCodeLink: "https://id.example.com/lc/123456",
expirationString: "15 minutes",
},
};
+10 -11
View File
@@ -1,17 +1,16 @@
import { Text } from "@react-email/components";
import { BaseTemplate } from "../components/base-template";
import CardHeader from "../components/card-header";
import { sharedPreviewProps, sharedTemplateProps } from "../props";
import { Paragraph } from "../components/text";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
interface TestEmailProps {
logoURL: string;
appName: string;
}
export const TestEmail = ({ logoURL, appName }: TestEmailProps) => (
<BaseTemplate logoURL={logoURL} appName={appName}>
<CardHeader title="Test Email" />
<Text>Your email setup is working correctly!</Text>
export const TestEmail = (props: SharedProps) => (
<BaseTemplate {...props} preview="Your email setup is working correctly">
<CardHeader title="Test email" />
<Paragraph>Your email setup is working correctly!</Paragraph>
</BaseTemplate>
);
+8 -8
View File
@@ -9,16 +9,16 @@
"export": "email export"
},
"dependencies": {
"@react-email/components": "1.0.12",
"react": "^19.2.6",
"react-dom": "^19.2.6"
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-email": "6.9.5"
},
"devDependencies": {
"@react-email/preview-server": "5.2.10",
"@types/node": "^25.9.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"react-email": "6.9.5",
"tsx": "^4.22.2"
"@react-email/ui": "6.9.5",
"@types/node": "^26.6.2",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"tsx": "^4.23.13"
}
}
+10 -2
View File
@@ -1,9 +1,17 @@
export const sharedPreviewProps = {
export interface SharedProps {
logoURL: string;
appName: string;
appURL: string;
}
export const sharedPreviewProps: SharedProps = {
logoURL: "https://pocket-id.org/img/logo.png",
appName: "Pocket ID",
appURL: "https://id.example.com",
};
export const sharedTemplateProps = {
export const sharedTemplateProps: SharedProps = {
logoURL: "{{.LogoURL}}",
appName: "{{.AppName}}",
appURL: "{{.AppURL}}",
};
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pocket-id-frontend",
"version": "2.15.0",
"version": "2.16.0",
"private": true,
"type": "module",
"scripts": {
@@ -53,6 +53,8 @@
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.5.2",
"prettier-plugin-tailwindcss": "^0.8.0",
"rollup": "^4.63.3",
"rollup-plugin-sbom": "4.0.0",
"shadcn-svelte": "^1.3.0",
"svelte": "^5.55.8",
"svelte-check": "^4.4.8",
+11 -1
View File
@@ -1,13 +1,23 @@
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import sbom from 'rollup-plugin-sbom';
import { defineConfig, type Plugin } from 'vite';
export default defineConfig(() => {
const frontendSbom = sbom({
includeWellKnown: false,
outDir: 'cyclonedx',
outFilename: 'frontend.cdx',
saveTimestamp: false
}) as Plugin;
frontendSbom.applyToEnvironment = (environment) => environment.name === 'client';
return {
plugins: [
sveltekit(),
tailwindcss(),
frontendSbom,
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide',
+722 -328
View File
File diff suppressed because it is too large Load Diff
-121
View File
@@ -1,121 +0,0 @@
#!/bin/bash
# Check if the script is being run from the root of the project
if [ ! -f .version ] || [ ! -f frontend/package.json ] || [ ! -f CHANGELOG.md ]; then
echo "Error: This script must be run from the root of the project."
exit 1
fi
# Check if git cliff is installed
if ! command -v git-cliff &>/dev/null; then
echo "Error: git cliff is not installed. Please install it from https://git-cliff.org/docs/installation."
exit 1
fi
# Check if GitHub CLI is installed
if ! command -v gh &>/dev/null; then
echo "Error: GitHub CLI (gh) is not installed. Please install it and authenticate using 'gh auth login'."
exit 1
fi
# Check if Snyk CLI is installed
if ! command -v snyk &>/dev/null; then
echo "Error: Snyk CLI is not installed. Please install it and authenticate using 'snyk auth'."
exit 1
fi
# Check if we're on the main branch
if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then
echo "Error: This script must be run on the main branch."
exit 1
fi
# Parse command line arguments
FORCE_MAJOR=false
for arg in "$@"; do
case $arg in
--major)
FORCE_MAJOR=true
shift
;;
*)
# Unknown option
;;
esac
done
BUMP_ARGUMENTS=(--bumped-version --unreleased --offline)
if [ "$FORCE_MAJOR" == true ]; then
BUMP_ARGUMENTS+=(--bump major)
fi
# Calculate the next version from the unreleased conventional commits
if ! NEW_VERSION=$(git cliff "${BUMP_ARGUMENTS[@]}"); then
echo "Error: Could not calculate the next version."
exit 1
fi
NEW_VERSION=${NEW_VERSION#v}
if [ "$NEW_VERSION" == "$(cat .version)" ]; then
echo "No commits requiring a version bump found since the latest release. No new release will be created."
exit 0
fi
echo "Running Snyk dependency scan..."
if ! snyk test --all-projects --dev --detection-depth=3 --strict-out-of-sync=false --severity-threshold=high; then
echo "Error: Snyk detected high-severity vulnerable dependencies. Release creation aborted."
exit 1
fi
# Confirm release creation
read -p "This will create a new release with version $NEW_VERSION. Do you want to proceed? (y/n) " CONFIRM
if [[ "$CONFIRM" != "y" ]]; then
echo "Release process canceled."
exit 1
fi
# Update the .version file with the new version
echo $NEW_VERSION >.version
git add .version
# Update version in frontend/package.json
jq --arg new_version "$NEW_VERSION" '.version = $new_version' frontend/package.json >frontend/package_tmp.json && mv frontend/package_tmp.json frontend/package.json
pnpm --dir frontend exec prettier --write package.json
git add frontend/package.json
# Generate changelog
echo "Generating changelog..."
git cliff --github-token=$(gh auth token) --prepend CHANGELOG.md --tag "v$NEW_VERSION" --unreleased
git add CHANGELOG.md
# Commit the changes with the new version
git commit -m "release: $NEW_VERSION"
# Create a Git tag with the new version
git tag "v$NEW_VERSION"
# Push the commit and the tag to the repository
git push
git push --tags
# Extract the changelog content for the latest release
echo "Extracting changelog content for version $NEW_VERSION..."
CHANGELOG=$(awk '/^## v[0-9]/ { if (found) exit; found=1; next } found' CHANGELOG.md)
if [ -z "$CHANGELOG" ]; then
echo "Error: Could not extract changelog for version $NEW_VERSION."
exit 1
fi
# Create the release on GitHub
echo "Creating GitHub release..."
gh release create "v$NEW_VERSION" --title "v$NEW_VERSION" --notes "$CHANGELOG" --draft
if [ $? -eq 0 ]; then
echo "GitHub release created successfully."
else
echo "Error: Failed to create GitHub release."
exit 1
fi
echo "Release process complete. New version: $NEW_VERSION"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
set -eu
artifact_path=$1
document_path=$2
source_name=$3
source_version=$4
frontend_sbom_path="../.tmp/frontend.cdx.json"
work_dir=$(mktemp -d "${TMPDIR:-/tmp}/pocket-id-binary-sbom.XXXXXX")
trap 'rm -rf "$work_dir"' EXIT
cp "$artifact_path" "$work_dir/$(basename "$artifact_path")"
cp "$frontend_sbom_path" "$work_dir/frontend.cdx.json"
syft "dir:$work_dir" \
--select-catalogers "+sbom-cataloger" \
--source-name "$source_name" \
--source-version "$source_version" \
--output "spdx-json=$document_path" \
--enrich all