ForgeFed

ForgeFed Modeling - draft - 2023-03-08 main a3a6d7d

1 Abstract

This document describes the rules and guidelines for representing version control and project management related objects as linked data, using the ForgeFed vocabulary, ActivityStreams 2, and other related vocabularies.

2 Introduction

The ForgeFed modeling specification is a set of rules and guidelines which describe version control repository and project management related objects and properties, and specify how to represent them as JSON-LD objects (and linked data in general) using the ForgeFed vocabulary and related vocabularies and ontologies. Using these modeling rules consistently across implementations and instances allows to have a common language spoken across networks of software forges, project management apps and more.

The ForgeFed vocabulary specification defines a dedicated vocabulary of forge-related terms, and the modeling specification uses these terms, along with terms that already exist in ActivityPub or elsewhere and can be reused for forge federation.

The ForgeFed behavior specification provides instructions for using Activities, and which Activities and properties to use, to represent forge events, and describes the side-effects these Activities should have. The objects used as inputs and outputs of behavior descriptions there are defined here in the modeling specification.

3 Commit

To represent a named set of changes committed into a repository’s history, use the ForgeFed Commit type. Such a committed change set is called e.g. a commit in Git, and a patch in Darcs.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://example.dev/alice/myrepo/commits/109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
          "type": "Commit",
          "context": "https://example.dev/alice/myrepo",
          "attributedTo": "https://example.dev/bob",
          "created": "2019-07-11T12:34:56Z",
          "committedBy": "https://example.dev/alice",
          "committed": "2019-07-26T23:45:01Z",
          "hash": "109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
          "summary": "Add an installation script, fixes issue #89",
          "description": {
              "mediaType": "text/plain",
              "content": "It's about time people can install it on their computers!"
          }
      }

4 Patch

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/aviva/game-of-life/pulls/825/versions/1/patches/1",
          "type": "Patch",
          "attributedTo": "https://forge.example/luke",
          "context": "https://dev.example/aviva/game-of-life/pulls/825/versions/1",
          "mediaType": "application/x-git-patch",
          "content": "From c9ae5f4ff4a330b6e1196ceb7db1665bd4c1..."
      }

5 Branch

To represent a repository branch, use the ForgeFed Branch type. It can be a real built-in version control system branch (such as a Git branch) or a copy of the repo used as a branch (e.g. in Darcs, which doesn’t implement branches, and the way to have branches is to keep multiple versions of the repo).

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://example.dev/luke/myrepo/branches/master",
          "type": "Branch",
          "context": "https://example.dev/luke/myrepo",
          "name": "master",
          "ref": "refs/heads/master"
      }

6 Repository

To represent a version control repository, use the ForgeFed Repository type.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://w3id.org/security/v1",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/aviva/treesim",
          "cloneUri": "https://dev.example/aviva/treesim.git",
          "type": "Repository",
          "publicKey": {
              "id": "https://dev.example/aviva/treesim#main-key",
              "owner": "https://dev.example/aviva/treesim",
              "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
          },
          "inbox": "https://dev.example/aviva/treesim/inbox",
          "outbox": "https://dev.example/aviva/treesim/outbox",
          "followers": "https://dev.example/aviva/treesim/followers",
          "team": "https://dev.example/aviva/treesim/team",
          "ticketsTrackedBy": "https://dev.example/aviva/treesim",
          "sendPatchesTo": "https://dev.example/aviva/treesim",
          "name": "Tree Growth 3D Simulation",
          "attributedTo": "https://example.dev/bob",
          "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>"
      }

7 Project

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/projects/wanderer",
          "type": "Project",
      
          "name": "Wanderer",
          "summary": "3D nature exploration game",
          "components": {
              "type": "Collection",
              "totalItems": 7,
              "items": [
                  https://dev.example/repos/opengl-vegetation",
                  https://dev.example/repos/opengl-vegetation/patch-tracker",
                  https://dev.example/repos/treesim",
                  https://dev.example/repos/treesim/patch-tracker",
                  https://dev.example/repos/wanderer",
                  https://dev.example/repos/wanderer/patch-tracker",
                  https://dev.example/issue-trackers/wanderer"
              ]
          },
          "subprojects": {
              "type": "Collection",
              "totalItems": 2,
              "items": [
                  "https://dev.example/projects/nature-3d-models",
                  "https://dev.example/projects/wanderer-fundraising"
              ]
          },
          "ticketsTrackedBy": "https://dev.example/issue-trackers/wanderer",
      
          "inbox": "https://dev.example/projects/wanderer/inbox",
          "outbox": "https://dev.example/projects/wanderer/outbox",
          "followers": "https://dev.example/projects/wanderer/followers"
      }

