Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"Bash(stack build)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(stack test)",
"Bash(stack test:*)",
"Bash(find:*)",
"Bash(stack build:*)",
"Bash(mkdir:*)",
"Bash(git rev-parse:*)",
"Bash(git remote get-url:*)"
],
"deny": [],
"ask": []
}
}
83 changes: 83 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Claude Code Workflow Instructions for taskrunner

## Project Overview
This is a Haskell project that implements a task runner with caching, parallel execution, and remote storage capabilities. It uses Stack for build management and tasty-golden for snapshot testing.

## Project Structure
- `src/` - Haskell source code (main library)
- `app/` - Executable main entry point
- `test/` - Test suite
- `test/t/` - Golden test files (`.txt` input, `.out` expected output)
- `test/Spec.hs` - Main test runner
- `test/FakeGithubApi.hs` - Mock GitHub API for testing
- `package.yaml` - Haskell package configuration (Stack format)
- `stack.yaml` - Stack resolver and build configuration
- `taskrunner.cabal` - Generated cabal file (don't edit directly)

## Build and Development Workflow

### Building the Project
```bash
stack build
```

### Running Tests
```bash
# Run tests (auto-detects S3 credentials and skips S3 tests if missing)
stack test

# Run tests, skipping slow ones
export SKIP_SLOW_TESTS=1
stack test

# Run specific test by pattern
stack test --test-arguments "--pattern hello"

# List all available tests
stack test --test-arguments "--list-tests"
```

### Accepting Golden Test Changes
When golden tests fail due to expected output changes:
```bash
stack test --test-arguments --accept
```

### Test Structure
- Test files are in `test/t/` directory
- Each test has:
- `.txt` file - shell script to execute
- `.out` file - expected output (golden file)
- Tests run through the taskrunner executable
- Special comments in `.txt` files control test behavior:
- `# check output` - check stdout/stderr
- `# check github` - check GitHub API calls
- `# no toplevel` - don't wrap in taskrunner
- `# s3` - enable S3 testing
- `# github keys` - provide GitHub credentials

## Key Commands for Development

### Building
- `stack build` - Build the project
- `stack build --fast` - Fast build (less optimization)
- `stack clean` - Clean build artifacts

### Testing
- `stack test` - Run all tests
- `stack test --test-arguments --accept` - Accept golden test changes
- `SKIP_SLOW_TESTS=1 stack test` - Skip slow tests

### Running the executable
- `stack exec taskrunner -- [args]` - Run the built executable
- `stack run -- [args]` - Build and run in one command

## Notes
- This project uses tasty-golden for snapshot/golden file testing
- The test suite includes integration tests that verify taskrunner behavior
- **S3 Test Auto-Detection**: 15 tests require S3 credentials (marked with `# s3` directive in test files)
- `stack test` automatically skips S3 tests if credentials are missing
- To run S3 tests, set: `TASKRUNNER_TEST_S3_ENDPOINT`, `TASKRUNNER_TEST_S3_ACCESS_KEY`, `TASKRUNNER_TEST_S3_SECRET_KEY`
- GitHub tests use a fake API server and don't require real GitHub credentials
- The project uses Universum as an alternative Prelude
- Build output and temporary files are in `.stack-work/`
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,61 @@ The `snapshot` command supports the following flags:
- `--long-running`: Indicates that the task is expected to run for a long time (e.g. a server). Currently doens't have any effect though, TODO: can we remove it?


## Tests: Update Golden Files
## Testing

This project uses [tasty-golden](https://github.com/UnkindPartition/tasty-golden) for snapshot-based testing.

To update the golden files, run the test suite with the `--accept` flag passed to the test executable.
If you're using stack, the full command is:
### Running Tests

```sh
```bash
# Run all tests (auto-detects S3 credentials)
stack test

# Run tests, skipping slow ones for faster development
export SKIP_SLOW_TESTS=1
stack test

# Run specific test by pattern
stack test --test-arguments "--pattern hello"

# List all available tests
stack test --test-arguments "--list-tests"
```

### Test Structure

Tests are located in `test/t/` directory with two files per test:
- `.txt` file - Shell script to execute
- `.out` file - Expected output (golden file)

#### Test Directives

Special comments in `.txt` files control test behavior:
- `# check output` - Check stdout/stderr (default)
- `# check github` - Check GitHub API calls
- `# no toplevel` - Don't wrap in taskrunner
- `# s3` - Requires S3 credentials (auto-skipped if missing)
- `# github keys` - Provide GitHub credentials
- `# quiet` - Run in quiet mode

### S3 Test Auto-Detection

15 tests require S3 credentials and are automatically skipped if credentials are missing.

To run S3 tests, set these environment variables:
```bash
export TASKRUNNER_TEST_S3_ENDPOINT=your-s3-endpoint
export TASKRUNNER_TEST_S3_ACCESS_KEY=your-access-key
export TASKRUNNER_TEST_S3_SECRET_KEY=your-secret-key
stack test
```

### Accepting Golden Test Changes

When golden tests fail due to expected output changes:

```bash
stack test --test-arguments --accept
```

This updates the `.out` files with new expected output. Review changes carefully before committing.
10 changes: 9 additions & 1 deletion src/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ getSettings = do
fuzzyCacheFallbackBranches <- maybe [] (Text.words . toText) <$> lookupEnv "TASKRUNNER_FALLBACK_BRANCHES"
primeCacheMode <- (==Just "1") <$> lookupEnv "TASKRUNNER_PRIME_CACHE_MODE"
mainBranch <- map toText <$> lookupEnv "TASKRUNNER_MAIN_BRANCH"
quietMode <- (==Just "1") <$> lookupEnv "TASKRUNNER_QUIET"
pure Settings
{ stateDirectory
, rootDirectory
Expand All @@ -76,6 +77,7 @@ getSettings = do
, primeCacheMode
, mainBranch
, force = False
, quietMode
}

main :: IO ()
Expand Down Expand Up @@ -129,7 +131,7 @@ main = do
-- Recursive: AppState is used before process is started (mostly for logging)
rec

appState <- AppState settings jobName buildId isToplevel <$> newIORef Nothing <*> newIORef Nothing <*> newIORef False <*> pure toplevelStderr <*> pure subprocessStderr <*> pure logFile
appState <- AppState settings jobName buildId isToplevel <$> newIORef Nothing <*> newIORef Nothing <*> newIORef False <*> pure toplevelStderr <*> pure subprocessStderr <*> pure logFile <*> newIORef []
<*> newIORef Nothing

when (isToplevel && appState.settings.enableCommitStatus) do
Expand Down Expand Up @@ -171,6 +173,12 @@ main = do

skipped <- readIORef appState.skipped

-- Handle quiet mode buffer based on exit code
when appState.settings.quietMode do
if exitCode == ExitSuccess
then discardQuietBuffer appState -- Success: discard buffered output
else flushQuietBuffer appState toplevelStderr -- Failure: show buffered output

logDebug appState $ "Command " <> show (args.cmd : args.args) <> " exited with code " <> show exitCode
logDebugParent m_parentRequestPipe $ "Subtask " <> toText jobName <> " finished with " <> show exitCode

Expand Down
4 changes: 3 additions & 1 deletion src/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ data Settings = Settings
, primeCacheMode :: Bool
, mainBranch :: Maybe Text
, force :: Bool
, quietMode :: Bool
} deriving (Show)

type JobName = String
Expand Down Expand Up @@ -49,7 +50,8 @@ data AppState = AppState
, toplevelStderr :: Handle
, subprocessStderr :: Handle
, logOutput :: Handle

, quietBuffer :: IORef [ByteString]

-- | Lazily initialized Github client
, githubClient :: IORef (Maybe GithubClient)
}
Expand Down
22 changes: 21 additions & 1 deletion src/Utils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ outputLine appState toplevelOutput streamName line = do
| otherwise = True

