Skip to content

changes to export course tar file#2319

Open
melissajinn wants to merge 10 commits intoec2-testingfrom
exportCourse
Open

changes to export course tar file#2319
melissajinn wants to merge 10 commits intoec2-testingfrom
exportCourse

Conversation

@melissajinn
Copy link
Copy Markdown

yaml is dump from table with assessments, users, cuds, and submissions, and new tar format in general

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

This PR introduces EC2/AWS autograder support featuring encrypted credential storage, instance type selection, and conditional job payload augmentation. Changes span frontend UI, controller logic, model validations, database schema, configuration flags, and comprehensive test coverage for the new authentication and provisioning workflow.

Changes

Cohort / File(s) Summary
Frontend JavaScript
app/assets/javascripts/autograder.js
New script initializing DOM-ready jQuery handlers for access key input toggling, Materialize tooltip initialization, and instance type dropdown highlighting on hover.
Form Templates & UI
app/views/autograders/_form.html.erb, app/views/autograders/_basic_settings.html.erb, app/views/autograders/_ec2_settings.html.erb
Refactored autograder form into tabbed UI with basic settings (image, timeout, makefile/tar uploads) and conditional EC2 settings pane (instance type dropdown, access key fields with validation hints). Added helper text, tooltips, and grouped instance type options.
Scoreboard Rendering
app/views/scoreboards/show.html.erb
Enhanced nil-safety for scoreboard cell rendering with fallback error icon display when entry values are missing or out of bounds.
Autograders Controller
app/controllers/autograders_controller.rb
Extended autograder_params to permit new EC2 fields (use_access_key, access_key, access_key_id, instance_type). Updated create to initialize EC2 fields with defaults. Rewired update to normalize credential handling based on use_access_key flag with conditional blank-clearing and nil-forcing logic.
Jobs & Scoreboards Controllers
app/controllers/jobs_controller.rb, app/controllers/scoreboards_controller.rb
Modified formatRawJob job-state derivation to use three-way trace-message mapping (Succeeded/Failed/Unknown State). Wrapped scoreboard JSON parsing in error handling with optional colspec-based padding and validation.
Autograder Model
app/models/autograder.rb
Added encrypted storage for access_key and access_key_id. Introduced INSTANCE_TYPES allowlist constant. Added validations for instance_type inclusion and conditional access key presence/format checks guarded by use_access_key?.
Assessment & Course Models
app/models/assessment.rb, app/models/course.rb
Changed Assessment#asmt_yaml_path to use fixed filename assignment_config.yml. Expanded Course#generate_tar with optional export sections (users, course_user_data, enhanced assessments with embedded penalties) and direct submission YAML writing.
Autograder Core Helper
app/helpers/assessment_autograde_core.rb
Modified tango_add_job to conditionally augment job payload with EC2 fields (ec2Vmms, instanceType, accessKey, accessKeyId) when Rails.configuration.x.ec2_ssh is enabled, based on autograder configuration.
Configuration & Logging
config/environments/development.rb, config/environments/production.rb.template, config/initializers/filter_parameter_logging.rb
Added config.x.ec2_ssh feature flag (enabled in development, disabled in production template). Expanded filtered parameters to include access_key and access_key_id for security.
Database Migrations
db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb, db/migrate/20241211042124_add_use_access_key_to_autograder.rb, db/migrate/20251021034813_encrypt_autograder_credentials.rb, db/schema.rb
Added three sequential migrations: initial EC2 field columns, use_access_key boolean flag, and encrypted credential migration (copies plaintext to ciphertext, removes plaintext columns). Updated schema with new autograders columns, new ssh_keys table, charset/collation standardization, and foreign key additions.
Tests & Factories
spec/factories/autograders.rb, spec/helpers/assessment_autograde_core_spec.rb, spec/models/autograder_spec.rb
Updated autograder factory with default instance_type and use_access_key values. Added comprehensive helper spec testing EC2 payload construction under both feature-flag states and credential scenarios. Added model validation specs for instance type allowlist and conditional access key presence/format rules.

Sequence Diagram

sequenceDiagram
    actor User
    participant Form as Autograder Form
    participant Controller as AutogradersController
    participant Model as Autograder Model
    participant Helper as AssessmentAutogradeCore
    participant TangoClient
    
    User->>Form: Submit EC2 settings (use_access_key, instanceType, credentials)
    Form->>Controller: POST /autograders/:id (use_access_key, access_key, access_key_id, instance_type)
    Controller->>Controller: Transform params based on use_access_key flag
    Controller->>Model: update(transformed_params)
    Model->>Model: Validate instance_type, access_key format (if use_access_key enabled)
    Model-->>Model: Encrypt access_key & access_key_id
    Model-->>Controller: Autograder saved
    Controller-->>User: Success response
    
    rect rgb(200, 150, 100, 0.5)
    Note over Helper,TangoClient: When EC2 SSH is enabled and job is submitted
    end
    
    User->>Helper: Submit job for grading
    Helper->>Helper: Build job_properties hash
    alt ec2_ssh enabled
        Helper->>Model: Read instance_type, use_access_key, access_key, access_key_id
        Model-->>Helper: EC2 configuration
        Helper->>Helper: Augment payload with ec2Vmms, instanceType, accessKey, accessKeyId
    end
    Helper->>Helper: JSON encode job_properties
    Helper->>TangoClient: addjob(job_properties)
    TangoClient-->>Helper: Job queued with EC2 provisioning
    Helper-->>User: Job submitted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #2209: Related through autograder form/view refactoring and controller field handling changes.

Suggested reviewers

  • dwang3851
  • coder6583
  • jhs-panda
🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is incomplete and does not follow the required template structure, missing key sections like Motivation/Context, Testing, and Types of changes. Fill out the PR description template completely, including Motivation and Context, How Has This Been Tested, Types of changes, and Checklist sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'changes to export course tar file' is vague and lacks specificity about the actual changes implemented. Provide a more specific title that clearly indicates the scope of changes, such as 'Refactor course tar export to include assessments, users, and submissions as YAML' or similar.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exportCourse

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (7)
app/controllers/scoreboards_controller.rb (1)

310-310: Use a descriptive error message when raising.

raise StandardError without a message results in an empty or unhelpful e.message when caught at line 340. Consider raising with a meaningful message for better logging.

♻️ Proposed fix
-        raise StandardError
+        raise StandardError, "Scoreboard result is not an array"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/scoreboards_controller.rb` at line 310, Replace the bare
raise StandardError with a descriptive message so the rescue that logs e.message
(the handler near the rescue block) gets useful context; specifically update the
raise in scoreboards_controller (the raise StandardError occurrence) to raise
StandardError, "Descriptive message about what failed (e.g. 'Failed to update
scoreboard: missing params or invalid state')" or raise a more specific custom
error class if available, ensuring the message explains the failure and includes
relevant variables (like scoreboard id or params) for better logging.
app/models/course.rb (1)

380-389: Consider explicit attribute selection for consistency.

The users export (lines 369-372) explicitly selects attributes, but here cud.attributes.to_yaml exports all attributes including timestamps and foreign keys. While this works for a full backup, consider using explicit attribute selection for consistency and to avoid exporting potentially unnecessary internal fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/course.rb` around lines 380 - 389, The export of course user data
currently uses cud.attributes.to_yaml which dumps all fields (including
timestamps and foreign keys); change the export to explicitly select the same
set of attributes used by the users export to ensure consistency — locate the
loop over course_user_data in Course (the course_user_data.each block) and
replace cud.attributes.to_yaml with a YAML serialization of a filtered hash
(e.g., build a hash via cud.slice(...) or manually select attribute names)
containing only the intended fields (IDs, user-facing fields, and any specific
metadata) before writing to the tar file.
spec/models/autograder_spec.rb (1)

14-18: Consider using a deterministic test value.

Using .sample makes the test non-deterministic. While it will pass, a specific value would make failures easier to reproduce.

Suggested change
     it "allows listed instance types" do
-      autograder.instance_type = Autograder::INSTANCE_TYPES.sample
+      autograder.instance_type = "t3.micro"

       expect(autograder).to be_valid
     end

Alternatively, if you want to test all types, consider using Autograder::INSTANCE_TYPES.each with a shared example.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/models/autograder_spec.rb` around lines 14 - 18, The test uses
Autograder::INSTANCE_TYPES.sample which makes it non-deterministic; change the
spec in spec/models/autograder_spec.rb to set autograder.instance_type to a
deterministic value (e.g., Autograder::INSTANCE_TYPES.first or a specific known
instance-type string) so the example "allows listed instance types" is
repeatable, or alternatively expand the spec to iterate
Autograder::INSTANCE_TYPES.each and use a shared example to assert validity for
every type.
app/views/autograders/_form.html.erb (1)

5-7: Consider simplifying boolean check.

Using .presence on a boolean (ec2_ssh) is unconventional. While it works (returns nil for false, true for true), a direct boolean check is clearer.

Suggested simplification
-      <% if Rails.configuration.x.ec2_ssh.presence %>
+      <% if Rails.configuration.x.ec2_ssh %>

Same applies to line 27.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_form.html.erb` around lines 5 - 7, The conditional
uses Rails.configuration.x.ec2_ssh.presence which is unconventional for a
boolean; change the checks that reference Rails.configuration.x.ec2_ssh (both
the occurrence that calls list.append("ec2") and the similar occurrence around
line 27) to a direct boolean conditional using Rails.configuration.x.ec2_ssh so
the intent is clear and idiomatic.
app/views/autograders/_ec2_settings.html.erb (1)

23-61: Keep the instance catalog in one place.

This list duplicates Autograder::INSTANCE_TYPES in app/models/autograder.rb, while the default t3.micro is also hard-coded in app/controllers/autograders_controller.rb and app/helpers/assessment_autograde_core.rb. The next instance-type change now has multiple places to keep in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_ec2_settings.html.erb` around lines 23 - 61, The view
duplicates instance-type data that already belongs in the model; replace the
hard-coded ec2_instance_options in _ec2_settings.html.erb with a reference to
Autograder::INSTANCE_TYPES (or move the grouped catalog into
Autograder::INSTANCE_TYPES if it isn’t already), and remove/hard-coded defaults
elsewhere by centralizing the default instance (e.g., add
Autograder::DEFAULT_INSTANCE or derive it from Autograder::INSTANCE_TYPES) then
update autograders_controller (where t3.micro is set) and
assessment_autograde_core to use that centralized constant; ensure all code
references the model constant (Autograder::INSTANCE_TYPES /
Autograder::DEFAULT_INSTANCE) so the catalog and default live in one place.
db/schema.rb (2)

62-63: Timestamp type has 2038 year limit.

TIMESTAMP columns in MySQL are limited to dates up to '2038-01-19 03:14:07' UTC. For start_date/end_date on announcements, this is likely acceptable for typical use cases, but worth noting. The benefit is automatic UTC timezone normalization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/schema.rb` around lines 62 - 63, The TIMESTAMP columns start_date and
end_date in db/schema.rb can hit the 2038 limit; change their type to datetime
(or Rails' t.datetime) to support a wider range and avoid overflow, and add a
short migration to alter the announcements table columns from timestamp to
datetime (preserve nullability/defaults and set precision if needed) so existing
data and UTC handling remain correct; update any model/schema references to use
start_date and end_date as datetimes.

445-457: Foreign key type mismatch with referenced tables.

watchlist_instances uses bigint for course_user_datum_id and course_id, but the referenced tables (course_user_data, courses) have integer primary keys. MySQL handles this implicitly, but for consistency, consider aligning types. This also applies to watchlist_configurations.course_id (line 438).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/schema.rb` around lines 445 - 457, The watchlist tables have column type
mismatches: change watchlist_instances.course_user_datum_id and
watchlist_instances.course_id (and watchlist_configurations.course_id) from
bigint to integer to match the referenced primary keys on course_user_data and
courses; implement this by adding a migration that alters those columns to
:integer (or recreate them with correct type), update any associated indexes and
foreign key constraints to use the new type, and run schema dump so schema.rb
reflects the integer type for course_user_datum_id, course_id and the
watchlist_configurations.course_id columns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/jobs_controller.rb`:
- Around line 250-255: The else branch in the JobsController terminal-state
logic currently sets job[:state] = "Unknown State" which surfaces ambiguous
values; change that fallback to a stable terminal state such as "Completed" (or
another agreed explicit terminal label) so any dead job that doesn't match the
two string checks shows a safe terminal state; update the else branch where
rjob["trace"][-1] is inspected (the block that sets job[:state] for "Autodriver
returned normally" and "Error") to assign "Completed" instead of "Unknown
State".

In `@app/models/assessment.rb`:
- Around line 97-99: The change to asmt_yaml_path returning a fixed
"assignment_config.yml" breaks compatibility with existing <name>.yml files;
update load_yaml to first try File.open(asmt_yaml_path) and if that file does
not exist fallback to File.open("#{name}.yml") (where name is the assessment
name used previously), and likewise update dump_yaml to write both the new
assignment_config.yml and (optionally) update/rename the old "#{name}.yml" to
avoid orphaning; also adjust the tar import/filtering logic that uses
asmt_yaml_path (in the import path) to accept either "assignment_config.yml" or
"#{name}.yml" during extraction so older tarballs still import.

In `@app/models/course.rb`:
- Around line 406-417: The current loop loads submissions per assessment causing
N+1 queries and duplicates iterating assessments; fix by loading submissions in
bulk (either eager load via assessments.includes(:submissions) before the export
block or, better, use the existing has_many :submissions, through: :assessments
and iterate course.submissions.find_each) and write each submission once to
File.join(course_dir, "submissions", "submission_#{submission.id}.yml") using
tar.add_file; ensure you remove the per-assessment submissions.each loop and the
duplicate assessments iteration so submissions are streamed efficiently with
find_each to reduce memory and DB queries.

In `@app/views/autograders/_basic_settings.html.erb`:
- Around line 30-35: The download link helpers in the view use
download_file_course_assessment_autograder_path incorrectly: they must be called
with the nested route positional objects (`@course`, `@assessment`) and should not
expose server file system paths via file_path; update the calls that reference
`@makefile_exists` and `@tar_exists` to pass `@course` and `@assessment` as the first
two positional arguments and only pass file_key (e.g., 'makefile' or 'tar') as a
named param expected by AutogradersController#download_file, removing file_path
from the public URL generation so the controller continues to reconstruct the
real path from file_key.

In `@app/views/autograders/_ec2_settings.html.erb`:
- Around line 64-76: The template raises ArgumentError when
`@autograder.instance_type` is stale; remove that raise and instead coerce the
selection to a safe fallback: compute selected_instance_type from
`@autograder.instance_type` but if it is not included in valid_instance_types
(derived from ec2_instance_options) set selected_instance_type to nil or a
default valid value, then pass that coerced selected_instance_type into
grouped_options_for_select (keep references to selected_instance_type,
valid_instance_types, ec2_instance_options and f.select) so the view renders
without throwing and allows instructors to correct the record.

In `@db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb`:
- Around line 3-8: The migration currently removes plaintext columns access_key
and access_key_id before the encrypting migration runs, which can delete the
only existing secrets; modify this migration
(add_ec2_ssh_fields_to_autograders.rb) to NOT remove the plaintext columns
(remove or comment out the remove_column lines for access_key and access_key_id)
so that the encryption migration
(db/migrate/20251021034813_encrypt_autograder_credentials.rb) can copy values
into access_key_ciphertext and access_key_id_ciphertext first; keep adding the
new columns instance_type, access_key_ciphertext, and access_key_id_ciphertext
as-is and only drop the plaintext columns after the
encrypt_autograder_credentials migration has successfully migrated data.

In `@db/migrate/20251021034813_encrypt_autograder_credentials.rb`:
- Around line 4-5: The migration unconditionally calls add_column for
access_key_ciphertext and access_key_id_ciphertext which will conflict with the
earlier migration
db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb; guard the column
additions by checking for their existence (use column_exists?(:autograders,
:access_key_ciphertext) and column_exists?(:autograders,
:access_key_id_ciphertext)) and only call add_column when the column is missing
so the migration becomes safe to run in either order.

---

Nitpick comments:
In `@app/controllers/scoreboards_controller.rb`:
- Line 310: Replace the bare raise StandardError with a descriptive message so
the rescue that logs e.message (the handler near the rescue block) gets useful
context; specifically update the raise in scoreboards_controller (the raise
StandardError occurrence) to raise StandardError, "Descriptive message about
what failed (e.g. 'Failed to update scoreboard: missing params or invalid
state')" or raise a more specific custom error class if available, ensuring the
message explains the failure and includes relevant variables (like scoreboard id
or params) for better logging.

In `@app/models/course.rb`:
- Around line 380-389: The export of course user data currently uses
cud.attributes.to_yaml which dumps all fields (including timestamps and foreign
keys); change the export to explicitly select the same set of attributes used by
the users export to ensure consistency — locate the loop over course_user_data
in Course (the course_user_data.each block) and replace cud.attributes.to_yaml
with a YAML serialization of a filtered hash (e.g., build a hash via
cud.slice(...) or manually select attribute names) containing only the intended
fields (IDs, user-facing fields, and any specific metadata) before writing to
the tar file.

In `@app/views/autograders/_ec2_settings.html.erb`:
- Around line 23-61: The view duplicates instance-type data that already belongs
in the model; replace the hard-coded ec2_instance_options in
_ec2_settings.html.erb with a reference to Autograder::INSTANCE_TYPES (or move
the grouped catalog into Autograder::INSTANCE_TYPES if it isn’t already), and
remove/hard-coded defaults elsewhere by centralizing the default instance (e.g.,
add Autograder::DEFAULT_INSTANCE or derive it from Autograder::INSTANCE_TYPES)
then update autograders_controller (where t3.micro is set) and
assessment_autograde_core to use that centralized constant; ensure all code
references the model constant (Autograder::INSTANCE_TYPES /
Autograder::DEFAULT_INSTANCE) so the catalog and default live in one place.

In `@app/views/autograders/_form.html.erb`:
- Around line 5-7: The conditional uses Rails.configuration.x.ec2_ssh.presence
which is unconventional for a boolean; change the checks that reference
Rails.configuration.x.ec2_ssh (both the occurrence that calls list.append("ec2")
and the similar occurrence around line 27) to a direct boolean conditional using
Rails.configuration.x.ec2_ssh so the intent is clear and idiomatic.

In `@db/schema.rb`:
- Around line 62-63: The TIMESTAMP columns start_date and end_date in
db/schema.rb can hit the 2038 limit; change their type to datetime (or Rails'
t.datetime) to support a wider range and avoid overflow, and add a short
migration to alter the announcements table columns from timestamp to datetime
(preserve nullability/defaults and set precision if needed) so existing data and
UTC handling remain correct; update any model/schema references to use
start_date and end_date as datetimes.
- Around line 445-457: The watchlist tables have column type mismatches: change
watchlist_instances.course_user_datum_id and watchlist_instances.course_id (and
watchlist_configurations.course_id) from bigint to integer to match the
referenced primary keys on course_user_data and courses; implement this by
adding a migration that alters those columns to :integer (or recreate them with
correct type), update any associated indexes and foreign key constraints to use
the new type, and run schema dump so schema.rb reflects the integer type for
course_user_datum_id, course_id and the watchlist_configurations.course_id
columns.

In `@spec/models/autograder_spec.rb`:
- Around line 14-18: The test uses Autograder::INSTANCE_TYPES.sample which makes
it non-deterministic; change the spec in spec/models/autograder_spec.rb to set
autograder.instance_type to a deterministic value (e.g.,
Autograder::INSTANCE_TYPES.first or a specific known instance-type string) so
the example "allows listed instance types" is repeatable, or alternatively
expand the spec to iterate Autograder::INSTANCE_TYPES.each and use a shared
example to assert validity for every type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c5d36992-15b1-42d3-8596-d46681b2942a

📥 Commits

Reviewing files that changed from the base of the PR and between 674efe9 and b3f1bb9.

📒 Files selected for processing (22)
  • app/assets/javascripts/autograder.js
  • app/controllers/autograders_controller.rb
  • app/controllers/jobs_controller.rb
  • app/controllers/scoreboards_controller.rb
  • app/helpers/assessment_autograde_core.rb
  • app/models/assessment.rb
  • app/models/autograder.rb
  • app/models/course.rb
  • app/views/autograders/_basic_settings.html.erb
  • app/views/autograders/_ec2_settings.html.erb
  • app/views/autograders/_form.html.erb
  • app/views/scoreboards/show.html.erb
  • config/environments/development.rb
  • config/environments/production.rb.template
  • config/initializers/filter_parameter_logging.rb
  • db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb
  • db/migrate/20241211042124_add_use_access_key_to_autograder.rb
  • db/migrate/20251021034813_encrypt_autograder_credentials.rb
  • db/schema.rb
  • spec/factories/autograders.rb
  • spec/helpers/assessment_autograde_core_spec.rb
  • spec/models/autograder_spec.rb

Comment thread app/controllers/jobs_controller.rb Outdated
Comment on lines +250 to +255
elsif rjob["trace"][-1].include?("Autodriver returned normally")
job[:state] = "Succeeded"
elsif rjob["trace"][-1].split("|")[1].include? "Error"
job[:state] = "Failed"
else
job[:state] = "Completed"
job[:state] = "Failed" if rjob["trace"][-1].split("|")[1].include? "Error"
job[:state] = "Unknown State"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a stable fallback state instead of "Unknown State" for terminal jobs.

Line 255 now emits "Unknown State" for any dead job that misses the two string checks, and that value is rendered directly in list/detail views. This can surface normal terminal jobs as ambiguous to users. Prefer a safe terminal fallback (e.g., "Completed") or an explicit exhaustive mapping.

Proposed minimal fix
-    elsif rjob["trace"][-1].include?("Autodriver returned normally")
-        job[:state] = "Succeeded"
-    elsif rjob["trace"][-1].split("|")[1].include? "Error"
+    elsif rjob["trace"][-1].include?("Autodriver returned normally")
+      job[:state] = "Succeeded"
+    elsif rjob["trace"][-1].split("|")[1].include?("Error")
       job[:state] = "Failed"
     else
-      job[:state] = "Unknown State"
+      job[:state] = "Completed"
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
elsif rjob["trace"][-1].include?("Autodriver returned normally")
job[:state] = "Succeeded"
elsif rjob["trace"][-1].split("|")[1].include? "Error"
job[:state] = "Failed"
else
job[:state] = "Completed"
job[:state] = "Failed" if rjob["trace"][-1].split("|")[1].include? "Error"
job[:state] = "Unknown State"
elsif rjob["trace"][-1].include?("Autodriver returned normally")
job[:state] = "Succeeded"
elsif rjob["trace"][-1].split("|")[1].include?("Error")
job[:state] = "Failed"
else
job[:state] = "Completed"
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/jobs_controller.rb` around lines 250 - 255, The else branch
in the JobsController terminal-state logic currently sets job[:state] = "Unknown
State" which surfaces ambiguous values; change that fallback to a stable
terminal state such as "Completed" (or another agreed explicit terminal label)
so any dead job that doesn't match the two string checks shows a safe terminal
state; update the else branch where rjob["trace"][-1] is inspected (the block
that sets job[:state] for "Autodriver returned normally" and "Error") to assign
"Completed" instead of "Unknown State".

Comment thread app/models/assessment.rb
Comment on lines 97 to 99
def asmt_yaml_path
path "#{name}.yml"
path "assignment_config.yml"
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find assessment YAML files in course directories
echo "=== Searching for assessment YAML files ==="
fd -t f '\.yml$' courses/ 2>/dev/null | head -30

echo ""
echo "=== Checking for assignment_config.yml files ==="
fd -t f 'assignment_config.yml' courses/ 2>/dev/null | head -20

echo ""
echo "=== Checking tar import logic for yaml path comparison ==="
rg -n 'asmt_yaml_path' --type ruby -C 3

Repository: autolab/Autolab

Length of output: 2059


🏁 Script executed:

#!/bin/bash
# Check the git history to see what the previous implementation was
echo "=== Git diff for asmt_yaml_path method ==="
git show HEAD:app/models/assessment.rb 2>/dev/null | sed -n '90,110p' || echo "Git not available, checking current implementation context"

echo ""
echo "=== Checking backwards_compatibility method ==="
rg -n 'backwards_compatibility' app/models/assessment.rb -A 10

echo ""
echo "=== Checking if there's any fallback logic in load_yaml ==="
rg -n 'def load_yaml' app/models/assessment.rb -A 15

echo ""
echo "=== Search for any reference to old filename patterns ==="
rg '\.yml' app/models/assessment.rb -n

Repository: autolab/Autolab

Length of output: 2036


🏁 Script executed:

#!/bin/bash
# Check git log for asmt_yaml_path changes
echo "=== Recent commits touching asmt_yaml_path ==="
git log --oneline -n 30 -- app/models/assessment.rb 2>/dev/null | head -15

echo ""
echo "=== Diff of recent changes to assessment.rb ==="
git diff HEAD~5..HEAD -- app/models/assessment.rb 2>/dev/null | grep -A 10 -B 5 'asmt_yaml_path' || echo "Unable to get diff"

echo ""
echo "=== Check for any migration files or upgrade logic ==="
fd -t f 'migrate' --extension rb | xargs rg -l 'asmt_yaml_path|assignment_config' 2>/dev/null

echo ""
echo "=== Check for file reading with name-derived path ==="
rg '#{.*name.*}.*yml' app/models/assessment.rb

Repository: autolab/Autolab

Length of output: 496


🏁 Script executed:

#!/bin/bash
# Check the specific commit
echo "=== Commit details for export tar updates ==="
git show b3f1bb9 --stat 2>/dev/null

echo ""
echo "=== Full diff of asmt_yaml_path change ==="
git show b3f1bb9 -- app/models/assessment.rb 2>/dev/null | grep -A 20 -B 20 'asmt_yaml_path' | head -50

echo ""
echo "=== Check for name-based yml references ==="
rg 'name.*yml|yml.*name' app/models/assessment.rb -n

echo ""
echo "=== Look for any file existence checks or fallbacks ==="
rg -n 'File\.exist|FileUtils|File\.open' app/models/assessment.rb | grep -i yml -A 2 -B 2

Repository: autolab/Autolab

Length of output: 50371


🏁 Script executed:

#!/bin/bash
# Check if load_yaml has any error handling for missing files
echo "=== Full load_yaml implementation ==="
rg -n 'def load_yaml' app/models/assessment.rb -A 20

echo ""
echo "=== Check for File.exist? checks near asmt_yaml_path ==="
rg -n 'File\.exist|asmt_yaml_path' app/models/assessment.rb -B 2 -A 2

echo ""
echo "=== Look for rescue or error handling in dump/load yaml methods ==="
rg -n 'def (dump_yaml|load_yaml)' app/models/assessment.rb -A 25 | head -60

echo ""
echo "=== Check unique_source_config_file_path referenced in tar extraction ==="
rg -n 'unique_source_config_file_path' app/models/assessment.rb -A 5 -B 5

Repository: autolab/Autolab

Length of output: 5305


🏁 Script executed:

#!/bin/bash
# Check how load_yaml is called - is it used in tar import?
echo "=== Where is load_yaml called? ==="
rg -n 'load_yaml' app/ -t ruby --type-add 'ruby:*.rb'

echo ""
echo "=== Check tar extraction and assessment creation logic ==="
rg -n 'extract.*tar|new_assessment|Assessment\.new' app/controllers/assessments_controller.rb -B 3 -A 3 | head -80

echo ""
echo "=== Check if there's any backward compatibility for yaml file location ==="
rg -n 'assignment_config\.yml|\.yml' app/models/assessment.rb | grep -v '^\s*#'

Repository: autolab/Autolab

Length of output: 4185


🏁 Script executed:

#!/bin/bash
# Check what happens before load_yaml is called - any existence checks?
echo "=== Code around load_yaml call in assessments_controller ==="
sed -n '295,315p' app/controllers/assessments_controller.rb

echo ""
echo "=== Check if there's any fallback filename logic in dump_yaml or load_yaml ==="
sed -n '295,345p' app/models/assessment.rb | grep -E 'def (dump_yaml|load_yaml)|File\.|asmt_yaml_path|\.yml|exist'

echo ""
echo "=== Check if name-based yml path is used anywhere else ==="
rg "#{name}" app/models/assessment.rb | grep -i yml

Repository: autolab/Autolab

Length of output: 1655


🏁 Script executed:

#!/bin/bash
# Verify when asmt_yaml_path was introduced and what the previous implementation was
echo "=== Check git log for asmt_yaml_path history ==="
git log --oneline -p -- app/models/assessment.rb 2>/dev/null | grep -A 10 -B 10 'asmt_yaml_path' | head -50

echo ""
echo "=== Check if there are any other references to assignment_config.yml in codebase ==="
rg 'assignment_config' app/ --type ruby

echo ""
echo "=== Check tar extraction filtering logic more carefully ==="
sed -n '165,180p' app/controllers/assessments_controller.rb

Repository: autolab/Autolab

Length of output: 2653


Breaking change: Fixed filename breaks backwards compatibility with existing assessments.

Changing from a name-derived path to a fixed "assignment_config.yml" creates compatibility issues:

  1. Existing assessments: Code comments reference the old pattern (homework01.yml, homework02.yml). Current assessments have configs at <assessment_name>.yml. After this change, load_yaml and dump_yaml reference a different file.
  2. Tar import/export: Old assessment tarballs contain <name>.yml, but load_yaml (line 340) opens asmt_yaml_path directly with no fallback—it will fail with FileNotFound.
  3. Orphan files: dump_yaml creates assignment_config.yml while leaving the old <name>.yml file orphaned.
  4. Tar extraction filtering: The tar import logic in assessments_controller.rb (line 171) filters by asmt_yaml_path—mismatched paths during import could cause issues.

Add fallback logic in load_yaml to check for the old filename if the new one doesn't exist, or implement a migration strategy for existing assessments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/assessment.rb` around lines 97 - 99, The change to asmt_yaml_path
returning a fixed "assignment_config.yml" breaks compatibility with existing
<name>.yml files; update load_yaml to first try File.open(asmt_yaml_path) and if
that file does not exist fallback to File.open("#{name}.yml") (where name is the
assessment name used previously), and likewise update dump_yaml to write both
the new assignment_config.yml and (optionally) update/rename the old
"#{name}.yml" to avoid orphaning; also adjust the tar import/filtering logic
that uses asmt_yaml_path (in the import path) to accept either
"assignment_config.yml" or "#{name}.yml" during extraction so older tarballs
still import.

Comment thread app/models/course.rb
Comment on lines +406 to 417
# save submissions
if export_configs&.include?('submissions')
tar.mkdir File.join(course_dir, "submissions"), mode

assessments.each do |assessment|
asmt_dir = assessment.name
assessment.dump_yaml
filter = [assessment.handin_directory_path]
assessment.load_dir_to_tar(base_path, asmt_dir, tar, filter, course_dir)
assessment.submissions.each do |submission|
tar.add_file File.join(course_dir, "submissions", "submission_#{submission.id}.yml"), mode do |tar_file|
tar_file.write(submission.attributes.to_yaml)
end
end
end
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

N+1 query issue: submissions are loaded separately for each assessment.

The current implementation triggers a separate database query for each assessment's submissions. For a course with many assessments, this causes significant performance degradation.

Additionally, assessments is iterated twice (lines 395 and 410) when both export options are enabled.

🔧 Proposed fix using eager loading or the through relationship

Option 1: Use eager loading

         # save submissions
         if export_configs&.include?('submissions')
           tar.mkdir File.join(course_dir, "submissions"), mode

-          assessments.each do |assessment|
-            assessment.submissions.each do |submission|
+          assessments.includes(:submissions).each do |assessment|
+            assessment.submissions.each do |submission|
               tar.add_file File.join(course_dir, "submissions", "submission_#{submission.id}.yml"), mode do |tar_file|
                 tar_file.write(submission.attributes.to_yaml)
               end
             end
           end
         end

Option 2: Use the existing has_many :submissions, through: :assessments relationship

         # save submissions
         if export_configs&.include?('submissions')
           tar.mkdir File.join(course_dir, "submissions"), mode

-          assessments.each do |assessment|
-            assessment.submissions.each do |submission|
-              tar.add_file File.join(course_dir, "submissions", "submission_#{submission.id}.yml"), mode do |tar_file|
-                tar_file.write(submission.attributes.to_yaml)
-              end
+          submissions.find_each do |submission|
+            tar.add_file File.join(course_dir, "submissions", "submission_#{submission.id}.yml"), mode do |tar_file|
+              tar_file.write(submission.attributes.to_yaml)
             end
           end
         end

Option 2 also uses find_each for better memory efficiency with large datasets.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# save submissions
if export_configs&.include?('submissions')
tar.mkdir File.join(course_dir, "submissions"), mode
assessments.each do |assessment|
asmt_dir = assessment.name
assessment.dump_yaml
filter = [assessment.handin_directory_path]
assessment.load_dir_to_tar(base_path, asmt_dir, tar, filter, course_dir)
assessment.submissions.each do |submission|
tar.add_file File.join(course_dir, "submissions", "submission_#{submission.id}.yml"), mode do |tar_file|
tar_file.write(submission.attributes.to_yaml)
end
end
end
end
# save submissions
if export_configs&.include?('submissions')
tar.mkdir File.join(course_dir, "submissions"), mode
assessments.includes(:submissions).each do |assessment|
assessment.submissions.each do |submission|
tar.add_file File.join(course_dir, "submissions", "submission_#{submission.id}.yml"), mode do |tar_file|
tar_file.write(submission.attributes.to_yaml)
end
end
end
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/course.rb` around lines 406 - 417, The current loop loads
submissions per assessment causing N+1 queries and duplicates iterating
assessments; fix by loading submissions in bulk (either eager load via
assessments.includes(:submissions) before the export block or, better, use the
existing has_many :submissions, through: :assessments and iterate
course.submissions.find_each) and write each submission once to
File.join(course_dir, "submissions", "submission_#{submission.id}.yml") using
tar.add_file; ensure you remove the per-assessment submissions.each loop and the
duplicate assessments iteration so submissions are streamed efficiently with
find_each to reduce memory and DB queries.

Comment on lines +30 to +35
<% unless @makefile_exists.nil? %>
<%= link_to "Download Makefile", download_file_course_assessment_autograder_path(file_path: @makefile_exists, file_key: 'makefile'), class: "btn" %>
<% end %>

<% unless @tar_exists.nil? %>
<%= link_to "Download Tar", download_file_course_assessment_autograder_path(file_path: @tar_exists, file_key: 'tar'), class: "btn" %>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd routes.rb . -x rg -n 'download_file|autograder' {}
rg -n 'download_file_course_assessment_autograder_path' app/views -C2

Repository: autolab/Autolab

Length of output: 921


🏁 Script executed:

cat config/routes.rb | grep -n -B15 -A5 "resource :autograder"

Repository: autolab/Autolab

Length of output: 720


🏁 Script executed:

fd -e rb app/controllers | xargs grep -l "autograders_controller" | head -1 | xargs cat | grep -n -A20 "def download_file"

Repository: autolab/Autolab

Length of output: 424


🏁 Script executed:

fd . app/controllers -name "*autograders*"

Repository: autolab/Autolab

Length of output: 289


🏁 Script executed:

fd . app/controllers -name "*autograders*" -x cat {} | grep -n -A20 "def download_file"

Repository: autolab/Autolab

Length of output: 289


🏁 Script executed:

rg -n "def download_file" app/controllers/

Repository: autolab/Autolab

Length of output: 302


🏁 Script executed:

rg -n "autograders_controller" app/controllers/ | head -1

Repository: autolab/Autolab

Length of output: 41


🏁 Script executed:

cat app/controllers/autograders_controller.rb | head -80

Repository: autolab/Autolab

Length of output: 3056


🏁 Script executed:

sed -n '105,150p' app/controllers/autograders_controller.rb

Repository: autolab/Autolab

Length of output: 1130


🏁 Script executed:

rg -n "@course" app/views/autograders/_basic_settings.html.erb

Repository: autolab/Autolab

Length of output: 151


🏁 Script executed:

rg -n "def edit" app/controllers/autograders_controller.rb -A30 | head -40

Repository: autolab/Autolab

Length of output: 1447


Build the download links from the nested route with proper positional parameters.

Lines 31 and 35 call download_file_course_assessment_autograder_path without passing @course and @assessment as positional route parameters, and they append file_path even though AutogradersController#download_file ignores that param and reconstructs the path from file_key instead. This breaks proper URL generation and exposes server-side paths in the rendered HTML.

💡 Suggested fix
-  <%= link_to "Download Makefile", download_file_course_assessment_autograder_path(file_path: `@makefile_exists`, file_key: 'makefile'), class: "btn" %>
+  <%= link_to "Download Makefile", download_file_course_assessment_autograder_path(`@course`, `@assessment`, file_key: "makefile"), class: "btn" %>
-  <%= link_to "Download Tar", download_file_course_assessment_autograder_path(file_path: `@tar_exists`, file_key: 'tar'), class: "btn" %>
+  <%= link_to "Download Tar", download_file_course_assessment_autograder_path(`@course`, `@assessment`, file_key: "tar"), class: "btn" %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_basic_settings.html.erb` around lines 30 - 35, The
download link helpers in the view use
download_file_course_assessment_autograder_path incorrectly: they must be called
with the nested route positional objects (`@course`, `@assessment`) and should not
expose server file system paths via file_path; update the calls that reference
`@makefile_exists` and `@tar_exists` to pass `@course` and `@assessment` as the first
two positional arguments and only pass file_key (e.g., 'makefile' or 'tar') as a
named param expected by AutogradersController#download_file, removing file_path
from the public URL generation so the controller continues to reconstruct the
real path from file_key.

Comment on lines +64 to +76
<%
selected_instance_type = @autograder.instance_type
valid_instance_types = ec2_instance_options.flat_map { |_group, instances| instances.map(&:last) }
if selected_instance_type.present? && !valid_instance_types.include?(selected_instance_type)
raise ArgumentError, "Invalid EC2 instance type: #{selected_instance_type}"
end
%>

<div style="margin-bottom: 5px;"><strong>EC2 Instance Type</strong></div>

<%= f.select :instance_type,
grouped_options_for_select(ec2_instance_options, @autograder.instance_type),
{ include_blank: false, label: "EC2 Instance Type" },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t raise from the template for stale instance_type values.

Lines 67-68 turn an invalid persisted value into a 500 on the edit page, which also blocks instructors from fixing the record. The model already rejects invalid saves; the view should fall back to a safe selection instead of crashing.

💡 Suggested fix
   selected_instance_type = `@autograder.instance_type`
   valid_instance_types = ec2_instance_options.flat_map { |_group, instances| instances.map(&:last) }
-  if selected_instance_type.present? && !valid_instance_types.include?(selected_instance_type)
-    raise ArgumentError, "Invalid EC2 instance type: #{selected_instance_type}"
-  end
+  selected_instance_type = "t3.micro" unless valid_instance_types.include?(selected_instance_type)
 %>
@@
 <%= f.select :instance_type,
-             grouped_options_for_select(ec2_instance_options, `@autograder.instance_type`),
+             grouped_options_for_select(ec2_instance_options, selected_instance_type),
              { include_blank: false, label: "EC2 Instance Type" },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_ec2_settings.html.erb` around lines 64 - 76, The
template raises ArgumentError when `@autograder.instance_type` is stale; remove
that raise and instead coerce the selection to a safe fallback: compute
selected_instance_type from `@autograder.instance_type` but if it is not included
in valid_instance_types (derived from ec2_instance_options) set
selected_instance_type to nil or a default valid value, then pass that coerced
selected_instance_type into grouped_options_for_select (keep references to
selected_instance_type, valid_instance_types, ec2_instance_options and f.select)
so the view renders without throwing and allows instructors to correct the
record.

Comment on lines +3 to +8
add_column :autograders, :instance_type, :string, default: "" unless column_exists?(:autograders, :instance_type)
add_column :autograders, :access_key_ciphertext, :text unless column_exists?(:autograders, :access_key_ciphertext)
add_column :autograders, :access_key_id_ciphertext, :text unless column_exists?(:autograders, :access_key_id_ciphertext)

remove_column :autograders, :access_key if column_exists?(:autograders, :access_key)
remove_column :autograders, :access_key_id if column_exists?(:autograders, :access_key_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t drop the plaintext credential columns before the copy happens.

Lines 7-8 remove access_key and access_key_id, but db/migrate/20251021034813_encrypt_autograder_credentials.rb is the migration that actually copies those values into encrypted storage. On an upgrade with existing secrets, this migration deletes the only source data before it is migrated.

💡 Minimal fix
   def up
     add_column :autograders, :instance_type, :string, default: "" unless column_exists?(:autograders, :instance_type)
     add_column :autograders, :access_key_ciphertext, :text unless column_exists?(:autograders, :access_key_ciphertext)
     add_column :autograders, :access_key_id_ciphertext, :text unless column_exists?(:autograders, :access_key_id_ciphertext)
-
-    remove_column :autograders, :access_key if column_exists?(:autograders, :access_key)
-    remove_column :autograders, :access_key_id if column_exists?(:autograders, :access_key_id)
   end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
add_column :autograders, :instance_type, :string, default: "" unless column_exists?(:autograders, :instance_type)
add_column :autograders, :access_key_ciphertext, :text unless column_exists?(:autograders, :access_key_ciphertext)
add_column :autograders, :access_key_id_ciphertext, :text unless column_exists?(:autograders, :access_key_id_ciphertext)
remove_column :autograders, :access_key if column_exists?(:autograders, :access_key)
remove_column :autograders, :access_key_id if column_exists?(:autograders, :access_key_id)
add_column :autograders, :instance_type, :string, default: "" unless column_exists?(:autograders, :instance_type)
add_column :autograders, :access_key_ciphertext, :text unless column_exists?(:autograders, :access_key_ciphertext)
add_column :autograders, :access_key_id_ciphertext, :text unless column_exists?(:autograders, :access_key_id_ciphertext)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb` around lines
3 - 8, The migration currently removes plaintext columns access_key and
access_key_id before the encrypting migration runs, which can delete the only
existing secrets; modify this migration (add_ec2_ssh_fields_to_autograders.rb)
to NOT remove the plaintext columns (remove or comment out the remove_column
lines for access_key and access_key_id) so that the encryption migration
(db/migrate/20251021034813_encrypt_autograder_credentials.rb) can copy values
into access_key_ciphertext and access_key_id_ciphertext first; keep adding the
new columns instance_type, access_key_ciphertext, and access_key_id_ciphertext
as-is and only drop the plaintext columns after the
encrypt_autograder_credentials migration has successfully migrated data.

Comment thread db/migrate/20251021034813_encrypt_autograder_credentials.rb Outdated
@melissajinn melissajinn marked this pull request as draft April 14, 2026 20:33
@melissajinn melissajinn changed the base branch from master to ec2-testing April 14, 2026 20:37
@melissajinn melissajinn marked this pull request as ready for review April 14, 2026 20:37
@coder6583 coder6583 requested review from a team and mmilunicmobile and removed request for a team April 15, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant