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:
| Status | Reason |
|---|---|
404 | Unknown badge, or you lack INTRA_GIVE_ACHIEVEMENT on its tenant |
404 | The login does not exist |
409 | The student already has this badge |
409 | The badge is disabled, or has been deleted |
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.
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
validationUnlockAchievementsis the exception: it is validated when youractivity.ymlis 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 assignment | validationUnlockAchievements |
| 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 else | Your own tooling, plus the give endpoint |