Repository: google/go-github (go)

Issue:
Audit Log API Returns Non-Standard Timestamp
The GitHub Audit Log API return a Unix timestamp in milliseconds, which the current timestamp unmarshalJSON implementation in go-github does not take into account. An implementation needs to be added to account for the audit log having a millisecond-based timestamp.

Pull request description:
Add Support For Millisecond Timestamps
The GitHub Audit Log API introduced timestamps with millisecond granularity. We add support for these timestamps by first parsing the timestamp into a Unix timestamp and then checking to see if the generated timestamp is in the future. If the timestamp is in the future we re-parse it as a Unix timestamp in Milliseconds.

Tests were added to ensure that when the timestamp is 1 millisecond off that the granularity of the timestamp is preserved by milliseconds correctly being parsed.

Closes #1849

Signed-off-by: Brett Logan <lindluni@github.com>

Patch:
diff --git a/github/actions_secrets.go b/github/actions_secrets.go
index c7e4d7b4..4763d3a1 100644
--- a/github/actions_secrets.go
+++ b/github/actions_secrets.go
@@ -85,6 +85,25 @@ func (s *ActionsService) GetOrgPublicKey(ctx context.Context, org string) (*Publ
 	return pubKey, resp, nil
 }
 
+// GetEnvPublicKey gets a public key that should be used for secret encryption.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#get-an-environment-public-key
+func (s *ActionsService) GetEnvPublicKey(ctx context.Context, repoID int, env string) (*PublicKey, *Response, error) {
+	u := fmt.Sprintf("repositories/%v/environments/%v/secrets/public-key", repoID, env)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pubKey := new(PublicKey)
+	resp, err := s.client.Do(ctx, req, pubKey)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pubKey, resp, nil
+}
+
 // Secret represents a repository action secret.
 type Secret struct {
 	Name                    string    `json:"name"`
@@ -328,3 +347,74 @@ func (s *ActionsService) DeleteOrgSecret(ctx context.Context, org, name string)
 
 	return s.client.Do(ctx, req, nil)
 }
