~emersion/public-inbox

hottub: Owner-only secrets v1 APPLIED

This has been tested with:

Installer committing to a branch: Includes secrets
Other committing to a branch: Does not include secrets
Installer / Other committing to a fork: Does not include secrets (forks
track installed apps separately)
Installer committing to a PR branch: Includes secrets
Other committing to a PR branch: Does not include secrets

Lixquid (1):
  Add support for secrets on owner-created events

 buildssrht/gql.go             | 232 +++++++++++++++++++++++++---------
 buildssrht/operations.graphql |   4 +-
 main.go                       |  33 ++---
 3 files changed, 191 insertions(+), 78 deletions(-)

-- 
2.38.5
Unfortunately I've had to disable this feature. See [1] for details.

I have a branch with WIP code to re-enable it [2], but not sure when
I'll be able to finish it. It assumes personal tokens always have the
fixed scope hardcoded in hottub, because there is no way to discover a
token's scope at the moment. Adding scope support to meta.sr.ht's OAuth
2.0 token introspection endpoint would fix that.

I assume you're using this feature by filling in a personal token
manually?

[1]: https://git.sr.ht/~emersion/hottub/commit/41fb29afd3f333cb77cf3aee81bad6da1838fb8a
[2]: https://git.sr.ht/~emersion/hottub/log/secrets
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/public-inbox/patches/42428/mbox | git am -3
Learn more about email & git

[PATCH hottub 1/1] Add support for secrets on owner-created events Export this patch

From: Lixquid <lixquid@lixquid.com>

This is a stop-gap solution until fully flexible per-repository,
per-action configuration is implemented.
---
 buildssrht/gql.go             | 232 +++++++++++++++++++++++++---------
 buildssrht/operations.graphql |   4 +-
 main.go                       |  33 ++---
 3 files changed, 191 insertions(+), 78 deletions(-)

diff --git a/buildssrht/gql.go b/buildssrht/gql.go
index 78d6afb..c594a7e 100644
--- a/buildssrht/gql.go
@@ -1,11 +1,12 @@
// Code generated by gqlclientgen - DO NOT EDIT
// Code generated by gqlclientgen - DO NOT EDIT.

package buildssrht

import (
	"context"
	"encoding/json"
	"fmt"
	gqlclient "git.sr.ht/~emersion/gqlclient"
	"time"
)

type AccessKind string
@@ -25,8 +26,8 @@ const (
)

