diff --git a/.cursor/rules/read-copilot-instructions.mdc b/.cursor/rules/read-copilot-instructions.mdc deleted file mode 100644 index fdbbd686..00000000 --- a/.cursor/rules/read-copilot-instructions.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Read and follow .github/copilot-instructions.md for this repo -alwaysApply: true ---- - -Before assisting, open and read `.github/copilot-instructions.md`. -Follow it as the authoritative source for coding practices, docs style, security, and compliance for this project. - -If the file is missing, say so, ask to create it, and proceed with safe defaults. -When “Copilot” is mentioned in that file, interpret it as referring to **you (Cursor)**. -Summarize the relevant sections into your working context before you act. - diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 4d7a5bbf..60e5d7a4 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -8,7 +8,7 @@ Containers extension) or GitHub Codespaces and use that environment for all work The devcontainer starts one service named `app` and exposes: - **Port 4000:** Ruby app -- **Port 4001:** Astro dev server +- **Port 4001:** Vite dev server The repo is mounted at `/workspace`. Bundler gems are cached in a Docker volume to speed up future launches. @@ -29,9 +29,9 @@ network access is available. ## Common commands (run inside the container) ``` -make dev # Ruby + Astro +make dev # Ruby + Vite make dev-ruby # Ruby only -make dev-frontend # Astro only +make dev-frontend # frontend only make test # Ruby + frontend tests make ready # RuboCop + RSpec (pre-commit gate) ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c345b86..b46452db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,6 @@ jobs: - uses: ruby/setup-ruby@v1 with: - ruby-version-file: ".tool-versions" bundler-cache: true - name: Run RuboCop @@ -53,7 +52,6 @@ jobs: - uses: ruby/setup-ruby@v1 with: - ruby-version-file: ".tool-versions" bundler-cache: true - name: Setup Node.js for OpenAPI lint tooling @@ -106,42 +104,56 @@ jobs: - name: Run frontend smoke test run: npm run test:e2e - docker-test: + docker-build-smoke-image: needs: - hadolint - ruby - openapi - frontend runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Build Docker smoke image + run: docker build -t html2rss/web -f Dockerfile . + + - name: Export Docker smoke image + run: docker save html2rss/web -o /tmp/html2rss-web-smoke-image.tar + + - name: Upload Docker smoke image + uses: actions/upload-artifact@v4 + with: + name: docker-smoke-image + path: /tmp/html2rss-web-smoke-image.tar + retention-days: 1 + + docker-test: + needs: + - docker-build-smoke-image + runs-on: ubuntu-latest strategy: fail-fast: false matrix: smoke_auto_source_enabled: ["false", "true"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: - ruby-version-file: ".tool-versions" bundler-cache: true - - name: Setup Node.js for Docker smoke test - uses: actions/setup-node@v4 + - name: Download Docker smoke image + uses: actions/download-artifact@v4 with: - node-version-file: ".tool-versions" - cache: npm - cache-dependency-path: frontend/package-lock.json - - - name: Install frontend dependencies - run: npm ci - working-directory: frontend + name: docker-smoke-image + path: /tmp - - name: Build frontend static assets - run: npm run build - working-directory: frontend + - name: Load Docker smoke image + run: docker load -i /tmp/html2rss-web-smoke-image.tar - name: Run Docker smoke test env: + DOCKER_SMOKE_SKIP_BUILD: "true" SMOKE_AUTO_SOURCE_ENABLED: ${{ matrix.smoke_auto_source_enabled }} run: bundle exec rake diff --git a/.gitignore b/.gitignore index 33c9874e..cb7d66e5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,8 @@ /tmp/rack-cache-* -# Ignore Astro frontend build output +# Ignore frontend build output and tooling caches /frontend/dist/ -/frontend/.astro/ /frontend/node_modules/ /public/frontend /frontend/playwright-report/ @@ -51,4 +50,8 @@ # Frontend logs *.log +# macOS Finder metadata +.DS_Store + .yardoc +frontend/.astro diff --git a/.redocly.yaml b/.redocly.yaml new file mode 100644 index 00000000..030d1424 --- /dev/null +++ b/.redocly.yaml @@ -0,0 +1,5 @@ +extends: + - recommended + +rules: + operation-4xx-response: off diff --git a/.rubocop.yml b/.rubocop.yml index b9821c39..ee5e2c04 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,12 @@ AllCops: - '**/*.yml' - '**/*.yaml' - '**/.tool-versions' + - 'coverage/**/*' + - 'frontend/node_modules/**/*' + - 'frontend/test-results/**/*' + - 'public/frontend/**/*' + - 'tmp/**/*' + - 'vendor/**/*' Layout/LineLength: Max: 120 diff --git a/Dockerfile b/Dockerfile index bf654cc9..8a503ae7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,7 @@ ARG UID=991 ARG GID=991 RUN apk add --no-cache \ + 'ca-certificates>=2024' \ 'curl>=8' \ 'gcompat>=0' \ 'tzdata>=2024' \ diff --git a/Rakefile b/Rakefile index 56b73257..d6470448 100644 --- a/Rakefile +++ b/Rakefile @@ -47,14 +47,21 @@ task :test do current_dir = ENV.fetch('GITHUB_WORKSPACE', __dir__) smoke_auto_source_enabled = ENV.fetch('SMOKE_AUTO_SOURCE_ENABLED', 'false') image_name = 'html2rss/web' + skip_build = ENV.fetch('DOCKER_SMOKE_SKIP_BUILD', 'false') == 'true' + + if skip_build + Output.describe 'Running with prebuilt docker image' + else + Output.describe 'Building and running' + sh "docker build -t #{image_name} -f Dockerfile ." + end - Output.describe 'Building and running' - sh "docker build -t #{image_name} -f Dockerfile ." sh ['docker run', '-d', '-p 4000:4000', '--env PUMA_LOG_CONFIG=1', '--env HEALTH_CHECK_TOKEN=CHANGE_ME_HEALTH_CHECK_TOKEN', + '--env HTML2RSS_SECRET_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', "--env AUTO_SOURCE_ENABLED=#{smoke_auto_source_enabled}", "--mount type=bind,source=#{current_dir}/config,target=/app/config", '--name html2rss-web-test', diff --git a/app.json b/app.json deleted file mode 100644 index 893285b7..00000000 --- a/app.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "html2rss-web", - "description": "Builds RSS feeds from websites. Comes with many html2rss-configs included.", - "website": "https://github.com/html2rss/html2rss-web", - "env": { - "RACK_ENV": "production" - } -} diff --git a/bin/dev-with-frontend b/bin/dev-with-frontend index 52712386..bcefdb7a 100755 --- a/bin/dev-with-frontend +++ b/bin/dev-with-frontend @@ -15,7 +15,7 @@ export RACK_ENV=${RACK_ENV:-development} echo "Starting html2rss-web development environment..." echo "Environment: $RACK_ENV" echo "Ruby server: http://localhost:4000" -echo "Astro dev server: http://localhost:4001 (with live reload)" +echo "Vite dev server: http://localhost:4001 (with live reload)" echo "Main development URL: http://localhost:4001" echo "" @@ -24,10 +24,10 @@ cleanup() { echo "" echo "Shutting down servers..." kill $RUBY_PID 2>/dev/null || true - kill $ASTRO_PID 2>/dev/null || true + kill $FRONTEND_PID 2>/dev/null || true kill $WATCHER_PID 2>/dev/null || true wait $RUBY_PID 2>/dev/null || true - wait $ASTRO_PID 2>/dev/null || true + wait $FRONTEND_PID 2>/dev/null || true wait $WATCHER_PID 2>/dev/null || true echo "Servers stopped." exit 0 @@ -44,16 +44,16 @@ RUBY_PID=$! # Wait a moment for Ruby server to start sleep 3 -# Start Astro dev server with API proxy -echo "Starting Astro dev server with API proxy..." +# Start frontend dev server with API proxy +echo "Starting frontend dev server with API proxy..." cd frontend -# Start Astro dev server (it will proxy API calls to Ruby server) +# Start frontend dev server (it will proxy API calls to Ruby server) npm run dev & -ASTRO_PID=$! +FRONTEND_PID=$! -# Wait a moment for Astro server to start +# Wait a moment for the frontend server to start sleep 3 # Wait for both processes -wait $RUBY_PID $ASTRO_PID +wait $RUBY_PID $FRONTEND_PID diff --git a/bin/setup b/bin/setup index 71769aa3..37290355 100755 --- a/bin/setup +++ b/bin/setup @@ -31,7 +31,7 @@ echo "Running tests to verify setup..." bundle exec rspec echo "Setup complete! You can now run:" -echo " bin/dev # Start development server (Ruby + Astro)" +echo " bin/dev # Start development server (Ruby + Vite)" echo " bin/dev-ruby # Start Ruby server only" echo " bundle exec rspec # Run tests" echo " bundle exec rubocop # Run linter" diff --git a/docs/api/v1/openapi.yaml b/docs/api/v1/openapi.yaml index f2e83139..da9c939a 100644 --- a/docs/api/v1/openapi.yaml +++ b/docs/api/v1/openapi.yaml @@ -20,6 +20,7 @@ paths: get: description: API metadata operationId: getApiMetadata + parameters: [] responses: '200': content: @@ -66,7 +67,7 @@ paths: - success - data type: object - description: returns instance feed-creation capability + description: returns API information with trailing slash security: [] summary: API metadata tags: @@ -110,6 +111,8 @@ paths: type: string id: type: string + json_public_url: + type: string name: type: string public_url: @@ -127,6 +130,7 @@ paths: - strategy - feed_token - public_url + - json_public_url - created_at - updated_at type: object @@ -221,9 +225,7 @@ paths: '401': content: application/feed+json: - example: - title: Error - version: https://jsonfeed.org/version/1.1 + example: '{"version":"https://jsonfeed.org/version/1.1","title":"Error"}' schema: type: string application/xml: @@ -236,9 +238,7 @@ paths: '403': content: application/feed+json: - example: - title: Error - version: https://jsonfeed.org/version/1.1 + example: '{"version":"https://jsonfeed.org/version/1.1","title":"Error"}' schema: type: string application/xml: @@ -252,9 +252,7 @@ paths: '500': content: application/feed+json: - example: - title: Error - version: https://jsonfeed.org/version/1.1 + example: '{"version":"https://jsonfeed.org/version/1.1","title":"Error"}' schema: type: string application/xml: @@ -317,7 +315,8 @@ paths: - success - data type: object - description: returns health status when token is valid + description: returns health status when the configured environment token + is valid '401': content: application/json: diff --git a/frontend/e2e/smoke.spec.ts b/frontend/e2e/smoke.spec.ts index a741535d..73e06e8d 100644 --- a/frontend/e2e/smoke.spec.ts +++ b/frontend/e2e/smoke.spec.ts @@ -1,18 +1,23 @@ import { expect, test } from '@playwright/test'; test.describe('frontend smoke', () => { - test('loads demo onboarding and auth toggle', async ({ page }) => { + test('loads create flow and inline access-token gate', async ({ page }) => { await page.goto('/'); - await expect(page.getByRole('heading', { name: 'Convert website to RSS' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Run demo' })).toBeVisible(); + await expect(page.getByLabel('PAGE URL')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'MORE' })).toBeVisible(); - await page.getByRole('button', { name: 'Sign in' }).click(); - await expect(page.getByRole('button', { name: 'Back to demo' })).toBeVisible(); - await expect(page.getByLabel('Username')).toBeVisible(); - await expect(page.getByLabel('Token')).toBeVisible(); + await page.getByLabel('PAGE URL').fill('https://example.com/articles'); + await page.getByRole('button', { name: 'Generate feed URL' }).click(); - await page.getByRole('button', { name: 'Back to demo' }).click(); - await expect(page.getByRole('button', { name: 'Run demo' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Add access token' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'Access token' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Save and continue' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Back' })).toBeVisible(); + + await page.getByRole('button', { name: 'Back' }).click(); + await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'MORE' })).toBeVisible(); }); }); diff --git a/frontend/package.json b/frontend/package.json index 677b51bd..fa55a684 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "format:check": "prettier --check .", "typecheck": "tsc -p tsconfig.typecheck.json --noEmit", "openapi:generate": "openapi-ts -i ../docs/api/v1/openapi.yaml -o src/api/generated -c @hey-api/client-fetch", - "openapi:verify": "npm run openapi:generate && git diff --exit-code -- src/api/generated", + "openapi:verify": "openapi-ts -i ../docs/api/v1/openapi.yaml -o src/api/generated -c @hey-api/client-fetch && git diff --exit-code -- src/api/generated", "test": "vitest", "test:run": "vitest run", "test:unit": "vitest run --reporter=verbose --config vitest.unit.config.ts", diff --git a/frontend/src/__tests__/ResultDisplay.test.tsx b/frontend/src/__tests__/ResultDisplay.test.tsx index 230a81d7..c9ad54a5 100644 --- a/frontend/src/__tests__/ResultDisplay.test.tsx +++ b/frontend/src/__tests__/ResultDisplay.test.tsx @@ -44,7 +44,7 @@ describe('ResultDisplay', () => { expect(screen.getByText(/points by canpan/i)).toBeInTheDocument(); expect(screen.getByText('Item Two')).toBeInTheDocument(); }); - expect(window.fetch).toHaveBeenCalledWith('https://example.com/feed.json', { + expect(window.fetch).toHaveBeenCalledWith('https://example.com/feed.xml', { headers: { Accept: 'application/feed+json' }, }); }); diff --git a/frontend/src/api/generated/types.gen.ts b/frontend/src/api/generated/types.gen.ts index 80d6d4f3..6a267960 100644 --- a/frontend/src/api/generated/types.gen.ts +++ b/frontend/src/api/generated/types.gen.ts @@ -13,7 +13,7 @@ export type GetApiMetadataData = { export type GetApiMetadataResponses = { /** - * returns instance feed-creation capability + * returns API information with trailing slash */ 200: { data: { @@ -172,7 +172,7 @@ export type GetHealthStatusError = GetHealthStatusErrors[keyof GetHealthStatusEr export type GetHealthStatusResponses = { /** - * returns health status when token is valid + * returns health status when the configured environment token is valid */ 200: { data: { diff --git a/spec/smoke/docker_spec.rb b/spec/smoke/docker_spec.rb index 1faf8aa1..a4ba31cd 100644 --- a/spec/smoke/docker_spec.rb +++ b/spec/smoke/docker_spec.rb @@ -9,6 +9,7 @@ let(:health_token) { ENV.fetch('SMOKE_HEALTH_TOKEN', 'CHANGE_ME_HEALTH_CHECK_TOKEN') } let(:feed_token) { ENV.fetch('SMOKE_API_TOKEN', 'CHANGE_ME_ADMIN_TOKEN') } let(:auto_source_enabled) { ENV.fetch('SMOKE_AUTO_SOURCE_ENABLED', 'false') == 'true' } + let(:feed_url) { 'https://www.ruby-lang.org/en/' } def get_json(path, headers: {}) uri = URI.join(base_url, path) @@ -69,7 +70,7 @@ def expect_json_feed_response(path) it 'creates a feed when provided with valid credentials', :aggregate_failures do payload = { - url: 'https://example.com/articles', + url: feed_url, strategy: 'ssrf_filter' } @@ -82,7 +83,7 @@ def expect_json_feed_response(path) next unless auto_source_enabled payload = { - url: 'https://example.com/articles', + url: feed_url, strategy: 'ssrf_filter' } @@ -99,7 +100,7 @@ def expect_json_feed_response(path) next if auto_source_enabled payload = { - url: 'https://example.com/articles', + url: feed_url, strategy: 'ssrf_filter' } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index aba60fed..4ee5b787 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,7 +4,7 @@ ENV['RACK_ENV'] = 'test' ENV['HTML2RSS_SECRET_KEY'] = 'test-secret-key-for-specs' -if ENV['CI'] || ENV['COVERAGE'] +if (ENV.fetch('CI', nil) || ENV.fetch('COVERAGE', nil)) && ENV['RUN_DOCKER_SPECS'] != 'true' require 'simplecov' SimpleCov.start do @@ -13,7 +13,7 @@ track_files '**/*.rb' - minimum_coverage 80 + minimum_coverage 80 unless ENV['OPENAPI'] maximum_coverage_drop 5 end end diff --git a/spec/support/openapi.rb b/spec/support/openapi.rb index 944406f0..2483dab2 100644 --- a/spec/support/openapi.rb +++ b/spec/support/openapi.rb @@ -109,10 +109,7 @@ XML }, 'application/feed+json' => { - 'example' => { - 'version' => 'https://jsonfeed.org/version/1.1', - 'title' => 'Error' - } + 'example' => '{"version":"https://jsonfeed.org/version/1.1","title":"Error"}' } }