when shouldOutputToToplevel do
B8.hPutStrLn toplevelOutput $ timestampStr <> "[" <> jobName <> "] " <> streamName <> " | " <> line
let formattedLine = timestampStr <> "[" <> jobName <> "] " <> streamName <> " | " <> line
if appState.settings.quietMode
then do
-- In quiet mode, add to buffer instead of outputting immediately
modifyIORef appState.quietBuffer (formattedLine :)
else
-- Normal mode: output immediately
B8.hPutStrLn toplevelOutput formattedLine

logLevel :: MonadIO m => ByteString -> AppState -> Text -> m ()
logLevel level appState msg =
Expand Down Expand Up @@ -121,3 +128,16 @@ getCurrentCommit _appState =

logFileName :: Settings -> BuildId -> JobName -> FilePath
logFileName settings buildId jobName = settings.stateDirectory </> "builds" </> toString buildId </> "logs" </> (jobName <> ".log")

-- | Flush buffered output to terminal (used when task fails in quiet mode)
flushQuietBuffer :: AppState -> Handle -> IO ()
flushQuietBuffer appState toplevelOutput = do
buffer <- readIORef appState.quietBuffer
-- Output in correct order (buffer was built in reverse)
mapM_ (B8.hPutStrLn toplevelOutput) (reverse buffer)
-- Clear the buffer after flushing
writeIORef appState.quietBuffer []

