Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
183ba8f
ci: setup infrastructure using alchemy
edwintantawi Dec 23, 2025
7233a95
ci(env): add validation schema on build
edwintantawi Dec 24, 2025
40ebe07
refactor(alchemy): group config by stages
edwintantawi Dec 25, 2025
626fbc7
feat(alchemy): add hostname for custom domain
edwintantawi Dec 25, 2025
9b67051
ci: add production and staging deployment workflow
edwintantawi Dec 25, 2025
1d8f776
ci: use alchemy scripts instead do dlx
edwintantawi Dec 25, 2025
b3e2f74
style: remove unused comment
edwintantawi Dec 25, 2025
b367a58
ci: test deployment workflow
edwintantawi Dec 26, 2025
aadc383
refactor: use base worker props
edwintantawi Dec 26, 2025
75311f7
refactor: group worker config by stage
edwintantawi Dec 26, 2025
5bf9f4d
ci: trigger deployment on staging branch
edwintantawi Dec 26, 2025
440c881
ci: adjust alchemy configuration and deployment workflows
edwintantawi Dec 27, 2025
e3093e4
ci: add missing hostname vars
edwintantawi Dec 27, 2025
f17f114
ci: add github comment on preview deployment
edwintantawi Dec 27, 2025
d630b8e
refactor: rename staging into preview
edwintantawi Dec 27, 2025
9e2e267
feat: add emoji for sub comment preview
edwintantawi Dec 27, 2025
69bf869
refactor: use data form package json
edwintantawi Dec 27, 2025
5a46993
fix(ci): missing attribute importing json file
edwintantawi Dec 27, 2025
a54f77a
refactor(ci): remove github comment fn
edwintantawi Dec 27, 2025
de48646
feat: add no empty validation for alchemy string env
edwintantawi Dec 28, 2025
975a6c1
chore(formatter): migrate prettier to oxfmt
edwintantawi Dec 28, 2025
b8ef08b
chore(linter): migrate eslint to oxlint
edwintantawi Jan 4, 2026
9d4b8b7
ci: only run pull request event for release branch
edwintantawi Jan 4, 2026
1af528b
ci(test): run pull request on branch main
edwintantawi Jan 4, 2026
990765c
ci(test): run pull request on ci/deployment branch
edwintantawi Jan 4, 2026
475f1fa
ci(test): maybe the branch name is invalid
edwintantawi Jan 4, 2026
1fb39f8
ci(test): remove pull request branches filter
edwintantawi Jan 4, 2026
cbb59ac
Merge branch 'main' of github.com:devsantara/kit into ci/deployment
edwintantawi Jan 4, 2026
f6c84ae
ci: add pull request branches filter for release branch
edwintantawi Jan 4, 2026
1aab86c
ci: missing hostname env on clenup job
edwintantawi Jan 4, 2026
bbb867e
ci: remove pull request branch filter
edwintantawi Jan 4, 2026
22a01c7
style: fix linter error
edwintantawi Jan 4, 2026
adc719e
ci: add pull request number on the github comment
edwintantawi Jan 4, 2026
85556fa
ci: add concurrency group based on pr number or github ref
edwintantawi Jan 4, 2026
72f27cf
ci: rename workflow name into deployment
edwintantawi Jan 4, 2026
6bdd4c6
revert: concurrency group back to github ref
edwintantawi Jan 4, 2026
33b93cb
chore(deps): add esbuild, sharp and workerd in built deps
edwintantawi Jan 4, 2026
e9954eb
ci: rename concurrency group name
edwintantawi Jan 5, 2026
6321b23
ci: filter pull request for branch main
edwintantawi Jan 5, 2026
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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Alchemy (required when using alchemy)
# Generate both the secret and the state token using `openssl rand -base64 32`.
ALCHEMY_SECRET=<***>
ALCHEMY_STATE_TOKEN=<***>
HOSTNAME=<devsantara.com | kit.devsantara.com | "your.domain">