8 Team Membership

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/teams/mobilizon-dev-team/members/ThmsicTj",
          "type": "Relationship",
          "subject": "https://dev.example/teams/mobilizon-dev-team",
          "relationship": "hasMember",
          "object": "https://dev.example/people/celine",
          "tag": "https://roles.example/developer"
      }

9 Team

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://w3id.org/security/v2",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/teams/mobilizon-dev-team",
          "type": "Team",
          "name": "Mobilizon Development Team",
          "summary": "We're creating a federated tool for organizing events!",
          "members": {
              "type": "Collection",
              "totalItems": 3,
              "items": [
                  { "type": "Relationship",
                    "subject": "https://dev.example/teams/mobilizon-dev-team",
                    "relationship": "hasMember",
                    "object": "https://dev.example/people/alice",
                    "tag": "https://roles.example/admin"
                  },
                  { "type": "Relationship",
                    "subject": "https://dev.example/teams/mobilizon-dev-team",
                    "relationship": "hasMember",
                    "object": "https://dev.example/people/bob",
                    "tag": "https://roles.example/maintainer"
                  },
                  { "type": "Relationship",
                    "subject": "https://dev.example/teams/mobilizon-dev-team",
                    "relationship": "hasMember",
                    "object": "https://dev.example/people/celine",
                    "tag": "https://roles.example/developer"
                  }
              ]
          },
          "subteams": {
              "type": "Collection",
              "totalItems": 2,
              "items": [
                  "https://dev.example/teams/mobilizon-backend-team",
                  "https://dev.example/teams/mobilizon-frontend-team"
              ]
          },
          "context": "https://dev.example/teams/framasoft-developers",
      
          "publicKey": {
              "id": "https://dev.example/teams/mobilizon-dev-team#main-key",
              "owner": "https://dev.example/teams/mobilizon-dev-team",
              "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
          },
          "inbox": "https://dev.example/teams/mobilizon-dev-team/inbox",
          "outbox": "https://dev.example/teams/mobilizon-dev-team/outbox",
          "followers": "https://dev.example/teams/mobilizon-dev-team/followers"
      }

10 Push

To represent an event of Commits being pushed to a Repository, use a ForgeFed Push activity.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/aviva/outbox/E26bE",
          "type": "Push",
          "actor": "https://dev.example/aviva",
          "to": [
              "https://dev.example/aviva/followers",
              "https://dev.example/aviva/game-of-life",
              "https://dev.example/aviva/game-of-life/team",
              "https://dev.example/aviva/game-of-life/followers"
          ],
          "context": "https://dev.example/aviva/game-of-life",
          "target": "https://dev.example/aviva/game-of-life/branches/master",
          "hashBefore": "017cbb00bc20d1cae85f46d638684898d095f0ae",
          "hashAfter": "be9f48a341c4bb5cd79ae7ab85fbf0c05d2837bb",
          "object": {
              "totalItems": 2,
              "type": "OrderedCollection",
              "orderedItems": [
                  {
                      "id": "https://dev.example/aviva/game-of-life/commits/be9f48a341c4bb5cd79ae7ab85fbf0c05d2837bb",
                      "type": "Commit",
                      "attributedTo": "https://dev.example/aviva",
                      "context": "https://dev.example/aviva/game-of-life",
                      "hash": "be9f48a341c4bb5cd79ae7ab85fbf0c05d2837bb",
                      "created": "2019-12-02T16:07:32Z",
                      "summary": "Add widget to alter simulation speed"
                  },
                  {
                      "id": "https://dev.example/aviva/game-of-life/commits/fa37fe100a8b1e69933889c5bf3caf95cd3ae1e6",
                      "type": "Commit",
                      "attributedTo": "https://dev.example/aviva",
                      "context": "https://dev.example/aviva/game-of-life",
                      "hash": "fa37fe100a8b1e69933889c5bf3caf95cd3ae1e6",
                      "created": "2019-12-02T15:51:52Z",
                      "summary": "Set window title correctly, fixes issue #7"
                  }
              ]
          }
      }