type Artifact struct {
	Id      int32     `json:"id"`
	Created time.Time `json:"created"`
	Id      int32          `json:"id"`
	Created gqlclient.Time `json:"created"`
	// Original path in the guest
	Path string `json:"path"`
	// Size in bytes
@@ -46,6 +47,8 @@ type EmailTrigger struct {
	InReplyTo *string          `json:"inReplyTo,omitempty"`
}

func (*EmailTrigger) isTrigger() {}

type EmailTriggerInput struct {
	To        string  `json:"to"`
	Cc        *string `json:"cc,omitempty"`
@@ -53,24 +56,54 @@ type EmailTriggerInput struct {
}

type Entity struct {
	Id      int32     `json:"id"`
	Created time.Time `json:"created"`
	Updated time.Time `json:"updated"`
	Id      int32          `json:"id"`
	Created gqlclient.Time `json:"created"`
	Updated gqlclient.Time `json:"updated"`
	// The canonical name of this entity. For users, this is their username
	// prefixed with '~'. Additional entity types will be supported in the future.
	CanonicalName string `json:"canonicalName"`

	// Underlying value of the GraphQL interface
	Value EntityValue `json:"-"`
}

func (base *Entity) UnmarshalJSON(b []byte) error {
	type Raw Entity
	var data struct {
		*Raw
		TypeName string `json:"__typename"`
	}
	data.Raw = (*Raw)(base)
	err := json.Unmarshal(b, &data)
	if err != nil {
		return err
	}
	switch data.TypeName {
	case "User":
		base.Value = new(User)
	case "":
		return nil
	default:
		return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName)
	}
	return json.Unmarshal(b, base.Value)
}

// EntityValue is one of: User
type EntityValue interface {
	isEntity()
}

type File string

type Job struct {
	Id       int32     `json:"id"`
	Created  time.Time `json:"created"`
	Updated  time.Time `json:"updated"`
	Status   JobStatus `json:"status"`
	Manifest string    `json:"manifest"`
	Note     *string   `json:"note,omitempty"`
	Tags     []*string `json:"tags"`
	Id       int32          `json:"id"`
	Created  gqlclient.Time `json:"created"`
	Updated  gqlclient.Time `json:"updated"`
	Status   JobStatus      `json:"status"`
	Manifest string         `json:"manifest"`
	Note     *string        `json:"note,omitempty"`
	Tags     []*string      `json:"tags"`
	// Name of the build image
	Image string `json:"image"`
	// Name of the build runner which picked up this job, or null if the job is
@@ -97,12 +130,12 @@ type JobCursor struct {
}

type JobGroup struct {
	Id       int32      `json:"id"`
	Created  time.Time  `json:"created"`
	Note     *string    `json:"note,omitempty"`
	Owner    *Entity    `json:"owner"`
	Jobs     []*Job     `json:"jobs"`
	Triggers []*Trigger `json:"triggers"`
	Id       int32          `json:"id"`
	Created  gqlclient.Time `json:"created"`
	Note     *string        `json:"note,omitempty"`
	Owner    *Entity        `json:"owner"`
	Jobs     []*Job         `json:"jobs"`
	Triggers []*Trigger     `json:"triggers"`
}

type JobStatus string
@@ -126,26 +159,64 @@ type Log struct {
}

type PGPKey struct {
	Id         int32     `json:"id"`
	Created    time.Time `json:"created"`
	Uuid       string    `json:"uuid"`
	Name       *string   `json:"name,omitempty"`
	PrivateKey Binary    `json:"privateKey"`
	Id         int32          `json:"id"`
	Created    gqlclient.Time `json:"created"`
	Uuid       string         `json:"uuid"`
	Name       *string        `json:"name,omitempty"`
	PrivateKey Binary         `json:"privateKey"`
}

func (*PGPKey) isSecret() {}

type SSHKey struct {
	Id         int32     `json:"id"`
	Created    time.Time `json:"created"`
	Uuid       string    `json:"uuid"`
	Name       *string   `json:"name,omitempty"`
	PrivateKey Binary    `json:"privateKey"`
	Id         int32          `json:"id"`
	Created    gqlclient.Time `json:"created"`
	Uuid       string         `json:"uuid"`
	Name       *string        `json:"name,omitempty"`
	PrivateKey Binary         `json:"privateKey"`
}

func (*SSHKey) isSecret() {}

type Secret struct {
	Id      int32     `json:"id"`
	Created time.Time `json:"created"`
	Uuid    string    `json:"uuid"`
	Name    *string   `json:"name,omitempty"`
	Id      int32          `json:"id"`
	Created gqlclient.Time `json:"created"`
	Uuid    string         `json:"uuid"`
	Name    *string        `json:"name,omitempty"`

	// Underlying value of the GraphQL interface
	Value SecretValue `json:"-"`
}

func (base *Secret) UnmarshalJSON(b []byte) error {
	type Raw Secret
	var data struct {
		*Raw
		TypeName string `json:"__typename"`
	}
	data.Raw = (*Raw)(base)
	err := json.Unmarshal(b, &data)
	if err != nil {
		return err
	}
	switch data.TypeName {
	case "SSHKey":
		base.Value = new(SSHKey)
	case "PGPKey":
		base.Value = new(PGPKey)
	case "SecretFile":
		base.Value = new(SecretFile)
	case "":
		return nil
	default:
		return fmt.Errorf("gqlclient: interface Secret: unknown __typename %q", data.TypeName)
	}
	return json.Unmarshal(b, base.Value)
}

// SecretValue is one of: SSHKey | PGPKey | SecretFile
type SecretValue interface {
	isSecret()
}

// A cursor for enumerating a list of secrets
@@ -159,28 +230,30 @@ type SecretCursor struct {
}

type SecretFile struct {
	Id      int32     `json:"id"`
	Created time.Time `json:"created"`
	Uuid    string    `json:"uuid"`
	Name    *string   `json:"name,omitempty"`
	Path    string    `json:"path"`
	Mode    int32     `json:"mode"`
	Data    Binary    `json:"data"`
	Id      int32          `json:"id"`
	Created gqlclient.Time `json:"created"`
	Uuid    string         `json:"uuid"`
	Name    *string        `json:"name,omitempty"`
	Path    string         `json:"path"`
	Mode    int32          `json:"mode"`
	Data    Binary         `json:"data"`
}

func (*SecretFile) isSecret() {}

type Settings struct {
	SshUser      string `json:"sshUser"`
	BuildTimeout string `json:"buildTimeout"`
}

type Task struct {
	Id      int32      `json:"id"`
	Created time.Time  `json:"created"`
	Updated time.Time  `json:"updated"`
	Name    string     `json:"name"`
	Status  TaskStatus `json:"status"`
	Log     *Log       `json:"log,omitempty"`
	Job     *Job       `json:"job"`
	Id      int32          `json:"id"`
	Created gqlclient.Time `json:"created"`
	Updated gqlclient.Time `json:"updated"`
	Name    string         `json:"name"`
	Status  TaskStatus     `json:"status"`
	Log     *Log           `json:"log,omitempty"`
	Job     *Job           `json:"job"`
}

type TaskStatus string
@@ -198,6 +271,38 @@ const (
// build manifest, but are similar in functionality.
type Trigger struct {
	Condition TriggerCondition `json:"condition"`

	// Underlying value of the GraphQL interface
	Value TriggerValue `json:"-"`
}

func (base *Trigger) UnmarshalJSON(b []byte) error {
	type Raw Trigger
	var data struct {
		*Raw
		TypeName string `json:"__typename"`
	}
	data.Raw = (*Raw)(base)
	err := json.Unmarshal(b, &data)
	if err != nil {
		return err
	}
	switch data.TypeName {
	case "EmailTrigger":
		base.Value = new(EmailTrigger)
	case "WebhookTrigger":
		base.Value = new(WebhookTrigger)
	case "":
		return nil
	default:
		return fmt.Errorf("gqlclient: interface Trigger: unknown __typename %q", data.TypeName)
	}
	return json.Unmarshal(b, base.Value)
}

// TriggerValue is one of: EmailTrigger | WebhookTrigger
type TriggerValue interface {
	isTrigger()
}

type TriggerCondition string
@@ -223,19 +328,21 @@ const (
)

type User struct {
	Id            int32     `json:"id"`
	Created       time.Time `json:"created"`
	Updated       time.Time `json:"updated"`
	CanonicalName string    `json:"canonicalName"`
	Username      string    `json:"username"`
	Email         string    `json:"email"`
	Url           *string   `json:"url,omitempty"`
	Location      *string   `json:"location,omitempty"`
	Bio           *string   `json:"bio,omitempty"`
	Id            int32          `json:"id"`
	Created       gqlclient.Time `json:"created"`
	Updated       gqlclient.Time `json:"updated"`
	CanonicalName string         `json:"canonicalName"`
	Username      string         `json:"username"`
	Email         string         `json:"email"`
	Url           *string        `json:"url,omitempty"`
	Location      *string        `json:"location,omitempty"`
	Bio           *string        `json:"bio,omitempty"`
	// Jobs submitted by this user.
	Jobs *JobCursor `json:"jobs"`
}

func (*User) isEntity() {}

type Version struct {
	Major int32 `json:"major"`
	Minor int32 `json:"minor"`
@@ -243,8 +350,8 @@ type Version struct {
	// If this API version is scheduled for deprecation, this is the date on which
	// it will stop working; or null if this API version is not scheduled for
	// deprecation.
	DeprecationDate time.Time `json:"deprecationDate,omitempty"`
	Settings        *Settings `json:"settings"`
	DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"`
	Settings        *Settings      `json:"settings"`
}

type WebhookTrigger struct {
@@ -252,15 +359,18 @@ type WebhookTrigger struct {
	Url       string           `json:"url"`
}

func (*WebhookTrigger) isTrigger() {}

type WebhookTriggerInput struct {
	Url string `json:"url"`
}

func SubmitJob(client *gqlclient.Client, ctx context.Context, manifest string, tags []string, note *string) (submit *Job, err error) {
	op := gqlclient.NewOperation("mutation submitJob ($manifest: String!, $tags: [String!], $note: String) {\n\tsubmit(manifest: $manifest, secrets: false, tags: $tags, note: $note) {\n\t\tid\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n")
func SubmitJob(client *gqlclient.Client, ctx context.Context, manifest string, tags []string, note *string, includeSecrets bool) (submit *Job, err error) {
	op := gqlclient.NewOperation("mutation submitJob ($manifest: String!, $tags: [String!], $note: String, $includeSecrets: Boolean!) {\n\tsubmit(manifest: $manifest, secrets: $includeSecrets, tags: $tags, note: $note) {\n\t\tid\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n")
	op.Var("manifest", manifest)
	op.Var("tags", tags)
	op.Var("note", note)
	op.Var("includeSecrets", includeSecrets)
	var respData struct {
		Submit *Job
	}
diff --git a/buildssrht/operations.graphql b/buildssrht/operations.graphql
index 228c9af..216c1fa 100644
--- a/buildssrht/operations.graphql
@@ -1,5 +1,5 @@
mutation submitJob($manifest: String!, $tags: [String!], $note: String) {
    submit(manifest: $manifest, secrets: false, tags: $tags, note: $note) {
mutation submitJob($manifest: String!, $tags: [String!], $note: String, $includeSecrets: Boolean!) {
    submit(manifest: $manifest, secrets: $includeSecrets, tags: $tags, note: $note) {
        id
        owner {
            canonicalName
diff --git a/main.go b/main.go
index da71cb0..963abc7 100644
--- a/main.go
+++ b/main.go
@@ -265,13 +265,14 @@ func main() {
			}

			ctx := &checkSuiteContext{
				Context:    r.Context(),
				gh:         newInstallationClient(atr, event.Installation),
				srht:       createSrhtClient(buildssrhtEndpoint, installation),
				baseRepo:   event.Repo,
				headRepo:   event.Repo,
				headCommit: event.CheckSuite.HeadCommit,
				headSHA:    event.CheckSuite.GetHeadSHA(),
				Context:        r.Context(),
				gh:             newInstallationClient(atr, event.Installation),
				srht:           createSrhtClient(buildssrhtEndpoint, installation),
				baseRepo:       event.Repo,
				headRepo:       event.Repo,
				headCommit:     event.CheckSuite.HeadCommit,
				headSHA:        event.CheckSuite.GetHeadSHA(),
				ownerSubmitted: event.Sender.GetLogin() == installation.Owner,
			}
			if len(event.CheckSuite.PullRequests) == 1 {
				ctx.pullRequest = event.CheckSuite.PullRequests[0]
@@ -298,13 +299,14 @@ func main() {
			}

			ctx := &checkSuiteContext{
				Context:     r.Context(),
				gh:          newInstallationClient(atr, event.Installation),
				srht:        createSrhtClient(buildssrhtEndpoint, installation),
				baseRepo:    event.Repo,
				headRepo:    event.PullRequest.Head.Repo,
				headSHA:     event.PullRequest.Head.GetSHA(),
				pullRequest: event.PullRequest,
				Context:        r.Context(),
				gh:             newInstallationClient(atr, event.Installation),
				srht:           createSrhtClient(buildssrhtEndpoint, installation),
				baseRepo:       event.Repo,
				headRepo:       event.PullRequest.Head.Repo,
				headSHA:        event.PullRequest.Head.GetSHA(),
				pullRequest:    event.PullRequest,
				ownerSubmitted: event.Sender.GetLogin() == installation.Owner,
			}

			var repoCommit *github.RepositoryCommit
@@ -359,6 +361,7 @@ type checkSuiteContext struct {
	baseRepo, headRepo *github.Repository
	headSHA            string
	headCommit         *github.Commit
	ownerSubmitted     bool

	pullRequest *github.PullRequest // may be nil
	headBranch  string              // may be empty
@@ -471,7 +474,7 @@ func startJob(ctx *checkSuiteContext, filename string) error {

[%v]: %v`, title, shortHash, commit.Author.GetName(), shortHash, commitURL)

	job, err := buildssrht.SubmitJob(ctx.srht.GQL, ctx, string(manifestBuf), tags, &note)
	job, err := buildssrht.SubmitJob(ctx.srht.GQL, ctx, string(manifestBuf), tags, &note, ctx.ownerSubmitted)
	if err != nil {
		return fmt.Errorf("failed to submit sr.ht job: %v", err)
	}
-- 
2.38.5