ground_control-api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +24 -0
- data/.github/workflows/ci.yml +59 -0
- data/.github/workflows/claude-code-review.yml +24 -0
- data/.github/workflows/claude.yml +38 -0
- data/.github/workflows/codeql-actions.yml +28 -0
- data/.github/workflows/release.yml +89 -0
- data/.lefthook/bundle-sync.sh +58 -0
- data/.lefthook/lib/git-upstream.sh +13 -0
- data/.lefthook/verify-signatures.sh +46 -0
- data/.lefthook/whitespace-check.sh +8 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +78 -0
- data/MIT-LICENSE +20 -0
- data/README.md +130 -0
- data/Rakefile +8 -0
- data/app/controllers/concerns/ground_control/api/adapter_features.rb +36 -0
- data/app/controllers/concerns/ground_control/api/error_handling.rb +25 -0
- data/app/controllers/concerns/ground_control/api/job_filters.rb +42 -0
- data/app/controllers/ground_control/api/application_controller.rb +24 -0
- data/app/controllers/ground_control/api/applications_controller.rb +13 -0
- data/app/controllers/ground_control/api/bulk_discards_controller.rb +21 -0
- data/app/controllers/ground_control/api/bulk_retries_controller.rb +15 -0
- data/app/controllers/ground_control/api/discards_controller.rb +14 -0
- data/app/controllers/ground_control/api/dispatches_controller.rb +14 -0
- data/app/controllers/ground_control/api/features_controller.rb +11 -0
- data/app/controllers/ground_control/api/jobs_controller.rb +37 -0
- data/app/controllers/ground_control/api/queues/pauses_controller.rb +29 -0
- data/app/controllers/ground_control/api/queues_controller.rb +26 -0
- data/app/controllers/ground_control/api/recurring_tasks_controller.rb +40 -0
- data/app/controllers/ground_control/api/retries_controller.rb +14 -0
- data/app/controllers/ground_control/api/workers_controller.rb +33 -0
- data/app/resources/ground_control/api/application_resource.rb +12 -0
- data/app/resources/ground_control/api/base_resource.rb +13 -0
- data/app/resources/ground_control/api/job_resource.rb +46 -0
- data/app/resources/ground_control/api/page_resource.rb +47 -0
- data/app/resources/ground_control/api/queue_resource.rb +22 -0
- data/app/resources/ground_control/api/recurring_task_resource.rb +17 -0
- data/app/resources/ground_control/api/server_resource.rb +10 -0
- data/app/resources/ground_control/api/worker_resource.rb +14 -0
- data/config/routes.rb +27 -0
- data/lefthook.yml +17 -0
- data/lib/ground_control/api/engine.rb +18 -0
- data/lib/ground_control/api/version.rb +7 -0
- data/lib/ground_control/api.rb +15 -0
- data/sig/ground_control/api.rbs +6 -0
- data/spec/spec_helper.rb +15 -0
- metadata +136 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fa4151207335fd8c45c84426ef8743b3291149dc545d093537c94237ec29a7f5
|
|
4
|
+
data.tar.gz: 3a5f9fcbc1939b84be71dea74a914039c5a8bbfd0f5ec60e50e1d6bdd6de1e98
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7fdc683e0ba9125b04fa84d18329719a84e40aa8226d9681f9287658f0e636dadb8c401223dadfde2aafbd9e7340bbcc451f933d708c74f1e85c8284e9a84667
|
|
7
|
+
data.tar.gz: dd589635c37e608fe4be787a1e039540da9bdf6dd31f5cc3b94bc5aded654020b2bcaa43c29c271647f6fe777defb3729dac3390254d0d800b22a81311b6d14d
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: bundler
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
day: monday
|
|
8
|
+
open-pull-requests-limit: 10
|
|
9
|
+
groups:
|
|
10
|
+
development-dependencies:
|
|
11
|
+
dependency-type: "development"
|
|
12
|
+
production-dependencies:
|
|
13
|
+
dependency-type: "production"
|
|
14
|
+
|
|
15
|
+
- package-ecosystem: github-actions
|
|
16
|
+
directory: "/"
|
|
17
|
+
schedule:
|
|
18
|
+
interval: weekly
|
|
19
|
+
day: monday
|
|
20
|
+
open-pull-requests-limit: 10
|
|
21
|
+
groups:
|
|
22
|
+
actions:
|
|
23
|
+
patterns:
|
|
24
|
+
- "*"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [ main ]
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
pull-requests: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 10
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout code
|
|
22
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
23
|
+
|
|
24
|
+
- name: Set up Ruby
|
|
25
|
+
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
|
|
26
|
+
with:
|
|
27
|
+
ruby-version: 3.4.4
|
|
28
|
+
bundler-cache: true
|
|
29
|
+
|
|
30
|
+
- name: Security scan with Brakeman
|
|
31
|
+
run: bundle exec brakeman --no-pager --quiet
|
|
32
|
+
- name: Audit gem dependencies for CVEs (bundler-audit)
|
|
33
|
+
run: bundle exec bundler-audit check --update
|
|
34
|
+
- name: Lint code for consistent style
|
|
35
|
+
run: bundle exec rubocop -f github
|
|
36
|
+
|
|
37
|
+
test:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
timeout-minutes: 15
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
- name: Checkout code
|
|
43
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
44
|
+
|
|
45
|
+
- name: Set up Ruby
|
|
46
|
+
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
|
|
47
|
+
with:
|
|
48
|
+
ruby-version: 3.4.4
|
|
49
|
+
bundler-cache: true
|
|
50
|
+
|
|
51
|
+
- name: Run tests
|
|
52
|
+
env:
|
|
53
|
+
RAILS_ENV: test
|
|
54
|
+
run: |
|
|
55
|
+
if find spec -name '*_spec.rb' 2>/dev/null | grep -q .; then
|
|
56
|
+
bundle exec rspec
|
|
57
|
+
else
|
|
58
|
+
echo "No spec files found, skipping."
|
|
59
|
+
fi
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Claude Code Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
review:
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
pull-requests: write
|
|
21
|
+
issues: write
|
|
22
|
+
id-token: write
|
|
23
|
+
uses: rarebit-one/.github/.github/workflows/claude-code-review.yml@main
|
|
24
|
+
secrets: inherit
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Claude Code
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types: [created]
|
|
6
|
+
pull_request_review_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
issues:
|
|
9
|
+
types: [opened, assigned]
|
|
10
|
+
pull_request_review:
|
|
11
|
+
types: [submitted]
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.comment.id }}
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
pull-requests: write
|
|
20
|
+
issues: write
|
|
21
|
+
id-token: write
|
|
22
|
+
actions: read
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
claude:
|
|
26
|
+
if: |
|
|
27
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
28
|
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
29
|
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
30
|
+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
pull-requests: read
|
|
34
|
+
issues: read
|
|
35
|
+
id-token: write
|
|
36
|
+
actions: read
|
|
37
|
+
uses: rarebit-one/.github/.github/workflows/claude-agent.yml@main
|
|
38
|
+
secrets: inherit
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CodeQL (Actions)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
paths:
|
|
9
|
+
- '.github/workflows/**'
|
|
10
|
+
schedule:
|
|
11
|
+
- cron: '30 6 * * 1'
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
security-events: write
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
analyze:
|
|
19
|
+
name: Analyze Actions workflows
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
23
|
+
- uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v3 (bundle 2.25.2)
|
|
24
|
+
with:
|
|
25
|
+
languages: actions
|
|
26
|
+
- uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
|
27
|
+
with:
|
|
28
|
+
category: "/language:actions"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
# CI is not gated here — branch protection ensures CI passes before
|
|
13
|
+
# merge to main, and tags are only pushed after the version bump PR
|
|
14
|
+
# is merged. Running CI again on tag push is redundant.
|
|
15
|
+
release:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 10
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
20
|
+
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: "3.4.4"
|
|
25
|
+
|
|
26
|
+
- name: Verify tag matches gem version
|
|
27
|
+
run: |
|
|
28
|
+
tag_version="${GITHUB_REF_NAME#v}"
|
|
29
|
+
gem_version=$(ruby -e "spec = Gem::Specification.load(Dir['*.gemspec'].first); puts spec.version")
|
|
30
|
+
if [ "$tag_version" != "$gem_version" ]; then
|
|
31
|
+
echo "::error::Tag version ($tag_version) does not match gemspec version ($gem_version)"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
- name: Extract changelog entry
|
|
36
|
+
id: changelog
|
|
37
|
+
run: |
|
|
38
|
+
version="${GITHUB_REF_NAME#v}"
|
|
39
|
+
notes=$(awk -v ver="$version" '
|
|
40
|
+
/^## \[/ {
|
|
41
|
+
if (found) exit
|
|
42
|
+
if ($0 ~ "^## \\[" ver "\\]") found=1
|
|
43
|
+
next
|
|
44
|
+
}
|
|
45
|
+
found { print }
|
|
46
|
+
' CHANGELOG.md)
|
|
47
|
+
|
|
48
|
+
if [ -z "$notes" ]; then
|
|
49
|
+
echo "::error::No CHANGELOG.md entry found for version $version"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Use a random delimiter to prevent injection via changelog content
|
|
54
|
+
delimiter="EOF_$(openssl rand -hex 8)"
|
|
55
|
+
{
|
|
56
|
+
echo "notes<<${delimiter}"
|
|
57
|
+
echo "$notes"
|
|
58
|
+
echo "${delimiter}"
|
|
59
|
+
} >> "$GITHUB_OUTPUT"
|
|
60
|
+
|
|
61
|
+
- name: Create GitHub Release
|
|
62
|
+
env:
|
|
63
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
64
|
+
RELEASE_NOTES: ${{ steps.changelog.outputs.notes }}
|
|
65
|
+
run: |
|
|
66
|
+
printf "%s" "$RELEASE_NOTES" > /tmp/release_notes.txt
|
|
67
|
+
gh release create "$GITHUB_REF_NAME" \
|
|
68
|
+
--title "$GITHUB_REF_NAME" \
|
|
69
|
+
--notes-file /tmp/release_notes.txt
|
|
70
|
+
|
|
71
|
+
publish:
|
|
72
|
+
needs: release
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
timeout-minutes: 10
|
|
75
|
+
environment: rubygems
|
|
76
|
+
permissions:
|
|
77
|
+
contents: read
|
|
78
|
+
id-token: write
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
81
|
+
|
|
82
|
+
- name: Set up Ruby
|
|
83
|
+
uses: ruby/setup-ruby@v1
|
|
84
|
+
with:
|
|
85
|
+
ruby-version: "3.4.4"
|
|
86
|
+
bundler-cache: true
|
|
87
|
+
|
|
88
|
+
- name: Push to RubyGems
|
|
89
|
+
uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Sync gems after git operations that may change Gemfile.lock.
|
|
3
|
+
|
|
4
|
+
GEMFILE_CHANGED=false
|
|
5
|
+
|
|
6
|
+
if [[ -n "$LEFTHOOK_GIT_HOOK" ]]; then
|
|
7
|
+
HOOK_NAME="$LEFTHOOK_GIT_HOOK"
|
|
8
|
+
else
|
|
9
|
+
HOOK_NAME="unknown"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
case "$HOOK_NAME" in
|
|
13
|
+
post-checkout)
|
|
14
|
+
OLD_REF="${1:-}"
|
|
15
|
+
NEW_REF="${2:-}"
|
|
16
|
+
BRANCH_FLAG="${3:-}"
|
|
17
|
+
if [[ "$BRANCH_FLAG" == "0" ]]; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
if [[ -n "$OLD_REF" && -n "$NEW_REF" && "$OLD_REF" != "$NEW_REF" ]]; then
|
|
21
|
+
if git diff --name-only "$OLD_REF" "$NEW_REF" 2>/dev/null | grep -q '^Gemfile\.lock$'; then
|
|
22
|
+
GEMFILE_CHANGED=true
|
|
23
|
+
fi
|
|
24
|
+
fi
|
|
25
|
+
;;
|
|
26
|
+
post-rewrite)
|
|
27
|
+
while read -r OLD_REF NEW_REF _REST; do
|
|
28
|
+
if [[ -n "$OLD_REF" && -n "$NEW_REF" ]]; then
|
|
29
|
+
if git diff --name-only "$OLD_REF" "$NEW_REF" 2>/dev/null | grep -q '^Gemfile\.lock$'; then
|
|
30
|
+
GEMFILE_CHANGED=true
|
|
31
|
+
break
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
done
|
|
35
|
+
;;
|
|
36
|
+
post-merge)
|
|
37
|
+
if git diff --name-only HEAD@{1} HEAD 2>/dev/null | grep -q '^Gemfile\.lock$'; then
|
|
38
|
+
GEMFILE_CHANGED=true
|
|
39
|
+
fi
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
if git diff --name-only HEAD@{1} HEAD 2>/dev/null | grep -q '^Gemfile\.lock$'; then
|
|
43
|
+
GEMFILE_CHANGED=true
|
|
44
|
+
fi
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
if [[ "$GEMFILE_CHANGED" != true ]]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "Gemfile.lock changed — syncing gems..." >&2
|
|
53
|
+
|
|
54
|
+
if ! bundle install; then
|
|
55
|
+
echo "Gem sync failed. Run manually: bundle install" >&2
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared upstream detection logic for lefthook scripts.
|
|
3
|
+
#
|
|
4
|
+
# Sets UPSTREAM and MERGE_BASE variables.
|
|
5
|
+
# Source this file from other scripts:
|
|
6
|
+
# source "$(dirname "$0")/lib/git-upstream.sh"
|
|
7
|
+
|
|
8
|
+
UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) || UPSTREAM="origin/main"
|
|
9
|
+
|
|
10
|
+
MERGE_BASE=$(git merge-base "$UPSTREAM" HEAD 2>/dev/null) || {
|
|
11
|
+
echo "Failed to find merge-base with $UPSTREAM" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Verify all commits to be pushed have valid signatures (GPG or SSH).
|
|
3
|
+
#
|
|
4
|
+
# Checks every commit between the remote tracking branch and HEAD.
|
|
5
|
+
# Exits non-zero if any unsigned or badly-signed commits are found.
|
|
6
|
+
#
|
|
7
|
+
# Accepted signature statuses: G (good), U (good but untrusted key)
|
|
8
|
+
# Rejected: N (none), B (bad), E (can't check), X (expired), Y (expired key), R (revoked)
|
|
9
|
+
|
|
10
|
+
set -eo pipefail
|
|
11
|
+
|
|
12
|
+
source "$(dirname "$0")/lib/git-upstream.sh"
|
|
13
|
+
|
|
14
|
+
COMMITS=$(git log "$MERGE_BASE..HEAD" --format="%H" 2>/dev/null) || {
|
|
15
|
+
echo "Failed to list commits between merge-base and HEAD" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if [[ -z "$COMMITS" ]]; then
|
|
20
|
+
echo "No new commits to verify"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
UNSIGNED_COMMITS=()
|
|
25
|
+
while IFS= read -r commit; do
|
|
26
|
+
SIG_STATUS=$(git log --format="%G?" -1 "$commit" 2>/dev/null || echo "N")
|
|
27
|
+
if [[ "$SIG_STATUS" != "G" && "$SIG_STATUS" != "U" ]]; then
|
|
28
|
+
SHORT_HASH=$(git rev-parse --short "$commit")
|
|
29
|
+
SUBJECT=$(git log --format="%s" -1 "$commit")
|
|
30
|
+
UNSIGNED_COMMITS+=(" $SHORT_HASH ($SIG_STATUS): $SUBJECT")
|
|
31
|
+
fi
|
|
32
|
+
done <<< "$COMMITS"
|
|
33
|
+
|
|
34
|
+
if [[ ${#UNSIGNED_COMMITS[@]} -gt 0 ]]; then
|
|
35
|
+
echo "Unsigned commits detected:" >&2
|
|
36
|
+
for line in "${UNSIGNED_COMMITS[@]}"; do
|
|
37
|
+
echo "$line" >&2
|
|
38
|
+
done
|
|
39
|
+
echo "" >&2
|
|
40
|
+
echo "To fix:" >&2
|
|
41
|
+
echo " Single commit: git commit --amend --no-edit -S" >&2
|
|
42
|
+
echo " Multiple commits: git rebase --exec 'git commit --amend --no-edit -S' $UPSTREAM" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
echo "All commits are signed"
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-04-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release of Ground Control API
|
|
15
|
+
- JSON API endpoints for managing Active Job queues, jobs, workers, and recurring tasks
|
|
16
|
+
- Built on top of mission_control-jobs
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Worktree-Only Workflow (Enforced)
|
|
4
|
+
|
|
5
|
+
**All file modifications are blocked in the main checkout.** A PreToolUse hook (`enforce-worktree.sh`) rejects Edit, Write, and NotebookEdit operations targeting files outside a worktree. There are no opt-outs. Do not use Bash to write files in the main checkout either (e.g., `echo >`, `sed -i`, `tee`, `cp`) — the hook cannot intercept shell commands, so this rule is instruction-enforced.
|
|
6
|
+
|
|
7
|
+
Before writing any code, create a worktree:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@refs/remotes/origin/@@')
|
|
11
|
+
DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
|
|
12
|
+
git fetch origin "$DEFAULT_BRANCH"
|
|
13
|
+
git worktree add .worktrees/<name> -b <branch-name> "origin/$DEFAULT_BRANCH"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then work inside `.worktrees/<name>/` for the rest of the session.
|
|
17
|
+
|
|
18
|
+
**Naming:** Use the Linear issue identifier if available (e.g., `.worktrees/<identifier>`), a task slug (e.g., `.worktrees/fix-auth-timeout`), or today's date (e.g., `.worktrees/2026-04-01`) as fallback.
|
|
19
|
+
|
|
20
|
+
**The hook allows modifications only when:**
|
|
21
|
+
|
|
22
|
+
1. The file is inside a git worktree (detected via `git rev-parse --git-dir` returning a path under `.git/worktrees/`)
|
|
23
|
+
2. Running in a CI/automated context where the checkout is already isolated
|
|
24
|
+
**Why this matters:** Working directly on the main checkout causes cross-contamination between sessions — uncommitted changes, wrong branches, and dirty state leak into unrelated work. Worktrees eliminate this entirely.
|
|
25
|
+
|
|
26
|
+
See the `/worktree` and `/start` skills for full conventions and flags.
|
|
27
|
+
|
|
28
|
+
Guidance for Claude Code working with the ground_control-api gem.
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
Ground Control API is a headless JSON API Rails engine for managing Active Job queues, jobs, workers, and recurring tasks. It builds on top of `mission_control-jobs` for the adapter/query layer and provides Alba-serialized JSON responses.
|
|
33
|
+
|
|
34
|
+
## Quick Reference
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Testing
|
|
38
|
+
bundle exec rspec # Run all tests
|
|
39
|
+
bundle exec rspec spec/path/to/file:42 # Run specific test
|
|
40
|
+
|
|
41
|
+
# Linting
|
|
42
|
+
bundle exec rubocop # Ruby linting
|
|
43
|
+
bundle exec rubocop -a # Auto-fix
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Architecture
|
|
47
|
+
|
|
48
|
+
**Type:** Rails engine gem (JSON API only, no views)
|
|
49
|
+
|
|
50
|
+
**Dependencies:**
|
|
51
|
+
- `mission_control-jobs` — adapter layer, query objects, job proxy models
|
|
52
|
+
- `alba` — JSON serialization
|
|
53
|
+
- `rails` — engine framework
|
|
54
|
+
|
|
55
|
+
**Key directories:**
|
|
56
|
+
- `app/controllers/ground_control/api/` — JSON API controllers
|
|
57
|
+
- `app/resources/ground_control/api/` — Alba serialization resources
|
|
58
|
+
- `app/controllers/concerns/ground_control/api/` — Shared controller concerns
|
|
59
|
+
- `lib/ground_control/api/` — Engine, configuration, version
|
|
60
|
+
- `config/routes.rb` — API route definitions
|
|
61
|
+
|
|
62
|
+
**Namespace:** `GroundControl::Api`
|
|
63
|
+
|
|
64
|
+
## Key Patterns
|
|
65
|
+
|
|
66
|
+
1. **Controllers** render JSON via Alba resources — never build hashes inline
|
|
67
|
+
2. **Mutating actions** (retry, discard, dispatch, pause) return `{ message: "..." }` with status 200
|
|
68
|
+
3. **Read actions** return `{ data: ..., meta: ... }` structure
|
|
69
|
+
4. **Pagination** uses `MissionControl::Jobs::Page` wrapped in `PageResource`
|
|
70
|
+
5. **Error handling** via `ErrorHandling` concern — rescues job/resource not found as 404
|
|
71
|
+
6. **Auth** is delegated to host apps via `GroundControl::Api.authenticate_with` proc
|
|
72
|
+
|
|
73
|
+
## Conventions
|
|
74
|
+
|
|
75
|
+
- All Ruby files use `frozen_string_literal: true`
|
|
76
|
+
- Alba resources inherit from `BaseResource` (lower_camel key transform)
|
|
77
|
+
- Rubocop follows `rubocop-rails-omakase` style
|
|
78
|
+
- Commits must be signed (GPG or SSH)
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) Jaryl Sim
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Ground Control API
|
|
2
|
+
|
|
3
|
+
A headless JSON API Rails engine for managing Active Job queues, jobs, workers, and recurring tasks. Built on top of [mission_control-jobs](https://github.com/rails/mission_control-jobs) for the adapter and query layer.
|
|
4
|
+
|
|
5
|
+
Use standalone for custom frontends, or as the foundation for [ground_control-inertia](https://github.com/rarebit-one/ground-control-inertia).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem "ground_control-api", github: "rarebit-one/ground-control-api"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Mount the engine in your routes:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# config/routes.rb
|
|
20
|
+
mount GroundControl::Api::Engine, at: "/ground-control/api"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Authentication
|
|
24
|
+
|
|
25
|
+
By default, endpoints are unauthenticated. Configure a custom auth strategy:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# config/initializers/ground_control.rb
|
|
29
|
+
GroundControl::Api.authenticate_with = ->(controller) do
|
|
30
|
+
controller.head :unauthorized unless controller.current_user&.admin?
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Multi-application support
|
|
35
|
+
|
|
36
|
+
If you run multiple queue backends, register them explicitly:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
MissionControl::Jobs.applications.add("Primary", {
|
|
40
|
+
"solid_queue" => ActiveJob::QueueAdapters::SolidQueueAdapter.new
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
MissionControl::Jobs.applications.add("Legacy", {
|
|
44
|
+
"resque" => ActiveJob::QueueAdapters::ResqueAdapter.new(Redis.new)
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Otherwise, the engine auto-registers your default `active_job.queue_adapter`.
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
All endpoints are scoped under `/applications/:application_id`. When only one application is registered, the application ID can be inferred.
|
|
53
|
+
|
|
54
|
+
### Queues
|
|
55
|
+
|
|
56
|
+
| Method | Path | Description |
|
|
57
|
+
|--------|------|-------------|
|
|
58
|
+
| GET | `/applications/:app/queues` | List all queues |
|
|
59
|
+
| GET | `/applications/:app/queues/:id` | Show queue with paginated jobs |
|
|
60
|
+
| POST | `/applications/:app/queues/:id/pause` | Pause a queue |
|
|
61
|
+
| DELETE | `/applications/:app/queues/:id/pause` | Resume a queue |
|
|
62
|
+
|
|
63
|
+
### Jobs
|
|
64
|
+
|
|
65
|
+
| Method | Path | Description |
|
|
66
|
+
|--------|------|-------------|
|
|
67
|
+
| GET | `/applications/:app/:status/jobs` | List jobs by status (paginated, filterable) |
|
|
68
|
+
| GET | `/applications/:app/jobs/:id` | Show job details |
|
|
69
|
+
| POST | `/applications/:app/jobs/:id/retry` | Retry a failed job |
|
|
70
|
+
| POST | `/applications/:app/jobs/:id/discard` | Discard a job |
|
|
71
|
+
| POST | `/applications/:app/jobs/:id/dispatch` | Dispatch a blocked/scheduled job |
|
|
72
|
+
| POST | `/applications/:app/jobs/bulk_retries` | Retry all matching failed jobs |
|
|
73
|
+
| POST | `/applications/:app/jobs/bulk_discards` | Discard all matching failed jobs |
|
|
74
|
+
|
|
75
|
+
**Filtering:** Pass `filter[job_class_name]`, `filter[queue_name]`, `filter[finished_at_start]`, `filter[finished_at_end]` as query params.
|
|
76
|
+
|
|
77
|
+
**Pagination:** Pass `page` as a query param. Default page size is 10, configurable via `GroundControl::Api.page_size`.
|
|
78
|
+
|
|
79
|
+
### Workers
|
|
80
|
+
|
|
81
|
+
| Method | Path | Description |
|
|
82
|
+
|--------|------|-------------|
|
|
83
|
+
| GET | `/applications/:app/workers` | List workers (if adapter supports it) |
|
|
84
|
+
| GET | `/applications/:app/workers/:id` | Show worker details |
|
|
85
|
+
|
|
86
|
+
### Recurring Tasks
|
|
87
|
+
|
|
88
|
+
| Method | Path | Description |
|
|
89
|
+
|--------|------|-------------|
|
|
90
|
+
| GET | `/applications/:app/recurring_tasks` | List recurring tasks |
|
|
91
|
+
| GET | `/applications/:app/recurring_tasks/:id` | Show task with execution history |
|
|
92
|
+
| PATCH | `/applications/:app/recurring_tasks/:id` | Trigger a recurring task |
|
|
93
|
+
|
|
94
|
+
### Feature Discovery
|
|
95
|
+
|
|
96
|
+
| Method | Path | Description |
|
|
97
|
+
|--------|------|-------------|
|
|
98
|
+
| GET | `/applications/:app/features` | Adapter capabilities (supported statuses, workers, recurring tasks, queue pausing) |
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
GroundControl::Api.authenticate_with = ->(controller) { ... } # Auth callback
|
|
104
|
+
GroundControl::Api.page_size = 25 # Default: 10
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Architecture
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
ground_control-api
|
|
111
|
+
├── app/controllers/ground_control/api/ # JSON API controllers
|
|
112
|
+
├── app/resources/ground_control/api/ # Alba serialization resources
|
|
113
|
+
├── app/controllers/concerns/ # JobFilters, AdapterFeatures, ErrorHandling
|
|
114
|
+
├── config/routes.rb # Engine routes
|
|
115
|
+
└── lib/ground_control/api/ # Engine, config, version
|
|
116
|
+
↓ depends on
|
|
117
|
+
mission_control-jobs # Adapter layer, query objects, models
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
bundle install
|
|
124
|
+
bundle exec rspec # Run tests
|
|
125
|
+
bundle exec rubocop # Lint
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|