# Cloudflare (required for deployment workflows)
# See https://alchemy.run/guides/cloudflare#api-token
CLOUDFLARE_API_TOKEN=<***>
# Your Cloudflare email
CLOUDFLARE_EMAIL=<***>
88 changes: 88 additions & 0 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Deployment
on:
push:
branches:
- main
pull_request:
branches:
- main
types:
- opened
- reopened
- synchronize
- closed
concurrency:
group: deployment-${{ github.ref }}
cancel-in-progress: false
env:
NODE_VERSION: '24.11.1'
PNPM_VERSION: '10.23.0'
STAGE: ${{ github.event_name == 'pull_request' && format('preview-{0}', github.event.number) || (github.ref == 'refs/heads/main' && 'production' || github.ref_name) }}
jobs:
deploy:
if: ${{ github.event.action != 'closed' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Deploy
run: pnpm alchemy deploy --stage ${{ env.STAGE }}
env:
HOSTNAME: ${{ vars.HOSTNAME }}
ALCHEMY_SECRET: ${{ secrets.ALCHEMY_SECRET }}
ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
PULL_REQUEST: ${{ github.event.number }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cleanup:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' }}
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Safety Check
run: |-
if [ "${{ env.STAGE }}" = "production" ]; then
echo "ERROR: Cannot destroy production environment in cleanup job"
exit 1
fi
- name: Destroy Preview Environment
run: pnpm alchemy destroy --stage ${{ env.STAGE }}
env:
HOSTNAME: ${{ vars.HOSTNAME }}
ALCHEMY_SECRET: ${{ secrets.ALCHEMY_SECRET }}
ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
PULL_REQUEST: ${{ github.event.number }}
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ dist/
dist-ssr/
storybook-static/

# Infrastructures
.alchemy/
.wrangler/

# Local files
*.local

Expand All @@ -20,9 +24,6 @@ logs
.pnpm-debug.log*
*storybook.log

# Cache
.eslintcache

# Misc
.DS_Store
*.pem
Expand Down
19 changes: 19 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "all",
"endOfLine": "lf",
"objectWrap": "preserve",
"bracketSpacing": true,
"bracketSameLine": false,
"singleAttributePerLine": false,
"arrowParens": "always",
"ignorePatterns": ["pnpm-lock.yaml", "routeTree.gen.ts", "public/"],
"experimentalSortPackageJson": true,
"experimentalSortImports": { "order": "asc" }
}
100 changes: 100 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": ["pnpm-lock.yaml", "routeTree.gen.ts", "public/"],
"env": {
"browser": true,
"node": true
},
"categories": {
"correctness": "error",
"suspicious": "error"
},
"plugins": [
"oxc",
"eslint",
"unicorn",
"typescript",
"promise",
"react",
"jsx-a11y",
"react-perf",
"import"
],
"rules": {
// Perf
"no-await-in-loop": "error",
"no-useless-call": "error",
"oxc/no-accumulating-spread": "error",
"oxc/no-map-spread": "error",
"unicorn/prefer-array-find": "error",
"unicorn/prefer-array-flat-map": "error",
"unicorn/prefer-set-has": "error",

// Restriction
"default-case": ["error", { "commentPattern": "^skip\\sdefault" }],
"no-console": ["warn", { "allow": ["error", "warn", "info"] }],
"no-empty": "error",
"no-param-reassign": "error",
"no-restricted-globals": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/no-explicit-any": "error",
"typescript/no-non-null-asserted-nullish-coalescing": "error",
"typescript/no-non-null-assertion": "error",
"typescript/promise-function-async": "error",
"typescript/use-unknown-in-catch-callback-variable": "error",
"unicorn/prefer-node-protocol": "error",
"react/no-unknown-property": "error",
"react/button-has-type": "error",
"import/no-cycle": ["error", { "maxDepth": 3 }],

// Suspicious
"eslint/no-unneeded-ternary": "off",
"react/react-in-jsx-scope": "off",
"import/no-unassigned-import": ["error", { "allow": ["**/*.css"] }],

// Pedantic
"eqeqeq": ["error", "smart"],
"radix": "error",
"require-await": "error",
"typescript/no-unsafe-argument": "error",
"typescript/return-await": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-no-useless-fragment": "error",
"react/rules-of-hooks": "error",

// Style
"capitalized-comments": "warn",
"default-case-last": "error",
"default-param-last": "error",
"func-names": ["error", "as-needed", { "generators": "as-needed" }],
"max-params": ["error", { "max": 3 }],
"no-duplicate-imports": "error",
"no-implicit-coercion": "error",
"no-template-curly-in-string": "error",
"yoda": "error",
"typescript/consistent-type-definitions": ["error", "interface"],
"unicorn/filename-case": ["error", { "case": "kebabCase" }],
"react/self-closing-comp": "warn",
"import/no-duplicates": "error"
},
"overrides": [
{
// Only allow named export in whole source code
"files": ["src/**/*.{js,jsx,ts,tsx}"],
"plugins": ["import"],
"rules": {
"import/no-default-export": "error",
"import/prefer-default-export": "off"
}
},
{
// Only allow default export in reserved files
"files": ["src/server.ts"],
"plugins": ["import"],
"rules": {
"import/no-default-export": "off",
"import/prefer-default-export": "error"
}
}
]
}
15 changes: 0 additions & 15 deletions .prettierignore

This file was deleted.

6 changes: 1 addition & 5 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
{
"recommendations": [
"prettier.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss"
]
"recommendations": ["oxc.oxc-vscode", "bradlc.vscode-tailwindcss"]
}
17 changes: 6 additions & 11 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
// Editor
"editor.formatOnSave": true,
"editor.defaultFormatter": "prettier.prettier-vscode",
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
},

