checkout

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

14posts · 30d
Nov 30 posts
  1. GitHub Actions : le tutoriel pas à pas pour tester et publier une API Node.js Les six notions de GitHub Actions Tout workflow GitHub Actions se lit avec six mots, définis dans la page Understanding GitHub Actions de la documentation officielle (consultée le 31 octobre 2026) : | Notion | Ce que c'est | Dans notre exemple | |---|---|---| | Workflow | Un processus automatisé décrit par un fichier YAML dans .github/workflows/ | ci.yml | | Événement | L'activité du dépôt qui déclenche le workflow | push et pull_request sur main | | Job | Une suite d'étapes exécutées sur un même runner ; les jobs tournent en parallèle sauf dépendance | test et image | | Step | Une commande shell ou une action, exécutées dans l'ordre | npm ci, npm test | | Runner | La machine qui exécute un job, un seul job à la fois | ubuntu-latest, hébergé par GitHub | | Action | Une extension réutilisable, souvent prise sur le GitHub Marketplace | actions/checkout, docker/login-action | Chaque exécution part d'une machine virtuelle neuve, et chaque action du Marketplace est du code tiers qui tourne avec vos droits, d'où l'étape 6. !Schéma du workflow GitHub Actions de ce tutoriel : un événement push ou pull_request déclenche deux jobs de test en parallèle sur Node.js 24 et 26, puis un job qui publie l'image Docker sur ghcr.io --- Étape 1 : un premier workflow pour l'API Node.js Le point de départ : récupérer le code, installer Node.js et les dépendances, lancer les tests. L'API du tutoriel déclare "test": "node --test" dans son package.json et exige Node.js 24 ou plus. Créez .github/workflows/ci.yml : name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read jobs: test: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 - run: npm ci - run: npm test npm ci installe exactement ce que fixe package-lock.json, que le dépôt doit contenir, et npm test lance node --test. Le timeout-minutes évite qu'un test bloqué tourne jusqu'à la limite de 6 heures par job des runners hébergés, fixée par la page des limites de GitHub Actions. Pourquoi checkout v7 et pas v4 ? Selon son README, la v5 est passée au moteur node24, la v6 range les identifiants Git hors de .git/config, et la v7 refuse par défaut le code d'un fork sous pull_request_target ou workflow_run. Les dernières versions au 31 octobre 2026 sont actions/checkout v7.0.1 et actions/setup-node v7.1.0. L'onglet Actions affiche ensuite chaque exécution ; pour lire les journaux d'un runner Ubuntu, voyez nos commandes Linux de base. --- Étape 2 : tester sur Node.js 24 et 26 avec une matrice Une matrice exécute le même job plusieurs fois avec des valeurs différentes. Pour l'API, deux versions comptent : Node.js 24, minimum exigé par le package.json, et Node.js 26, passé en LTS avec la version 26.13.0 du 28 octobre 2026 selon l'index des versions de Node.js. Remplacez le job test : test: runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: node-version: [24, 26] steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm test Les deux jobs tournent en parallèle. fail-fast: false laisse finir le second si le premier échoue. La page des limites plafonne une matrice à 256 jobs, et chaque job est facturé séparément. --- Étape 3 : le cache, plus rapide mais pas sans risque Le cache de GitHub Actions évite de retélécharger les paquets npm à chaque exécution : ajoutez cache: npm sous with. D'après le README de setup-node, la clé dérive du hachage de package-lock.json et l'action met en cache le dossier global de npm, pas node_modules. - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node-version }} cache: npm Les limites, lues dans la référence du cache de dépendances le 31 octobre 2026 : - 10 Go par dépôt par défaut. Une entrée non lue depuis plus de 7 jours est supprimée. - Aucun secret dans le cache. Toute personne ayant un accès en lecture peut ouvrir une pull request et lire un cache. Le mot-clé cache-mode (read, write, write-only ou none) fixe les droits d'un job sur le cache. Il se place en tête du workflow ou dans un job, par exemple cache-mode: read sous test:, d'après la référence de syntaxe. Selon la documentation du cache consultée le 31 octobre 2026, un déclencheur peu fiable comme pull_request_target n'obtient par défaut que la lecture, et y déclarer write réintroduit le risque d'empoisonnement du cache. --- Étape 4 : secrets, variables et environnements Un jeton n'entre jamais dans le YAML : il se range dans Settings, Secrets and variables, Actions, et se lit avec ${{ secrets.NOM }}. Une valeur non sensible, comme une URL, devient une variable lue avec ${{ vars.NOM }}, au niveau du dépôt, de l'organisation ou d'un environnement, selon la page sur les variables. Pour rattacher le job image de l'étape 5 à un environnement production, avec une variable PRODUCTION_URL créée dans cet environnement, ajoutez au job : environment: name: production url: ${{ vars.PRODUCTION_URL }} La référence de syntaxe autorise le contexte vars dans url. Mais la page de gestion des environnements réserve les environnements aux dépôts publics sur GitHub Free ; en privé, il faut Pro, Team ou Enterprise. C'est pourquoi le fichier complet plus bas s'en passe. Notre workflow n'a besoin d'aucun secret ajouté à la main. Les tests de l'API fixent eux-mêmes JWT_SECRET et DB_PATH=':memory:' dans test/api.test.js, et le job image construit l'image sans démarrer le conteneur. Quant au GITHUB_TOKEN, créé pour chaque job, il suffit à publier sur le registre de GitHub. --- Étape 5 : construire et publier l'image Docker sur GHCR GHCR, le registre de conteneurs de GitHub (ghcr.io), accepte un push authentifié par GITHUB_TOKEN. L'image reprend le Dockerfile multi-étapes de notre tutoriel Docker pour débutants, basé sur node:24-alpine, placé à la racine du projet. Ajoutez ce job à ci.yml : image: needs: test if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read packages: write steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=sha - uses: docker/build-push-action@v7 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max Ce que fait chaque bloc : - needs et if. L'image n'est construite que si les deux tests réussissent, et seulement pour un push sur main. - docker/metadata-action. L'image prend le nom du dépôt, ghcr.io/<compte>/<dépôt>, soit ghcr.io/<compte>/api-taches si le dépôt s'appelle comme le dossier du tutoriel. L'action produit les tags main et sha- suivi du commit abrégé, plus les labels OCI. Son README précise qu'il met le nom d'image en minuscules, ce qu'exige le registre. - context: . avec checkout. Sans checkout, build-push-action lit le dépôt par Git et ignore les modifications des étapes précédentes, selon son README. - type=gha. Les couches Docker vont dans le cache de GitHub Actions. Avec plusieurs images, fixez un scope distinct, sinon chaque build écrase le cache du précédent, prévient la documentation de Docker. Deux pièges documentés par GitHub sur la page du Container registry. Un paquet publié pour la première fois est privé par défaut. Et le GITHUB_TOKEN ne peut pas pousser si une image du même nom a déjà été poussée à la main sans être reliée au dépôt. Côté coût, la facturation de GitHub Packages indique le 31 octobre 2026 que le stockage du Container registry est gratuit pour l'instant, avec un mois de préavis avant tout changement. L'image se déploie ensuite derrière notre reverse proxy Nginx avec Docker Compose. --- Étape 6 : GITHUB_TOKEN minimal et actions épinglées Cette étape a deux volets : limiter ce que le jeton peut faire, et figer le code des actions exécutées. Le principe du moindre privilège Le GITHUB_TOKEN est limité au dépôt et valable le temps du job. Ses droits dépendent du réglage du dépôt, puis du bloc permissions. La référence de syntaxe fixe la règle utile : dès que vous citez une permission, toutes celles que vous ne citez pas passent à none. D'où notre schéma : contents: read au niveau du workflow, packages: write dans le seul job image. C'est ce que récompense le test Token-Permissions d'OpenSSF Scorecard, qui classe ce risque comme élevé. L'option persist-credentials: false de checkout évite en plus de conserver le jeton pour les commandes Git suivantes. Épingler les actions sur un SHA Un tag comme @v7 peut être déplacé. Selon l'avis de sécurité GitHub GHSA-mrrh-fwg8-r2c3 (CVE-2025-30066), des attaquants ont redirigé en mars 2025 plusieurs tags de tj-actions/changed-files vers un commit malveillant, qui affichait les secrets dans les journaux entre le 14 et le 15 mars. La référence de sécurité de GitHub Actions recommande donc d'épingler chaque action sur l'empreinte complète d'un commit, en vérifiant qu'elle vient du dépôt de l'action et non d'un fork. La forme est uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 : l'empreinte fige le code, le commentaire garde la version lisible. Le fichier complet plus bas épingle ainsi les six actions sur les versions publiées au 31 octobre 2026. Nous avons lu ces empreintes sur l'API de GitHub ; vérifiez-les sur la page des versions de chaque action. Pour ne pas figer des versions vulnérables, laissez Dependabot proposer les mises à jour avec .github/dependabot.yml, selon la documentation de Dependabot : version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" Selon la référence de sécurité, les alertes Dependabot ignorent les actions épinglées par SHA ; les mises à jour de version, elles, fonctionnent. GitHub va plus loin avec les fichiers de verrouillage des workflows : .github/workflows/actions.lock, généré par l'extension gh actions-lock, fige le commit de chaque action, dépendances indirectes comprises, et vérifie l'identité du dépôt source. La référence de sécurité le présente comme la meilleure protection, qui remplace l'épinglage par SHA pour les dépôts qui l'adoptent. Ce tutoriel garde les SHA : la documentation ne dit pas si le dispositif est en préversion ou disponible pour tous, et il exige une extension de GitHub CLI. --- Étape 7 : concurrence et workflows réutilisables Deux mots-clés évitent le gaspillage et la copie de YAML. Le premier, concurrency, se place en tête de ci.yml : concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} D'après la page sur la concurrence, un seul workflow s'exécute à la fois par groupe, et une exécution en attente est remplacée par la plus récente. L'expression annule aussi l'exécution en cours, mais seulement pour une pull request. Sur main, l'exécution en cours va à son terme, mais une exécution en attente reste remplacée par la plus récente : seul le dernier push en file est publié. Le second, workflow_call, transforme un workflow en brique réutilisable. Déplacez le job de test dans .github/workflows/tests-node.yml : name: Tests Node.js on: workflow_call: inputs: node-versions: description: Versions de Node.js, au format JSON type: string default: '[24, 26]' jobs: test: runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read strategy: fail-fast: false matrix: node-version: ${{ fromJSON(inputs.node-versions) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - run: npm test Dans ci.yml, le job test devient un simple appel, et image garde son needs: test : jobs: test: uses: ./.github/workflows/tests-node.yml permissions: contents: read with: node-versions: '[24, 26]' La forme ./.github/workflows/{filename} est celle que la documentation sur la réutilisation donne pour un workflow du même dépôt. La même page, consultée le 31 octobre 2026, mentionne aussi une forme $/.github/workflows/{filename}, qu'elle dit recommandée et indisponible sur GitHub Enterprise Server ; nous ne l'avons pas testée. La référence des workflows réutilisables fixe dix niveaux d'imbrication au plus et 50 workflows réutilisables distincts par fichier ; les variables env de l'appelant ne sont pas transmises, et le workflow appelé ne peut pas élever les permissions reçues. --- Le fichier ci.yml complet Voici .github/workflows/ci.yml une fois les étapes 1 à 7 assemblées, avec le job test écrit en entier (sans le workflow réutilisable de l'étape 7 ni l'environnement optionnel de l'étape 4) : déclencheurs, permissions minimales, concurrence, matrice avec cache, publication sur GHCR, et chaque action épinglée sur son SHA. Sa syntaxe a été vérifiée avec le module yaml de Python ; il n'a pas été exécuté sur GitHub. Avec le workflow réutilisable de l'étape 7, le job test ci-dessous est remplacé par l'appel à tests-node.yml. name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: test: runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: node-version: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - run: npm test image: needs: test if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read packages: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=sha - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max --- Combien coûte GitHub Actions en octobre 2026 GitHub Actions reste gratuit pour les dépôts publics sur les runners standards, et sur vos propres machines. Pour les dépôts privés, la page de facturation de GitHub Actions, lue le 31 octobre 2026, exprime désormais le quota en dollars de calcul inclus, et non plus en minutes, déduits selon le tarif de chaque runner : | Offre | Calcul inclus par mois | Soit, sur Linux 2 cœurs | Stockage d'artefacts | Cache par dépôt | |---|---|---|---|---| | GitHub Free | 12 $ | 2 000 minutes | 500 Mo | 10 Go | | GitHub Pro | 18 $ | 3 000 minutes | 1 Go | 10 Go | | GitHub Team | 18 $ | 3 000 minutes | 2 Go | 10 Go | | GitHub Enterprise Cloud | 300 $ | 50 000 minutes | 50 Go | 10 Go | La conversion est notre calcul à 0,006 $ la minute. Les 2 000 minutes souvent citées, y compris dans notre guide CI/CD, correspondent donc aux 12 $ de l'offre Free sur Linux 2 cœurs ; la page de prix de GitHub affiche bien 2 000, 3 000 et 50 000 minutes pour Free, Team et Enterprise, avec Team à 4 $ et Enterprise à 21 $ par utilisateur et par mois. Le quota en dollars change la donne dès que vous quittez Linux. La grille des runners facture 0,002 $ la minute sur Linux 1 cœur, 0,005 $ sur Linux arm64 2 cœurs, 0,010 $ sur Windows 2 cœurs et 0,062 $ sur macOS 3 ou 4 cœurs. Les 12 $ de l'offre Free couvrent donc environ 193 minutes de macOS seulement. !Graphique en barres horizontales GitHub Actions : selon notre calcul, les 12 dollars inclus dans GitHub Free couvrent 6 000 minutes sur Linux 1 cœur, 2 400 sur Linux arm64 2 cœurs, 2 000 sur Linux 2 cœurs, 1 200 sur Windows 2 cœurs et 193 sur macOS 3 ou 4 cœurs > Notre calcul. Chaque job étant arrondi à la minute supérieure, notre ci.yml coûte au moins 3 minutes par push sur main et 2 par pull request. À 50 de chaque par mois sur un dépôt privé, cela fait au moins 250 minutes, bien dans le quota Free. Une matrice sur trois systèmes d'exploitation changerait tout : une minute de macOS coûte plus de dix minutes de Linux 2 cœurs. Le stockage se paie à part : 0,25 $ par Go et par mois au-delà de l'allocation d'artefacts, 0,07 $ pour le cache si vous avez relevé sa limite de 10 Go, selon la même page. --- Runners auto-hébergés : gratuits, pas sans danger Un runner auto-hébergé est une machine à vous qui exécute les jobs à la place des machines de GitHub. La page de facturation les range, le 31 octobre 2026, parmi les usages gratuits qui ne réduisent pas le calcul inclus. GitHub avait annoncé en décembre 2025 une redevance de 0,002 $ la minute sur les runners auto-hébergés à partir du 1er mars 2026, puis a écrit la reporter « pour prendre le temps de réévaluer notre approche » (notre traduction), sur sa page consacrée aux changements de prix de 2026. Selon la page sur les runners auto-hébergés, vous payez la machine et mettez à jour vous-même système et outils. Checkout et setup-node, passées au moteur node24, exigent le runner v2.327.1 ou plus selon leurs README. Un job peut y tourner 5 jours d'après la page des limites, mais le GITHUB_TOKEN ne se renouvelle que pendant 24 heures, précise la page GITHUB_TOKEN. Le vrai risque est ailleurs. Selon la référence de sécurité, un runner auto-hébergé ne garantit pas une machine propre à chaque job et peut être compromis durablement : il ne devrait presque jamais servir à un dépôt public, où n'importe qui peut ouvrir une pull request. Elle mentionne aussi des runners éphémères, dits JIT, qui n'exécutent qu'un job. Verdict : pour cette API, les runners hébergés suffisent. Notre comparatif de l'hébergement cloud pour développeurs aide à choisir la machine. --- Ce que nous n'avons pas pu vérifier Ce tutoriel est une synthèse documentaire. Restent incertains : - L'exécution des workflows. Leur syntaxe YAML a été vérifiée avec un analyseur Python, mais aucun n'a tourné sur GitHub. Testez-les d'abord sur une branche. - Le sort de la redevance des runners auto-hébergés. Reportée, mais nous n'avons trouvé aucune annonce qui l'annule. - Le prix de GitHub Pro. La page de prix lue ne l'affiche pas, ni aucun prix en euros. - Le statut exact des fichiers de verrouillage. La documentation les décrit sans préciser s'ils sont en préversion ou disponibles pour tous. - La durée réelle des jobs. Nos chiffres de minutes sont des minimums dus à l'arrondi, pas des mesures. --- Questions fréquentes Combien de minutes GitHub Actions gratuites pour un dépôt privé ? Sur l'offre Free, 12 $ de calcul par mois, soit 2 000 minutes sur le runner Linux 2 cœurs à 0,006 $ la minute, mais 193 seulement sur macOS, selon la page de facturation de GitHub lue le 31 octobre 2026. Les dépôts publics sur les runners standards et les runners auto-hébergés ne consomment rien. Quelle version d'actions/checkout utiliser ? La v7 : au 31 octobre 2026, la dernière version publiée est la 7.0.1. Épinglez-la sur son SHA complet, avec la version en commentaire. Quelle différence entre une action et un workflow ? Le workflow est votre fichier YAML, déclenché par un événement, qui contient des jobs. Une action est une brique appelée dans un step avec uses, comme actions/setup-node. Un workflow réutilisable s'appelle, lui, au niveau d'un job. Comment publier une image Docker avec GitHub Actions ? Connectez-vous à ghcr.io avec docker/login-action et le GITHUB_TOKEN, puis construisez et poussez avec docker/build-push-action, dans un job qui déclare packages: write (étape 5). --- À propos de l'auteur Abdelilah Ourti est ingénieur IA et architecte logiciel. Il conçoit et déploie des applications web et des systèmes d'IA, et publie sur NeurauTech des guides et des comparatifs destinés aux développeurs francophones. Ses autres articles sont réunis sur sa page auteur. Méthode. Synthèse documentaire fondée exclusivement sur des sources publiques consultées le 31 octobre 2026 : documentation de GitHub Actions, de GitHub Packages et de Dependabot, pages de facturation et de prix de GitHub, README et pages de versions des actions officielles et des actions Docker, documentation de Docker, OpenSSF Scorecard, avis de sécurité de GitHub, index des versions de Node.js. Les empreintes SHA ont été lues sur l'API de GitHub. La syntaxe YAML a été vérifiée localement, mais aucun workflow n'a été exécuté. Aucun lien n'est affilié et aucune relation commerciale ne lie l'auteur aux éditeurs mentionnés. Une erreur ou une version périmée ? Écrivez à contact@neurautech.com : l'article sera corrigé et la date de mise à jour modifiée. Dernière mise à jour : 31 octobre 2026. Image : Alberto Rodríguez (Unsplash License).

  2. GitHub Actions Node 20 Deprecation: What Broke on 23 September (and Why node20 Isn't the Problem) GitHub removed Node 20 from the Actions runners on 23 September 2026, and an action that declares using: node20 still doesn't fail for that: since 16 June the runner rewrites node20 to node24 before it executes anything, and it does the same with node12 and node16. What went away on the 23rd was the escape hatch: the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION variable no longer brings Node 20 back. The next date with real consequences is 2 November, when macos-14 is retired. That inverts who is at risk. If your CI has been green since June without that variable, the 23rd changed nothing for you on GitHub-hosted runners. The ones that break are precisely the teams that already hit an action that can't handle Node 24, set the variable to keep moving, and filed the real fix under later. And around that date there are four more with real consequences: the self-hosted runner minimum version (25 September), retention for checks and runs (1 October), the macos-14 brownouts (October) and its retirement (2 November). I checked this against the runner's source code and against this site's own CI, which uses two node20 actions. Everything below comes with its source. The calendar, by date | Date | What changes | What breaks | How to detect it | |---|---|---|---| | 14, 16 and 18 Sep (11:00-15:00 ET) | Brownouts ahead of the self-hosted runner minimum version | Outdated runners cannot register or run jobs during the window | Runner deprecations API (below) | | 17 Sep | ubuntu-22.04 and ubuntu-22.04-arm enter deprecation | Nothing fails yet; queues may be longer | grep for the label | | 21-30 Sep | windows-11-arm moves to the Visual Studio 2026 image | The toolchain underneath changes | grep windows-11-arm | | 23 Sep | Node 20 leaves the runners | ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION is ignored; Linux ARM32 runners lose support | grep for the variable | | 25 Sep | Self-hosted runner minimum version is enforced | Registering requires ≥ 2.329.0; a runner that doesn't install each release within 30 days stops receiving jobs | Deprecations API | | 1 Oct | Actions retention now covers checks, workflow runs and statuses | Anything past your retention period (90 days by default) is deleted | Repo/org retention setting | | 5-31 Oct | Eight macos-14 brownouts | Jobs scheduled inside the window fail | grep macos-14 | | 2 Nov | macos-14 retired | Jobs with that label terminate with an error | grep macos-14 | | 17 Apr 2027 | ubuntu-22.04 retired (brownouts before, in March and April) | Jobs terminate with an error | grep for the label | 1. Node 20: what actually happens on 23 September What the notice says GitHub's changelog post from 19 September 2025, with an editor's note dated 25 August 2026, sets two dates: from 16 June 2026 runners use Node 24 by default, and the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true opt-out back to Node 20 "will only work until we upgrade the runner and remove Node20 on September 23rd, 2026." It adds two platform limits: Node 24 is incompatible with macOS 13.4 and lower, and it has no official ARM32 support. What the notice does not explain is what happens to an action whose action.yml still says using: node20. That lives in the code. What the runner code says In HandlerFactory.cs (identical at tag v2.337.0, the current release), the runner does two things before launching a JavaScript action: 1. If the action declares node12 or node16, it bumps it to node20. 2. If it ends up on node20, it asks NodeUtil.DetermineActionsNodeVersion which version to use. That function has three phases, driven by feature flags on GitHub's side: // Phase 3: Always use Node 24 regardless of environment variables if (requireNode24) { return (Constants.Runner.NodeMigration.Node24, null); } // ... // Phase 2: Node 24 is the default if (useNode24ByDefault) { if (allowUnsecureNode) { return (Constants.Runner.NodeMigration.Node20, null); } return (Constants.Runner.NodeMigration.Node24, null); } PR #3948, which introduced this, is blunt about it: in phase 3, "All actions use Node 24" and "No opt-out options available." No branch returns an error for declaring node20. The action runs on a different Node, which is very different from not running. Note the order too: node12 → node20 → node24. An old action with using: node16 (for example actions/checkout@v3) ends up on Node 24 as well. The proof: this site's CI This blog's CI workflow uses actions/checkout@v4 and actions/setup-node@v4. The official action.yml files at those tags declare using: node20; from v5 onward they declare node24 (I checked tags v4 through v7 of both). The run on 11 September, on runner 2.337.0 with the ubuntu-24.04 image, finished green with this annotation: Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: actions/checkout@v4, actions/setup-node@v4. And in the log of those two actions' steps, this line: Node 20 is being deprecated. This workflow is running with Node 24 by default. If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. The same annotation appears on the very first run of that workflow, on 23 August. In other words, my two node20 actions have never run on Node 20 in this CI, and I didn't touch anything to make that happen. 23 September doesn't change their runtime, because that change already happened in June. Bumping them to v5 or later clears the annotation, and it's worth doing, but there's no clock on it. What it looks like when it does break There are three real ways to fail, and none of them is a GitHub message saying "Node 20 no longer exists." You had the variable set. Some action of yours tripped on Node 24 after 16 June, you set ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true, and it stayed on Node 20. In phase 3 the runner ignores the variable without telling you: the requireNode24 branch returns null as its message. The action goes back to Node 24, and the error you see is its own code's error, the same one you saw the first time. One hint that phase 3 is live: with the current runner code, the "If you need to temporarily use Node 20…" line disappears from the log, because it is only printed while Node 24 is the default but not yet mandatory. The action uses something Node 24 removed. Per the Node 24 deprecations table and the 24.0.0 release notes, these reached end-of-life in that release: | API | Status in Node 24 | How it fails | |---|---|---| | tls.createSecurePair() | Removed (DEP0064) | TypeError: ... is not a function when called | | fs.truncate() with a file descriptor | Removed (DEP0081) | Throws ERR_INVALID_ARG_TYPE: it expects a path. Use fs.ftruncate() | | dirent.path | Removed (DEP0178) | Doesn't throw on its own: the property is gone and you read undefined. Use dirent.parentPath | | OutgoingMessage.prototype._headers / _headerNames | Removed (DEP0066) | You read undefined. Use getHeaders() | dirent.path is the sneakiest one on the list, because reading it throws nothing: the failure shows up later, wherever that path gets used, or never, if it's only concatenated into a string and comes out as undefined/file. If you maintain an action that walks directories, search for that property before you trust the green check. Your self-hosted runner can't run Node 24. On Linux ARM32, the same NodeUtil.cs has a kill switch that fails the step with: Linux ARM32 runners are no longer supported. Please migrate to a supported platform. Before it's flipped, the runner keeps those actions on Node 20 with a warning whose default date is the Node 20 removal date: "Linux ARM32 runners are deprecated and will no longer be supported after September 23rd, 2026." GitHub hasn't published the exact day the switch flips; the changelog only says ARM32 loses support after the Node 20 deprecation. On macOS, Node 24's BUILDING.md requires macOS 13.5 or later. 2. The audit: what to look for and where In one repository Run this from the root: # 1. The switch that stops working on 23 September grep -rn 'ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION' .github/ # 2. Image labels with a date. No "runs-on:" on purpose, # so matrices show up too (os: [ubuntu-22.04, ...]) grep -rnE 'ubuntu-22\.04|macos-14|windows-11-arm' .github/ # 3. Your own or local actions that declare an old Node grep -rnE --include=action.yml --include=action.yaml \ "using:[[:space:]]*['\"]?node(12|16|20)" . Search 2 has a blind spot: runs-on: ${{ vars.RUNNER }} or a label passed as an input to a reusable workflow won't show up in any grep. For those, the log is the source of truth. The "Set up job" step prints the real image: gh run view <run-id> --log | grep -m1 'Image:' # Image: ubuntu-24.04 If the matrix is long or nested, converting it to JSON and filtering it with jq is usually faster than reading the YAML by eye: > Free tool: JSON and YAML Converter — Convert JSON to YAML and YAML to JSON instantly, with configurable indentation, download and copy. All in your browser, nothing uploaded. To find out which runtime each third-party action you use declares, including SHA-pinned ones, you have to read its action.yml at that exact ref. This script does it with gh: grep -rhoE "uses:[[:space:]]*['\"]?[^[:space:]'\"#]+@[^[:space:]'\"#]+" .github/ \ | sed -E "s/uses:[[:space:]]*['\"]?//" | sort -u \ | while read -r ref; do spec=${ref%@*}; ver=${ref#*@} case "$spec" in ./*|docker://*) continue ;; esac repo=$(cut -d/ -f1-2 <<<"$spec"); sub=$(cut -d/ -f3- <<<"$spec") for f in action.yml action.yaml; do rt=$(gh api "repos/$repo/contents/${sub:+$sub/}$f?ref=$ver" --jq .content 2>/dev/null \ | base64 -d | grep -E '^[[:space:]]*using:' | tr -d " '\"" | cut -d: -f2) [ -n "$rt" ] && { echo "$rt $ref"; break; } done done On this repo it prints: node20 actions/checkout@v4 node20 actions/setup-node@v4 It handles subpaths (github/codeql-action/init@v3) and SHAs. A SHA pointing at a node20 release doesn't fail on the 23rd, for the same reason @v4 doesn't: the runner forces it onto Node 24. What's different with a SHA is how it gets updated: nothing moves on its own, so Dependabot or a human has to bump it. One warning before bumping a major: read the release notes. actions/setup-node v5 turned on automatic caching when package.json has a packageManager field, and v6 limited it to npm. If you already pass cache: npm explicitly, as my CI does, it doesn't affect you. Across an organization GitHub code search reaches every repo at once: gh search code --owner YOUR_ORG 'ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION' gh search code --owner YOUR_ORG 'ubuntu-22.04 path:.github/workflows' gh search code --owner YOUR_ORG 'macos-14 path:.github/workflows' gh search code --owner YOUR_ORG node20 --filename action.yml Two limits of the code search API that will bite you: it only searches the default branch (a workflow on a release branch won't show up) and it allows 10 requests per minute. While preparing this post I hit HTTP 403: API rate limit exceeded as soon as I chained a handful of searches; add a sleep between them if you automate it. 3. Self-hosted runners: two dates in three days If you only use GitHub-hosted runners, skip this section. If you run your own, 25 September matters more than the 23rd. The 12 June changelog post resumes enforcement of two rules on github.com: - To register (or re-register) a runner you need version 2.329.0 or later. - To keep running jobs, the runner must install each new release within 30 days of its publication. "A runner pinned to 2.329.0 that never updates again will not pick up jobs." Full enforcement begins on 25 September 2026 (the notice's table labels that date as GitHub Enterprise Cloud, and the same notice states the change applies to github.com), with brownouts first: on 14, 16 and 18 September, from 11:00 AM to 3:00 PM Eastern, out-of-date runners can neither register nor run jobs. What you see, per the notice, is that "workflows targeting unsupported runners may remain queued or fail." The way to find out when your version expires is an endpoint GitHub added on 3 September. I queried it today: gh api repos/OWNER/REPO/actions/runners/deprecations/2.335.1 # {"runner_version":"2.335.1","runtime_deprecates_at":"2026-09-24T15:30:55Z"} gh api repos/OWNER/REPO/actions/runners/deprecations/2.337.0 # {"runner_version":"2.337.0","runtime_deprecates_at":null} A runner on 2.335.1 falls out of support on 24 September, so by the time full enforcement starts on the 25th it no longer gets jobs. You get the installed version on the machine itself with ./config.sh --version. The self-hosted checklist, in order: 1. Update the runner to the current release and leave auto-update on. If it's off, you need a manual cadence shorter than 30 days. 2. Rebuild the images, VM templates and containers you create runners from. The changelog asks you to recreate runners built from older cached images or templates: if the image ships a runner older than 2.329.0, it won't register. 3. Linux ARM32: move to arm64. Node 24 has no official support there, the runner's linux-arm package only bundles the Node 20 binary (externals.sh), and the code that fails the step is already in the runner. 4. macOS 13.4 or earlier: upgrade the OS. GitHub lists Node 24 as incompatible with those versions. 5. Actions that already declare node24 (such as actions/checkout@v5) require runner v2.327.1 at minimum. If you've done step 1, you already have it. 4. Retention, 1 October: history gets deleted This change breaks no build, but it deletes data. According to the 27 August changelog post, starting 1 October 2026 checks, workflow runs and statuses follow the same retention setting that already governed artifacts and logs, 90 days by default. Until now they were kept for "400+ days" regardless of your configuration. - For public repositories the maximum is 90 days. - The change is not retroactive: raising retention later won't restore anything. - Check and workflow run metadata isn't billed as storage; the associated artifacts and logs are. What breaks is anything that reads that history beyond your period: deployment metrics computed from gh run list, audits that link to a specific run, quarterly reports. Export what you need before 1 October. What the changelog does not say: what happens to a pull request open for more than 90 days whose required check expires. I couldn't find it documented. If you have long-lived PRs under branch protection, look at them after 1 October before assuming anything. 5. Images: macos-14 has a date, ubuntu-22.04 doesn't yet macos-14 (actions/runner-images#13518): deprecation started on 6 July and support ends on 2 November 2026. Before that there are eight brownouts during which jobs fail: 5, 12, 16, 19, 23, 26, 29 and 30 October, each from 14:00 UTC to 00:00 UTC the next day. It covers macos-14, macos-14-large and macos-14-xlarge. The replacement is macos-15, macos-26 or macos-latest, which started pointing to macOS 26 with the migration that began on 15 June. ubuntu-22.04 (actions/runner-images#14254): on 17 September deprecation only begins. The issue warns about longer queue times at peak hours, not failures. Retirement is on 17 April 2027, with brownouts that the issue schedules for March and April (no year given, but they fall before the retirement). It also covers ubuntu-22.04-arm. The replacement is ubuntu-24.04, ubuntu-26.04 or ubuntu-latest. If someone tells you ubuntu-22.04 breaks this week, it doesn't. Migrate calmly, but don't leave it for March. What's in the changelog and breaks nothing - cache-mode (10 September): a new key to limit cache access per workflow or job. It's opt-in: workflows that don't set it "continue to use the existing secure defaults." - The separate Code Quality path (20 August): only affects you if you have reports filtering on dynamic/github-code-scanning/codeql or on the github-advanced-security actor. - actions/checkout and pull_request_target (18 June): this one does break, but it already happened. Since 20 July the floating tags, @v4 included, refuse to check out fork code in pull_request_target. If your workflow depended on that, it's already red. It's the same family of risk I covered in the worm that got in through npm trusted publishing. What I'm doing with my CI Nothing urgent. actions/checkout@v4 and actions/setup-node@v4 have been running on Node 24 since June, the workflow doesn't use the opt-out variable, it runs on ubuntu-latest (currently ubuntu-24.04) and on GitHub-hosted runners. 23 September changes nothing for it. I am going to bump both actions to their current major, for two reasons unrelated to the date: a permanent yellow annotation trains you to ignore annotations, and an action its authors no longer test on the runtime it actually runs on is debt. If you're also thinking about your own project's Node rather than the actions', I covered that in what breaks when upgrading to Node 26. Frequently asked questions What happens to GitHub Actions on 23 September 2026? GitHub removes Node 20 from the runners. Since 16 June 2026 runners had already been running actions on Node 24 by default, and ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true let you go back to Node 20. From 23 September that variable stops working and every JavaScript action runs on Node 24. Self-hosted runners on Linux ARM32 lose support, because Node 24 has no official support for that platform. Does an action that declares using: node20 fail after 23 September? Not for declaring it. The runner code (HandlerFactory.cs and NodeUtil.cs in actions/runner) rewrites node12 and node16 to node20, and node20 to node24; in the final phase it ignores the environment variables and always picks Node 24, without returning an error. The action fails if its code uses something Node 24 removed, such as tls.createSecurePair or fs.truncate with a file descriptor, or if it runs on a self-hosted Linux ARM32 runner or on macOS 13.4 or earlier. The logs show the annotation: The following actions target Node.js 20 but are being forced to run on Node.js 24. Do I have to update actions/checkout@v4 and actions/setup-node@v4? It's a good idea, but no date forces it. The v4 tags of both declare node20 and v5 onward declare node24. On GitHub-hosted runners, the v4 tags have already been running on Node 24 since June and keep working. When bumping setup-node, read the release notes: v5 turned on automatic caching when package.json has packageManager, and v6 limited it to npm. How do I find affected workflows across my organization? With GitHub code search: gh search code --owner YOUR_ORG with ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION, with ubuntu-22.04 path:.github/workflows, with macos-14 path:.github/workflows, and with node20 --filename action.yml for your own actions. The API only searches the default branch and allows 10 requests per minute. Labels that come from variables or expressions don't show up in any search: for those, check the Image: line of the Set up job step in the log. Does ubuntu-22.04 break on 17 September 2026? No. On 17 September deprecation only begins, and the notice talks about longer queue times at peak hours. Jobs start failing during the brownouts that issue 14254 in actions/runner-images schedules for March and April, before the image is retired on 17 April 2027. The one with a date this fall is macos-14: brownouts in October and retirement on 2 November 2026. What do self-hosted runners need to do before 25 September? Update the runner. From 25 September 2026 GitHub requires version 2.329.0 to register a runner, and each new release must be installed within 30 days to keep receiving jobs. There are brownouts first on 14, 16 and 18 September, from 11:00 AM to 3:00 PM Eastern. The endpoint GET /repos/OWNER/REPO/actions/runners/deprecations/VERSION returns when your version stops running jobs. Also rebuild the images you create runners from. What changes with GitHub Actions retention on 1 October 2026? Checks, workflow runs and statuses start following the artifact and log retention setting, which defaults to 90 days; before, they were kept for more than 400 days. For public repositories the maximum is 90 days, and the change is not retroactive. If you have metrics or audits that read old runs, export the data before that date. --- Originally published on ortamarco.me.

  3. GitHub ActionsからAWSへ安全に接続する:OIDCとIAMロールの実装手順 GitHub ActionsからAWSへデプロイする際、AWSアクセスキーをSecretsに保存する方法だけが選択肢ではありません。OIDC(OpenID Connect)を使えば、GitHubが発行する身元証明用トークンをAWSに渡し、短期間だけ有効な認証情報を受け取れます。 この記事では、GitHub ActionsからS3へファイルをアップロードする例で、実装と失敗しやすい箇所を整理します。2026年10月時点の公式ドキュメントを確認しています。 仕組みを理解する GitHub Actions → GitHubからOIDCトークンを取得 → AWS STSでIAMロールを引き受ける → 一時的なAWS認証情報を使ってS3にアクセス OIDC:異なるサービス間で実行元の身元を証明するための仕組み。 STS:AWSの一時的な認証情報を発行するサービス。 IAMロール:AWSで引き受けられる権限のまとまり。 信頼ポリシー:そのロールを誰が引き受けられるか決める設定。 長期アクセスキーの保管とローテーションを減らせる一方で、ロールの権限は引き続き最小限に設定する必要があります。 1. GitHubのOIDCプロバイダーをAWSへ登録する AWS IAMでIDプロバイダーを作成し、次の値を設定します。 項目 設定値 プロバイダーURL token.actions.githubusercontent.com Audience sts.amazonaws.com Audience(オーディエンス)はトークンの利用先を表します。一般的なAWSの構成で公式Actionを使う場合の値です。既存のIDプロバイダーがある場合は重複して作らず設定を確認します。 2. 信頼ポリシーで実行元を限定する IAMロールの信頼ポリシー例です。AWSアカウントIDやリポジトリ名はダミー値です。 { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:example-org/example-api:ref:refs/heads/main" } } }] } sub(subject)はトークンの実行元を識別する値です。この例は特定のリポジトリのmainブランチだけを許可します。AWS公式ドキュメントも、組織・リポジトリ・ブランチなどで信頼対象を絞ることを推奨しています。 2026年7月15日以降に作成したGitHubリポジトリは要注意です。 GitHub公式リファレンスによると、新規リポジトリのデフォルトsubには変更されない所有者IDとリポジトリIDが含まれます。 従来形式 repo:example-org/example-api:ref:refs/heads/main 新形式(IDは説明用の値) repo:example-org@111111/example-api@222222:ref:refs/heads/main 実際に使用する形式とIAMの信頼ポリシーを一致させてください。従来から存在するリポジトリも設定変更や移転で形式が変わる場合があります。またGitHub Environmentを使う場合は、ブランチ参照の代わりに環境名を含むsubを使うため、環境のデプロイ保護ルールも設定します。 3. AWS操作権限を最小にする 信頼ポリシーは「誰が使えるか」、権限ポリシーは「何をできるか」で、別々の設定です。S3の特定の場所にファイルをアップロードするだけなら、次のように絞れます。 { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:PutObject"], "Resource": ["arn:aws:s3:::example-deploy-bucket/releases/*"] }] } s3:PutObjectはS3へのファイル書き込み権限です。s3:*やAdministratorAccessを最初から付けないようにします。S3バケットの設定やKMS暗号化などによっては追加権限が必要です。 4. GitHub Actionsの設定例 .github/workflows/deploy.ymlに次のように定義します。アカウントID・ロールARN・バケット名は自分の環境に置き換えてください。 name: Deploy to S3 on: push: branches: [main] permissions: contents: read id-token: write jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7.0.1 - uses: aws-actions/configure-aws-credentials@v6.3.0 with: role-to-assume: arn:aws:iam::123456789012:role/example-github-oidc aws-region: ap-northeast-1 allowed-account-ids: "123456789012" - name: Verify identity run: aws sts get-caller-identity - name: Upload run: aws s3 cp public/index.html s3://example-deploy-bucket/releases/index.html contents: readはリポジトリを読む権限、id-token: writeはOIDCトークンを要求する権限です。後者はAWSへの無制限な書き込み権限ではありません。 allowed-account-idsは、想定外のAWSアカウントの認証情報が返されたときに処理を失敗させます。 コマンドaws sts get-caller-identityは利用中のAWSアカウントと権限主体を確認します。aws s3 cpは指定したローカルファイルをS3へコピーします。ファイルpublic/index.htmlがリポジトリに存在する想定です。 Actionのバージョンは2026年10月10日時点の公式リリースを確認しています。本番では更新内容をレビューし、必要ならコミットSHAで固定します。 よくある失敗と対処 Not authorized to perform sts:AssumeRoleWithWebIdentity:IAMの信頼ポリシーを確認してください。subのリポジトリ名・ID形式・ブランチ・Environment、audの不一致が典型的です。 Unable to locate credentials:id-token: writeやActionの入力値、実行環境を確認します。 認証は成功したのにS3でAccessDenied:IAM権限ポリシー、S3バケットポリシー、暗号化設定などを確認します。信頼ポリシーと操作権限は異なります。 不要なブランチまで許可している:repo:example-org/example-api:*のように広く許可すると、意図しないワークフローでもロールを利用できる可能性があります。必要なブランチ・環境のみに制限し、信頼できないPull Requestから強いAWS権限を利用しない設計にします。 長期アクセスキーとどう使い分ける? OIDCは初期設定としてプロバイダー・IAMロールの作成が必要ですが、長期アクセスキーをGitHub Secretsへ保存する必要がありません。アクセスキー方式はセットアップが簡単な場合がある一方、漏えいや鍵の交換を継続して管理します。 新しいGitHub Actions→AWS連携では、公式ガイドでも案内されているOIDCを第一候補にすると運用しやすくなります。 まとめ 認証失敗の調査では、GitHubのid-token: write→IAM信頼ポリシーのsub→IAM操作権限の順に確認すると原因を切り分けやすくなります。特に2026年7月以降のsub形式変更に注意してください。 一次情報 GitHub Docs:AWS向けOIDC設定 GitHub Docs:OIDCリファレンス AWS IAM:GitHub OIDC向けIAMロール aws-actions/configure-aws-credentials公式 公式リリース:configure-aws-credentials 公式リリース:checkout

  4. 76 of 77 trivy-action Tags Were Repointed in One Night. Some SHA Pins Didn't Help Either. On March 19, 2026, someone with stolen credentials force-pushed 76 of the 77 version tags on aquasecurity/trivy-action to point at a credential stealer. All 7 tags on aquasecurity/setup-trivy too. The only clean trivy-action tag was 0.35.0, because it was an immutable release. If your workflow said uses: aquasecurity/trivy-action@0.34.2, you didn't change a line. Your repo looked exactly the same. The next run just executed different code, for about 12 hours (roughly 17:43 to 05:40 UTC per the advisory), and that code read secrets out of the runner's memory and shipped them off. The part I keep coming back to is in Aqua's own advisory, under "You are affected if you used": > SHA pinning to a commit prior to 2025-04-09. So some people who did the "right" thing and pinned trivy-action to a full SHA were still exposed, if their job ran during the ~4 hours setup-trivy was poisoned. Not because the SHA moved. Because the action they pinned called another action by tag. A lot of small SaaS repos have a CI file that was copied from a README in an afternoon and never opened again. Often a handful of uses: lines, some on @v3 or @main, and a deploy job with a cloud key or an npm token in it. This post is the one-hour version of fixing that. Three incidents, same shape These are all public, with advisories you can read: | When | Action | What moved | Window | |---|---|---|---| | Mar 14-15, 2025 | tj-actions/changed-files | multiple version tags repointed to one malicious commit; secrets dumped into build logs | ~1 day; GitHub's advisory says it impacted "over 23,000 repositories" | | Mar 3-10, 2026 | xygeni/xygeni-action | the v5 tag moved to a commit from an unmerged pull request | ~6 days | | Mar 19-20, 2026 | aquasecurity/trivy-action + setup-trivy | 76 of 77 tags, all 7 tags | ~12 h and ~4 h | In none of these did the victim's repository change. That's the whole problem with a tag. @v5 is not a version, it's a pointer that gets looked up every time the job starts. The xygeni one is worth a second look. Branch protection worked, the bad PRs were never merged. But the commit still existed in the repo's git objects, and moving a tag to it was enough. Merged or not doesn't matter to a tag. Three ways a pin fails People argue "tags vs SHAs" like it's one decision. It's really three separate failure modes: 1. The tag moves. @v4, @v4.2.2, @main. All mutable unless the publisher uses immutable releases. This is tj-actions, xygeni and trivy-action. 2. Your pin isn't transitive. You pin owner/action@<sha>. That action's own action.yml says uses: owner/other-thing@v1. You get whatever v1 is today. This is the trivy-action pre-April-2025 case: until a commit on 2025-04-09 ("Pin aquasecuriy/setup-trivy to hash instead of tag"), trivy-action's action.yaml called aquasecurity/setup-trivy@v0.2.2. It's right there in the repo history. GitHub's own docs say it plainly now: lockfiles cover transitive dependencies, "which SHA pinning does not." 3. The SHA is real but it's not from where you think. A full SHA can point at a commit that isn't on any branch or tag of the real repo. In the trivy binary part of the same incident, the attacker swapped an actions/checkout reference to what the advisory calls an "imposter commit". GitHub's docs have a one-liner on this: when you pick a SHA, "verify it is from the action's repository and not a repository fork." It's very easy to copy a SHA from a blog post or a bot PR and never check where it lives. Here's the order I'd fix things in. Step 1: list what you actually run (10 min) From the repo root: grep -rnE '^\s*-?\s*uses:\s*[^ ./$]' .github/workflows \ | grep -vE '@[0-9a-f]{40}([^0-9a-f]|$)' This prints every uses: that points outside your repo and isn't pinned to a full 40-character SHA. It skips local ./ actions. It will also show docker:// steps and reusable workflows (jobs.<id>.uses: org/repo/.github/workflows/x.yml@main), which is good, you want to see those too. On a test workflow it gave me lines like: .github/workflows/ci.yml:10: - uses: actions/setup-node@v4 .github/workflows/ci.yml:12: - uses: docker/setup-buildx-action@v3 .github/workflows/ci.yml:14: - uses: github/codeql-action/init@v3 .github/workflows/ci.yml:16: uses: docker://alpine:3.20 .github/workflows/ci.yml:18: uses: octo-org/shared/.github/workflows/deploy.yml@main Same idea as grepping your server for hardcoded renewal days before the Let's Encrypt change. You can't fix what you haven't listed. Write the list down in three groups: - GitHub's own (actions/*, github/*) - Big, well-known publishers (docker, aws, google, cloudflare, vercel...) - Everything else. Small repos, one maintainer, something you found in a Marketplace search at 1am. Step 2: delete before you pin (10 min) The cheapest action to secure is the one you remove. A lot of third-party actions in small repos do something like "post a comment", "get changed files", "set up a tool", "send a Slack message". Some of those are 3 to 10 lines of shell plus gh or curl, which runs with the same token anyway but without someone else's code in the middle. For example, a lot of what people use tj-actions/changed-files for is a git diff --name-only between two commits. For the ones you keep, look at them once before you trust them. Open the repo: how many maintainers, how old, does it pin its own dependencies, does the action.yml call other actions by tag (that's failure mode 2 again). It's basically the same spelling, age and real-usage check as checking a package an AI coding agent wants to install. A typo in an action name runs someone else's code with your secrets, same as a typo in npm install. Step 3: pin, and check the pin is honest (15 min) To get the commit a tag points to right now: git ls-remote https://github.com/actions/checkout \ 'refs/tags/v4.2.2' 'refs/tags/v4.2.2^{}' | tail -n1 | cut -f1 The ^{} line matters. Some repos use annotated tags, and then the first SHA you see is the tag object, not the commit. tail -n1 takes the peeled commit when it exists. (Before you pin, check that the tag is the release you meant and not already compromised. Pinning a bad commit just makes it permanent.) Then write it with the version as a comment so humans can read it: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 Now the check most people skip. This script reads every @<sha> # <tag> line and asks GitHub what that tag points to today: #!/usr/bin/env bash # For every "uses: owner/repo@<full sha> # <tag>" line, ask GitHub # what that tag points to right now and compare it with the pin. grep -rhoE 'uses:\s*[^ ./$][^ @]*@[0-9a-f]{40}\s*#\s*\S+' .github/workflows | while read -r _ ref _ tag; do sha=${ref##*@} repo=$(echo "${ref%@*}" | cut -d/ -f1-2) now=$(git ls-remote "https://github.com/$repo" \ "refs/tags/$tag" "refs/tags/$tag^{}" | tail -n1 | cut -f1) if [ "$now" = "$sha" ]; then echo "ok $repo $tag" else echo "CHECK $repo $tag pinned=$sha tag-now=${now:-missing}" fi done On my test file, with one deliberately wrong pin: ok actions/checkout v4.2.2 CHECK actions/checkout v4.2.2 pinned=d632683dd7b4114ad314bca15554477dd762a938 tag-now=11bd71901bbe5b1630ceea73d27597364c9af683 ok pnpm/action-setup v3.0.0 ok github/codeql-action v3.29.0 A CHECK means one of two things, and both are worth five minutes: either your SHA was never that tag (copied wrong, or an imposter commit), or the tag moved after you pinned it. The second one is exactly the event you want to hear about. Your pin protected you, but it's also a signal that upstream had a bad day and you should go read their issues. It only checks the format @sha # tag, it handles owner/repo/subdir actions like github/codeql-action/init, and it needs nothing but git and grep. You could run it weekly in a scheduled job. It does not look inside the actions for failure mode 2, that's what the next step is for. One tradeoff that's easy to miss: GitHub's docs say Dependabot "will not create alerts for actions pinned to SHA values", alerts are only for actions using semantic versioning. So if you pin by SHA, turn on Dependabot version updates for github-actions so the pins don't rot. Since there's a default cooldown now (Dependabot waits 3 days after a release before proposing it, on github.com), a freshly poisoned release has some time to get caught before a bot PR shows up. That helps with bad new releases. It does nothing for a tag that gets repointed under you, only the pin does. Step 4: or try the new lockfile (15 min) On October 14, 2026, GitHub put workflow dependency lockfiles into public preview, including on the Free plan. It's basically package-lock.json for Actions. A CLI extension writes .github/workflows/actions.lock with the commit for every action your workflows use, including the transitive ones, plus the numeric owner and repo IDs. Once a workflow is onboarded, Actions runs the locked commit even if the tag moves, and every dependency of that workflow has to be in the lockfile. gh extension install github/gh-actions-lock gh actions-lock # scan .github/workflows, write actions.lock gh actions-lock --verify # read-only check, good for a CI job Your workflow keeps readable refs (it may narrow @v4 to @v4.2.2). GitHub's docs now say that for repos with a lockfile, SHA pinning in the workflow file is "no longer necessary". There's also a "Require lockfile" policy at repo/org level, and Dependabot regenerates the lockfile entry in the same PR when it bumps an onboarded workflow. Things to know before you rely on it, straight from the README and release notes: - It's pre-1.0 and in preview. v0.3.0 (Oct 22) changed behavior enough that some lockfiles that used to pass --verify now fail it. - Reusable workflows called from another repo (jobs.<id>.uses) are not locked by your lockfile. - Container images are not locked. Anything without an @sha256: digest is only a warning. - A lockfile change that moves a floating ref like v4 back to an older commit in its history isn't detected. So review lockfile diffs, or pin exact tags. - Since v0.3.0 it rejects full-SHA pins to fork-only (imposter) commits, every locked commit must be in the history of its recorded tag or branch. That covers failure mode 3. If you have one repo and a few workflows, I'd honestly try this over hand-pinning. If you're not ready for a preview tool, steps 1 to 3 still get you most of the way. Step 5: make a stolen run worth less (10 min) Pinning is about which code runs. This step is about what that code can grab if something slips through anyway. The tj-actions and trivy payloads both went for secrets in the runner's memory. The xygeni one gave the attacker a remote shell on the runner for up to 180 seconds per run, which reaches the same things. At the top of every workflow: permissions: contents: read Then give extra scopes only to the job that needs them: jobs: comment: runs-on: ubuntu-latest permissions: contents: read pull-requests: write # steps: ... Once you set any permission, every one you didn't list becomes none. permissions: {} turns them all off. Why bother if new repos default to read-only? Because that default only applies to things created after February 2, 2023. GitHub's changelog was explicit: the change "will not impact any existing enterprises, organizations or repositories." If your repo or org is older than that, go to Settings → Actions → General → Workflow permissions and look. An older repo can still say "Read and write" there. Two more: - Keep prod secrets out of the PR workflow. Your test job doesn't need the production database URL or the live payment key. Put deploy secrets in an environment with required reviewers. Then only jobs that use that environment can read them, and only after someone clicks approve. - Prefer short-lived cloud creds. If you deploy to AWS/GCP/Azure, OIDC gives the job a token that expires, instead of a long-lived key sitting in repo secrets forever. On GitHub-hosted runners the GITHUB_TOKEN itself already dies with the job, max 6 hours. Step 6: if you ran a bad version All three advisories say some version of the same thing: treat every secret the pipeline could read as exposed, and rotate it. Not "probably fine". The trivy payload read the runner's memory, so masked secrets in the logs didn't matter. The order I'd go in: 1. Find the runs inside the advisory's window. gh run list --created 2026-03-19..2026-03-20 works, or just scroll the Actions tab. 2. Rotate whatever those jobs could reach. Cloud keys, deploy keys, package registry tokens, any API key in repo or org secrets. 3. For trivy specifically, look for a repo named tpcp-docs in your account or org. Per the advisory, the stealer creates it as a fallback upload spot when its normal exfiltration fails. 4. Check what got published from those runs (releases, packages, images) in case something was tampered. Rotation is the slow part if you don't know where each key is used. Writing that list down before you need it is worth doing just for this. The rules I'd actually adopt At under 50 customers, with one or two people touching CI: - Third-party actions: always locked (lockfile) or SHA-pinned, with the version comment. No exceptions for "it's popular". - GitHub's own actions/*: I'd pin them too. GitHub's "Require actions to be pinned to a full-length commit SHA" policy (added August 2025) treats them the same, and it makes the rule simple enough to enforce. - Fewer actions every quarter, not more. - permissions: contents: read at the top of every workflow, widen per job. - The pin check, weekly. Total time is about an hour for a typical small repo. The trivy-action window was about twelve hours, starting on a Thursday evening UTC, and the real scan still ran after the stealer, so the job looked normal. Nobody watching their CI casually would have caught it. I'm curious how other small teams handle this. Are you pinning SHAs by hand, trying the new gh actions-lock preview, or still on @v4? And if you already pin by SHA, have you ever opened the action.yml of what you pinned to see if it calls other actions by tag?

  5. Up to node24 runtime and ESM support come with actions/checkout, the official GitHub Action for checking out your repository. Fetches a single commit by default for the triggering ref or SHA, with options for full history. actions/checkout

    2 views
  6. YouTube · · YouTube description · Tech Unlocked

    GitHub-Explained-Complete-Tutorial-2026. GitHub Explained: Complete Beginner-to-Advanced Tutorial (2026) Learn how GitHub actually works, from your first repository to pull requests, GitHub Pages, GitHub Actions and security. We follow one project, a student portfolio website, from start to finish: create a repository, clone it, branch, open a pull request, run an automated check, merge, and publish. What you'll learn • Git vs GitHub: what each one does • Repositories, commits, branches and merging • Pull requests, code review and merge conflicts • Issues, labels, milestones and GitHub Projects • Forking and contributing to open source (and why a public repo is not automatically free to reuse) • Publishing a static site with GitHub Pages • Automating checks with GitHub Actions • Account and repository security: two-factor authentication, tokens, SSH keys, rulesets, Dependabot, secrets • Releases, GitHub CLI, GitHub Desktop, Codespaces, Packages, templates and Copilot • The most common beginner mistakes, and how to fix them CHAPTERS 0:00 Intro: Git vs GitHub 1:20 Getting started 2:41 Understanding a repository 3:39 Create your first project 5:05 Learn Git in the terminal 8:18 Branches and merging 10:19 Pull requests and collaboration 12:25 Issues and project management 13:26 Fork and open source 14:33 GitHub Pages 15:53 GitHub Actions and CI/CD 17:38 Security and best practices 20:17 Advanced features 21:50 A complete real-world workflow 22:36 Common mistakes 24:15 Recap and next steps COMMANDS USED git --version git config --global user.name "Your Name" git config --global user.email "you@example.com" git clone REPOSITORY_URL (replace with your own repository address) git status git add . git commit -m "Add homepage" git log --oneline git push git pull git switch -c feature/dark-mode git merge feature/dark-mode ABOUT THIS VIDEO • Terminal output is real, captured from actual Git commands. • GitHub web pages are simplified recreations, labelled on screen. The real interface changes over time, so follow along on github.com. • Pages and rulesets availability, Codespaces allowances and Copilot features depend on your plan. Check GitHub's official docs for current details. • Narration is AI-generated. SOURCES GitHub Docs: docs.github.com GitHub Pages publishing source: docs.github.com Git documentation: git-scm.com actions/checkout: actions/checkout Practice repository used in the video: octocat/Spoon-Knife Subscribe for more practical programming, AI, and developer tutorials. #GitHub #Git #GitTutorial #Programming #LearnToCode #DevOps #OpenSource

    11 likes · 1 comment · 347 views
  7. Zenn · · @4no

    GitHub ActionsでTerraformの品質チェックを自動化する はじめに 普段の業務では、Terraformを利用したAWSインフラの設計・構築に取り組んでいます。 Terraformの設定変更時には、フォーマットや構文のチェックが重要です。ローカルでも実行できますが、その実行結果はGitHub上に共有されないため、PRのレビュアーがチェック結果を確認しにくいという課題があります。 そこで今回は、GitHub Actionsを利用して、PR作成時にTerraformの品質チェックを自動実行し、チェック結果をGitHub上で確認できるようにしました。 今回やること PR作成時に、以下の3つを自動実行します。 terraform fmt:Terraformコードのフォーマットをチェックする terraform init:Terraformの初期化を行う terraform validate:Terraformの設定を検証する 今回はTerraformの設定ファイルがリポジトリのルートディレクトリにあることを前提としています。 GitHub Actionsのワークフロー 今回作成したワークフローは以下のとおりです。 name: Terraform Quality Check on: pull_request: permissions: contents: read jobs: terraform-quality: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v7 - name: Setup Terraform uses: hashicorp/setup-terraform@v4 - name: Check formatting run: terraform fmt -check -recursive - name: Initialize Terraform run: terraform init -backend=false - name: Validate Terraform run: terraform validate ※ Actionのバージョンは、利用時に公式リポジトリで最新バージョンや互換性を確認してください。 各処理の役割 1. PR作成時にワークフローを実行する on: pull_request: pull_request を指定することで、PRに関連するイベントを契機にワークフローが実行されます。 これにより、PR上でチェック結果を確認できるようになります。 2. Terraformのフォーマットをチェックする - name: Check formatting run: terraform fmt -check -recursive terraform fmt は、Terraformの設定ファイルを標準的な形式に自動整形するコマンドです。 一方、-check を指定すると、自動整形は行わず、フォーマットが正しいかをチェックします。また、-recursive を指定することで、サブディレクトリも対象にできます。 フォーマットに問題がある場合は、コマンドが失敗します。 ローカルでは terraform fmt -recursive で自動整形し、GitHub Actionsでは terraform fmt -check -recursive で修正漏れを検出する方針にしました。 3. Terraformを初期化する - name: Initialize Terraform run: terraform init -backend=false terraform init は、Terraformの実行に必要なプロバイダーなどを初期化するコマンドです。 今回は -backend=false を指定しています。 このオプションにより、バックエンドの初期化をスキップできます。例えば、S3などをTerraformのState保存先として設定している場合でも、バックエンドへの接続・初期化を避けて検証を進められます。 なお、バックエンドをスキップしても、プロバイダーやモジュールのダウンロードが発生する場合があります。 4. Terraformの設定を検証する - name: Validate Terraform run: terraform validate terraform validate は、Terraformの構文や設定の内部的な整合性を検証するコマンドです。 例えば、設定の構文エラーや、リソースの引数・属性の指定に問題がある場合などを検出できます。 ただし、AWS上にリソースを作成できるか、IAM権限が十分か、設計が業務要件を満たしているかといったことまでは検証できません。 実行前には、terraform init などで必要な初期化を済ませておく必要があります。 実際に動かしてみた 作成したワークフローをPRで実行し、各ステップの結果を確認しました。 Check formatting:成功 Initialize Terraform:成功 Validate Terraform:成功 terraform validate では、以下のメッセージが表示されました。 Success! The configuration is valid. 一方、terraform fmt -check -recursive は、問題がなければ特にメッセージが表示されない場合があります。GitHub Actionsのステップが成功していることを確認しました。 このように、PR上で各チェックの実行結果を確認できることが分かりました。 やってみて学んだこと 今回、特に学びになったのは、Terraformのコマンドをローカルで実行することと、CIで自動実行することの違いです。 ローカルでチェックするだけでなく、PR作成時に自動実行することで、チェックの実行漏れを防ぎやすくなり、結果もGitHub上で確認できます。 また、各コマンドには異なる役割があります。 terraform fmt -check:フォーマットのチェック terraform init -backend=false:バックエンドを初期化せずにTerraformを初期化 terraform validate:設定の構文や内部整合性の検証 これらを組み合わせることで、AWSへのデプロイを行う前に、Terraformコードの基本的な品質を確認できるようになります。 まとめ 今回は、GitHub Actionsを使って、PR作成時にTerraformの品質チェックを自動実行する仕組みを作りました。 これまではローカルで実行していたチェックをCIに組み込むことで、PR上で結果を確認できるようになりました。 今後は、Terraformのセキュリティスキャンやテストなども組み合わせ、PRの段階で問題を検出できる仕組みを少しずつ改善していきたいと思います。 参考 Terraform fmt Terraform init Terraform validate GitHub Actions ワークフロー構文 actions/checkout hashicorp/setup-terraform

  8. DEV · · @merbayerp

    A 9 MiB Clone, Then Checkout Fetched 204 MiB More This blog's repository got fat: 3,538 commits, 46,219 objects, a 276 MiB pack. The weight is not in the history, it is in the present — every article also has a cover PNG, and PNGs do not compress. Once cloning started to annoy me, my reflex was everybody's reflex: --depth 1. Shallow clone. Drop the history, take the current state, get on with it. The reflex was wrong. --depth 1 fetched 204.3 MiB; --filter=blob:none fetched 9.2 MiB. A twenty-two-fold difference, and against the shallow clone. The real lesson came afterwards: those 9 MiB were not a saving, they were a loan. The first git checkout showed up at the door and collected 204.1 MiB in one payment. A saving that was 30-fold at clone time came down to 1.3-fold once the repository was workable. Three tables, three traps, and at the end the one combination that actually stays small. Plus a constraint in my own CI that I do not like. The ground the measurements stand on Environment: macOS (arm64), Homebrew git 2.48.1. Everything was taken against a single-branch bare copy derived from the repository's origin/main tip — df0de9d4, 22 October 2026: $ git init --bare srv.git $ git push srv.git origin/main:refs/heads/main $ git -C srv.git config uploadpack.allowFilter true $ git -C srv.git rev-list --count HEAD # 3538 $ git -C srv.git count-objects -v | grep in-pack # in-pack: 46219 The single branch matters: rev-list --count HEAD and --all give the same number, and all 46,219 objects in the pack are reachable. That removes the question "which ref set does this number come from". There is also the matter of the unit. The first round used du -s, and APFS told a lie: two pack files of 289,923,968 bytes with identical md5 sums occupied 566,264 and 590,824 512-byte blocks in two different clones. Same content, 4.3 percent apart. So every number below is not du but the byte sum of the pack files — unchanged by whatever the filesystem decides to do, and the quantity we actually care about. The first two traps: the filter drops out quietly The first attempt at a filtered clone gave a local path: $ LC_ALL=C git clone --filter=blob:none --no-checkout /tmp/lab/srv.git a1 Cloning into 'a1'... warning: --filter is ignored in local clones; use file:// instead. done. $ echo $? 0 Adding the file:// prefix fixed that. Then the second road: right prefix, but the server setting off. $ git -C srv.git config --unset uploadpack.allowFilter $ LC_ALL=C git clone --filter=blob:none --no-checkout "file:///tmp/lab/srv.git" a2 Cloning into 'a2'... warning: filtering not recognized by server, ignoring $ echo $? 0 Both warn, both return zero, and both produce a full clone — 21,213 blobs, 276 MiB. Filtering has two separate gates, and with either one shut the command still counts as a success. The server-side gate is uploadpack.allowFilter; its documentation says what the setting does ("if this option is set, upload-pack will support partial clone and partial fetch object filtering") but not what happens when it is unset. The measurement says: the request is not refused, it falls back to a full clone without a sound. The genuinely unpleasant part is common to both. The filter was not applied, yet Git still wrote this into the config: $ git -C a2 config --local --list | grep -E 'promisor|partialclone' remote.origin.promisor=true remote.origin.partialclonefilter=blob:none The repository is complete but labelled a partial clone, and later fetches honour the label. In the file://-with-the-setting-off case Git also left a .promisor file behind. That file is not a symptom — every partial clone has one. The oddity here is that the pack behind it is complete. So how do you spot it? The two indicators that come to mind first both turn out to be useless: git config remote.origin.promisor returns true in a successful partial clone too, and the warning: This repository uses promisor remotes. Some objects may not be loaded. line that appears on commands touching the object store shows up in that clone too. Both tell you the repository is labelled partial; your question is whether the filter was applied. The one thing that separates them is counting blobs: $ git -C a2 cat-file --batch-all-objects --batch-check | awk '{print $2}' | sort | uniq -c 21213 blob 3538 commit 21468 tree If you were expecting zero blobs, that table answers the question: 21,213 in the degraded clone, 0 in the successful one. The clone ladder All taken with --no-checkout, so the only thing compared is the set of downloaded objects. The server's own pack is 289,908,372 bytes. | clone | time | pack fetched | objects | |---|---|---|---| | full | 2.9 s | 289,923,968 B — 276.5 MiB | 21,213 blobs · 3,538 commits · 21,468 trees | | --depth 1 | 2.0 s | 214,235,359 B — 204.3 MiB | 6,321 blobs · 1 commit · 84 trees | | --filter=blob:limit=10k | 0.5 s | 14,034,161 B — 13.4 MiB | 8,371 blobs · 3,538 commits · 21,468 trees | | --filter=blob:none | 0.4 s | 9,656,017 B — 9.2 MiB | 0 blobs · 3,538 commits · 21,468 trees | | --filter=tree:0 | 0.1 s | 1,086,548 B — 1.0 MiB | 3,538 commits only | Why the shallow clone is so large sits in the blob column: 6,321 blobs. --depth 1 cuts the history but brings the complete photograph of HEAD — every file as it stands today, cover PNGs included. Dropping the history takes 276.5 down to 204.3, twenty-six percent. Dropping the blobs takes it to 9.2. The tree:0 row is instructive too. The local man page (2.48.1) says of tree:<depth>: it omits all blobs and trees whose depth from the root tree is greater than or equal to <depth>, and <depth>=0 will not include any trees or blobs unless given explicitly on the command line. That is exactly the table — 3,538 commits, nothing else, 1 MiB. blob:limit=10k is there because its documentation says it omits blobs of size at least <n> bytes, meaning it keeps the small ones. Your text files are local, the PNGs are remote: 13.4 MiB. A reasonable middle road for anyone who wants the code but not the assets. The loan: the first checkout Starting work in the blobless clone changes the table. Pack bytes were re-totalled after each command: | command | time | fetched | |---|---|---| | (clone) | 0.4 s | 9,656,017 B — 9.2 MiB | | git checkout | 3.6 s | +214,046,263 B — 204.1 MiB | | git log --oneline (3,538 lines) | 0.0 s | 0 B | | git log -p -1 | 0.1 s | +15,379 B | | git blame README.md | 0.2 s | +12,571 B | | git log -p -20 | 0.9 s | +1,653,754 B — 1.6 MiB | Everything after the first row is good news: walking the history is free. Listing 3,538 commits fetches not one byte, because the commits and the trees are already here. Ask for a patch and Git goes for that commit's blobs; twenty commits' worth of patches costs 1.6 MiB. For someone who says "I want to look at the past, but not at every version of every file", the model fits. The bad news is the second row. git checkout alone fetched 204.1 MiB. The clone was 9.2 MiB, the working repository is 213.3 MiB, a full clone is 276.5 MiB. The saving that was 30-fold at clone time is now 1.3-fold. Why is in the partial clone documentation, and the mechanism was fixed by a patch: checkout (and any other command using unpack-trees) has been taught to bulk pre-fetch all required missing blobs in a single batch. Without it things would be worse; the same document says dynamic object fetching tends to be slow as objects are fetched one at a time, and among the known limitations it lists that dynamic fetching invokes fetch-pack once for each item, which may incur significant overhead — and multiple authentication requests — if many objects are needed. The batch pre-fetch is why it finished in 3.6 seconds. That one-at-a-time fetching leaves a trace on disk. After the five commands above the repository held 27 pack files, every one of them marked promisor: each lazy fetch leaves its own pack. I was about to write something here, and the measurement stopped me. The partial clone design document says gc does "not touch promisor packfiles at all"; but the same document's "Current Limitations" section says repack treats promisor and non-promisor packfiles as two distinct partitions and does not mix them. The document contradicts itself: the first sentence has gone stale, the second matches both git-repack's 2.48.1 man page and today's behaviour. The measurement backs the second — running git gc took those 27 packs down to a single promisor pack. That consolidation came from the gc I ran by hand; at 27, below gc.autoPackLimit's default of 50, automatic maintenance would not have fired yet. In a long-lived copy, once the threshold is crossed, it tidies up on its own. For the record: that one sentence is the stale part. The batch pre-fetch, the per-object fetch-pack and the online requirement I take from the same document all agree with the man pages and with the measurements. In the treeless clone the ratio is wilder still: a 1,086,548-byte repository climbs to 215,334,800 bytes after checkout — 198-fold. In the blobless clone that multiplier is 23. The absolute figure is smaller than the blobless clone's 213.3 MiB (205.4), but it shows well how little the number you see at clone time tells you. The rule that falls out: --filter has deferred the download, not reduced it. If you are going to open the whole working tree, you pay the loan at the first checkout. The shallow clone's bill is not in size, it is in correctness Size is not the shallow clone's only problem: $ git -C shallow rev-list --count HEAD 1 $ git -C blobless rev-list --count HEAD 3538 The number dropping was expected. What is uncomfortable is the same line of the same file across the two clones: $ git -C shallow blame README.md | head -1 ^df0de9d (Social Bot 2026-10-22 07:15:19 +0000 1) # Mustafa Erbay — Blog & Teknoloji $ git -C blobless blame README.md | head -1 d263086f6 (mustafaerbay 2026-06-09 20:55:09 +0300 1) # Mustafa Erbay — Blog & Teknoloji git blame does not fail. It runs, it produces output, and it attributes that line to the commit a social-posting bot pushed that same morning. The true answer is 9 June, a different hand. The leading ^ marks a boundary commit, but to an eye not trained on that table this is simply a wrong date and a wrong name. A slow tool is liveable; a wrong one is not. One more side effect is in the documentation: --depth implies --single-branch unless --no-single-branch is given. For CI that is usually right; but the answer to "why does git fetch not see that branch" lands here months later. The combination that actually stays small The only way not to pay the loan is never to open the whole working tree. git clone carries --sparse for this, and its documentation says word for word what it does: it employs a sparse-checkout, with only files in the toplevel directory initially being present. | setup | pack | working tree | files | |---|---|---|---| | full clone + checkout | 276.5 MiB | 232.8 MiB | 6,336 | | full clone + sparse-checkout set | 276.5 MiB | 15.9 MiB | 1,112 | | --filter=blob:none --sparse (at clone) | 9.3 MiB | 0.5 MiB | 14 | | + sparse-checkout set (3 dirs) | 16.7 MiB | 15.9 MiB | 1,112 | The second row explains why sparse-checkout does not deliver what people expect. The working tree went from 232.8 MiB to 15.9 — but the pack stayed at 276.5 MiB, not a byte moved. sparse-checkout does not shrink the download, and the command's own tagline promises no more than that: it reduces your working tree to a subset of tracked files. The mechanism is described in the documentation's INTERNALS section and has nothing to do with the object store: the skip-worktree bit goes into the index, Git avoids populating those files' contents, and their absence is ignored. The index entry stays put — as in the lab: in a working tree with 14 files, git ls-files still counts 6,336 paths. The last row is the whole point: a 32.6 MiB setup holding all the articles of one category plus src/lib and scripts. The full setup is 509.3 MiB; less than one fifteenth. And the history is still there. The recipe is three lines: $ git clone --filter=blob:none --sparse "file://$SRV" work $ cd work $ git sparse-checkout set src/content/blog/tutorials src/lib scripts After the first line you will have 14 files; until the cone widens, the repository is just its root directory. The "it came up empty" panic is normal. !Diagram The sparse index does not come unless you ask Switching to cone mode was supposed to shrink the index too. It did not: $ git sparse-checkout set src/lib $ git config index.sparse ; stat -f%z .git/index # no line printed 938361 The documentation says so explicitly: the --[no-]sparse-index option can be used to get a sparse index, and the default is not to use it. With the flag, 938,361 bytes drop to 5,839. In a wider three-directory cone, 936,257 → 162,430 bytes, a factor of 5.8. On the speed side there was nothing, and that goes down as it is. git status --porcelain ran five times each; best values: 0.040 s with the sparse index, 0.041 s with the full one. The difference sits inside the noise. The documentation says this can have significant performance advantages for commands such as git status or git add; in a repository with 6,336 paths it does not show. This setting's promise is for monorepos with tens of thousands of files. The same page says the feature is still experimental and that some commands might be slower with a sparse index, and the capitalised "this command is experimental" warning sits at the end of the DESCRIPTION section. One more trap: if files are left outside the cone, Git warns during git status that the sparse index is expanding to a full index, a slow operation — and that run's numbers are garbage. My first attempt landed exactly there. The constraint in my CI that I do not like All of this was for my own publishing pipeline. Of 17 workflow files, 7 use actions/checkout; 2 of those declare fetch-depth (one 1, one 0) and the remaining 5 take the default. The pinned version's action.yml gives 1 as that default, described as "Number of commits to fetch. 0 indicates all history for all branches and tags." So my pipeline implements my reflex: a shallow clone. Per table 1 that is a 204 MiB photograph per run — with the caveat that the 204 MiB was taken on the local file:// side; GitHub's upload-pack packs differently and actions/checkout does not run git clone, it fetches a single ref. It gives the order of magnitude, not the bill. The workflow that says fetch-depth: 0 pays at least 276 MiB — the server in the table had a single branch, while fetch-depth: 0 asks for all branches and tags. The action also has filter and sparse-checkout inputs — precisely the winning combination above. But the filter input's description is two sentences, and the second is this: it overrides sparse-checkout if set. The two cannot be used together in this action. The smallest setup in this article — blobless clone and a narrow cone — cannot be expressed with actions/checkout's inputs. Either take the 9 MiB clone and pay the loan by opening the full tree, or ask for the narrow tree and download the full object set. There is a way out, and it is not cheap: use actions/checkout only to place credentials (persist-credentials), then run git clone --filter=blob:none --sparse yourself. The price is that credential handling becomes your problem — and since every later log -p/blame in a partial clone is a network request, that credential has to outlive the checkout step. I have not done it in my pipeline yet; the measurement is on the table, the constraint is named, the decision is pending. Before you apply any of this - Is history needed? git log, git blame, git bisect. If yes, --depth 1 is off the table: it gives a wrong answer without erroring. If no, it is the simplest road — provided your repository's weight is in its history. - Where is the weight? Of my repository's 276 MiB, 204 is today's photograph. Measure once: clone with --depth 1 --no-checkout and look at the pack bytes. If the photograph is large, cutting history will not help. - Is the whole working tree needed? If yes, --filter defers the download, it does not reduce it. If no, --filter=blob:none --sparse is the only real win. - Will you work offline? The documentation is explicit: use of partial clone requires that the user be online and the origin remote or other promisor remotes be available. When the promisor is unreachable a partial clone does not get slow: commands that make do with local objects (status, commit, log --oneline) keep working, while every command needing a missing object fails — and you cannot repair that offline. - Does the server allow it? uploadpack.allowFilter is not the only gate: uploadpackfilter.<filter>.allow can ban individual filters and uploadpackfilter.tree.maxDepth caps tree:<n>. blob:none may work while tree:0 is refused. On your own Git server this is the first place to look. Three things to avoid: asking for a filter without file://, asking for one while allowFilter is off on the server (both quietly give you a full clone), and assuming cone mode brings the sparse index along. None of the three breaks anything; they just do not do what you asked, and the first two exit zero while doing it. A two-line assert in a CI job catches those two — but not via the promisor setting, which is true in a successful clone as well. After the clone, count the blobs, or test the pack bytes against a threshold. A deferred cost is not a reduced cost What made me think most this round was the delight at first seeing that 9 MiB clone. The number was real, the measurement was correct, it said thirty-fold. The only wrong thing was the conclusion drawn from it: "the repository got smaller". It had not; its bill had been postponed, and it came back the moment I typed git checkout. The second lesson was in the measuring itself. The first round used du, and showed a 4 percent gap between two copies of the same pack — until it became clear the filesystem was reporting on its own block accounting, not on the repository. In a measurement article, picking the unit is as much work as taking the measurement. Other instances of the same reflex: the article where Git wrote this repo half a commit-graph, where a correct and fresh cache was not speeding up the question actually being asked, and the one where git status walked 36,811 entries for three lines. A five-minute exercise on your own repository: take a blobless clone, note the pack bytes, run git checkout, note them again. The gap between those two numbers is the loan you deferred. Official Sources - technical/partial-clone.adoc — promisor remotes, batch pre-fetch and known limitations - git-sparse-checkout — cone mode, skip-worktree and the sparse index - git-clone — the --filter, --sparse and --depth options - config/uploadpack.adoc — allowFilter and the uploadpackfilter.* permissions - rev-list-options.adoc — the definition of filter specifications - actions/checkout action.yml — the filter, sparse-checkout and fetch-depth inputs

    6 reactions
  9. AIと作る公開リポジトリの守り方:無料でできるルールセット・SHA固定・Dependabot・CodeQL AIにコードを書かせて、GitHubで公開する。とても手軽になりましたが、守りの設定は後回しになりがちです。 この記事では、15個の公開リポジトリに無料で入れた守りの設定を、手順つきで紹介します。AIがmainに直接pushする運用でも困らない形にしています。 題材は shoyo-maya-bot のリポジトリ群です。 全体像 守り 防ぐもの だれが設定するか ルールセット mainの削除・履歴の書き換え 人(設定画面) 非公開の脆弱性報告 脆弱性が公開のIssueに書かれる 人(設定画面) Dependabot alerts 依存の既知の脆弱性に気づかない 人(設定画面) SECURITY.md 報告の方法が分からない AIでも書ける CODEOWNERS だれがレビューするか決まっていない AIでも書ける CodeQL コードの脆弱性 AIでも書ける ActionsのSHA固定 使っているActionが差し替えられる AIでも書ける Dependabot(更新) 依存が古いまま AIでも書ける gitleaks 秘密情報をコミットする AIでも書ける 上の3つは、リポジトリの設定を変える操作です。AIのセッションからは変えられないことが多いので、人がやります。下の6つはファイルを置くだけなので、AIに任せられます。 1. ルールセット:mainの削除と強制pushを禁止 GitHubのルールセットは、JSONで書いて読み込めます。リポジトリが多いときに便利です。 { "name": "protect-default-branch", "target": "branch", "enforcement": "active", "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" } ], "bypass_actors": [] } 読み込む手順です。 リポジトリの Settings → Rules → Rulesets New ruleset の▼ → Import a ruleset → JSONを選ぶ Enforcement statusが Active なのを確かめて Create あえて入れなかったもの 「PR必須」「CI合格必須」「レビュー必須」は、最初は入れていません。理由は2つです。 AIがmainに直接pushして作業しているので、入れると作業が止まる 1人のアカウントだと、自分のPRを自分で承認できない(レビュー必須にすると、だれもマージできない) チームで使い始めるときに、「PR必須+CI合格必須」を足すのがおすすめです。 2. 非公開の脆弱性報告とDependabot alerts 全リポジトリまとめて有効にできます。 右上のアイコン → Settings → Code security 「Private vulnerability reporting」の Enable all 「Enable by default for new public repositories」にもチェック 有効になると、リポジトリの Security タブに「Report a vulnerability」ボタンが出ます。 Dependabot alertsは、各リポジトリの Settings → Advanced Security(またはCode security)で有効にします。Dependabotには2つの機能があるので注意してください。 機能 何をするか Dependabot alerts 依存に既知の脆弱性があると知らせる Dependabot version updates 依存を新しい版に上げるPRを出す(dependabot.yml で設定) dependabot.yml を置いただけでは、alertsは有効になりません。 3. SECURITY.md と CODEOWNERS SECURITY.md があると、Securityタブの「Security policy」が有効になります。書く内容は短くて構いません。 セキュリティ 脆弱性を見つけたら 公開の Issue には書かず、GitHub の Security タブ → Report a vulnerability から知らせてください(非公開で届きます)。 CODEOWNERS は .github/CODEOWNERS に置きます。PRが出ると、書いた人に自動でレビューが依頼されます。 社内に展開するときは、チームのアカウントに書き換える(例: @your-org/your-team) - @your-account 4. CodeQL:ビルドなしで解析する CodeQLは、公開リポジトリなら無料で使えます。build-mode: none にすると、JavaやPythonをビルドせずに解析できます。言語に actions を入れると、ワークフローのファイルも調べてくれます。 name: codeql on: push: branches: [main] pull_request: schedule: - cron: "23 3 * * 1" permissions: contents: read jobs: analyze: runs-on: ubuntu-latest timeout-minutes: 30 permissions: security-events: write contents: read steps: - uses: actions/checkout@<コミットSHA> # v7.0.1 with: persist-credentials: false - uses: github/codeql-action/init@<コミットSHA> # v4.38.2 with: languages: python, actions build-mode: none queries: security-extended - uses: github/codeql-action/analyze@<コミットSHA> # v4.38.2 5. GitHub ActionsはコミットSHAで固定する actions/checkout@v7 のようなタグ指定は、タグが付け替えられると、別の中身が動きます。コミットSHAで固定し、版をコメントに残すのが安全です。 SHAは git ls-remote で調べられます。 git ls-remote actions/checkout 'refs/tags/v7.0.1*' 注釈付きのタグ(annotated tag)のときは、^{} が付いた行のSHAがコミットです。 88585263c0627ee42c0e1c5143a112c8d6f4aa18 refs/tags/v4.38.2 2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 refs/tags/v4.38.2^{} ← こちらを使う あわせて、チェックアウトには persist-credentials: false を付けます。付けないと、後のステップから認証情報を読めてしまいます。push が必要なジョブだけ、例外にします。 固定し忘れを防ぐには、テストで確かめるのが確実です。 PIN = re.compile(r"uses:\s*([^@\s]+)@([0-9a-f]{40})\s+#\s*v\d") def test_actions_are_pinned(): for wf in Path(".github/workflows").glob("*.yml"): for line in wf.read_text().splitlines(): if "uses:" in line and not line.strip().startswith("#"): assert PIN.search(line) or "uses: ./" in line, f"{wf}: {line.strip()}" 6. Dependabotの更新PRを溜めない SHAで固定しても、Dependabotは新しい版のSHAと版のコメントを書き換えたPRを出してくれます。 溜めないコツは2つです。 グループにまとめる。 Actionsの更新を1本のPRにまとめれば、数が減ります。 移行が必要なメジャー版は、提案させない。 APIが変わる依存は、手作業で移行します。 version: 2 updates: - package-ecosystem: github-actions directory: / schedule: { interval: monthly } groups: actions: { patterns: ["*"] } - package-ecosystem: pip directory: / schedule: { interval: monthly } ignore: 次のメジャー版は API が変わるため、移行は手作業で行う - dependency-name: mcp versions: [">=3"] 7. gitleaksで秘密情報の混入を調べる gitleaksは、すべてのコミットからAPIキーやトークンを探します。Dockerイメージで動かせば、インストールもいりません。 secret-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@<コミットSHA> # v7.0.1 with: fetch-depth: 0 # 履歴のすべてを調べる persist-credentials: false - run: | docker run --rm -v "$PWD:/repo" -w /repo zricethezav/gitleaks:v8.30.1 \ git --config .gitleaks.toml --redact --no-banner -v . エミュレーターの公開の開発用キーのような誤検知は、.gitleaks.toml で理由つきで除外します。 [extend] useDefault = true [allowlist] description = "公開されている開発用の値(本物の秘密ではない)" regexes = [ '''<除外する値>''' ] 8. コミットのメールアドレスにも注意 見落としがちなのが、コミットに記録されるメールアドレスです。個人のアドレスでコミットすると、公開リポジトリではだれでも見られます。 GitHubの Settings → Emails で、次の2つを有効にしましょう。 Keep my email addresses private Block command line pushes that expose my email すでに公開してしまった分は、履歴を書き換えて強制pushするしかありません。コミットのSHAがすべて変わり、閉じたPRからは古いコミットが見えたまま残ることもあるので、最初に設定しておくのが一番です。 よくある失敗 ルールセットで自分も止まる。 レビュー必須を1人のアカウントで入れると、だれもマージできなくなります。段階的に強めましょう。 dependabot.yml だけでalertsも有効と思い込む。 更新のPRとalertsは別の機能です。 タグで固定したつもりになる。 @v4 は「v4系の最新」に付け替えられます。固定するならSHAです。 AIに設定画面の操作まで頼む。 設定を変える操作は、多くの場合AIのセッションからはできません。ファイルで済むものはAIに、設定画面は人に、と分けましょう。 まとめ 公開リポジトリの守りは、ほとんどが無料で、ファイルを置くだけで入れられます。 設定画面の操作(ルールセット・脆弱性報告・alerts)だけは人がやります。 ActionsはコミットSHAで固定し、固定し忘れはテストで防ぎます。 コミットのメールアドレスは、最初に非公開にしておきましょう。

    2 likes
  10. Qiita · · @cauchye_asia

    PRのテスト項目にチェック漏れがあったらCIを落とす|jq+awkの41行スクリプトと踏みやすい3つの落とし穴 TL;DR - PR本文の「## テスト」見出しの下に - [ ](未チェック)が残っていたら、GitHub Actions のジョブを失敗させます。 - 素朴な grep -c '- \[ \]' では、別の見出しのチェック項目やコードブロック内の例まで数えます。改行が CRLF だと見出しの一致にも失敗し、未チェックがあるのに通ってしまいます。 - bash 5.2.37 / jq 1.7 / mawk 1.3.4・GNU Awk 5.2.1 で、合格・不合格・見出しなし・CRLF・コードブロックの5ケースと、イベントペイロード3件を確認しました。 全体像 flowchart LR A["PR を作成・本文を編集・push"] --> B["pull_request イベント<br>(opened / edited / synchronize / reopened)"] B --> C["jq で $GITHUB_EVENT_PATH から<br>pull_request.body を取り出す"] C --> D["check_pr_tests.sh<br>CRLF除去 → テスト見出しだけ読む"] D -->|すべて x| E["exit 0: 成功"] D -->|"- [ ] が残る"| F["exit 1: 失敗 + ::error 注釈"] D -->|見出し・項目なし| G["exit 2: 失敗(テンプレ未記入)"] PR本文のチェックボックスは、GitHub のタスクリスト記法(- [ ] が未完了、- [x] が完了)です。 素朴な方法で起きること サンプルとして、次の本文を使います。テスト項目はすべて済んでいて、別の見出し「メモ」に未チェックが1つあります。 ## 概要 ログ出力を追加 ## テスト - [x] 単体テストが通る - [x] 手元で動作確認した ## メモ - [ ] 後でドキュメントを更新 落とし穴1: 本文全体を grep すると、関係ない項目で落ちる $ grep -c -- "- \[ \]" samples/pass.md 1 テストは終わっているのに、「メモ」の項目を数えて失敗扱いになります。数える範囲をテスト見出しの下に限定する必要があります。 落とし穴2: CRLF の本文だと見出しが一致せず、素通りする 本文の改行が CRLF(\r\n)だった場合、行末に \r が残ります。/^## テスト$/ のように行末を固定した範囲指定は一致しません。 $ sed -n '/^## テスト$/,/^## /p' samples/crlf.md | grep -c -- '- \[ \]' 0 samples/crlf.md には未チェックが1つあるのに、0件になりました。これを「0件なら成功」と判定すると、チェック漏れが通ります。先に tr -d '\r' で改行をそろえます。 落とし穴3: 本文が空だと jq が null という文字列を出す イベントペイロードの body が null の場合(本文が空のPRを想定したサンプル event_null.json)、jq -r はそれを文字列 null として出力します。 $ jq -r ".pull_request.body" samples/event_null.json null // "" を付けて空文字にし、スクリプト側で「見出しがない」として扱います。 スクリプト テスト見出しの下だけを読み、未チェックがあれば終了コード 1 を返します。見出しやチェック項目そのものがない場合は、テンプレートが消されたとみなして 2 を返します。 #!/usr/bin/env bash # PR本文の「テスト」見出しの下にある未チェック項目(- [ ])を検出する。 # 使い方: ./check_pr_tests.sh [本文ファイル] (省略時は標準入力) # 終了コード: 0=すべてチェック済み / 1=未チェックあり / 2=見出しかチェック項目がない set -euo pipefail heading="${TEST_HEADING:-テスト}" input="${1:--}" # CRLF を LF にそろえてから awk で該当セクションだけを読む tr -d '\r' < "$( [ "$input" = "-" ] && echo /dev/stdin || echo "$input" )" | awk -v want="$heading" -v gha="${GITHUB_ACTIONS:-}" ' # コードブロック(``` / ~~~)の中は数えない /^[ \t]*(```|~~~)/ { infence = !infence; next } infence { next } /^#+[ \t]/ { match($0, /^#+/); level = RLENGTH title = substr($0, level + 1); sub(/^[ \t]+/, "", title); sub(/[ \t#]+$/, "", title) if (insec && level <= seclevel) insec = 0 # 同じか上位の見出しで終了 if (!insec && index(title, want) == 1) { insec = 1; seclevel = level; found = 1 } next } insec && /^[ \t]*[-*+][ \t]+\[[ xX]\]/ { if ($0 ~ /\[[xX]\]/) { checked++ } else { unchecked++ item = $0; sub(/^[ \t]*[-*+][ \t]+\[ \][ \t]*/, "", item) if (gha == "true") printf "::error title=未チェックのテスト項目::%s\n", item else printf "未チェック: %s\n", item } } END { if (!found) { print "テスト見出し(" want ")が見つかりません"; exit 2 } if (checked + unchecked == 0) { print "テスト見出しの下にチェック項目がありません"; exit 2 } printf "checked=%d unchecked=%d\n", checked, unchecked exit (unchecked > 0 ? 1 : 0) } ' ポイントは次の4つです。 - 見出しは「テスト」で始まるものに一致します(## テスト項目 も対象)。別の名前にするときは TEST_HEADING で変えます。 - 同じか上位レベルの見出しが来たらセクションを閉じます。### 補足 のような下位見出しの項目は、テストの一部として数えます。 - コードブロック内の - [ ] は記入例とみなして数えません。 - GITHUB_ACTIONS=true のときは、ワークフローコマンド ::error で注釈を出します。 動作を確認する 5つのサンプル本文 $ ./check_pr_tests.sh samples/pass.md; echo rc=$? checked=2 unchecked=0 rc=0 $ ./check_pr_tests.sh samples/fail.md; echo rc=$? 未チェック: ステージングで確認した 未チェック: ロールバック手順を確認した checked=1 unchecked=2 rc=1 $ ./check_pr_tests.sh samples/nosection.md; echo rc=$? テスト見出し(テスト)が見つかりません rc=2 $ ./check_pr_tests.sh samples/crlf.md; echo rc=$? 未チェック: 手元で動作確認した checked=1 unchecked=1 rc=1 $ ./check_pr_tests.sh samples/fence.md; echo rc=$? checked=2 unchecked=0 rc=0 fence.md は、## テスト項目 の下にコードブロック内の - [ ] と、### 補足 の下に大文字の - [X] を置いたものです。コードブロック内は数えず、[X] はチェック済みとして数えています。 | サンプル | 内容 | 期待 | 結果 | |---|---|---|---| | pass.md | テストは全部x、別見出しに未チェック | 0 | 0 | | fail.md | テストに未チェック2件 | 1 | 1 | | nosection.md | テスト見出しなし | 2 | 2 | | crlf.md | CRLF、未チェック1件 | 1 | 1 | | fence.md | コードブロック内の例+下位見出し+[X] | 0 | 0 | 同じ5件を、mawk 1.3.4、GNU Awk 5.2.1、gawk --posix で実行し、終了コードがすべて一致することも確認しました。 CI上の出力 $ GITHUB_ACTIONS=true ./check_pr_tests.sh < samples/fail.md ::error title=未チェックのテスト項目::ステージングで確認した ::error title=未チェックのテスト項目::ロールバック手順を確認した checked=1 unchecked=2 GitHub Actions のワークフロー name: pr-test-checklist on: pull_request: # 既定は opened / synchronize / reopened のみ。本文の編集でも再実行するため edited を足す types: [opened, edited, synchronize, reopened] permissions: contents: read jobs: checklist: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: テスト項目がすべてチェック済みか確認 shell: bash # -o pipefail が付く # 本文は ${{ }} で run に埋め込まず、イベントペイロードのファイルから読む run: jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH" | ./scripts/check_pr_tests.sh 公式ドキュメントで確認した前提は次のとおりです。 - edited を足す理由:pull_request は、types を指定しないと opened・synchronize・reopened でしか動きません。チェックを付けるのは本文の編集なので、edited がないと再判定されません。 - shell: bash を書く理由:shell を省略した場合は bash -e {0} です。shell: bash を指定すると -o pipefail も付き、パイプの途中(ここでは jq)の失敗でもステップが失敗します。ステップの成否は終了コードで決まります。 - 本文を ${{ }} で埋め込まない理由:github.event.pull_request.body は信頼できない入力として扱うよう明記されています。ここでは GITHUB_EVENT_PATH(イベントのペイロード全体が入ったファイル)から jq で読みます。公式が勧める「中間の環境変数に入れる」方法でも構いません(同じ3件の本文を環境変数経由で渡し、終了コードが一致することを確認しました)。 - name: 環境変数経由の書き方 shell: bash env: BODY: ${{ github.event.pull_request.body }} run: printf '%s\n' "$BODY" | ./scripts/check_pr_tests.sh ワークフローの静的チェックと run 行の確認 actionlint 1.7.12 で、上のワークフローは指摘なしでした。比較のため run: echo "${{ github.event.pull_request.body }}" | ... と書いた版を通すと、次の指摘が出ました。 $ actionlint .github/workflows/pr-test-checklist.yml; echo rc=$? rc=0 $ actionlint .github/workflows/zz-bad-example.yml; echo rc=$? .github/workflows/zz-bad-example.yml:7:24: "github.event.pull_request.body" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. ... rc=1 bash 5.2.37 / jq 1.7 の環境で、ワークフローの run 行を bash --noprofile --norc -eo pipefail で実行し、GITHUB_EVENT_PATH にペイロード3件を渡して確認しました。event_inject.json は、本文に $(touch /tmp/pwned_by_body) とバッククォートを含めたものです。文字列として扱われ、ファイルは作られませんでした。 $ ./verify.sh pass.md rc=0 fail.md rc=1 nosection.md rc=2 crlf.md rc=1 fence.md rc=0 event_null.json (run行) rc=2 event_fail.json (run行) rc=1 event_inject.json (run行) rc=1 $ ls /tmp/pwned_by_body ls: cannot access '/tmp/pwned_by_body': No such file or directory 運用で気をつけること - ジョブが失敗しても、それだけではマージは止まりません。止めるには、ブランチ保護(またはルールセット)の「Require status checks before merging」でこのジョブを必須にします。必須チェックは successful・skipped・neutral で通る仕様です。if: でジョブをスキップする条件を付けると、そのまま通ってしまう点に注意します。 - 公式ドキュメントの pull_request の項には、「The pull_request webhook event payload is empty for merged pull requests and pull requests that come from forked repositories.」(マージ済みのPRと、フォークしたリポジトリからのPRでは、pull_request の webhook イベントペイロードが空になる)と書かれています(Events that trigger workflows)。フォークからのPRを受けるリポジトリでは、本文が取れるかを先に確かめてください。ペイロードに pull_request.body がない場合({} を渡して確認)、このスクリプトは「見出しなし」(終了コード2)で失敗します。 - チェックボックスは自己申告です。このCIが保証するのは「テスト項目に全部チェックが付いている」ことまでで、テストを実施した証拠にはなりません。自動テストのジョブと組み合わせて使います。 まとめ - 数える範囲はテスト見出しの下に限定し、コードブロック内は除きます。 - 先に CRLF を LF にそろえ、本文が空の null は // "" で空文字にします。 - pull_request の types に edited を足し、shell: bash で pipefail を有効にします。本文は ${{ }} で run に埋め込みません。 - 終了コードを 0/1/2 に分けると、「チェック漏れ」と「テンプレート未記入」を区別できます。 実行環境 - bash 5.2.37 / jq 1.7 / mawk 1.3.4 / GNU Awk 5.2.1 / actionlint 1.7.12(Linux x86_64) - ワークフローの run 行は、上記の bash で bash --noprofile --norc -eo pipefail として実行して確認しました。actions/checkout は、2026-10-20 時点の最新メジャーバージョン v7(最新リリース v7.0.1)を指定しています。 - GitHub Actions のイベントや shell の既定値などの仕様は、下記の公式ドキュメントで確認しました。 参考 - タスクリスト: docs.github.com - pull_request イベントと既定の activity types: docs.github.com - shell の既定値と終了コード: docs.github.com - GITHUB_EVENT_PATH: docs.github.com - ワークフローコマンド(::error): docs.github.com - スクリプトインジェクション: docs.github.com - 安全な使い方(中間環境変数): docs.github.com - 必須ステータスチェック: docs.github.com - actions/checkout のリリース: github.com/actions/checkout/releases - actionlint: rhysd/actionlint

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

Scan pending.

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
actions/checkout mentions in the last 30 days