-- | Discard buffered output (used when task succeeds in quiet mode)
discardQuietBuffer :: AppState -> IO ()
discardQuietBuffer appState = writeIORef appState.quietBuffer []
49 changes: 47 additions & 2 deletions test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,34 @@
goldenTests :: IO TestTree
goldenTests = do
skipSlow <- (==Just "1") <$> lookupEnv "SKIP_SLOW_TESTS"
skipS3Explicit <- (==Just "1") <$> lookupEnv "SKIP_S3_TESTS"
hasS3Creds <- hasS3Credentials
let skipS3 = skipS3Explicit || not hasS3Creds

inputFiles0 <- sort <$> findByExtension [".txt"] "test/t"
inputFiles1 <- if skipS3
then filterM (fmap not . hasS3Directive) inputFiles0
else pure inputFiles0
let inputFiles
| skipSlow = filter (\filename -> not ("/slow/" `isInfixOf` filename)) inputFiles0
| otherwise = inputFiles0
| skipSlow = filter (\filename -> not ("/slow/" `isInfixOf` filename)) inputFiles1
| otherwise = inputFiles1

-- Print informative message about what tests are running
let totalTests = length inputFiles0
s3Tests = length inputFiles0 - length inputFiles1
slowTests = length inputFiles1 - length inputFiles
runningTests = length inputFiles

when (skipS3 && s3Tests > 0) $ do
if skipS3Explicit
then System.IO.putStrLn $ "SKIP_S3_TESTS=1 - skipping " <> show s3Tests <> " S3-dependent tests"
else System.IO.putStrLn $ "S3 credentials not found - skipping " <> show s3Tests <> " S3-dependent tests"
System.IO.putStrLn $ "To run S3 tests, set: TASKRUNNER_TEST_S3_ENDPOINT, TASKRUNNER_TEST_S3_ACCESS_KEY, TASKRUNNER_TEST_S3_SECRET_KEY"

when (skipSlow && slowTests > 0) $
System.IO.putStrLn $ "SKIP_SLOW_TESTS=1 - skipping " <> show slowTests <> " slow tests"