// Files
"files.associations": {
Expand All @@ -23,18 +26,10 @@
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.preferTypeOnlyAutoImports": true,

// Extension - Prettier
"prettier.enable": true,
"prettier.configPath": "prettier.config.js",

// Extension - ESLint
"eslint.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
// Extension - Oxc
"oxc.enable": true,

// Extension - Tailwind CSS
"tailwindCSS.lint.cssConflict": "ignore", // Avoid confusion with Eslint
"tailwindCSS.classAttributes": ["class", "className", "classNames"],
"tailwindCSS.classFunctions": ["clsx", "twMerge", "cn", "cva"]
}
16 changes: 16 additions & 0 deletions @types/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This file infers types for the cloudflare:workers environment from your Alchemy Worker.
// @see https://alchemy.run/concepts/bindings/#type-safe-bindings

import type { worker } from '../alchemy.run.ts';

export type CloudflareEnv = typeof worker.Env;

declare global {
type Env = CloudflareEnv;
}

declare module 'cloudflare:workers' {
namespace Cloudflare {
export interface Env extends CloudflareEnv {}
}
}
77 changes: 77 additions & 0 deletions alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Infrastructure provisioning with **Alchemy** for Cloudflare platform
*
* By default (when running locally) the stage will be your username ($USER, or $USERNAME on Windows).
* We can also specify a stage with the `--stage` flag.
*
* @example
* ```bash
* pnpm alchemy:deploy --stage production
* ```
*/

import alchemy, { type Scope } from 'alchemy';
import { TanStackStart } from 'alchemy/cloudflare';
import { GitHubComment } from 'alchemy/github';
import { CloudflareStateStore, FileSystemStateStore } from 'alchemy/state';

import packageJson from './package.json' with { type: 'json' };
import { alchemyEnv } from './src/lib/env/alchemy.ts';

const ALCHEMY_SECRET = alchemyEnv.ALCHEMY_SECRET;
const ALCHEMY_STATE_TOKEN = alchemy.secret(alchemyEnv.ALCHEMY_STATE_TOKEN);

function isProductionStage(scope: Scope) {
return scope.stage === 'production';
}
function isPreviewStage(scope: Scope) {
return scope.stage.startsWith('preview');
}

const app = await alchemy('kit', {
password: ALCHEMY_SECRET,
stateStore: (scope) => {
if (isProductionStage(scope) || isPreviewStage(scope)) {
return new CloudflareStateStore(scope, {
scriptName: 'alchemy-state-service',
stateToken: ALCHEMY_STATE_TOKEN,
});
}
return new FileSystemStateStore(scope);
},
});

export const worker = await TanStackStart('website', {
adopt: true,
observability: isProductionStage(app) ? { enabled: true } : undefined,
url: isProductionStage(app) ? false : true,
domains: isProductionStage(app) ? [alchemyEnv.HOSTNAME] : undefined,
placement: isProductionStage(app) ? { mode: 'smart' } : undefined,
bindings: {},
});

const workerDomain = worker.domains?.[0];
const workerUrl = workerDomain ? `https://${workerDomain.name}` : worker.url;
console.info({ worker: worker.name, url: workerUrl });

if (process.env.PULL_REQUEST) {
// If this is a PR, add a comment to the PR with the preview URL
// It will auto-update with each push
await GitHubComment('preview-comment', {
owner: packageJson.author,
repository: packageJson.name,
issueNumber: Number(process.env.PULL_REQUEST),
body: `## 🚀 Preview Deployment (${process.env.PULL_REQUEST})

Your changes have been deployed to a preview environment:

| Name | Preview URL | Commit | Updated (UTC) |
| :----------------- | :---------------------------- | :------------------------------------- | :------------------------------------------ |
| **${worker.name}** | [Visit Preview](${workerUrl}) | ${process.env.GITHUB_SHA?.slice(0, 7)} | ${new Date(worker.updatedAt).toUTCString()} |

---
<sub>🏗️ This comment updates automatically with each push.</sub>`,
});
}

await app.finalize();
Loading