11 Ticket

To represent a work item in a project, use the ForgeFed Ticket type.

TODO decide on ticket categories/subtypes and update below

TODO decide on property for titles, update below

TODO properly document history or remove it from example

Properties:

There’s an important distinction between these two kinds of tickets:

11.1 Task / Issue

A task is represented as a Ticket as described above, with the following additional requirements:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/aviva/game-of-life/issues/107",
          "type": "Ticket",
          "context": "https://dev.example/aviva/game-of-life",
          "attributedTo": "https://forge.example/luke",
          "summary": "Window title is empty",
          "content": "<p>When I start the simulation, window title disappears suddenly</p>",
          "mediaType": "text/html",
          "source": {
              "mediaType": "text/markdown; variant=Commonmark",
              "content": "When I start the simulation, window title disappears suddenly",
          },
          "published": "2019-11-04T07:00:04.465807Z",
          "followers": "https://dev.example/aviva/game-of-life/issues/107/followers",
          "team": "https://dev.example/aviva/game-of-life/issues/107/team",
          "replies": "https://dev.example/aviva/game-of-life/issues/107/discussion",
          "history": "https://dev.example/aviva/game-of-life/issues/107/activity",
          "dependants": "https://dev.example/aviva/game-of-life/issues/107/rdeps",
          "dependencies": "https://dev.example/aviva/game-of-life/issues/107/deps",
          "isResolved": true,
          "resolvedBy": "https://code.example/martin",
          "resolved": "2020-02-07T06:45:03.281314Z"
      }

11.2 Merge Request / Pull Request

A merge request is represented as a Ticket as described above, with the following additional requirements:

In that special attachment of type Offer:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/aviva/game-of-life/pulls/825",
          "type": "Ticket",
          "context": "https://dev.example/aviva/game-of-life",
          "attributedTo": "https://forge.example/luke",
          "summary": "Fix the empty window title bug",
          "content": "<p>This fixes the bug making the title disappear</p>",
          "mediaType": "text/html",
          "source": {
              "mediaType": "text/markdown; variant=Commonmark",
              "content": "This fixes the bug making the title disappear",
          },
          "published": "2022-09-15T14:52:00.125987Z",
          "followers": "https://dev.example/aviva/game-of-life/pulls/825/followers",
          "replies": "https://dev.example/aviva/game-of-life/pulls/825/discussion",
          "isResolved": false,
          "attachment": {
              "type": "Offer",
              "origin": {
                  "type": "Branch",
                  "context": "https://forge.example/luke/game-of-life",
                  "ref": "refs/heads/fix-title-bug"
              },
              "target": {
                  "type": "Branch",
                  "context": "https://dev.example/aviva/game-of-life",
                  "ref": "refs/heads/main"
              },
              "object": {
                  "id": "https://dev.example/aviva/game-of-life/pulls/825/versions/1",
                  "type": "OrderedCollection",
                  "totalItems": 1,
                  "items": [
                      {
                          "type": "Patch",
                          "attributedTo": "https://forge.example/luke",
                          "context": "https://dev.example/aviva/game-of-life/pulls/825/versions/1",
                          "mediaType": "application/x-git-patch",
                          "content": "From c9ae5f4ff4a330b6e1196ceb7db1665bd4c1..."
                      }
                  ],
                  "context": "https://dev.example/aviva/game-of-life/pulls/825"
              }
          }
      }

12 Comment

To represent a comment, e.g. a comment on a ticket or a merge request, use the ActivityPub Note type.

Properties:

Example:

{
          "@context": "https://www.w3.org/ns/activitystreams",
          "id": "https://forge.example/luke/comments/rD05r",
          "type": "Note",
          "attributedTo": "https://forge.example/luke",
          "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"
      }

13 Access Control

13.1 Giving Access

13.1.1 Invite

To offer some actor access to a shared resource (such as a repository or a ticket tracker), use an ActivityPub Invite activity.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/aviva/outbox/B47d3",
          "type": "Invite",
          "actor": "https://dev.example/aviva",
          "to": [
              "https://dev.example/aviva/followers",
              "https://coding.community/repos/game-of-life",
              "https://coding.community/repos/game-of-life/followers",
              "https://software.site/bob",
              "https://software.site/bob/followers"
          ],
          "instrument": "https://roles.example/maintainer",
          "target": "https://coding.community/repos/game-of-life",
          "object": "https://software.site/bob",
          "capability": "https://coding.community/repos/game-of-life/outbox/2c53A"
      }