System.IO.putStrLn $ "Running " <> show runningTests <> "/" <> show totalTests <> " tests"
pure $ Tasty.withResource (FakeGithubApi.start fakeGithubPort) FakeGithubApi.stop \fakeGithubServer ->
testGroup "tests"
[ goldenVsStringDiff
Expand Down Expand Up @@ -105,6 +129,9 @@
, ("GITHUB_REPOSITORY_OWNER", "fakeowner")
, ("GITHUB_REPOSITORY", "fakerepo")
] <>
mwhen options.quiet
[ ("TASKRUNNER_QUIET", "1")
] <>
s3ExtraEnv)
, cwd = Just dir
} \_ _ _ processHandle -> do
Expand Down Expand Up @@ -142,6 +169,7 @@
-- | Whether to provide GitHub app credentials in environment.
-- If github status is disabled, taskrunner should work without them.
, githubKeys :: Bool
, quiet :: Bool
}

instance Default Options where
Expand All @@ -150,6 +178,7 @@
, toplevel = True
, s3 = False
, githubKeys = False
, quiet = False
}

getOptions :: Text -> Options
Expand All @@ -169,6 +198,9 @@
["#", "github", "keys"] -> do
modify (\s -> s { githubKeys = True })
go rest
["#", "quiet"] -> do
modify (\s -> (s :: Options) { quiet = True })

Check warning on line 202 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build

The record update (s :: Options)
go rest
-- TODO: validate?
_ ->
-- stop iteration
Expand Down Expand Up @@ -213,3 +245,16 @@
mwhen :: Monoid a => Bool -> a -> a
mwhen True x = x
mwhen False _ = mempty

hasS3Directive :: FilePath -> IO Bool
hasS3Directive file = do
content <- System.IO.readFile file
let options = getOptions (toText content)
pure options.s3

hasS3Credentials :: IO Bool
hasS3Credentials = do
endpoint <- lookupEnv "TASKRUNNER_TEST_S3_ENDPOINT"
accessKey <- lookupEnv "TASKRUNNER_TEST_S3_ACCESS_KEY"
secretKey <- lookupEnv "TASKRUNNER_TEST_S3_SECRET_KEY"
pure $ isJust endpoint && isJust accessKey && isJust secretKey
4 changes: 4 additions & 0 deletions test/t/quiet-mode-failure.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- output:
[toplevel] stdout | This output should be shown because the command fails
[toplevel] stdout | Second line of output
-- exit code: 1
4 changes: 4 additions & 0 deletions test/t/quiet-mode-failure.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# quiet
echo "This output should be shown because the command fails"
echo "Second line of output"
exit 1
2 changes: 2 additions & 0 deletions test/t/quiet-mode-nested-child-fail.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- output:
[nested] stdout | Nested output before failure
4 changes: 4 additions & 0 deletions test/t/quiet-mode-nested-child-fail.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# quiet
echo "Toplevel output before nested call"
taskrunner -n nested sh -c 'echo "Nested output before failure"; exit 1' || echo "Handling nested failure"
echo "Toplevel continues after nested failure"
5 changes: 5 additions & 0 deletions test/t/quiet-mode-nested-parent-fail.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- output:
[toplevel] stdout | Toplevel output before nested call
[toplevel] stdout | Toplevel output after nested call
[toplevel] stdout | This is the last line before failure
-- exit code: 1
6 changes: 6 additions & 0 deletions test/t/quiet-mode-nested-parent-fail.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# quiet
echo "Toplevel output before nested call"
taskrunner -n nested echo "Nested task succeeds"
echo "Toplevel output after nested call"
echo "This is the last line before failure"
exit 1
1 change: 1 addition & 0 deletions test/t/quiet-mode-nested-success.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- output:
5 changes: 5 additions & 0 deletions test/t/quiet-mode-nested-success.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# quiet
echo "Toplevel output (should be hidden)"
taskrunner -n nested echo "Nested output (should also be hidden)"
echo "More toplevel output (should be hidden)"
taskrunner -n deeper sh -c 'echo "Deep nested (should be hidden)"'
1 change: 1 addition & 0 deletions test/t/quiet-mode-success.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- output:
3 changes: 3 additions & 0 deletions test/t/quiet-mode-success.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# quiet
echo "This output should be hidden in quiet mode because the command succeeds"
echo "Second line of output"
Loading