+
+// ListEnvSecrets lists all secrets available in an environment.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#list-environment-secrets
+func (s *ActionsService) ListEnvSecrets(ctx context.Context, repoID int, env string, opts *ListOptions) (*Secrets, *Response, error) {
+	u := fmt.Sprintf("repositories/%v/environments/%v/secrets", repoID, env)
+	u, err := addOptions(u, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	secrets := new(Secrets)
+	resp, err := s.client.Do(ctx, req, &secrets)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return secrets, resp, nil
+}
+
+// GetEnvSecret gets a single environment secret without revealing its encrypted value.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#list-environment-secrets
+func (s *ActionsService) GetEnvSecret(ctx context.Context, repoID int, env, secretName string) (*Secret, *Response, error) {
+	u := fmt.Sprintf("repositories/%v/environments/%v/secrets/%v", repoID, env, secretName)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	secret := new(Secret)
+	resp, err := s.client.Do(ctx, req, secret)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return secret, resp, nil
+}
+
+// CreateOrUpdateEnvSecret creates or updates a repository secret with an encrypted value.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#create-or-update-an-environment-secret
+func (s *ActionsService) CreateOrUpdateEnvSecret(ctx context.Context, repoID int, env string, eSecret *EncryptedSecret) (*Response, error) {
+	u := fmt.Sprintf("repositories/%v/environments/%v/secrets/%v", repoID, env, eSecret.Name)
+
+	req, err := s.client.NewRequest("PUT", u, eSecret)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(ctx, req, nil)
+}
+
+// DeleteEnvSecret deletes a secret in an environment using the secret name.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#delete-an-environment-secret
+func (s *ActionsService) DeleteEnvSecret(ctx context.Context, repoID int, env, secretName string) (*Response, error) {
+	u := fmt.Sprintf("repositories/%v/environments/%v/secrets/%v", repoID, env, secretName)
+
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(ctx, req, nil)
+}
diff --git a/github/event.go b/github/event.go
index eec90515..4b87142a 100644
--- a/github/event.go
+++ b/github/event.go
@@ -98,6 +98,8 @@ func (e *Event) ParsePayload() (payload interface{}, err error) {
 		payload = &PullRequestReviewEvent{}
 	case "PullRequestReviewCommentEvent":
 		payload = &PullRequestReviewCommentEvent{}
+	case "PullRequestTargetEvent":
+		payload = &PullRequestTargetEvent{}
 	case "PushEvent":
 		payload = &PushEvent{}
 	case "ReleaseEvent":
diff --git a/github/event_types.go b/github/event_types.go
index eee919e2..c5fedb25 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -685,6 +685,48 @@ type PullRequestReviewCommentEvent struct {
 	Installation *Installation `json:"installation,omitempty"`
 }
 
+// PullRequestTargetEvent is triggered when a pull request is assigned, unassigned, labeled,
+// unlabeled, opened, edited, closed, reopened, synchronize, ready_for_review,
+// locked, unlocked, a pull request review is requested, or a review request is removed.
+// The Webhook event name is "pull_request_target".
+//
+// GitHub API docs: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target
+type PullRequestTargetEvent struct {
+	// Action is the action that was performed. Possible values are:
+	// "assigned", "unassigned", "labeled", "unlabeled", "opened", "edited", "closed", "reopened",
+	// "ready_for_review", "locked", "unlocked", "review_requested" or "review_request_removed".
+	// If the action is "closed" and the "merged" key is "false", the pull request was closed with unmerged commits.
+	// If the action is "closed" and the "merged" key is "true", the pull request was merged.
+	// While webhooks are also triggered when a pull request is synchronized, Events API timelines
+	// don't include pull request events with the "synchronize" action.
+	Action      *string      `json:"action,omitempty"`
+	Assignee    *User        `json:"assignee,omitempty"`
+	Number      *int         `json:"number,omitempty"`
+	PullRequest *PullRequest `json:"pull_request,omitempty"`
+
+	// The following fields are only populated by Webhook events.
+	Changes *EditChange `json:"changes,omitempty"`
+	// RequestedReviewer is populated in "review_requested", "review_request_removed" event deliveries.
+	// A request affecting multiple reviewers at once is split into multiple
+	// such event deliveries, each with a single, different RequestedReviewer.
+	RequestedReviewer *User `json:"requested_reviewer,omitempty"`
+	// In the event that a team is requested instead of a user, "requested_team" gets sent in place of
+	// "requested_user" with the same delivery behavior.
+	RequestedTeam *Team         `json:"requested_team,omitempty"`
+	Repo          *Repository   `json:"repository,omitempty"`
+	Sender        *User         `json:"sender,omitempty"`
+	Installation  *Installation `json:"installation,omitempty"`
+	Label         *Label        `json:"label,omitempty"` // Populated in "labeled" event deliveries.
+
+	// The following field is only present when the webhook is triggered on
+	// a repository belonging to an organization.
+	Organization *Organization `json:"organization,omitempty"`
+
+	// The following fields are only populated when the Action is "synchronize".
+	Before *string `json:"before,omitempty"`
+	After  *string `json:"after,omitempty"`
+}
+
 // PushEvent represents a git push to a GitHub repository.
 //
 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#push
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 89798ab5..678c55f5 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -1252,6 +1252,22 @@ func (b *BranchListOptions) GetProtected() bool {
 	return *b.Protected
 }
 
+// GetCustomBranchPolicies returns the CustomBranchPolicies field if it's non-nil, zero value otherwise.
+func (b *BranchPolicy) GetCustomBranchPolicies() bool {
+	if b == nil || b.CustomBranchPolicies == nil {
+		return false
+	}
+	return *b.CustomBranchPolicies
+}
+
+// GetProtectedBranches returns the ProtectedBranches field if it's non-nil, zero value otherwise.
+func (b *BranchPolicy) GetProtectedBranches() bool {
+	if b == nil || b.ProtectedBranches == nil {
+		return false
+	}
+	return *b.ProtectedBranches
+}
+
 // GetApp returns the App field.
 func (c *CheckRun) GetApp() *App {
 	if c == nil {
@@ -2900,6 +2916,22 @@ func (c *CreateRunnerGroupRequest) GetVisibility() string {
 	return *c.Visibility
 }
 
+// GetDeploymentBranchPolicy returns the DeploymentBranchPolicy field.
+func (c *CreateUpdateEnvironment) GetDeploymentBranchPolicy() *BranchPolicy {
+	if c == nil {
+		return nil
+	}
+	return c.DeploymentBranchPolicy
+}
+
+// GetWaitTimer returns the WaitTimer field if it's non-nil, zero value otherwise.
+func (c *CreateUpdateEnvironment) GetWaitTimer() int {
+	if c == nil || c.WaitTimer == nil {
+		return 0
+	}
+	return *c.WaitTimer
+}
+
 // GetBody returns the Body field if it's non-nil, zero value otherwise.
 func (c *CreateUserProjectOptions) GetBody() string {
 	if c == nil || c.Body == nil {
@@ -3668,6 +3700,126 @@ func (e *Enterprise) GetWebsiteURL() string {
 	return *e.WebsiteURL
 }
 
+// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
+func (e *Environment) GetCreatedAt() Timestamp {
+	if e == nil || e.CreatedAt == nil {
+		return Timestamp{}
+	}
+	return *e.CreatedAt
+}
+
+// GetDeploymentBranchPolicy returns the DeploymentBranchPolicy field.
+func (e *Environment) GetDeploymentBranchPolicy() *BranchPolicy {
+	if e == nil {
+		return nil
+	}
+	return e.DeploymentBranchPolicy
+}
+
+// GetEnvironmentName returns the EnvironmentName field if it's non-nil, zero value otherwise.
+func (e *Environment) GetEnvironmentName() string {
+	if e == nil || e.EnvironmentName == nil {
+		return ""
+	}
+	return *e.EnvironmentName
+}
+
+// GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise.
+func (e *Environment) GetHTMLURL() string {
+	if e == nil || e.HTMLURL == nil {
+		return ""
+	}
+	return *e.HTMLURL
+}
+
+// GetID returns the ID field if it's non-nil, zero value otherwise.
+func (e *Environment) GetID() int64 {
+	if e == nil || e.ID == nil {
+		return 0
+	}
+	return *e.ID
+}
+
+// GetName returns the Name field if it's non-nil, zero value otherwise.
+func (e *Environment) GetName() string {
+	if e == nil || e.Name == nil {
+		return ""
+	}
+	return *e.Name
+}
+
+// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise.
+func (e *Environment) GetNodeID() string {
+	if e == nil || e.NodeID == nil {
+		return ""
+	}
+	return *e.NodeID
+}
+
+// GetOwner returns the Owner field if it's non-nil, zero value otherwise.
+func (e *Environment) GetOwner() string {
+	if e == nil || e.Owner == nil {
+		return ""
+	}
+	return *e.Owner
+}
+
+// GetRepo returns the Repo field if it's non-nil, zero value otherwise.
+func (e *Environment) GetRepo() string {
+	if e == nil || e.Repo == nil {
+		return ""
+	}
+	return *e.Repo
+}
+
+// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
+func (e *Environment) GetUpdatedAt() Timestamp {
+	if e == nil || e.UpdatedAt == nil {
+		return Timestamp{}
+	}
+	return *e.UpdatedAt
+}
+
+// GetURL returns the URL field if it's non-nil, zero value otherwise.
+func (e *Environment) GetURL() string {
+	if e == nil || e.URL == nil {
+		return ""
+	}
+	return *e.URL
+}
+
+// GetWaitTimer returns the WaitTimer field if it's non-nil, zero value otherwise.
+func (e *Environment) GetWaitTimer() int {
+	if e == nil || e.WaitTimer == nil {
+		return 0
+	}
+	return *e.WaitTimer
+}
+
+// GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise.
+func (e *EnvResponse) GetTotalCount() int {
+	if e == nil || e.TotalCount == nil {
+		return 0
+	}
+	return *e.TotalCount
+}
+
+// GetID returns the ID field if it's non-nil, zero value otherwise.
+func (e *EnvReviewers) GetID() int64 {
+	if e == nil || e.ID == nil {
+		return 0
+	}
+	return *e.ID
+}
+
+// GetType returns the Type field if it's non-nil, zero value otherwise.
+func (e *EnvReviewers) GetType() string {
+	if e == nil || e.Type == nil {
+		return ""
+	}
+	return *e.Type
+}
+
 // GetActor returns the Actor field.
 func (e *Event) GetActor() *User {
 	if e == nil {
@@ -9620,6 +9772,38 @@ func (p *ProtectionRequest) GetRestrictions() *BranchRestrictionsRequest {
 	return p.Restrictions
 }
 
+// GetID returns the ID field if it's non-nil, zero value otherwise.
+func (p *ProtectionRule) GetID() int64 {
+	if p == nil || p.ID == nil {
+		return 0
+	}
+	return *p.ID
+}
+
+// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise.
+func (p *ProtectionRule) GetNodeID() string {
+	if p == nil || p.NodeID == nil {
+		return ""
+	}
+	return *p.NodeID
+}
+
+// GetType returns the Type field if it's non-nil, zero value otherwise.
+func (p *ProtectionRule) GetType() string {
+	if p == nil || p.Type == nil {
+		return ""
+	}
+	return *p.Type
+}
+
+// GetWaitTimer returns the WaitTimer field if it's non-nil, zero value otherwise.
+func (p *ProtectionRule) GetWaitTimer() int {
+	if p == nil || p.WaitTimer == nil {
+		return 0
+	}
+	return *p.WaitTimer
+}
+
 // GetInstallation returns the Installation field.
 func (p *PublicEvent) GetInstallation() *Installation {
 	if p == nil {
@@ -10700,6 +10884,118 @@ func (p *PullRequestReviewsEnforcementUpdate) GetDismissStaleReviews() bool {
 	return *p.DismissStaleReviews
 }
 
+// GetAction returns the Action field if it's non-nil, zero value otherwise.
+func (p *PullRequestTargetEvent) GetAction() string {
+	if p == nil || p.Action == nil {
+		return ""
+	}
+	return *p.Action
+}
+
+// GetAfter returns the After field if it's non-nil, zero value otherwise.
+func (p *PullRequestTargetEvent) GetAfter() string {
+	if p == nil || p.After == nil {
+		return ""
+	}
+	return *p.After
+}
+
+// GetAssignee returns the Assignee field.
+func (p *PullRequestTargetEvent) GetAssignee() *User {
+	if p == nil {
+		return nil
+	}
+	return p.Assignee
+}
+
+// GetBefore returns the Before field if it's non-nil, zero value otherwise.
+func (p *PullRequestTargetEvent) GetBefore() string {
+	if p == nil || p.Before == nil {
+		return ""
+	}
+	return *p.Before
+}
+
+// GetChanges returns the Changes field.
+func (p *PullRequestTargetEvent) GetChanges() *EditChange {
+	if p == nil {
+		return nil
+	}
+	return p.Changes
+}
+
+// GetInstallation returns the Installation field.
+func (p *PullRequestTargetEvent) GetInstallation() *Installation {
+	if p == nil {
+		return nil
+	}
+	return p.Installation
+}
+
+// GetLabel returns the Label field.
+func (p *PullRequestTargetEvent) GetLabel() *Label {
+	if p == nil {
+		return nil
+	}
+	return p.Label
+}
+
+// GetNumber returns the Number field if it's non-nil, zero value otherwise.
+func (p *PullRequestTargetEvent) GetNumber() int {
+	if p == nil || p.Number == nil {
+		return 0
+	}
+	return *p.Number
+}
+
+// GetOrganization returns the Organization field.
+func (p *PullRequestTargetEvent) GetOrganization() *Organization {
+	if p == nil {
+		return nil
+	}
+	return p.Organization
+}
+
+// GetPullRequest returns the PullRequest field.
+func (p *PullRequestTargetEvent) GetPullRequest() *PullRequest {
+	if p == nil {
+		return nil
+	}
+	return p.PullRequest
+}
+
+// GetRepo returns the Repo field.
+func (p *PullRequestTargetEvent) GetRepo() *Repository {
+	if p == nil {
+		return nil
+	}
+	return p.Repo
+}
+
+// GetRequestedReviewer returns the RequestedReviewer field.
+func (p *PullRequestTargetEvent) GetRequestedReviewer() *User {
+	if p == nil {
+		return nil
+	}
+	return p.RequestedReviewer
+}
+
+// GetRequestedTeam returns the RequestedTeam field.
+func (p *PullRequestTargetEvent) GetRequestedTeam() *Team {
+	if p == nil {
+		return nil
+	}
+	return p.RequestedTeam
+}
+
+// GetSender returns the Sender field.
+func (p *PullRequestTargetEvent) GetSender() *User {
+	if p == nil {
+		return nil
+	}
+	return p.Sender
+}
+
 // GetMergablePulls returns the MergablePulls field if it's non-nil, zero value otherwise.
 func (p *PullStats) GetMergablePulls() int {
 	if p == nil || p.MergablePulls == nil {
@@ -13196,6 +13492,14 @@ func (r *RepoStatus) GetURL() string {
 	return *r.URL
 }
 
+// GetType returns the Type field if it's non-nil, zero value otherwise.
+func (r *RequiredReviewer) GetType() string {
+	if r == nil || r.Type == nil {
+		return ""
+	}
+	return *r.Type
+}
+
 // GetStrict returns the Strict field if it's non-nil, zero value otherwise.
 func (r *RequiredStatusChecksRequest) GetStrict() bool {
 	if r == nil || r.Strict == nil {
diff --git a/github/messages.go b/github/messages.go
index 428684d6..98a76da6 100644
--- a/github/messages.go
+++ b/github/messages.go
@@ -74,9 +74,10 @@ var (
 		"project_card":                   "ProjectCardEvent",
 		"project_column":                 "ProjectColumnEvent",
 		"public":                         "PublicEvent",
+		"pull_request":                   "PullRequestEvent",
 		"pull_request_review":            "PullRequestReviewEvent",
 		"pull_request_review_comment":    "PullRequestReviewCommentEvent",
-		"pull_request":                   "PullRequestEvent",
+		"pull_request_target":            "PullRequestTargetEvent",
 		"push":                           "PushEvent",
 		"repository":                     "RepositoryEvent",
 		"repository_dispatch":            "RepositoryDispatchEvent",
diff --git a/github/repos_environments.go b/github/repos_environments.go
new file mode 100644
index 00000000..25cb005e
--- /dev/null
+++ b/github/repos_environments.go
@@ -0,0 +1,191 @@
+// Copyright 2021 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+)
+
+// Environment represents a single environment in a repository.
+type Environment struct {
+	Owner                  *string         `json:"owner,omitempty"`
+	Repo                   *string         `json:"repo,omitempty"`
+	EnvironmentName        *string         `json:"environment_name,omitempty"`
+	WaitTimer              *int            `json:"wait_timer,omitempty"`
+	Reviewers              []*EnvReviewers `json:"reviewers,omitempty"`
+	DeploymentBranchPolicy *BranchPolicy   `json:"deployment_branch_policy,omitempty"`
+	// Return/response only values
+	ID              *int64            `json:"id,omitempty"`
+	NodeID          *string           `json:"node_id,omitempty"`
+	Name            *string           `json:"name,omitempty"`
+	URL             *string           `json:"url,omitempty"`
+	HTMLURL         *string           `json:"html_url,omitempty"`
+	CreatedAt       *Timestamp        `json:"created_at,omitempty"`
+	UpdatedAt       *Timestamp        `json:"updated_at,omitempty"`
+	ProtectionRules []*ProtectionRule `json:"protection_rules,omitempty"`
+}
+
+// EnvReviewers represents a single environment reviewer entry.
+type EnvReviewers struct {
+	Type *string `json:"type,omitempty"`
+	ID   *int64  `json:"id,omitempty"`
+}
+
+// BranchPolicy represents the options for whether a branch deployment policy is applied to this environment.
+type BranchPolicy struct {
+	ProtectedBranches    *bool `json:"protected_branches,omitempty"`
+	CustomBranchPolicies *bool `json:"custom_branch_policies,omitempty"`
+}
+
+// EnvResponse represents the slightly different format of response that comes back when you list an environment.
+type EnvResponse struct {
+	TotalCount   *int           `json:"total_count,omitempty"`
+	Environments []*Environment `json:"environments,omitempty"`
+}
+
+// ProtectionRule represents a single protection rule applied to the environment.
+type ProtectionRule struct {
+	ID        *int64              `json:"id,omitempty"`
+	NodeID    *string             `json:"node_id,omitempty"`
+	Type      *string             `json:"type,omitempty"`
+	WaitTimer *int                `json:"wait_timer,omitempty"`
+	Reviewers []*RequiredReviewer `json:"reviewers,omitempty"`
+}
+
+// RequiredReviewer represents a required reviewer.
+type RequiredReviewer struct {
+	Type     *string     `json:"type,omitempty"`
+	Reviewer interface{} `json:"reviewer,omitempty"`
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+// This helps us handle the fact that RequiredReviewer can have either a User or Team type reviewer field.
+func (r *RequiredReviewer) UnmarshalJSON(data []byte) error {
+	type aliasReviewer RequiredReviewer
+	var reviewer aliasReviewer
+	if err := json.Unmarshal(data, &reviewer); err != nil {
+		return err
+	}
+
+	r.Type = reviewer.Type
+
+	switch *reviewer.Type {
+	case "User":
+		reviewer.Reviewer = &User{}
+		if err := json.Unmarshal(data, &reviewer); err != nil {
+			return err
+		}
+		r.Reviewer = reviewer.Reviewer
+	case "Team":
+		reviewer.Reviewer = &Team{}
+		if err := json.Unmarshal(data, &reviewer); err != nil {
+			return err
+		}
+		r.Reviewer = reviewer.Reviewer
+	default:
+		r.Type = nil
+		r.Reviewer = nil
+		return fmt.Errorf("reviewer.Type is %T, not a string of 'User' or 'Team', unable to unmarshal", reviewer.Type)
+	}
+
+	return nil
+}
+
+// ListEnvironments lists all environments for a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/repos#get-all-environments
+func (s *RepositoriesService) ListEnvironments(ctx context.Context, owner, repo string) (*EnvResponse, *Response, error) {
+	u := fmt.Sprintf("repos/%s/%s/environments", owner, repo)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var list *EnvResponse
+	resp, err := s.client.Do(ctx, req, &list)
+	if err != nil {
+		return nil, resp, err
+	}
+	return list, resp, nil
+}
+
+// GetEnvironment get a single environment for a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/repos#get-an-environment
+func (s *RepositoriesService) GetEnvironment(ctx context.Context, owner, repo, name string) (*Environment, *Response, error) {
+	u := fmt.Sprintf("repos/%s/%s/environments/%s", owner, repo, name)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var env *Environment
+	resp, err := s.client.Do(ctx, req, &env)
+	if err != nil {
+		return nil, resp, err
+	}
+	return env, resp, nil
+}
+
+// MarshalJSON implements the json.Marshaler interface.
+// As the only way to clear a WaitTimer is to set it to 0, a missing WaitTimer object should default to 0, not null.
+func (c *CreateUpdateEnvironment) MarshalJSON() ([]byte, error) {
+	type Alias CreateUpdateEnvironment
+	if c.WaitTimer == nil {
+		c.WaitTimer = Int(0)
+	}
+	return json.Marshal(&struct {
+		*Alias
+	}{
+		Alias: (*Alias)(c),
+	})
+}
+
+// CreateUpdateEnvironment represents the fields required for the create/update operation
+// following the Create/Update release example.
+// See https://github.com/google/go-github/issues/992 for more information.
+// Removed omitempty here as the API expects null values for reviewers and deployment_branch_policy to clear them.
+type CreateUpdateEnvironment struct {
+	WaitTimer              *int            `json:"wait_timer"`
+	Reviewers              []*EnvReviewers `json:"reviewers"`
+	DeploymentBranchPolicy *BranchPolicy   `json:"deployment_branch_policy"`
+}
+
+// CreateUpdateEnvironment create or update a new environment for a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/repos#create-or-update-an-environment
+func (s *RepositoriesService) CreateUpdateEnvironment(ctx context.Context, owner, repo, name string, environment *CreateUpdateEnvironment) (*Environment, *Response, error) {
+	u := fmt.Sprintf("repos/%s/%s/environments/%s", owner, repo, name)
+
+	req, err := s.client.NewRequest("PUT", u, environment)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	e := new(Environment)
+	resp, err := s.client.Do(ctx, req, e)
+	if err != nil {
+		return nil, resp, err
+	}
+	return e, resp, nil
+}
+
+// DeleteEnvironment delete an environment from a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/repos#delete-an-environment
+func (s *RepositoriesService) DeleteEnvironment(ctx context.Context, owner, repo, name string) (*Response, error) {
+	u := fmt.Sprintf("repos/%s/%s/environments/%s", owner, repo, name)
+
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+	return s.client.Do(ctx, req, nil)
+}
diff --git a/github/timestamp.go b/github/timestamp.go
index 90929d57..1061a55e 100644
--- a/github/timestamp.go
+++ b/github/timestamp.go
@@ -29,6 +29,9 @@ func (t *Timestamp) UnmarshalJSON(data []byte) (err error) {
 	i, err := strconv.ParseInt(str, 10, 64)
 	if err == nil {
 		t.Time = time.Unix(i, 0)
+		if t.Time.Year() > 3000 {
+			t.Time = time.Unix(0, i*1e6)
+		}
 	} else {
 		t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str)
 	}


Test patch:
diff --git a/github/actions_secrets_test.go b/github/actions_secrets_test.go
index 6c06dfe2..5580bd18 100644
--- a/github/actions_secrets_test.go
+++ b/github/actions_secrets_test.go
@@ -599,3 +599,212 @@ func TestActionsService_DeleteOrgSecret(t *testing.T) {
 		return client.Actions.DeleteOrgSecret(ctx, "o", "NAME")
 	})
 }
+
+func TestActionsService_GetEnvPublicKey(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+	mux.HandleFunc("/repositories/1/environments/e/secrets/public-key", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"key_id":"1234","key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}`)
+	})
+
+	ctx := context.Background()
+	key, _, err := client.Actions.GetEnvPublicKey(ctx, 1, "e")
+	if err != nil {
+		t.Errorf("Actions.GetEnvPublicKey returned error: %v", err)
+	}
+
+	want := &PublicKey{KeyID: String("1234"), Key: String("2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234")}
+	if !reflect.DeepEqual(key, want) {
+		t.Errorf("Actions.GetEnvPublicKey returned %+v, want %+v", key, want)
+	}
+
+	const methodName = "GetEnvPublicKey"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Actions.GetEnvPublicKey(ctx, 0.0, "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Actions.GetEnvPublicKey(ctx, 1, "e")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestActionsService_GetEnvPublicKeyNumeric(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repositories/1/environments/e/secrets/public-key", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"key_id":1234,"key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}`)
+	})
+
+	ctx := context.Background()
+	key, _, err := client.Actions.GetEnvPublicKey(ctx, 1, "e")
+	if err != nil {
+		t.Errorf("Actions.GetEnvPublicKey returned error: %v", err)
+	}
+
+	want := &PublicKey{KeyID: String("1234"), Key: String("2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234")}
+	if !reflect.DeepEqual(key, want) {
+		t.Errorf("Actions.GetEnvPublicKey returned %+v, want %+v", key, want)
+	}
+
+	const methodName = "GetEnvPublicKey"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Actions.GetEnvPublicKey(ctx, 0.0, "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Actions.GetEnvPublicKey(ctx, 1, "e")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestActionsService_ListEnvSecrets(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repositories/1/environments/e/secrets", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testFormValues(t, r, values{"per_page": "2", "page": "2"})
+		fmt.Fprint(w, `{"total_count":4,"secrets":[{"name":"A","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"},{"name":"B","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}]}`)
+	})
+
+	opts := &ListOptions{Page: 2, PerPage: 2}
+	ctx := context.Background()
+	secrets, _, err := client.Actions.ListEnvSecrets(ctx, 1, "e", opts)
+	if err != nil {
+		t.Errorf("Actions.ListEnvSecrets returned error: %v", err)
+	}
+
+	want := &Secrets{
+		TotalCount: 4,
+		Secrets: []*Secret{
+			{Name: "A", CreatedAt: Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}},
+			{Name: "B", CreatedAt: Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}},
+		},
+	}
+	if !reflect.DeepEqual(secrets, want) {
+		t.Errorf("Actions.ListEnvSecrets returned %+v, want %+v", secrets, want)
+	}
+
+	const methodName = "ListEnvSecrets"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Actions.ListEnvSecrets(ctx, 0.0, "\n", opts)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Actions.ListEnvSecrets(ctx, 1, "e", opts)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestActionsService_GetEnvSecret(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repositories/1/environments/e/secrets/secret", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"name":"secret","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}`)
+	})
+
+	ctx := context.Background()
+	secret, _, err := client.Actions.GetEnvSecret(ctx, 1, "e", "secret")
+	if err != nil {
+		t.Errorf("Actions.GetEnvSecret returned error: %v", err)
+	}
+
+	want := &Secret{
+		Name:      "secret",
+		CreatedAt: Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)},
+		UpdatedAt: Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)},
+	}
+	if !reflect.DeepEqual(secret, want) {
+		t.Errorf("Actions.GetEnvSecret returned %+v, want %+v", secret, want)
+	}
+
+	const methodName = "GetEnvSecret"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Actions.GetEnvSecret(ctx, 0.0, "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Actions.GetEnvSecret(ctx, 1, "e", "secret")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestActionsService_CreateOrUpdateEnvSecret(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repositories/1/environments/e/secrets/secret", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PUT")
+		testHeader(t, r, "Content-Type", "application/json")
+		testBody(t, r, `{"key_id":"1234","encrypted_value":"QIv="}`+"\n")
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	input := &EncryptedSecret{
+		Name:           "secret",
+		EncryptedValue: "QIv=",
+		KeyID:          "1234",
+	}
+	ctx := context.Background()
+	_, err := client.Actions.CreateOrUpdateEnvSecret(ctx, 1, "e", input)
+	if err != nil {
+		t.Errorf("Actions.CreateOrUpdateEnvSecret returned error: %v", err)
+	}
+
+	const methodName = "CreateOrUpdateEnvSecret"
+	testBadOptions(t, methodName, func() (err error) {
+		_, err = client.Actions.CreateOrUpdateEnvSecret(ctx, 0.0, "\n", input)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		return client.Actions.CreateOrUpdateEnvSecret(ctx, 1, "e", input)
+	})
+}
+
+func TestActionsService_DeleteEnvSecret(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repositories/1/environments/e/secrets/secret", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+	})
+
+	ctx := context.Background()
+	_, err := client.Actions.DeleteEnvSecret(ctx, 1, "e", "secret")
+	if err != nil {
+		t.Errorf("Actions.DeleteEnvSecret returned error: %v", err)
+	}
+
+	const methodName = "DeleteEnvSecret"
+	testBadOptions(t, methodName, func() (err error) {
+		_, err = client.Actions.DeleteEnvSecret(ctx, 0.0, "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		return client.Actions.DeleteEnvSecret(ctx, 1, "r", "secret")
+	})
+}
diff --git a/github/enterprise_audit_log_test.go b/github/enterprise_audit_log_test.go
index 2e13408e..d0068158 100644
--- a/github/enterprise_audit_log_test.go
+++ b/github/enterprise_audit_log_test.go
@@ -55,7 +55,7 @@ func TestEnterpriseService_GetAuditLog(t *testing.T) {
 	}
 	startedAt, _ := time.Parse(time.RFC3339, "2021-03-07T00:33:04.000Z")
 	completedAt, _ := time.Parse(time.RFC3339, "2021-03-07T00:35:08.000Z")
-	timestamp := time.Unix(1615077308538, 0)
+	timestamp := time.Unix(0, 1615077308538*1e6)
 
 	want := []*AuditEntry{
 		{
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index 03ee3242..66f0c99a 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -1503,6 +1503,26 @@ func TestBranchListOptions_GetProtected(tt *testing.T) {
 	b.GetProtected()
 }
 
+func TestBranchPolicy_GetCustomBranchPolicies(tt *testing.T) {
+	var zeroValue bool
+	b := &BranchPolicy{CustomBranchPolicies: &zeroValue}
+	b.GetCustomBranchPolicies()
+	b = &BranchPolicy{}
+	b.GetCustomBranchPolicies()
+	b = nil
+	b.GetCustomBranchPolicies()
+}
+
+func TestBranchPolicy_GetProtectedBranches(tt *testing.T) {
+	var zeroValue bool
+	b := &BranchPolicy{ProtectedBranches: &zeroValue}
+	b.GetProtectedBranches()
+	b = &BranchPolicy{}
+	b.GetProtectedBranches()
+	b = nil
+	b.GetProtectedBranches()
+}
+
 func TestCheckRun_GetApp(tt *testing.T) {
 	c := &CheckRun{}
 	c.GetApp()
@@ -3398,6 +3418,23 @@ func TestCreateRunnerGroupRequest_GetVisibility(tt *testing.T) {
 	c.GetVisibility()
 }
 
+func TestCreateUpdateEnvironment_GetDeploymentBranchPolicy(tt *testing.T) {
+	c := &CreateUpdateEnvironment{}
+	c.GetDeploymentBranchPolicy()
+	c = nil
+	c.GetDeploymentBranchPolicy()
+}
+
+func TestCreateUpdateEnvironment_GetWaitTimer(tt *testing.T) {
+	var zeroValue int
+	c := &CreateUpdateEnvironment{WaitTimer: &zeroValue}
+	c.GetWaitTimer()
+	c = &CreateUpdateEnvironment{}
+	c.GetWaitTimer()
+	c = nil
+	c.GetWaitTimer()
+}
+
 func TestCreateUserProjectOptions_GetBody(tt *testing.T) {
 	var zeroValue string
 	c := &CreateUserProjectOptions{Body: &zeroValue}
@@ -4307,6 +4344,153 @@ func TestEnterprise_GetWebsiteURL(tt *testing.T) {
 	e.GetWebsiteURL()
 }
 
+func TestEnvironment_GetCreatedAt(tt *testing.T) {
+	var zeroValue Timestamp
+	e := &Environment{CreatedAt: &zeroValue}
+	e.GetCreatedAt()
+	e = &Environment{}
+	e.GetCreatedAt()
+	e = nil
+	e.GetCreatedAt()
+}
+
+func TestEnvironment_GetDeploymentBranchPolicy(tt *testing.T) {
+	e := &Environment{}
+	e.GetDeploymentBranchPolicy()
+	e = nil
+	e.GetDeploymentBranchPolicy()
+}
+
+func TestEnvironment_GetEnvironmentName(tt *testing.T) {
+	var zeroValue string
+	e := &Environment{EnvironmentName: &zeroValue}
+	e.GetEnvironmentName()
+	e = &Environment{}
+	e.GetEnvironmentName()
+	e = nil
+	e.GetEnvironmentName()
+}
+
+func TestEnvironment_GetHTMLURL(tt *testing.T) {
+	var zeroValue string
+	e := &Environment{HTMLURL: &zeroValue}
+	e.GetHTMLURL()
+	e = &Environment{}
+	e.GetHTMLURL()
+	e = nil
+	e.GetHTMLURL()
+}
+
+func TestEnvironment_GetID(tt *testing.T) {
+	var zeroValue int64
+	e := &Environment{ID: &zeroValue}
+	e.GetID()
+	e = &Environment{}
+	e.GetID()
+	e = nil
+	e.GetID()
+}
+
+func TestEnvironment_GetName(tt *testing.T) {
+	var zeroValue string
+	e := &Environment{Name: &zeroValue}
+	e.GetName()
+	e = &Environment{}
+	e.GetName()
+	e = nil
+	e.GetName()
+}
+
+func TestEnvironment_GetNodeID(tt *testing.T) {
+	var zeroValue string
+	e := &Environment{NodeID: &zeroValue}
+	e.GetNodeID()
+	e = &Environment{}
+	e.GetNodeID()
+	e = nil
+	e.GetNodeID()
+}
+
+func TestEnvironment_GetOwner(tt *testing.T) {
+	var zeroValue string
+	e := &Environment{Owner: &zeroValue}
+	e.GetOwner()
+	e = &Environment{}
+	e.GetOwner()
+	e = nil
+	e.GetOwner()
+}
+
+func TestEnvironment_GetRepo(tt *testing.T) {
+	var zeroValue string
+	e := &Environment{Repo: &zeroValue}
+	e.GetRepo()
+	e = &Environment{}
+	e.GetRepo()
+	e = nil
+	e.GetRepo()
+}
+
+func TestEnvironment_GetUpdatedAt(tt *testing.T) {
+	var zeroValue Timestamp
+	e := &Environment{UpdatedAt: &zeroValue}
+	e.GetUpdatedAt()
+	e = &Environment{}
+	e.GetUpdatedAt()
+	e = nil
+	e.GetUpdatedAt()
+}
+
+func TestEnvironment_GetURL(tt *testing.T) {
+	var zeroValue string
+	e := &Environment{URL: &zeroValue}
+	e.GetURL()
+	e = &Environment{}
+	e.GetURL()
+	e = nil
+	e.GetURL()
+}
+
+func TestEnvironment_GetWaitTimer(tt *testing.T) {
+	var zeroValue int
+	e := &Environment{WaitTimer: &zeroValue}
+	e.GetWaitTimer()
+	e = &Environment{}
+	e.GetWaitTimer()
+	e = nil
+	e.GetWaitTimer()
+}
+
+func TestEnvResponse_GetTotalCount(tt *testing.T) {
+	var zeroValue int
+	e := &EnvResponse{TotalCount: &zeroValue}
+	e.GetTotalCount()
+	e = &EnvResponse{}
+	e.GetTotalCount()
+	e = nil
+	e.GetTotalCount()
+}
+
+func TestEnvReviewers_GetID(tt *testing.T) {
+	var zeroValue int64
+	e := &EnvReviewers{ID: &zeroValue}
+	e.GetID()
+	e = &EnvReviewers{}
+	e.GetID()
+	e = nil
+	e.GetID()
+}
+
+func TestEnvReviewers_GetType(tt *testing.T) {
+	var zeroValue string
+	e := &EnvReviewers{Type: &zeroValue}
+	e.GetType()
+	e = &EnvReviewers{}
+	e.GetType()
+	e = nil
+	e.GetType()
+}
+
 func TestEvent_GetActor(tt *testing.T) {
 	e := &Event{}
 	e.GetActor()
@@ -11249,6 +11433,46 @@ func TestProtectionRequest_GetRestrictions(tt *testing.T) {
 	p.GetRestrictions()
 }
 
+func TestProtectionRule_GetID(tt *testing.T) {
+	var zeroValue int64
+	p := &ProtectionRule{ID: &zeroValue}
+	p.GetID()
+	p = &ProtectionRule{}
+	p.GetID()
+	p = nil
+	p.GetID()
+}
+
+func TestProtectionRule_GetNodeID(tt *testing.T) {
+	var zeroValue string
+	p := &ProtectionRule{NodeID: &zeroValue}
+	p.GetNodeID()
+	p = &ProtectionRule{}
+	p.GetNodeID()
+	p = nil
+	p.GetNodeID()
+}
+
+func TestProtectionRule_GetType(tt *testing.T) {
+	var zeroValue string
+	p := &ProtectionRule{Type: &zeroValue}
+	p.GetType()
+	p = &ProtectionRule{}
+	p.GetType()
+	p = nil
+	p.GetType()
+}
+
+func TestProtectionRule_GetWaitTimer(tt *testing.T) {
+	var zeroValue int
+	p := &ProtectionRule{WaitTimer: &zeroValue}
+	p.GetWaitTimer()
+	p = &ProtectionRule{}
+	p.GetWaitTimer()
+	p = nil
+	p.GetWaitTimer()
+}
+
 func TestPublicEvent_GetInstallation(tt *testing.T) {
 	p := &PublicEvent{}
 	p.GetInstallation()
@@ -12479,6 +12703,116 @@ func TestPullRequestReviewsEnforcementUpdate_GetDismissStaleReviews(tt *testing.
 	p.GetDismissStaleReviews()
 }
 
+func TestPullRequestTargetEvent_GetAction(tt *testing.T) {
+	var zeroValue string
+	p := &PullRequestTargetEvent{Action: &zeroValue}
+	p.GetAction()
+	p = &PullRequestTargetEvent{}
+	p.GetAction()
+	p = nil
+	p.GetAction()
+}
+
+func TestPullRequestTargetEvent_GetAfter(tt *testing.T) {
+	var zeroValue string
+	p := &PullRequestTargetEvent{After: &zeroValue}
+	p.GetAfter()
+	p = &PullRequestTargetEvent{}
+	p.GetAfter()
+	p = nil
+	p.GetAfter()
+}
+
+func TestPullRequestTargetEvent_GetAssignee(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetAssignee()
+	p = nil
+	p.GetAssignee()
+}
+
+func TestPullRequestTargetEvent_GetBefore(tt *testing.T) {
+	var zeroValue string
+	p := &PullRequestTargetEvent{Before: &zeroValue}
+	p.GetBefore()
+	p = &PullRequestTargetEvent{}
+	p.GetBefore()
+	p = nil
+	p.GetBefore()
+}
+
+func TestPullRequestTargetEvent_GetChanges(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetChanges()
+	p = nil
+	p.GetChanges()
+}
+
+func TestPullRequestTargetEvent_GetInstallation(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetInstallation()
+	p = nil
+	p.GetInstallation()
+}
+
+func TestPullRequestTargetEvent_GetLabel(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetLabel()
+	p = nil
+	p.GetLabel()
+}
+
+func TestPullRequestTargetEvent_GetNumber(tt *testing.T) {
+	var zeroValue int
+	p := &PullRequestTargetEvent{Number: &zeroValue}
+	p.GetNumber()
+	p = &PullRequestTargetEvent{}
+	p.GetNumber()
+	p = nil
+	p.GetNumber()
+}
+
+func TestPullRequestTargetEvent_GetOrganization(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetOrganization()
+	p = nil
+	p.GetOrganization()
+}
+
+func TestPullRequestTargetEvent_GetPullRequest(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetPullRequest()
+	p = nil
+	p.GetPullRequest()
+}
+
+func TestPullRequestTargetEvent_GetRepo(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetRepo()
+	p = nil
+	p.GetRepo()
+}
+
+func TestPullRequestTargetEvent_GetRequestedReviewer(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetRequestedReviewer()
+	p = nil
+	p.GetRequestedReviewer()
+}
+
+func TestPullRequestTargetEvent_GetRequestedTeam(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetRequestedTeam()
+	p = nil
+	p.GetRequestedTeam()
+}
+
+func TestPullRequestTargetEvent_GetSender(tt *testing.T) {
+	p := &PullRequestTargetEvent{}
+	p.GetSender()
+	p = nil
+	p.GetSender()
+}
+
 func TestPullStats_GetMergablePulls(tt *testing.T) {
 	var zeroValue int
 	p := &PullStats{MergablePulls: &zeroValue}
@@ -15455,6 +15789,16 @@ func TestRepoStatus_GetURL(tt *testing.T) {
 	r.GetURL()
 }
 
+func TestRequiredReviewer_GetType(tt *testing.T) {
+	var zeroValue string
+	r := &RequiredReviewer{Type: &zeroValue}
+	r.GetType()
+	r = &RequiredReviewer{}
+	r.GetType()
+	r = nil
+	r.GetType()
+}
+
 func TestRequiredStatusChecksRequest_GetStrict(tt *testing.T) {
 	var zeroValue bool
 	r := &RequiredStatusChecksRequest{Strict: &zeroValue}
diff --git a/github/messages_test.go b/github/messages_test.go
index 8b31b9d0..4eb154ec 100644
--- a/github/messages_test.go
+++ b/github/messages_test.go
@@ -370,6 +370,10 @@ func TestParseWebHook(t *testing.T) {
 			payload:     &PullRequestReviewCommentEvent{},
 			messageType: "pull_request_review_comment",
 		},
+		{
+			payload:     &PullRequestTargetEvent{},
+			messageType: "pull_request_target",
+		},
 		{
 			payload:     &PushEvent{},
 			messageType: "push",
diff --git a/github/orgs_audit_log_test.go b/github/orgs_audit_log_test.go
index c6287d87..cc68fc5e 100644
--- a/github/orgs_audit_log_test.go
+++ b/github/orgs_audit_log_test.go
@@ -62,7 +62,8 @@ func TestOrganizationService_GetAuditLog(t *testing.T) {
 	}
 	startedAt, _ := time.Parse(time.RFC3339, "2021-03-07T00:33:04.000Z")
 	completedAt, _ := time.Parse(time.RFC3339, "2021-03-07T00:35:08.000Z")
-	timestamp := time.Unix(1615077308538, 0)
+	timestamp := time.Unix(0, 1615077308538*1e6)
+
 	want := []*AuditEntry{
 		{
 			Timestamp:     &Timestamp{timestamp},
diff --git a/github/repos_environments_test.go b/github/repos_environments_test.go
new file mode 100644
index 00000000..554c2f33
--- /dev/null
+++ b/github/repos_environments_test.go
@@ -0,0 +1,239 @@
+// Copyright 2021 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestRequiredReviewer_UnmarshalJSON(t *testing.T) {
+	var testCases = map[string]struct {
+		data      []byte
+		wantRule  []*RequiredReviewer
+		wantError bool
+	}{
+		"User Reviewer": {
+			data:      []byte(`[{"type": "User", "reviewer": {"id": 1,"login": "octocat"}}]`),
+			wantRule:  []*RequiredReviewer{{Type: String("User"), Reviewer: &User{ID: Int64(1), Login: String("octocat")}}},
+			wantError: false,
+		},
+		"Team Reviewer": {
+			data:      []byte(`[{"type": "Team", "reviewer": {"id": 1, "name": "Justice League"}}]`),
+			wantRule:  []*RequiredReviewer{{Type: String("Team"), Reviewer: &Team{ID: Int64(1), Name: String("Justice League")}}},
+			wantError: false,
+		},
+		"Both Types Reviewer": {
+			data:      []byte(`[{"type": "User", "reviewer": {"id": 1,"login": "octocat"}},{"type": "Team", "reviewer": {"id": 1, "name": "Justice League"}}]`),
+			wantRule:  []*RequiredReviewer{{Type: String("User"), Reviewer: &User{ID: Int64(1), Login: String("octocat")}}, {Type: String("Team"), Reviewer: &Team{ID: Int64(1), Name: String("Justice League")}}},
+			wantError: false,
+		},
+		"Empty JSON Object": {
+			data:      []byte(`[]`),
+			wantRule:  []*RequiredReviewer{},
+			wantError: false,
+		},
+		"Bad JSON Object": {
+			data:      []byte(`[badjson: 1]`),
+			wantRule:  []*RequiredReviewer{},
+			wantError: true,
+		},
+		"Wrong Type Type in Reviewer Object": {
+			data:      []byte(`[{"type": 1, "reviewer": {"id": 1}}]`),
+			wantRule:  []*RequiredReviewer{{Type: nil, Reviewer: nil}},
+			wantError: true,
+		},
+		"Wrong ID Type in User Object": {
+			data:      []byte(`[{"type": "User", "reviewer": {"id": "string"}}]`),
+			wantRule:  []*RequiredReviewer{{Type: String("User"), Reviewer: nil}},
+			wantError: true,
+		},
+		"Wrong ID Type in Team Object": {
+			data:      []byte(`[{"type": "Team", "reviewer": {"id": "string"}}]`),
+			wantRule:  []*RequiredReviewer{{Type: String("Team"), Reviewer: nil}},
+			wantError: true,
+		},
+		"Wrong Type of Reviewer": {
+			data:      []byte(`[{"type": "Cat", "reviewer": {"id": 1,"login": "octocat"}}]`),
+			wantRule:  []*RequiredReviewer{{Type: nil, Reviewer: nil}},
+			wantError: true,
+		},
+	}
+
+	for name, test := range testCases {
+		t.Run(name, func(t *testing.T) {
+			rule := []*RequiredReviewer{}
+			err := json.Unmarshal(test.data, &rule)
+			if err != nil && !test.wantError {
+				t.Errorf("RequiredReviewer.UnmarshalJSON returned an error when we expected nil")
+			}
+			if err == nil && test.wantError {
+				t.Errorf("RequiredReviewer.UnmarshalJSON returned no error when we expected one")
+			}
+			if !reflect.DeepEqual(test.wantRule, rule) {
+				t.Errorf("RequiredReviewer.UnmarshalJSON expected rule %+v, got %+v", test.wantRule, rule)
+			}
+		})
+	}
+}
+
+func TestCreateUpdateEnvironment_MarshalJSON(t *testing.T) {
+	cu := &CreateUpdateEnvironment{}
+
+	got, err := cu.MarshalJSON()
+	if err != nil {
+		t.Errorf("MarshalJSON: %v", err)
+	}
+
+	want := `{"wait_timer":0,"reviewers":null,"deployment_branch_policy":null}`
+	if string(got) != want {
+		t.Errorf("MarshalJSON = %s, want %v", got, want)
+	}
+}
+
+func TestRepositoriesService_ListEnvironments(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/environments", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"total_count":1, "environments":[{"id":1}, {"id": 2}]}`)
+	})
+
+	ctx := context.Background()
+	environments, _, err := client.Repositories.ListEnvironments(ctx, "o", "r")
+	if err != nil {
+		t.Errorf("Repositories.ListEnvironments returned error: %v", err)
+	}
+	want := &EnvResponse{TotalCount: Int(1), Environments: []*Environment{{ID: Int64(1)}, {ID: Int64(2)}}}
+	if !reflect.DeepEqual(environments, want) {
+		t.Errorf("Repositories.ListEnvironments returned %+v, want %+v", environments, want)
+	}
+
+	const methodName = "ListEnvironments"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.ListEnvironments(ctx, "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.ListEnvironments(ctx, "o", "r")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_GetEnvironment(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/environments/e", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"id": 1,"name": "staging", "deployment_branch_policy": {"protected_branches": true,	"custom_branch_policies": false}}`)
+	})
+
+	ctx := context.Background()
+	release, resp, err := client.Repositories.GetEnvironment(ctx, "o", "r", "e")
+	if err != nil {
+		t.Errorf("Repositories.GetEnvironment returned error: %v\n%v", err, resp.Body)
+	}
+
+	want := &Environment{ID: Int64(1), Name: String("staging"), DeploymentBranchPolicy: &BranchPolicy{ProtectedBranches: Bool(true), CustomBranchPolicies: Bool(false)}}
+	if !reflect.DeepEqual(release, want) {
+		t.Errorf("Repositories.GetEnvironment returned %+v, want %+v", release, want)
+	}
+
+	const methodName = "GetEnvironment"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.GetEnvironment(ctx, "\n", "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.GetEnvironment(ctx, "o", "r", "e")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_CreateEnvironment(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	input := &CreateUpdateEnvironment{
+		WaitTimer: Int(30),
+	}
+
+	mux.HandleFunc("/repos/o/r/environments/e", func(w http.ResponseWriter, r *http.Request) {
+		v := new(CreateUpdateEnvironment)
+		json.NewDecoder(r.Body).Decode(v)
+
+		testMethod(t, r, "PUT")
+		want := &CreateUpdateEnvironment{WaitTimer: Int(30)}
+		if !reflect.DeepEqual(v, want) {
+			t.Errorf("Request body = %+v, want %+v", v, want)
+		}
+		fmt.Fprint(w, `{"id": 1, "name": "staging",	"protection_rules": [{"id": 1, "type": "wait_timer", "wait_timer": 30}]}`)
+	})
+
+	ctx := context.Background()
+	release, _, err := client.Repositories.CreateUpdateEnvironment(ctx, "o", "r", "e", input)
+	if err != nil {
+		t.Errorf("Repositories.CreateUpdateEnvironment returned error: %v", err)
+	}
+
+	want := &Environment{ID: Int64(1), Name: String("staging"), ProtectionRules: []*ProtectionRule{{ID: Int64(1), Type: String("wait_timer"), WaitTimer: Int(30)}}}
+	if !reflect.DeepEqual(release, want) {
+		t.Errorf("Repositories.CreateUpdateEnvironment returned %+v, want %+v", release, want)
+	}
+
+	const methodName = "CreateUpdateEnvironment"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.CreateUpdateEnvironment(ctx, "\n", "\n", "\n", input)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.CreateUpdateEnvironment(ctx, "o", "r", "e", input)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_DeleteEnvironment(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/environments/e", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+	})
+
+	ctx := context.Background()
+	_, err := client.Repositories.DeleteEnvironment(ctx, "o", "r", "e")
+	if err != nil {
+		t.Errorf("Repositories.DeleteEnvironment returned error: %v", err)
+	}
+
+	const methodName = "DeleteEnvironment"
+	testBadOptions(t, methodName, func() (err error) {
+		_, err = client.Repositories.DeleteEnvironment(ctx, "\n", "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		return client.Repositories.DeleteEnvironment(ctx, "o", "r", "e")
+	})
+}
diff --git a/github/timestamp_test.go b/github/timestamp_test.go
index 7d5b712e..09724951 100644
--- a/github/timestamp_test.go
+++ b/github/timestamp_test.go
@@ -13,10 +13,11 @@ import (
 )
 
 const (
-	emptyTimeStr               = `"0001-01-01T00:00:00Z"`
-	referenceTimeStr           = `"2006-01-02T15:04:05Z"`
-	referenceTimeStrFractional = `"2006-01-02T15:04:05.000Z"` // This format was returned by the Projects API before October 1, 2017.
-	referenceUnixTimeStr       = `1136214245`
+	emptyTimeStr                     = `"0001-01-01T00:00:00Z"`
+	referenceTimeStr                 = `"2006-01-02T15:04:05Z"`
+	referenceTimeStrFractional       = `"2006-01-02T15:04:05.000Z"` // This format was returned by the Projects API before October 1, 2017.
+	referenceUnixTimeStr             = `1136214245`
+	referenceUnixTimeStrMilliSeconds = `1136214245000` // Millisecond-granular timestamps were introduced in the Audit log API.
 )
 
 var (
@@ -59,12 +60,14 @@ func TestTimestamp_Unmarshal(t *testing.T) {
 	}{
 		{"Reference", referenceTimeStr, Timestamp{referenceTime}, false, true},
 		{"ReferenceUnix", referenceUnixTimeStr, Timestamp{referenceTime}, false, true},
+		{"ReferenceUnixMillisecond", referenceUnixTimeStrMilliSeconds, Timestamp{referenceTime}, false, true},
 		{"ReferenceFractional", referenceTimeStrFractional, Timestamp{referenceTime}, false, true},
 		{"Empty", emptyTimeStr, Timestamp{}, false, true},
 		{"UnixStart", `0`, Timestamp{unixOrigin}, false, true},
 		{"Mismatch", referenceTimeStr, Timestamp{}, false, false},
 		{"MismatchUnix", `0`, Timestamp{}, false, false},
 		{"Invalid", `"asdf"`, Timestamp{referenceTime}, true, false},
+		{"OffByMillisecond", `1136214245001`, Timestamp{referenceTime}, false, false},
 	}
 	for _, tc := range testCases {
 		var got Timestamp
@@ -144,11 +147,13 @@ func TestWrappedTimestamp_Unmarshal(t *testing.T) {
 	}{
 		{"Reference", referenceTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
 		{"ReferenceUnix", referenceUnixTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
+		{"ReferenceUnixMillisecond", referenceUnixTimeStrMilliSeconds, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
 		{"Empty", emptyTimeStr, WrappedTimestamp{0, Timestamp{}}, false, true},
 		{"UnixStart", `0`, WrappedTimestamp{0, Timestamp{unixOrigin}}, false, true},
 		{"Mismatch", referenceTimeStr, WrappedTimestamp{0, Timestamp{}}, false, false},
 		{"MismatchUnix", `0`, WrappedTimestamp{0, Timestamp{}}, false, false},
 		{"Invalid", `"asdf"`, WrappedTimestamp{0, Timestamp{referenceTime}}, true, false},
+		{"OffByMillisecond", `1136214245001`, WrappedTimestamp{0, Timestamp{referenceTime}}, false, false},
 	}
 	for _, tc := range testCases {
 		var got Timestamp