Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions .github/workflows/update-projects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,39 @@ on:
- cron: '0 0 * * *'
workflow_dispatch:

permissions:
contents: write

concurrency:
group: update-projects
cancel-in-progress: false

jobs:
update-projects:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpt =="
cat -n .github/workflows/update-projects.yml | sed -n '1,180p'

echo
echo "== relevant GHA checkout references =="
rg -n "actions/checkout|persist-credentials|github\.token|git push|go test|update_projects" .github/workflows/update-projects.yml

echo
echo "== repository-controlled Go files paths/stat =="
git ls -l -- . | head -20
git ls-files scripts update_projects.go | head -50

echo
echo "== file sizes if relevant scripts exist =="
for f in $(git ls-files scripts/update_projects.go 2>/dev/null | tr '\n' ' ') scripts/update_projects.go; do
  [ -f "$f" ] && echo "--- $f" && wc -l "$f"
done

Repository: HappyHackingSpace/.github

Length of output: 2253


🌐 Web query:

actions/checkout persist-credentials default true github.token local git config

💡 Result:

In the actions/checkout GitHub Action, the persist-credentials input determines whether the authentication token (typically the GITHUB_TOKEN or a provided Personal Access Token) is configured for use with Git [1][2]. When persist-credentials is set to true (which is the default value) [1][2], the action configures the provided credentials so that subsequent steps in the workflow can run authenticated Git commands [1][3]. Regarding how these credentials are stored: - In older versions of actions/checkout, the token was persisted directly in the local.git/config file [4][5]. - As of version 6.0.0 and later, the action has been updated to store these credentials in a separate file located under $RUNNER_TEMP, rather than directly in the local git config [4][6]. This change helps improve security by keeping the credentials out of the local Git configuration file [4][5]. The action automatically removes these credentials during the post-job cleanup phase [1][3]. If you prefer to disable this behavior for security reasons, you can set persist-credentials: false in your workflow step [1][7].

Citations:


🌐 Web query:

actions/checkout v4 persist-credentials default github.token local git config

💡 Result:

In actions/checkout v4, the persist-credentials input defaults to true [1][2][3]. When set to true, the action automatically configures the local git repository's configuration (stored in.git/config) with the authentication token provided to the action [1][4][5]. If you do not specify a custom token, the action defaults to using the GitHub token (typically secrets.GITHUB_TOKEN) [6][7]. By persisting this token into the local git config, the action enables your workflow scripts to run authenticated git commands (such as git push or git pull) directly within the workspace [1][4][5]. To opt-out of this behavior—which is often recommended for security to prevent the token from being accessible to other processes or potentially leaked through artifacts—you must explicitly set persist-credentials to false [1][5][7]: - uses: actions/checkout@v4 with: persist-credentials: false When persist-credentials is set to false, the action does not write the token into the local git configuration [1][5]. Any subsequent git commands that require authentication will need to be configured separately (e.g., using a credential helper or by manually configuring the git url with credentials) [8]. The token used by the action is automatically removed during the post-job cleanup process in either case [1][4][5].

Citations:


🌐 Web query:

GitHub Actions GITHUB_TOKEN contents: write permissions git push token permissions contents read

💡 Result:

The GITHUB_TOKEN is a powerful, repository-scoped token automatically provided by GitHub Actions to authenticate on behalf of the GitHub App installed on your repository [1]. By default, it often has only read access to repository contents, meaning workflows attempting to push commits, create branches, or delete files will fail with a "permission denied" error unless permissions are explicitly elevated [2][3]. To allow a workflow to perform write operations like git push, you must grant the contents: write permission in your workflow file [2][4]. How to grant write permissions: You can define permissions at either the workflow level (for all jobs) or the job level (for a specific job) [5][4]. Specifying the permissions key will override the repository's default token permissions [2][4]. Example configuration: jobs: build: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 # Now you can commit and push changes - run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -am "Update via GITHUB_TOKEN" git push Important considerations: 1. Explicit Override: When you define a permissions block, any permissions not explicitly listed are set to none [4]. If your workflow requires other actions (such as reading metadata or interacting with issues), ensure they are included in the permissions block [4]. 2. Scope: The GITHUB_TOKEN is strictly scoped to the repository containing the workflow [1][6]. It cannot be used to push to other repositories, even if those repositories are under the same organization [6][7]. To push to a different repository, you must use a Personal Access Token (PAT) or a GitHub App installation token stored as a secret [1][7]. 3. Security: Always follow the principle of least privilege [5][2]. Only grant contents: write if the workflow genuinely needs to modify the repository [5][2]. 4. Recursive Workflows: When a workflow uses the GITHUB_TOKEN to push code, it generally does not trigger new workflow runs, which prevents infinite loops [1]. Exceptions exist for certain pull_request events, which may create workflow runs in an "approval-required" state [1].

Citations:


Disable checked-out token persistence.

actions/checkout@v4 persists github.token in the local Git config by default, and this job runs repository-controlled Go code before git push. Set persist-credentials: false on checkout and pass the token directly to the push command only if the final step needs it.

🧰 Tools
🪛 zizmor (1.28.0)

[warning] 20-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/update-projects.yml at line 20, Update the
actions/checkout step in the update-projects workflow to set persist-credentials
to false, preventing the GitHub token from being stored in local Git
configuration. If the final push step requires authentication, provide the token
directly to that push command rather than relying on checkout persistence.

Source: Linters/SAST tools

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.24'
- name: Prepare Go module and install dependencies
go-version: '1.26'
- name: Download and verify dependencies
run: |
if [ ! -f go.mod ]; then
go mod init tempmod
fi
go mod tidy
go mod download
go mod verify
go test ./...
- name: Run update_projects.go
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Falls back to the repository token until an organization owner adds
# the read-only ORG_STATS_TOKEN secret to the upstream repository.
GITHUB_TOKEN: ${{ secrets.ORG_STATS_TOKEN || github.token }}
run: |
go run scripts/update_projects.go
- name: Commit and push if changed
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add profile/README.md
git diff --cached --quiet || git commit -m 'chore: update projects section [auto]'
git push
22 changes: 14 additions & 8 deletions scripts/update_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
context "context"
"errors"
"fmt"
"os"
"regexp"
Expand Down Expand Up @@ -52,7 +53,7 @@ var (

func fetchOrgRepos(client *gh.Client, ctx context.Context, org string) ([]*gh.Repository, error) {
var allRepos []*gh.Repository
opt := &gh.RepositoryListByOrgOptions{Type: "public", ListOptions: gh.ListOptions{PerPage: 100}}
opt := &gh.RepositoryListByOrgOptions{Type: "all", ListOptions: gh.ListOptions{PerPage: 100}}
for {
repos, resp, err := client.Repositories.ListByOrg(ctx, org, opt)
if err != nil {
Expand Down Expand Up @@ -215,7 +216,7 @@ func formatMarkdown(repos []GhProjects) string {
return b.String()
}

func fetchContributors(client *gh.Client, ctx context.Context, org string, repos []*gh.Repository) []ContributorStats {
func fetchContributors(client *gh.Client, ctx context.Context, org string, repos []*gh.Repository) ([]ContributorStats, error) {
contribMap := map[string]*ContributorStats{}
since := time.Now().AddDate(0, 0, -30)

Expand All @@ -231,7 +232,7 @@ func fetchContributors(client *gh.Client, ctx context.Context, org string, repos
for {
commits, resp, err := client.Repositories.ListCommits(ctx, org, repo.GetName(), commitOpt)
if err != nil {
break
return nil, errors.New("failed to fetch contributor commit statistics")
}
for _, c := range commits {
if c.Author == nil {
Expand Down Expand Up @@ -267,7 +268,7 @@ func fetchContributors(client *gh.Client, ctx context.Context, org string, repos
for {
issues, resp, err := client.Issues.ListByRepo(ctx, org, repo.GetName(), issueOpt)
if err != nil {
break
return nil, errors.New("failed to fetch contributor issue statistics")
}
for _, issue := range issues {
if issue.User == nil || issue.PullRequestLinks != nil {
Expand Down Expand Up @@ -304,7 +305,7 @@ func fetchContributors(client *gh.Client, ctx context.Context, org string, repos
for {
prs, resp, err := client.PullRequests.List(ctx, org, repo.GetName(), prOpt)
if err != nil {
break
return nil, errors.New("failed to fetch contributor pull request statistics")
}
shouldBreak := false
for _, pr := range prs {
Expand Down Expand Up @@ -362,7 +363,7 @@ func fetchContributors(client *gh.Client, ctx context.Context, org string, repos
contribs[i].Badges = badges
}
sort.Slice(contribs, func(i, j int) bool { return contribs[i].XP > contribs[j].XP })
return contribs
return contribs, nil
}

func formatContributorsMarkdown(contribs []ContributorStats) string {
Expand Down Expand Up @@ -405,7 +406,9 @@ func main() {
var statsList []RepoStats
mostStars := 0
for _, repo := range repos {
if slices.Contains(excludedProjects, repo.GetName()) {
// Private repositories contribute to the contributor ranking, but their
// names and project statistics must never be published in the public README.
if repo.GetPrivate() || slices.Contains(excludedProjects, repo.GetName()) {
continue
}
stats, err := fetchRepoStats(client, ctx, org, repo)
Expand All @@ -432,7 +435,10 @@ func main() {
md := formatMarkdown(projects[:displayCount])
md += "\n\n[...and more projects](https://github.com/HappyHackingSpace?tab=repositories)"

contributors := fetchContributors(client, ctx, org, repos)
contributors, err := fetchContributors(client, ctx, org, repos)
if err != nil {
panic(err)
}
contribDisplayCount := min(len(contributors), 10)
contribMd := formatContributorsMarkdown(contributors[:contribDisplayCount])
contribMd += "\n\n[...and more contributors](https://github.com/orgs/HappyHackingSpace/people)"
Expand Down