Skip to content

Commit 2657f8c

Browse files
f-fthomashoneymanfsoikinpacchettibotti
authored
Job queue, matrix builder, concurrency control (#709)
* Update database schemas and add job executor loop * Split Server module into Env, Router, JobExecutor, and Main * Fix up build * Run job executor * Fix integration tests * WIP matrix builds * add missing version to publish fixtures the publishCodec requires a version file but the test fixtures weren't updated to include it * Add missing packageName and packageVersion to InsertMatrixJob The JS insertMatrixJobImpl expects columns [jobId, packageName, packageVersion, compilerVersion, payload] but the PureScript types were missing packageName and packageVersion * Fix finishedAt timestamp to capture time after job execution * Implement matrix jobs, and the recursive enqueuing of new ones * Reset incomplete jobs so they can be picked up again * Run matrix jobs for the whole registry when finding a new compiler version * resolve build issues * fix smoke test * Split package jobs into separate tables, return all data from the job endpoint * implement thin client for github issues replaces the old GitHubIssue which ran registry jobs directly with one that hits the registry api instead. also added integration tests that ensure various jobs can be kicked off as github issue events and we get the resulting comments, issue close events, etc. * clean up test failures * reinstate missing comments * Remove COMMENT effect, add NOTIFY log * Implement endpoint for returning jobs * Check for existing jobs before enqueueing new ones * Add E2E test: publishing a package enqueues matrix jobs * Add E2E test: run a whole-registry upgrade when detecting a new compiler * Don't fail job fetch on unreadable logs * Fix archive seeder build * remove effect-4.0.0 from storage in unit tests * avoid race condition in initial jobs test The "can list jobs" test was asserting that initial matrix jobs have success: true, but the job executor runs asynchronously and jobs may not have completed by the time the test queries the API. Fixed by normalizing the 'success' field to a constant before comparison. * format * second test * Refactor e2e tests with wiremock scenarios (#713) * refactor e2e tests with wiremock scenarios also adds a number of new e2e tests for various scenarios * format, etc. * move out fixtures * relax cache deletion * strengthen assertions, fix discovered bugs * drop ref, move to manifest (#714) * review feedback * more feedback * trim tests down a bit to optimize speed to ~60s * Add endpoint for package set jobs + e2e tests for it * tweak unpublish test to verify matrix jobs fail gracefully * tweak agents to refer to scratch logs * remove slow archive seeder test * fix tests by bumping compiler --------- Co-authored-by: Thomas Honeyman <hello@thomashoneyman.com> Co-authored-by: Fyodor Soikin <name.fa@gmail.com> Co-authored-by: pacchettibotti <pacchettibotti@purescript.org> Co-authored-by: Thomas Honeyman <admin@thomashoneyman.com>
1 parent 8c8c792 commit 2657f8c

101 files changed

Lines changed: 6171 additions & 2269 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,44 @@
1-
# =====
2-
# Dev Configuration
3-
# The devShell reads this file to set defaults, so changing values here
4-
# affects local development.
5-
# =====
1+
# -----------------------------------------------------------------------------
2+
# Server Configuration (dev defaults, required in all environments)
3+
# -----------------------------------------------------------------------------
64

7-
# Server port - used by both the server and E2E tests
5+
# Port the registry server listens on
6+
# - Dev/Test: 9000 (from this file)
7+
# - Prod: Set in deployment config
88
SERVER_PORT=9000
99

1010
# SQLite database path (relative to working directory)
11+
# - Dev: Uses local ./db directory
12+
# - Test: Overridden to use temp state directory
13+
# - Prod: Set to production database path
1114
DATABASE_URL="sqlite:db/registry.sqlite3"
1215

13-
# =====
14-
# Dev Secrets
15-
# these must be set in .env when running scripts like legacy-importer
16-
# =====
16+
# -----------------------------------------------------------------------------
17+
# Secrets (required for production, use dummy values for local dev)
18+
# -----------------------------------------------------------------------------
19+
# IMPORTANT: Never commit real secrets. The values below are dummies for testing.
1720

18-
# GitHub personal access token for API requests when running scripts
19-
GITHUB_TOKEN="ghp_your_personal_access_token"
20-
21-
# =====
22-
# Prod Secrets
23-
# these must be set in .env to run the production server and some scripts
24-
# =====
25-
26-
# DigitalOcean Spaces credentials for S3-compatible storage
27-
SPACES_KEY="digitalocean_spaces_key"
28-
SPACES_SECRET="digitalocean_spaces_secret"
29-
30-
# Pacchettibotti bot account credentials
31-
# Used for automated registry operations (commits, releases, etc.)
21+
# GitHub personal access token for pacchettibotti bot
22+
# Used for: commits to registry repos, issue management
3223
PACCHETTIBOTTI_TOKEN="ghp_pacchettibotti_token"
3324

3425
# Pacchettibotti SSH keys (base64-encoded)
26+
# Used for: signing authenticated operations (unpublish, transfer)
3527
# Generate with: ssh-keygen -t ed25519 -C "pacchettibotti@purescript.org"
3628
# Encode with: cat key | base64 | tr -d '\n'
3729
PACCHETTIBOTTI_ED25519_PUB="c3NoLWVkMjU1MTkgYWJjeHl6IHBhY2NoZXR0aWJvdHRpQHB1cmVzY3JpcHQub3Jn"
3830
PACCHETTIBOTTI_ED25519="YWJjeHl6"
31+
32+
# DigitalOcean Spaces credentials for S3-compatible storage
33+
# Used for: uploading/downloading package tarballs
34+
SPACES_KEY="digitalocean_spaces_key"
35+
SPACES_SECRET="digitalocean_spaces_secret"
36+
37+
38+
# -----------------------------------------------------------------------------
39+
# Script-only Secrets (not used by server, used by scripts like legacy-importer)
40+
# -----------------------------------------------------------------------------
41+
42+
# Personal GitHub token for API requests when running scripts
43+
# This is YOUR token, not pacchettibotti's
44+
GITHUB_TOKEN="ghp_your_personal_access_token"

AGENTS.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,63 @@ This project uses Nix with direnv. You should already be in the Nix shell automa
1010
nix develop
1111
```
1212

13-
### Build and Test
13+
Watch out for these Nix quirks:
14+
- If Nix tries to fetch from git during a build, it is likely that spago.yaml files were changed but the lock file was not updated; if so, update the lockfile with `spago build`
15+
- If a Nix build appears to be stale, then it is likely files were modified but are untracked by Git; if so, add modified files with `git add` and retry.
1416

15-
The registry is implemented in PureScript. Use spago to build it and run PureScript tests. These are cheap and fast and should be used when working on the registry packages.
17+
### Build
18+
19+
The registry is implemented in PureScript. Use spago to build it.
1620

1721
```sh
1822
spago build # Build all PureScript code
19-
spago test # Run unit tests
2023
```
2124

22-
Integration tests require two terminals (or the use of test-env in detached mode). The integration tests are only necessary to run if working on the server (app).
25+
The registry infrastructure is defined in Nix. Build it with Nix:
26+
27+
```sh
28+
nix build .#server
29+
```
30+
31+
### Test
32+
33+
The registry contains a mixture of unit tests, e2e tests, and nix flake checks. When you complete a change you should generally run the unit tests. When working on the server, you should generally also run the e2e tests. If you are on a Linux system, you can run `nix flake check -L` to run the flake checks prior to committing code to ensure it works.
34+
35+
#### Unit Tests
36+
37+
Unit tests can be run with `spago`. They are fast and cheap.
38+
39+
```sh
40+
spago test # Run all unit tests
41+
spago test -p <package-name> # Run tests for a specific package
42+
```
43+
44+
#### End-to-End Tests
45+
46+
The end-to-end (integration) tests are in `app-e2e`. They can be run via Nix on Linux:
47+
48+
```sh
49+
nix build .#checks.x86_64-linux.integration
50+
```
51+
52+
Alternately, they can be run on macOS or for more iterative development of tests using two terminals: one to start the test env, and one to execute the tests.
2353

2454
```sh
2555
# Terminal 1: Start test environment (wiremock mocks + registry server on port 9000)
2656
nix run .#test-env
2757

2858
# Terminal 2: Run E2E tests once server is ready
29-
spago run -p registry-app-e2e
59+
spago-test-e2e
3060
```
3161

32-
Options: `nix run .#test-env -- --tui` for interactive TUI, `-- --detached` for background mode.
62+
Options: `nix run .#test-env -- --tui` for interactive TUI, `-- --detached` for background mode to use a single terminal.
63+
64+
State is stored in `/tmp/registry-test-env` and cleaned up on each `nix run .#test-env`. To examine state after a test run (for debugging), stop the test-env but don't restart it. This is useful, for example, to read the logs of the most recent run. For example:
65+
66+
```sh
67+
# after a test run, see the logs (log name is today's date)
68+
cat /tmp/registry-test-env/scratch/logs/*.log
69+
```
3370

3471
#### Smoke Test (Linux only)
3572

CONTRIBUTING.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,29 @@ nix build .#checks.x86_64-linux.smoke -L
7272

7373
### Integration Test
7474

75+
You can run the integration tests with the following on Linux:
76+
77+
```sh
78+
nix build .#checks.x86_64-linux.integration -L
79+
```
80+
81+
On macOS or for iterative development, you can instead start the test environment and run the tests separately.
82+
7583
```sh
7684
# Terminal 1: Start the test environment (wiremock mocks + registry server)
7785
nix run .#test-env
7886

79-
# Terminal 2: Once the server is ready, run the E2E tests
80-
spago run -p registry-app-e2e
87+
# Terminal 2: Run E2E tests once server is ready
88+
spago-test-e2e
8189
```
8290

8391
The test environment:
8492
- Starts wiremock services mocking GitHub, S3, Pursuit, etc.
85-
- Starts the registry server on port 9000 with a temporary SQLite database
93+
- Starts the registry server with a temporary SQLite database
8694
- Uses fixture data from `app/fixtures/`
95+
- State is stored in `/tmp/registry-test-env` and cleaned up on each `nix run .#test-env`
8796

88-
Press `Ctrl+C` in Terminal 1 to stop all services. State is cleaned up automatically.
97+
Press `Ctrl+C` in Terminal 1 to stop all services.
8998

9099
All arguments after `--` are passed directly to process-compose:
91100

@@ -101,7 +110,11 @@ process-compose attach # Attach TUI
101110
process-compose down # Stop all services
102111
```
103112

104-
You can also set `STATE_DIR` to use a persistent state directory instead of a temp dir.
113+
To examine state after a test run (e.g., for debugging), stop the test-env but don't restart it. The state remains in `/tmp/registry-test-env`:
114+
- `db/registry.sqlite3` — SQLite database
115+
- `scratch/registry/` — Local registry clone with metadata
116+
- `scratch/registry-index/` — Local manifest index clone
117+
- `repo-fixtures/` — Git fixture repositories
105118

106119
## Available Nix Commands
107120

SPEC.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ All packages in the registry contain a `purs.json` manifest file in their root d
197197
- `version`: a valid [`Version`](#version)
198198
- `license`: a valid [`License`](#license)
199199
- `location`: a valid [`Location`](#location)
200+
- `ref`: a `string` representing the reference (e.g., a Git commit or Git tag) at the `location` that was used to fetch this version's source code
200201
- `owners` (optional): a non-empty array of [`Owner`](#owner)
201202
- `description` (optional): a description of your library as a plain text string, not markdown, up to 300 characters
202203
- `includeFiles` (optional): a non-empty array of globs, where globs are used to match file paths (in addition to the `src` directory and other [always-included files](#always-included-files)) that you want included in your package tarball
@@ -221,6 +222,7 @@ For example:
221222
"githubOwner": "purescript",
222223
"githubRepo": "purescript-control"
223224
},
225+
"ref": "v4.2.0",
224226
"include": ["test/**/*.purs"],
225227
"exclude": ["test/graphs"],
226228
"dependencies": { "newtype": ">=3.0.0 <4.0.0", "prelude": ">=4.0.0 <5.0.0" }

app-e2e/spago.yaml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,27 @@ package:
55
dependencies:
66
- aff
77
- arrays
8+
- codec-json
89
- console
910
- datetime
10-
- effect
11-
- either
12-
- maybe
13-
- prelude
11+
- exceptions
12+
- fetch
13+
- integers
14+
- json
15+
- node-child-process
16+
- node-execa
17+
- node-fs
18+
- node-path
19+
- node-process
20+
- ordered-collections
21+
- registry-app
22+
- registry-foreign
1423
- registry-lib
1524
- registry-test-utils
25+
- routing-duplex
1626
- spec
1727
- spec-node
1828
- strings
29+
- transformers
1930
run:
2031
main: Test.E2E.Main
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
module Test.E2E.Endpoint.Jobs (spec) where
2+
3+
import Registry.App.Prelude
4+
5+
import Data.Array as Array
6+
import Registry.API.V1 (JobId(..))
7+
import Registry.API.V1 as V1
8+
import Registry.Test.Assert as Assert
9+
import Test.E2E.Support.Client as Client
10+
import Test.E2E.Support.Env (E2ESpec)
11+
import Test.E2E.Support.Env as Env
12+
import Test.E2E.Support.Fixtures as Fixtures
13+
import Test.Spec as Spec
14+
15+
spec :: E2ESpec
16+
spec = do
17+
Spec.describe "Status endpoint" do
18+
Spec.it "can reach the status endpoint" do
19+
Client.getStatus
20+
21+
Spec.describe "Jobs API" do
22+
Spec.it "query parameters and filtering work correctly" do
23+
-- Publish once and test all Jobs API features
24+
{ jobId } <- Client.publish Fixtures.effectPublishData
25+
job <- Env.pollJobOrFail jobId
26+
let info = V1.jobInfo job
27+
28+
-- Test: include_completed filtering
29+
recentJobs <- Client.getJobsWith Client.ActiveOnly
30+
allJobs <- Client.getJobsWith Client.IncludeCompleted
31+
let allCount = Array.length allJobs
32+
Assert.shouldSatisfy allCount (_ > 0)
33+
let recentCount = Array.length recentJobs
34+
Assert.shouldSatisfy recentCount (_ <= allCount)
35+
let completedJob = Array.find (\j -> isJust (V1.jobInfo j).finishedAt) allJobs
36+
case completedJob of
37+
Just completed -> do
38+
let
39+
completedId = (V1.jobInfo completed).jobId
40+
inRecent = Array.any (\j -> (V1.jobInfo j).jobId == completedId) recentJobs
41+
when inRecent do
42+
Assert.fail $ "Completed job " <> unwrap completedId <> " should be excluded from include_completed=false results"
43+
Nothing -> pure unit
44+
45+
-- Test: query parameters (level and since)
46+
baseJob <- Client.getJob jobId Nothing Nothing
47+
Assert.shouldEqual (V1.jobInfo baseJob).jobId info.jobId
48+
debugJob <- Client.getJob jobId (Just V1.Debug) Nothing
49+
Assert.shouldEqual (V1.jobInfo debugJob).jobId info.jobId
50+
let sinceTime = fromMaybe info.createdAt info.finishedAt
51+
sinceJob <- Client.getJob jobId Nothing (Just sinceTime)
52+
Assert.shouldEqual (V1.jobInfo sinceJob).jobId info.jobId
53+
54+
Spec.it "returns HTTP 404 for non-existent job ID" do
55+
let fakeJobId = JobId "nonexistent-job-id-12345"
56+
result <- Client.tryGetJob fakeJobId Nothing Nothing
57+
case result of
58+
Right _ ->
59+
Assert.fail "Expected HTTP 404 for non-existent job"
60+
Left err ->
61+
case Client.clientErrorStatus err of
62+
Just 404 -> pure unit
63+
_ -> Assert.fail $ "Expected HTTP 404, got: " <> Client.printClientError err
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module Test.E2E.Endpoint.PackageSets (spec) where
2+
3+
import Registry.App.Prelude
4+
5+
import Control.Monad.Reader (ask)
6+
import Effect.Aff as Aff
7+
import Registry.API.V1 as V1
8+
import Registry.Test.Assert as Assert
9+
import Test.E2E.Support.Client as Client
10+
import Test.E2E.Support.Env (E2ESpec)
11+
import Test.E2E.Support.Env as Env
12+
import Test.E2E.Support.Fixtures as Fixtures
13+
import Test.Spec as Spec
14+
15+
spec :: E2ESpec
16+
spec = do
17+
Spec.describe "Package Sets endpoint" do
18+
Spec.it "accepts unauthenticated add/upgrade requests" do
19+
{ jobId } <- Client.packageSets Fixtures.packageSetAddRequest
20+
job <- Env.pollJobOrFail jobId
21+
Assert.shouldSatisfy (V1.jobInfo job).finishedAt isJust
22+
23+
Spec.it "rejects unauthenticated compiler change requests" do
24+
result <- Client.tryPackageSets Fixtures.packageSetCompilerChangeRequest
25+
case result of
26+
Left err -> do
27+
Assert.shouldSatisfy (Client.clientErrorStatus err) (_ == Just 400)
28+
Right _ ->
29+
Assert.fail "Expected 400 error for unauthenticated compiler change"
30+
31+
Spec.it "rejects unauthenticated package removal requests" do
32+
result <- Client.tryPackageSets Fixtures.packageSetRemoveRequest
33+
case result of
34+
Left err -> do
35+
Assert.shouldSatisfy (Client.clientErrorStatus err) (_ == Just 400)
36+
Right _ ->
37+
Assert.fail "Expected 400 error for unauthenticated package removal"
38+
39+
Spec.it "accepts authenticated compiler change requests" do
40+
{ privateKey } <- ask
41+
case Fixtures.signPackageSet privateKey Fixtures.packageSetCompilerChangeRequest of
42+
Left err ->
43+
liftAff $ Aff.throwError $ Aff.error $ "Failed to sign request: " <> err
44+
Right signedRequest -> do
45+
{ jobId } <- Client.packageSets signedRequest
46+
job <- Env.pollJobOrFail jobId
47+
Assert.shouldSatisfy (V1.jobInfo job).finishedAt isJust
48+
49+
Spec.it "returns existing job for duplicate requests" do
50+
{ jobId: firstJobId } <- Client.packageSets Fixtures.packageSetAddRequest
51+
{ jobId: secondJobId } <- Client.packageSets Fixtures.packageSetAddRequest
52+
Assert.shouldEqual firstJobId secondJobId

0 commit comments

Comments
 (0)