This document provides instructions for using ActivityPub activities and properties to represent forge events, and describes the side-effects these activities should have.
The ForgeFed behavior specification is a set of instructions for representing version control system and project management related transactions using ActivityPub activity objects, and it describes the side effects and expected results of sending and receiving these activities. The vocabulary for these activities includes standard ActivityPub terms, new terms defined by ForgeFed, and terms borrowed from other external vocabularies.
The ForgeFed vocabulary specification defines a dedicated vocabulary of forge-related terms, and the behavior specification uses these terms, along with terms that already exist in ActivityPub or elsewhere and can be reused for forge federation.
The ForgeFed modeling specification defines rules for representing forge related objects as ActivityPub JSON-LD objects, and these objects are used in the behavior specification, included in activities or mentioned in activities or modified due to activity side-effects.
The key words MAY, MUST, MUST NOT, SHOULD, and SHOULD NOT are to be interpreted as described in RFC2119.
Objects are the core concept around which both ActivityPub and ForgeFed are built. Examples of Objects are Note, Ticket, Image, Create, Push. Some objects are resources, which are objects that contain or represent information and user made or program made content, and some objects are helpers that exist as implementation detail aren't necessarily exposed to humans or are useful to humans. But everything is an Object, represented as compacted JSON-LD.
ForgeFed is an ActivityPub extension, and communication between ForgeFed implementations occurs using activity objects sent to actor inboxes and outboxes.
There are 4 kinds of objects in ForgeFed:
Actors, children and globals are referred in ForgeFed as the static objects, while activities are the dynamic objects (the terms constant and variable are used for stating whether an object changes during its lifetime or not).
Static objects, in addition to being an actor or child or global, also have a resource/helper distinction:
This specification doesn't mandate which types and objects should be actors, but it does provide guidelines that implementations SHOULD follow:
Here are some examples and their rationale:
The proposal here is that the following types typically be actors:
And other types such as these typically not be actors:
There are 2 primary flows for publishing child objects:
Whenever an object can be published using either of these flows, it is recommended to use the creation flow as a default, and resort to the offer flow only when really necessary (if you're unsure, it's not necessary).
The creation flow may optionally have a target actor, to whom the sender is proposing the object. But the sender is only asking that the target actor lists the URI of the newly published object, while the sender actor is the one hosting and controlling the object.
The creation flow begines with a Create activity, in which object is the new object being published, and that object's context may be specified and indicates where the sender would like the newly published object to be listed by the target actor. If there's no context, then there's no target actor.
Among the recipients listed in the Create's recipient fields, at most one recipient is the actor who's responsible for processing, inspecting and possibly listing the newly published object, and possibly sending back an Accept or a Reject. We'll refer to this actor as the target actor. Specific object types described throughout this specification have a specific meaning for the target actor, which processing it is expected to do, and where it is expected to list the URI of the newly published object.
When an actor A receives the Create activity, they can determine whether they're the target actor as follows: If the object ticket's context is A or a child object of A, then A is the target actor. Otherwise, A isn't the target actor.
In the following example, Luke wants to open a ticket under Aviva's Game Of Life simulation app:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://forge.example/luke/outbox/02Ljp",
"type": "Create",
"actor": "https://forge.example/luke",
"to": [
"https://forge.example/luke/followers",
"https://dev.example/aviva/game-of-life",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": {
"id": "https://forge.example/luke/issues/k49fn",
"type": "Ticket",
"attributedTo": "https://forge.example/luke",
"summary": "Test test test",
"content": "<p>Just testing</p>",
"mediaType": "text/html",
"source": {
"mediaType": "text/markdown; variant=Commonmark",
"content": "Just testing"
},
"context": "https://dev.example/aviva/game-of-life"
}
}
The target actor SHOULD send an Accept or a Reject activity to the Create's author in response. In the creation flow, the target actor (if there is one) is asked to merely process and remember the URI of the newly published object (the exact expected processing and location of listing the URI depends on the object type, see the next sections of this specification). If the target actor sends an Accept, it MUST either add the object's id to the expected list, or host its own copy as in the offer flow described below. It SHOULD just list the object id, and that is the recommended behavior.
If the target actor sends a Reject, it MUST NOT list the object and MUST
NOT host a copy. However if the target actor doesn't make any use of the
object, it MAY choose not to send a Reject, e.g. to protect user privacy. The
Accept
or Reject
may also be delayed, e.g. until review by a human user;
that is implementation dependent, and implementations should not rely on a
In the Accept activity:
In the following example, Luke's ticket is listed automatically and Aviva's Game Of Life repository, which is an actor, automatically sends Luke an Accept activity:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://dev.example/aviva/game-of-life/outbox/096al",
"type": "Accept",
"actor": "https://dev.example/aviva/game-of-life",
"to": [
"https://forge.example/luke",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": "https://forge.example/luke/outbox/02Ljp"
}
The offer flow always has a target actor, to whom the sender is proposing the object. The sender is asking that the target actor hosts the object as a child object and assigns is a URI, allowing to observe and interact with the object. The target actor will be responsible for hosting and controlling the object, and the sender will just be mentioned as the author.
The offer flow begins with an Offer activity, in which object is the object being offered for publishing, and target indicates under which list or collection the sender would like the object to be published, if the target actor accepts the offer and publishes it.
Among the recipients listed in the Offer's recipient fields, exactly one recipient is the actor who's responsible for inspecting and possibly publishing the newly object, and possibly sending back an Accept or a Reject. We'll refer to this actor as the target actor. Specific object types described throughout this specification have a specific meaning for the target actor, which processing and inspection it is expected to do, and where it is expected to list the URI of the object once it publishes it.
When an actor A receives the Create activity, they can determine whether they're the target actor as follows: If the object ticket's context is A or a child object of A, then A is the target actor. Otherwise, A isn't the target actor.
In the following example, Luke wants to open a ticket under Aviva's Game Of Life simulation app:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://forge.example/luke/outbox/02Ljp",
"type": "Offer",
"actor": "https://forge.example/luke",
"to": [
"https://dev.example/aviva/game-of-life",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": {
"type": "Ticket",
"attributedTo": "https://forge.example/luke",
"summary": "Test test test",
"content": "<p>Just testing</p>",
"mediaType": "text/html",
"source": {
"mediaType": "text/markdown; variant=Commonmark",
"content": "Just testing"
}
},
"target": "https://dev.example/aviva/game-of-life"
}
The target actor SHOULD send an Accept or a Reject activity to the Offer's author in response. In the offer flow, the target actor is asked to host a copy of the object. If the target actor sends an Accept, it MUST either host its own copy, or add the object's id to the expected list, as in the Creation flow described above (but that's possible only if the Offer's object has an id) It SHOULD host a copy, and that is the recommended behavior.
If the target actor sends a Reject, it MUST NOT host a copy and MUST NOT
list the object. However if the target actor doesn't make any use of the
object, it MAY choose not to send a Reject, e.g. to protect user privacy. The
Accept
or Reject
may also be delayed, e.g. until review by a human user;
that is implementation dependent, and implementations should not rely on a
In the Accept activity:
In the following example, Luke's ticket is opened automatically and Aviva's Game Of Life repository, which is an actor, automatically sends Luke an Accept activity:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://dev.example/aviva/game-of-life/outbox/096al",
"type": "Accept",
"actor": "https://dev.example/aviva/game-of-life",
"to": [
"https://forge.example/luke",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": "https://forge.example/luke/outbox/02Ljp",
"result": "https://dev.example/aviva/game-of-life/issues/113"
}
Create and Offer indicate different requests made by the sender, but regardless of which one is sent, the target actor has a choice how to react. It may choose to list the object's URI, and it may choose to host its own copy, regardless of what the sender asked (although the recommended behavior is to react according to what the sender asked). Therefore senders should be prepared for both options.
The action that has been taken by the target actor is indicated to the object author as follows:
ForgeFed uses Activities for client to server interactions, as described by ActivityPub. A client will send objects (eg. a Ticket) wrapped in a Activity (eg. Create) to an actor's outbox, and in turn the server will take care of delivery.
The Follow activity is used to subscribe to the activities of a Repository. The client MUST send a Follow activity to the Person's outbox. The server in turn delivers the message to the destination inbox.
The Push activity is used to notify followers when somebody has pushed changes to a Repository. The client MUST send a Push activity to the Repository's outbox. The server in turn delivers the message to the Repository followers.
The ForgeFed Push activity can be used for representing an action of pushing commits into a Repository. Two actors are involved in the process, the pusher (usually a person) and the repository, and they may be hosted on different instances. We therefore refer to 2 kinds of pushes:
At this time, the representation of Federated Push isn't provided yet. Below we discuss Local Push.
Upon a successful push, a ForgeFed implementation that publishes a Push activity MUST provide the type, actor, context and target properties as described in the modeling specification. If the Push activity's recipient fields list collections that belong to the repository, such as its followers and team, the repository MUST verify the authenticity and correctness of the Push activity's fields before it performs inbox forwarding (i.e. delivery to the members of those collections), and MUST NOT perform inbox delivery if the correctness check doesn't pass.
In a Local Push, if the Push activity is generated on the server, that obviates the need to perform correctness checking. Implementations MAY forbid clients from publishing Push activities (via the ActivityPub C2S API or any other mechanism), in order to guarantee the authenticity of Push activities.
See example in the modeling specification.
The first step for opening a ticket is to determine to which actor to send the ticket. We'll refer to this actor as the ticket tracker. Given an object obj against which you'd like to open a ticket (e.g. some application's source code repository), look at the ticketsTrackedBy property of obj.
ticketsTrackedBy
isn't specified, then obj does't declare a way to
open tickets via ForgeFed.ticketsTrackedBy
is specified and is set to the id of obj itself,
that means obj manages its own tickets, i.e. it is the ticket tracker to
which you'll send the ticket.ticketsTrackedBy
is specified and is set to some other object, look at
the tracksTicketsFor property of that other object.
If the id of obj is listed there under tracksTicketsFor
, then that
other object is the ticket tracker to which you'll send the ticket.
Implementations SHOULD verify this bidirectional reference between the object
and the tracker, and SHOULD NOT send a ticket if the bidirectional reference
isn't found.Now that we've determined the ticket tracker, i.e. the actor to whom we'll send the Ticket, the ticket may be opened using either Create of Offer as described in the Object Publishing and Hosting section. Create asks the tracker to list the ticket's URI, while Offer asks the tracker to host and list its own copy (however what the tracker chooses to do is up to the tracker).
The target actor MAY then send back an Accept or Reject. The action that has been taken by the target actor is indicated to the ticket author as follows:
A ticket can be opened using a Create activity, in which object is a
Ticket with fields as described in the modeling
specification. The ticket MUST specify at least id,
attributedTo, [summary][], content and context. The context
property specifies the project or tracker to which the actor is reporting the
Ticket (e.g. a repository or project etc. under which the ticket will be listed
if accepted). context MUST be either an actor or a child object. If it's a
child object, the actor to whom the child object belongs MUST be listed as a
recipient in the Create's to field. If it's an actor, then that actor MUST
be listed in the to
field.
In the following example, Luke wants to open a ticket under Aviva's Game Of Life simulation app:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://forge.example/luke/outbox/02Ljp",
"type": "Create",
"actor": "https://forge.example/luke",
"to": [
"https://forge.example/luke/followers",
"https://dev.example/aviva/game-of-life",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": {
"id": "https://forge.example/luke/issues/k49fn",
"type": "Ticket",
"attributedTo": "https://forge.example/luke",
"summary": "Test test test",
"content": "<p>Just testing</p>",
"mediaType": "text/html",
"source": {
"mediaType": "text/markdown; variant=Commonmark",
"content": "Just testing"
},
"context": "https://dev.example/aviva/game-of-life"
}
}
Luke's ticket is listed automatically and Aviva's Game Of Life repository, which is an actor, automatically sends Luke an Accept activity:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://dev.example/aviva/game-of-life/outbox/096al",
"type": "Accept",
"actor": "https://dev.example/aviva/game-of-life",
"to": [
"https://forge.example/luke",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": "https://forge.example/luke/outbox/02Ljp"
}
A ticket may be opened using an Offer activity, in which:
to
field.In the following example, Luke wants to open a ticket under Aviva's Game Of Life simulation app:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://forge.example/luke/outbox/02Ljp",
"type": "Offer",
"actor": "https://forge.example/luke",
"to": [
"https://dev.example/aviva/game-of-life",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": {
"type": "Ticket",
"attributedTo": "https://forge.example/luke",
"summary": "Test test test",
"content": "<p>Just testing</p>",
"mediaType": "text/html",
"source": {
"mediaType": "text/markdown; variant=Commonmark",
"content": "Just testing"
}
},
"target": "https://dev.example/aviva/game-of-life"
}
Luke's ticket is opened automatically and Aviva's Game Of Life repository, which is an actor, automatically sends Luke an Accept activity:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://forgefed.peers.community/ns"
],
"id": "https://dev.example/aviva/game-of-life/outbox/096al",
"type": "Accept",
"actor": "https://dev.example/aviva/game-of-life",
"to": [
"https://forge.example/luke",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/followers"
],
"object": "https://forge.example/luke/outbox/02Ljp",
"result": "https://dev.example/aviva/game-of-life/issues/113"
}
A comment on a ForgeFed resource object (such as tickets, merge requests) MUST be published as a Create activity, in which object is a Note with fields as described in the modeling specification.
In the following example, Luke replies to Aviva's comment under a merge request he submitted earlier against her Game Of Life simulation app repository:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://forge.example/luke/outbox/rLaYo",
"type": "Create",
"actor": "https://forge.example/luke",
"to": [
"https://forge.example/luke/followers",
"https://dev.example/aviva/game-of-life",
"https://dev.example/aviva/game-of-life/followers",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/merge-requests/19/followers",
"https://dev.example/aviva/game-of-life/merge-requests/19/team"
],
"object": {
"id": "https://forge.example/luke/comments/rD05r",
"type": "Note",
"attributedTo": "https://forge.example/luke",
"to": [
"https://forge.example/luke/followers",
"https://dev.example/aviva/game-of-life",
"https://dev.example/aviva/game-of-life/followers",
"https://dev.example/aviva/game-of-life/team",
"https://dev.example/aviva/game-of-life/merge-requests/19/followers",
"https://dev.example/aviva/game-of-life/merge-requests/19/team"
],
"context": "https://dev.example/aviva/game-of-life/merge-requests/19",
"inReplyTo": "https://dev.example/aviva/comments/E9AGE",
"mediaType": "text/html",
"content": "<p>Thank you for the review! I'll submit a correction ASAP</p>",
"source": {
"mediaType": "text/markdown; variant=Commonmark",
"content": "Thank you for the review! I'll submit a correction ASAP"
},
"published": "2019-11-06T20:49:05.604488Z"
}
}