13.1.2 Join

To request access to a shared resource, use an ActivityPub Join activity.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://software.site/bob/outbox/c97E3",
          "type": "Join",
          "actor": "https://software.site/bob",
          "to": [
              "https://coding.community/repos/game-of-life",
              "https://coding.community/repos/game-of-life/followers",
              "https://software.site/bob/followers"
          ],
          "instrument": "https://roles.example/maintainer",
          "object": "https://coding.community/repos/game-of-life",
          "capability": "https://coding.community/repos/game-of-life/outbox/d38Fa"
      }

13.1.3 Grant

To give some actor access to a shared resource, use a ForgeFed Grant activity.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://coding.community/repos/game-of-life/outbox/9fA8c",
          "type": "Grant",
          "actor": "https://coding.community/repos/game-of-life",
          "to": [
              "https://dev.example/aviva",
              "https://dev.example/aviva/followers",
              "https://coding.community/repos/game-of-life/followers",
              "https://software.site/bob",
              "https://software.site/bob/followers"
          ],
          "object": "https://roles.example/maintainer",
          "context": "https://coding.community/repos/game-of-life",
          "target": "https://software.site/bob",
          "fulfills": "https://dev.example/aviva/outbox/B47d3",
          "allows": "invoke"
      }

13.2 Canceling Access

13.2.1 Remove

To disable an actor’s membership in a shared resource, invalidating their access to it, use an ActivityPub Remove activity.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://dev.example/aviva/outbox/F941b",
          "type": "Remove",
          "actor": "https://dev.example/aviva",
          "to": [
              "https://dev.example/aviva/followers",
              "https://coding.community/repos/game-of-life",
              "https://coding.community/repos/game-of-life/followers",
              "https://software.site/bob",
              "https://software.site/bob/followers"
          ],
          "origin": "https://coding.community/repos/game-of-life",
          "object": "https://software.site/bob",
          "capability": "https://coding.community/repos/game-of-life/outbox/2c53A"
      }

13.2.2 Leave

To withdraw your consent for membership in a shared resource, invalidating your access to it, use an ActivityPub Leave activity.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://software.site/bob/outbox/d08F4",
          "type": "Leave",
          "actor": "https://software.site/bob",
          "to": [
              "https://coding.community/repos/game-of-life",
              "https://coding.community/repos/game-of-life/followers",
              "https://software.site/bob/followers"
          ],
          "object": "https://coding.community/repos/game-of-life"
      }

13.2.3 Revoke

Another activity that can be used for disabling access is Revoke. While Remove and Leave are meant for undoing the effects of Invite and Join, Revoke is provided as an opposite of Grant. See the Behavior specification for more information about the usage of these different activity types in revocation of access to shared resources.

Properties:

Example:

{
          "@context": [
              "https://www.w3.org/ns/activitystreams",
              "https://forgefed.org/ns"
          ],
          "id": "https://coding.community/repos/game-of-life/outbox/1C0e2",
          "type": "Revoke",
          "actor": "https://coding.community/repos/game-of-life",
          "to": [
              "https://coding.community/repos/game-of-life/followers",
              "https://software.site/bob",
              "https://software.site/bob/followers"
          ],
          "instrument": "https://roles.example/maintainer",
          "context": "https://coding.community/repos/game-of-life",
          "origin": "https://software.site/bob",
          "object": "https://coding.community/repos/game-of-life/outbox/9fA8c"
      }

13.2.4 Undo a Grant

The Behavior spec describes flows in which the Revoke activity is used by resources (more accurately, by the actors managing them) to announce that they’re disabling Grants that they previously sent. To allow for a clear distinction, another activity is provided here, for other actors to request the revocation of specific Grants: The ActivityPub Undo activity.

It’s likely that Grants would exist behind-the-scenes in applications, and human actors would then use activities such as Remove and Leave for disabling access. But the ability to disable specific Grants may be required for ensuring and maintaining system security, therefore Undo is provided here as well.

Properties: