Skip to main content

Unlocking achievements

There are three ways to unlock a badge for a student: giving it by hand, letting the validation of a part of your activity grant it, or tagging it in a test trace. A fourth one, based on logical expressions, is designed but not implemented yet.

All three methods grant a badge once per student: giving a badge that is already unlocked does nothing.

Giving a badge by hand​

From Operator, open Achievements, pick your badge, and give it to a student from its detail page. This requires INTRA_GIVE_ACHIEVEMENT on the badge's tenant — see who can manage achievements.

Use this for anything that cannot be measured automatically: participation, an oral defence, a contest won during a workshop.

Giving a badge from a script​

If you want to automate attribution from outside the intranet — a leaderboard of your own, a CTF scoreboard, a side tool that watches something the intranet cannot see — Operator exposes the give endpoint:

POST https://operator.forge.epita.fr/api/users/{login}/achievements/{achievementId}

achievementId is the badge's UUID, which you can read from its Operator URL. The call takes no body. Authenticate it with a JWT, for instance from the Forge CLI:

curl --fail -X POST -H "Authorization: Bearer $(forge auth token)" \
"https://operator.forge.epita.fr/api/users/login_x/achievements/$ACHIEVEMENT_ID"

The call is rejected when:

StatusReason
404Unknown badge, or you lack INTRA_GIVE_ACHIEVEMENT on its tenant
404The login does not exist
409The student already has this badge
409The badge is disabled, or has been deleted
note

Retry logic is up to you, but you do not need to deduplicate: a badge the student already has answers 409 and changes nothing.

On validation of an activity, a group or an assignment​

Declare the badge in your activity.yml with validationUnlockAchievements. It is unlocked as soon as the resource carrying the key becomes validated for the student's group.

# at the root of the activity: unlocked when the whole activity is validated
validationUnlockAchievements:
- epita-ing-assistants-acu/piscine-2029/survivor

assignmentGroups:
- slug: day-01
# unlocked when this assignment group is validated
validationUnlockAchievements:
- epita-ing-assistants-acu/piscine-2029/day-01-done
assignments:
- slug: tp01-01
# unlocked when this single assignment is validated
validationUnlockAchievements:
- epita-ing-assistants-acu/first-blood

The key takes a list, so one resource can unlock several badges at once.

Each URI must point to a badge scoped either to this activity or to its tenant. Anything else is refused when the activity is ingested, with validationUnlockAchievements on '<uri>' references achievement URI '<uri>', which is not scoped to the activity nor its tenant. In particular, a GLOBAL badge can never be unlocked from an activity.yml.

Example of use: "validated every exercise of the piscine".

From a test trace​

If your tests produce a JUnit report, you can unlock a badge straight from the trace with a forge-unlock-achievements property. It can be set on a single test case:

<testsuite name="...">
<testcase name="..." classname="...">
<properties>
<property name="forge-unlock-achievements"
value="epita-ing-assistants-acu/piscine-2029/memory-leak"/>
</properties>
</testcase>
</testsuite>

…or on a whole test suite:

<testsuite name="...">
<properties>
<property name="forge-unlock-achievements"
value="epita-ing-assistants-acu/piscine-2029/memory-leak"/>
</properties>
</testsuite>

The badge is unlocked as soon as the property appears in the trace, whatever the outcome of the test it sits on. Putting it on a failing test is a legitimate way to reward a specific failure — it is up to your test suite to decide when to emit the property.

A property holds one URI; emit several properties to unlock several badges. Duplicates across suites and cases are collapsed.

As with validation rules, a trace may only unlock a badge scoped to its own activity or its tenant. GLOBAL badges and badges belonging to another activity are ignored.

note

Only the trace of the submission's current job is read, once its results have been uploaded. A job that never produced a trace cannot unlock anything.

Example of use: "triggered a memory leak on hello world".

Behaviour you should know about​

  • Group work. An automatic unlock is resolved for the whole group, then granted to each of its members individually. For individual work, where a group holds a single student, this amounts to a personal unlock.
  • No retroactive catch-up. Adding or changing an automatic rule does not re-evaluate work that was already submitted. To apply a rule to past submissions, give the badge by hand, or ask the Forge to replay the submissions.
  • Revocations win over automatic rules. Once an operator revokes a badge, no automatic rule grants it again. Only an explicit manual give restores it.
  • Bad references are skipped silently. A rule or a trace property pointing to a badge that is unknown, disabled, deleted, or scoped elsewhere is ignored at unlock time. Nothing is reported to the student, and nothing fails — the only trace is a warning in the intranet's logs. Check in Operator that the badge exists, is enabled, and has the scope you expect. (The scope shape of validationUnlockAchievements is the exception: it is validated when your activity.yml is ingested.)

Which method should I use?​

You want to reward…Use
Something not measurable automatically (participation, a defence)A manual give from Operator
Finishing an activity, an assignment group or an assignmentvalidationUnlockAchievements
A precise event your tests detect (a bug triggered, an edge case hit)The forge-unlock-achievements trace property
A condition over the submission history, or anything elseYour own tooling, plus the give endpoint